[codex] Send request-scoped turn state over WebSocket (#27996)

## Context

Turn state is scoped to one logical turn, but the WebSocket path
currently exchanges it through upgrade headers, which are scoped to the
physical connection. A connection may be reused across turns, so its
handshake cannot represent the turn lifecycle reliably.

## Change

Exchange turn state on each WebSocket response request instead:

- send an established value in `response.create.client_metadata`
- read the returned value from the existing `response.metadata` event
- retain the first value in the turn-scoped `ModelClientSession`
`OnceLock`
- start the next logical turn without state, even when it reuses the
same WebSocket connection

This gives WebSocket requests the same first-value-wins contract as the
existing HTTP path.

## Test plan

Integration coverage verifies that:

- WebSocket replays returned state on same-turn follow-ups
- later response metadata does not replace the first value
- state resets at the logical turn boundary without requiring a
reconnect

CI validates the full change.

## Stack

This is 1/2. #28002 builds on this request-scoped transport to carry
established state through compact requests.
This commit is contained in:
Ahmed Ibrahim
2026-06-13 00:44:39 -07:00
committed by GitHub
Unverified
parent 9d938a46d9
commit 640d61b121
5 changed files with 140 additions and 70 deletions
@@ -215,6 +215,7 @@ impl ResponsesWebsocketConnection {
&self,
request: ResponsesWsRequest,
connection_reused: bool,
turn_state: Option<Arc<OnceLock<String>>>,
) -> Result<ResponseStream, ApiError> {
let (tx_event, rx_event) =
mpsc::channel::<std::result::Result<ResponseEvent, ApiError>>(1600);
@@ -264,6 +265,7 @@ impl ResponsesWebsocketConnection {
idle_timeout,
telemetry,
connection_reused,
turn_state.as_deref(),
)
.await
};
@@ -631,6 +633,7 @@ async fn run_websocket_response_stream(
idle_timeout: Duration,
telemetry: Option<Arc<dyn WebsocketTelemetry>>,
connection_reused: bool,
turn_state: Option<&OnceLock<String>>,
) -> Result<(), ApiError> {
let mut last_server_model: Option<String> = None;
send_websocket_request(
@@ -682,6 +685,11 @@ async fn run_websocket_response_stream(
continue;
}
};
if let Some(response_turn_state) = event.turn_state()
&& let Some(turn_state) = turn_state
{
let _ = turn_state.set(response_turn_state);
}
let model_verifications = event.model_verifications();
let turn_moderation_metadata = event.turn_moderation_metadata();
if event.kind() == "codex.rate_limits" {