Preserve terminal turn errors in replayed history (#35524)

## Why

When rebuilding a thread from rollout events, errors embedded in turn completion
events were ignored. Failed retries could therefore be restored as completed
turns, causing warnings such as model-overload errors to disappear from the TUI
transcript.

## What changed

- Mark turns with terminal completion errors as failed and retain their message
  and structured error information.
- Apply late completion errors to their matching historical turn without
  interrupting the active turn.

## Testing

Added history-builder coverage for current and late completion errors, plus a
TUI replay snapshot covering repeated overload failures.

GitOrigin-RevId: 5c474baa0fd5c73368b444d79fb850cbd4c8023a
This commit is contained in:
Felipe Coury authored and copyberry committed 2026-07-26 22:15:29 +00:00
1 parent 5a3808b77d
commit 9ca6df68da
3 files changed
+242 -5

No files matched your search

@@ -75,6 +75,54 @@ async fn resumed_initial_messages_render_history() {
);
}
#[tokio::test]
async fn replayed_failed_turns_preserve_overload_warnings_between_retries() {
let (mut chat, mut rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await;
let prompt = "The workspace also looks super confusing with its separator.";
let error_message = "Selected model is at capacity. Please try a different model.";
let failed_turn = |turn_id: &str, item_id: &str| AppServerTurn {
items: vec![AppServerThreadItem::UserMessage {
id: item_id.to_string(),
client_id: None,
content: vec![AppServerUserInput::Text {
text: prompt.to_string(),
text_elements: Vec::new(),
}],
}],
..app_server_turn(
turn_id,
AppServerTurnStatus::Failed,
/*duration_ms*/ None,
/*error*/
Some(AppServerTurnError {
message: error_message.to_string(),
codex_error_info: Some(CodexErrorInfo::ServerOverloaded),
additional_details: None,
}),
)
};
chat.replay_thread_turns(
vec![
failed_turn("turn-1", "user-1"),
failed_turn("turn-2", "user-2"),
],
ReplayKind::ResumeInitialMessages,
);
let rendered = drain_insert_history(&mut rx)
.into_iter()
.map(|lines| lines_to_single_string(&lines))
.collect::<String>();
assert_eq!(rendered.matches(prompt).count(), 2);
assert_eq!(rendered.matches(error_message).count(), 2);
insta::assert_snapshot!(
"replayed_failed_turns_preserve_overload_warnings_between_retries",
rendered
);
}
#[tokio::test]
async fn restored_conversation_ultra_remains_selected_after_switching_to_plan() {
let (mut chat, _rx, _ops) = make_chatwidget_manual(Some("gpt-5.4")).await;
@@ -0,0 +1,15 @@
---
source: tui/src/chatwidget/tests/history_replay.rs
expression: rendered
---
› The workspace also looks super confusing with its separator.
⚠ Selected model is at capacity. Please try a different model.
› The workspace also looks super confusing with its separator.
⚠ Selected model is at capacity. Please try a different model.