Add a dual-WebSocket transport for code mode (#36812)

## Why

Large nested-tool callbacks can occupy a WebSocket and delay unrelated session
operations on the same code-mode connection.

## What changed

- Negotiate the optional `dual-websocket-v1` capability and pair a second,
  token-scoped WebSocket with the control connection.
- Route nested-tool callbacks and their results over the bulk socket while
  keeping session operations, notifications, and execution responses on the
  control socket. Reject messages sent on the wrong lane.
- Preserve the single-connection transport when the capability is unavailable,
  and bound pairing, queued callbacks, and deferred cross-socket messages.
- Defer callbacks that arrive before their execution-started response, and
  return delegate errors without disconnecting the connection.

## Testing

Add protocol, transport, driver, and WebSocket integration coverage for
capability negotiation, lane routing, pairing failures, out-of-order messages,
and progress during large concurrent tool results.

GitOrigin-RevId: fa4504653e7cbf3c4ec930ae57aa0a41345bad66
This commit is contained in:
Channing Conger
2026-08-03 23:48:28 +00:00
committed by copyberry
parent b258c028fe
commit 60c722e075
21 changed files with 1809 additions and 135 deletions
@@ -7,8 +7,10 @@ use codex_code_mode_protocol::CodeModeSessionProvider;
use codex_code_mode_protocol::ExecuteRequest;
use codex_code_mode_protocol::FunctionCallOutputContentItem;
use codex_code_mode_protocol::RuntimeResponse;
use codex_code_mode_protocol::host::Capability;
use codex_code_mode_protocol::host::CapabilitySet;
use codex_code_mode_protocol::host::ClientToHost;
use codex_code_mode_protocol::host::DUAL_WEBSOCKET_CAPABILITY;
use codex_code_mode_protocol::host::EncodedFrame;
use codex_code_mode_protocol::host::HostHello;
use codex_code_mode_protocol::host::HostRequest;
@@ -24,6 +26,8 @@ use codex_http_client::OutboundProxyPolicy;
use futures::SinkExt;
use futures::StreamExt;
use pretty_assertions::assert_eq;
use tokio::io::AsyncReadExt;
use tokio::io::AsyncWriteExt;
use tokio::net::TcpListener;
use tokio::time::timeout;
use tokio_tungstenite::accept_async;
@@ -224,6 +228,91 @@ async fn websocket_provider_executes_over_shared_connector() {
.expect("websocket test host task should succeed");
}
#[tokio::test]
async fn websocket_provider_fails_when_a_negotiated_bulk_connection_is_unavailable() {
let listener = TcpListener::bind("127.0.0.1:0")
.await
.expect("websocket test listener should bind");
let websocket_url = format!(
"ws://{}/?access_token=shared-token",
listener
.local_addr()
.expect("websocket test listener should have an address")
);
let server = tokio::spawn(async move {
let (stream, _) = listener
.accept()
.await
.expect("websocket test host should accept the first connection");
let mut control = accept_async(stream)
.await
.expect("first control websocket should connect");
let frame = control
.next()
.await
.expect("first client hello")
.expect("first websocket frame")
.into_data();
let ClientToHost::ClientHello(hello) =
EncodedFrame::decode_framed(&frame).expect("decode first client hello")
else {
panic!("expected first client hello");
};
let capability =
Capability::new(DUAL_WEBSOCKET_CAPABILITY).expect("dual websocket capability");
assert!(hello.optional_capabilities().contains(&capability));
let hello = HostToClient::HostHello(
HostHello::new(
ProtocolVersion::V1,
CapabilitySet::try_new([capability.clone()]).expect("host capabilities"),
)
.with_bulk_connection_token("fallback-token".to_string()),
);
let frame = EncodedFrame::encode(&hello).expect("encode dual host hello");
control
.send(Message::Binary(frame.into_framed_bytes().into()))
.await
.expect("send dual host hello");
let (mut bulk, _) = listener
.accept()
.await
.expect("websocket test host should accept the bulk connection");
let mut request = [0_u8; 1024];
let request_len = bulk
.read(&mut request)
.await
.expect("read bulk websocket handshake");
let request = std::str::from_utf8(&request[..request_len]).expect("bulk HTTP request");
assert!(
request.starts_with("GET /bulk/fallback-token?access_token=shared-token HTTP/1.1\r\n")
);
bulk.write_all(b"HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\nConnection: close\r\n\r\n")
.await
.expect("reject unavailable bulk websocket");
drop(bulk);
drop(control);
});
let provider = WebSocketCodeModeSessionProvider::new(websocket_url);
let error = match provider
.create_session(Arc::new(NoopCodeModeSessionDelegate))
.await
{
Ok(_) => panic!("provider should reject an unavailable negotiated bulk websocket"),
Err(error) => error,
};
assert!(
error.contains("404"),
"unexpected negotiated bulk websocket error: {error}"
);
drop(provider);
timeout(Duration::from_secs(5), server)
.await
.expect("negotiated bulk websocket test host should disconnect promptly")
.expect("negotiated bulk websocket test host task should succeed");
}
#[tokio::test]
async fn provider_returns_missing_host_error() {
let provider = ProcessOwnedCodeModeSessionProvider::with_host_program(