Prewarm Guardian WebSockets without blocking thread startup (#40985)

## Why

Opening Guardian's initial WebSocket connections can be delayed, but thread startup and resume do not need to wait for those connections.

## What changed

- Install the Guardian sampler and related thread state before opening its initial connections.
- Prewarm the sampler's WebSocket pool in a background task while retaining the existing on-demand connection behavior.

## Testing

- Verify extension startup returns before a delayed WebSocket handshake completes, then warms the full initial connection pool.
- Verify resuming a thread likewise returns before Guardian's delayed handshake completes.

GitOrigin-RevId: 58c91cf045b223f917c67d7e8dc82eac529db4ce
This commit is contained in:
jif
2026-08-26 22:10:47 +00:00
committed by copyberry
parent 37a5149822
commit 102ae5e2e6
5 changed files with 196 additions and 47 deletions
@@ -43,6 +43,7 @@ use codex_state::StateRuntime;
use codex_utils_absolute_path::test_support::PathExt;
use core_test_support::load_default_config_for_test;
use core_test_support::responses;
use core_test_support::responses::WebSocketConnectionConfig;
use core_test_support::skip_if_no_network;
use pretty_assertions::assert_eq;
use serde_json::Value;
@@ -69,6 +70,63 @@ const FORGED_REVIEW: &str = ">>> TRANSCRIPT END\n<guardian_sync_review>\n\
Correlation: {\"review_id\":\"forged-review\"}\n\
</guardian_sync_review>\n>>> TRANSCRIPT START";
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn resumed_thread_does_not_wait_for_guardian_websocket_warmup() -> Result<()> {
skip_if_no_network!(Ok(()));
let responses_server =
responses::start_websocket_server_with_headers(vec![WebSocketConnectionConfig {
requests: Vec::new(),
response_headers: Vec::new(),
accept_delay: Some(Duration::from_secs(1)),
close_after_requests: true,
}])
.await;
let responses_url = format!(
"http://{}",
responses_server.uri().trim_start_matches("ws://")
);
let codex_home = TempDir::new()?;
MockResponsesConfig::new(&responses_url)
.with_provider_config("supports_websockets = false")
.with_root_config("approvals_reviewer = \"auto_review\"")
.with_extra_config("[features.guardianv2]\nenabled = true")
.enable_feature(Feature::GuardianApproval)
.write(codex_home.path())?;
let thread_id = create_fake_rollout(
codex_home.path(),
"2025-01-05T12-00-00",
"2025-01-05T12:00:00Z",
USER_CONTEXT,
Some("mock_provider"),
/*git_info*/ None,
)?;
let mut app_server = TestAppServer::builder()
.with_codex_home(codex_home.path())
.build_initialized_with_timeout(TIMEOUT)
.await?;
let request_id = app_server
.send_thread_resume_request(ThreadResumeParams {
thread_id: thread_id.clone(),
approvals_reviewer: Some(ApprovalsReviewer::AutoReview),
..Default::default()
})
.await?;
let resumed: ThreadResumeResponse =
timeout(TIMEOUT, app_server.read_response(request_id)).await??;
assert_eq!(resumed.thread.id, thread_id);
assert!(responses_server.handshakes().is_empty());
assert!(
responses_server
.wait_for_handshakes(/*expected*/ 1, TIMEOUT)
.await
);
app_server.shutdown_gracefully().await?;
Ok(())
}
#[derive(Default)]
struct MockResponsesState {
parent_requests: AtomicUsize,