Codex Handbook
core/src/responses_retry.rs 105 lines
//! Shared retry and transport fallback decisions for Responses requests.use std::time::Duration;use crate::client::ModelClientSession;use crate::session::session::Session;use crate::session::turn_context::TurnContext;use crate::util::backoff;use codex_protocol::error::CodexErr;use codex_protocol::protocol::EventMsg;use codex_protocol::protocol::WarningEvent;use tracing::warn;#[derive(Debug, Clone, Copy)]pub(crate) enum ResponsesStreamRequest {    Sampling,    RemoteCompactionV2,}/// Handles a retryable stream error and returns `Ok(())` when the caller should/// retry the request loop.pub(crate) async fn handle_retryable_response_stream_error(    retries: &mut u64,    max_retries: u64,    err: CodexErr,    client_session: &mut ModelClientSession,    sess: &Session,    turn_context: &TurnContext,    request: ResponsesStreamRequest,) -> Result<(), CodexErr> {    if *retries >= max_retries        && client_session.try_switch_fallback_transport(            &turn_context.session_telemetry,            &turn_context.model_info,        )    {        sess.send_event(            turn_context,            EventMsg::Warning(WarningEvent {                message: format!("Falling back from WebSockets to HTTPS transport. {err:#}"),            }),        )        .await;        *retries = 0;        return Ok(());    }    if *retries < max_retries {        *retries += 1;        let retry_count = *retries;        let delay = match &err {            CodexErr::Stream(_, requested_delay) => {                requested_delay.unwrap_or_else(|| backoff(retry_count))            }            _ => backoff(retry_count),        };        log_retry(request, turn_context, &err, retry_count, max_retries, delay);        // In release builds, hide the first websocket retry notification to reduce noisy        // transient reconnect messages. In debug builds, keep full visibility for diagnosis.        let report_error = retry_count > 1            || cfg!(debug_assertions)            || !sess.services.model_client.responses_websocket_enabled();        if report_error {            // Surface retry information to any UI/front-end so the user understands what is            // happening instead of staring at a seemingly frozen screen.            sess.notify_stream_error(                turn_context,                format!("Reconnecting... {retry_count}/{max_retries}"),                err,            )            .await;        }        tokio::time::sleep(delay).await;        return Ok(());    }    Err(err)}fn log_retry(    request: ResponsesStreamRequest,    turn_context: &TurnContext,    err: &CodexErr,    retries: u64,    max_retries: u64,    delay: Duration,) {    match request {        ResponsesStreamRequest::Sampling => {            warn!(                "stream disconnected - retrying sampling request ({retries}/{max_retries} in {delay:?})...",            );        }        ResponsesStreamRequest::RemoteCompactionV2 => {            warn!(                turn_id = %turn_context.sub_id,                retries,                max_retries,                compact_error = %err,                "remote compaction v2 stream failed; retrying request after delay"            );        }    }}