From 25270df2615eb4da5b9d4a9a392226933fb096c5 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Sat, 26 Sep 2026 00:38:39 +0000 Subject: [PATCH] Keep TUI reconnect attempts running until the shared deadline (#48318) ## Why The five-attempt limit could stop reconnection before the existing 120-second budget expired, preventing recovery from longer temporary failures. ## What changed Continue retrying with an eight-second delay after the initial backoff until reconnection succeeds or the shared deadline expires. ## Testing Update the exhaustion test to require the full 120-second budget. Extend the integration test to recover after five failed `thread/resume` attempts and verify draft preservation and notification delivery. GitOrigin-RevId: 321d4ad100dbec1254bfe8aafd67ff63fb9a6e93 --- codex-rs/tui/src/app/reconnect.rs | 2 +- codex-rs/tui/src/app/tests/active_reconnect_tests.rs | 2 +- codex-rs/tui/tests/suite/reconnect.rs | 11 ++++++----- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/codex-rs/tui/src/app/reconnect.rs b/codex-rs/tui/src/app/reconnect.rs index ab6c32aff0..a68ae3916b 100644 --- a/codex-rs/tui/src/app/reconnect.rs +++ b/codex-rs/tui/src/app/reconnect.rs @@ -51,7 +51,7 @@ pub(super) async fn reconnect( // Connecting already has transport deadlines. Give healthy history/inventory hydration one // shared budget instead of repeatedly discarding its progress on a short per-attempt timer. let deadline = tokio::time::Instant::now() + Duration::from_secs(/*secs*/ 120); - for delay in [0, 1, 2, 4, 8] { + for delay in [0, 1, 2, 4].into_iter().chain(std::iter::repeat(/*elt*/ 8)) { let attempt = async { tokio::time::sleep(Duration::from_secs(delay)).await; let client = crate::app_server_connection::connect(&target).await?; diff --git a/codex-rs/tui/src/app/tests/active_reconnect_tests.rs b/codex-rs/tui/src/app/tests/active_reconnect_tests.rs index 683e8fc461..36f1054bdb 100644 --- a/codex-rs/tui/src/app/tests/active_reconnect_tests.rs +++ b/codex-rs/tui/src/app/tests/active_reconnect_tests.rs @@ -618,7 +618,7 @@ async fn reconnect_exhaustion_and_unknown_initial_thread_stay_offline() -> Resul .is_err() ); } - assert!((15..=65).contains(&start.elapsed().as_secs())); + assert_eq!(start.elapsed().as_secs(), 120); app.begin_reconnect(); app.chat_widget.reconnect_failed(); assert_snapshot!( diff --git a/codex-rs/tui/tests/suite/reconnect.rs b/codex-rs/tui/tests/suite/reconnect.rs index b5ab0707dd..0064d0c889 100644 --- a/codex-rs/tui/tests/suite/reconnect.rs +++ b/codex-rs/tui/tests/suite/reconnect.rs @@ -43,7 +43,7 @@ async fn automatic_reconnect_restores_draft_and_routes_new_notifications() -> Re "cliVersion": "0.0.0", "source": "cli", "turns": [{"id": "running", "items": [], "status": "inProgress", "error": null}] }); // The first connection checks daemon compatibility before TUI startup. - for connection in -1..3 { + for connection in -1..7 { let mut socket = loop { let (stream, _) = listener.accept().await?; // Startup probes the default daemon socket before opening its WebSocket. @@ -69,7 +69,8 @@ async fn automatic_reconnect_restores_draft_and_routes_new_notifications() -> Re if connection == 1 && request.method == "initialize" { restore_rx.take().unwrap().await?; } - if connection == 1 && request.method == "thread/resume" { + // Keep failing past the original five-attempt limit. + if (1..=5).contains(&connection) && request.method == "thread/resume" { socket .send(Message::Text( json!({"id": request.id, "error": { @@ -131,7 +132,7 @@ async fn automatic_reconnect_restores_draft_and_routes_new_notifications() -> Re .into(), )) .await?; - if connection == 2 && request.method == "thread/resume" { + if connection == 6 && request.method == "thread/resume" { // Keep the recovered turn running: its output must appear without waiting // for turn/completed or rebuilding the transcript. socket @@ -162,7 +163,7 @@ async fn automatic_reconnect_restores_draft_and_routes_new_notifications() -> Re "preserved-draft!", "fresh-notification-after-reconnect", ] { - let deadline = Instant::now() + Duration::from_secs(/*secs*/ 30); + let deadline = Instant::now() + Duration::from_secs(/*secs*/ 60); while !terminal.screen_contains(expected) && Instant::now() < deadline { terminal.read_output(Duration::from_millis(/*millis*/ 20))?; } @@ -205,7 +206,7 @@ async fn automatic_reconnect_restores_draft_and_routes_new_notifications() -> Re .iter() .filter(|method| *method == "thread/resume") .count(), - 2 + 6 ); assert!(!methods.iter().any(|method| method == "turn/start")); Ok(())