Recover WebSocket transport after transient HTTP fallback (#58950)

## Why

A transient WebSocket failure previously kept the session on HTTP for its remaining lifetime, even if WebSocket connectivity recovered.

## What changed

- Probe WebSocket connectivity in the background during HTTP fallback, allowing a subsequent request to use a ready connection without delaying HTTP requests.
- Verify recovery with a real request and bound its first-response wait to five seconds. Keep HTTP 426 responses and recovery timeouts on HTTP for the rest of the session.
- Keep retries of an HTTP request on HTTP. If a recovered connection fails before output, replay the request over HTTP without consuming its ordinary retry budget; failures after output use normal retry accounting.
- Rate-limit recovery probes and honor `Retry-After` deadlines.

## Testing

Extend integration coverage for successful recovery within a turn, disconnects before and after output, full-context replay followed by incremental WebSocket requests, persistent HTTP 426 fallback, and retry deadlines.

GitOrigin-RevId: 1c139a4bbce1a67442db5622fd70ff0cc8056c9c
This commit is contained in:
Charlie Marsh
2026-11-02 19:02:50 +00:00
committed by copyberry
parent 00df7f65ab
commit 0a2dda4b5e
10 changed files with 959 additions and 260 deletions
+5 -1
View File
@@ -36,6 +36,7 @@ use crate::mentions::collect_tool_mentions_from_messages;
use crate::plugins::build_plugin_injections;
use crate::responses_metadata::CodexResponsesMetadata;
use crate::responses_metadata::CodexResponsesRequestKind;
use crate::responses_retry::ResponsesRequestTransport;
use crate::responses_retry::ResponsesStreamRequest;
use crate::responses_retry::ResponsesStreamRetryState;
use crate::responses_retry::handle_response_stream_error;
@@ -1880,6 +1881,7 @@ async fn run_sampling_request(
Arc::clone(&step_context),
Arc::clone(&turn_store),
client_session,
&mut retry_state.transport,
&responses_metadata,
Arc::clone(&turn_diff_tracker),
prompt,
@@ -2733,6 +2735,7 @@ async fn try_run_sampling_request(
step_context: Arc<StepContext>,
turn_store: Arc<codex_extension_api::ExtensionData>,
client_session: &mut ModelClientSession,
transport: &mut ResponsesRequestTransport,
responses_metadata: &CodexResponsesMetadata,
turn_diff_tracker: SharedTurnDiffTracker,
prompt: Prompt,
@@ -2831,7 +2834,7 @@ async fn try_run_sampling_request(
None
};
let stream = client_session
.stream(
.stream_attempt(
&prompt,
&step_context.settings.model_info,
&step_context.session_telemetry,
@@ -2840,6 +2843,7 @@ async fn try_run_sampling_request(
step_context.settings.service_tier.clone(),
responses_metadata,
&inference_trace,
transport,
)
.instrument(trace_span!("stream_request"))
.or_cancel(&cancellation_token)