From f9ed58e6aca4f774cc5b2e063d405566ddf01589 Mon Sep 17 00:00:00 2001 From: JonthanaHanh <92574114+JonthanaHanh@users.noreply.github.com> Date: Mon, 3 Aug 2026 10:05:45 +0700 Subject: [PATCH] fix(tui-gateway): merge agent output on model-switch history_version mismatch (#76870) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a model switch occurs mid-turn, `_append_model_switch_marker()` appends a marker to session history and increments `history_version`. The turn completion guard then sees `current_version != history_version` and discards all agent output — producing empty assistant messages in the session DB. Detect when the only history mutation during the turn was one or more model-switch markers. In that case, merge the agent's new messages into the current history (which now contains the marker) instead of discarding them. Genuine desyncs (undo/compress/retry) still surface the warning as before. Fixes #76870 --- tui_gateway/server.py | 47 +++++++++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/tui_gateway/server.py b/tui_gateway/server.py index a8ab110b6dd42..4662a1ed72b50 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -9689,24 +9689,37 @@ def _run_prompt_submit( session["history"] = result["messages"] session["history_version"] = history_version + 1 else: - # History mutated externally during the turn - # (undo/compress/retry/rollback now guard on - # session.running, but this is the defensive - # backstop for any path that slips past). - # Surface the desync rather than silently - # dropping the agent's output — the UI can - # show the response and warn that it was - # not persisted. - print( - f"[tui_gateway] prompt.submit: history_version mismatch " - f"(expected={history_version} current={current_version}) — " - f"agent output NOT written to session history", - file=sys.stderr, - ) - status_note = ( - "History changed during this turn — the response above is visible " - "but was not saved to session history." + # History mutated externally during the turn. + # Check if the only mutation was a model-switch + # marker inserted mid-turn (#76870). If so the + # agent output is still valid — merge it into the + # current history that now contains the marker. + current_history = list(session["history"]) + added = current_history[len(history):] + model_switch_only = ( + len(added) >= 1 + and all(_is_model_switch_marker(e) for e in added) ) + if model_switch_only: + new_messages = result["messages"][len(history):] + session["history"] = current_history + new_messages + session["history_version"] = current_version + 1 + else: + # Genuine desync (undo/compress/retry/rollback). + # Surface the desync rather than silently + # dropping the agent's output — the UI can + # show the response and warn that it was + # not persisted. + print( + f"[tui_gateway] prompt.submit: history_version mismatch " + f"(expected={history_version} current={current_version}) — " + f"agent output NOT written to session history", + file=sys.stderr, + ) + status_note = ( + "History changed during this turn — the response above is visible " + "but was not saved to session history." + ) # If auto-compression fired inside run_conversation(), agent.session_id # may have rotated. Sync session_key before downstream title/goal/finalize