use std::sync::Arc; use std::time::Duration; use std::time::Instant; use tokio::task::JoinHandle; use tokio_util::sync::CancellationToken; use tokio_util::task::AbortOnDropHandle; use tracing::Instrument; use tracing::info; use tracing::instrument; use tracing::trace_span; use tracing::warn; use crate::client::ModelClientSession; use crate::guardian::routes_approval_to_guardian; use crate::responses_metadata::CodexResponsesRequestKind; use crate::session::INITIAL_SUBMIT_ID; use crate::session::RequestEffortUsage; use crate::session::session::Session; use crate::session::turn::build_prompt; use codex_features::Feature; use codex_otel::STARTUP_PREWARM_AGE_AT_FIRST_TURN_METRIC; use codex_otel::STARTUP_PREWARM_DURATION_METRIC; use codex_otel::SessionTelemetry; use codex_protocol::error::Result as CodexResult; use codex_protocol::models::BaseInstructions; pub(crate) struct SessionStartupPrewarmHandle { task: AbortOnDropHandle>, started_at: Instant, timeout: Duration, } pub(crate) enum SessionStartupPrewarmResolution { Cancelled, Ready(Box), Unavailable { status: &'static str, prewarm_duration: Option, }, } impl SessionStartupPrewarmHandle { pub(crate) fn new( task: JoinHandle>, started_at: Instant, timeout: Duration, ) -> Self { Self { task: AbortOnDropHandle::new(task), started_at, timeout, } } pub(crate) async fn abort(self) { self.task.abort(); let _ = self.task.await; } #[instrument(name = "startup_prewarm.resolve", level = "trace", skip_all)] async fn resolve( self, session_telemetry: &SessionTelemetry, cancellation_token: &CancellationToken, ) -> SessionStartupPrewarmResolution { let resolve_started_at = Instant::now(); let Self { mut task, started_at, timeout, } = self; let age_at_first_turn = started_at.elapsed(); let remaining = timeout.saturating_sub(age_at_first_turn); let resolution = if task.is_finished() { Self::resolution_from_join_result(task.await, started_at) } else { match tokio::select! { _ = cancellation_token.cancelled() => None, result = tokio::time::timeout(remaining, &mut task) => Some(result), } { Some(Ok(result)) => Self::resolution_from_join_result(result, started_at), Some(Err(_elapsed)) => { task.abort(); info!("startup websocket prewarm timed out before the first turn could use it"); SessionStartupPrewarmResolution::Unavailable { status: "timed_out", prewarm_duration: Some(started_at.elapsed()), } } None => { task.abort(); session_telemetry.record_startup_phase( "startup_prewarm_resolve", resolve_started_at.elapsed(), Some("cancelled"), ); session_telemetry.record_duration( STARTUP_PREWARM_AGE_AT_FIRST_TURN_METRIC, age_at_first_turn, &[("status", "cancelled")], ); session_telemetry.record_duration( STARTUP_PREWARM_DURATION_METRIC, started_at.elapsed(), &[("status", "cancelled")], ); return SessionStartupPrewarmResolution::Cancelled; } } }; let status = match &resolution { SessionStartupPrewarmResolution::Cancelled => "cancelled", SessionStartupPrewarmResolution::Ready(_) => "ready", SessionStartupPrewarmResolution::Unavailable { status, .. } => status, }; session_telemetry.record_startup_phase( "startup_prewarm_resolve", resolve_started_at.elapsed(), Some(status), ); match resolution { SessionStartupPrewarmResolution::Cancelled => { SessionStartupPrewarmResolution::Cancelled } SessionStartupPrewarmResolution::Ready(prewarmed_session) => { session_telemetry.record_duration( STARTUP_PREWARM_AGE_AT_FIRST_TURN_METRIC, age_at_first_turn, &[("status", "consumed")], ); SessionStartupPrewarmResolution::Ready(prewarmed_session) } SessionStartupPrewarmResolution::Unavailable { status, prewarm_duration, } => { session_telemetry.record_duration( STARTUP_PREWARM_AGE_AT_FIRST_TURN_METRIC, age_at_first_turn, &[("status", status)], ); if let Some(prewarm_duration) = prewarm_duration { session_telemetry.record_duration( STARTUP_PREWARM_DURATION_METRIC, prewarm_duration, &[("status", status)], ); } SessionStartupPrewarmResolution::Unavailable { status, prewarm_duration, } } } } fn resolution_from_join_result( result: std::result::Result, tokio::task::JoinError>, started_at: Instant, ) -> SessionStartupPrewarmResolution { match result { Ok(Ok(prewarmed_session)) => { SessionStartupPrewarmResolution::Ready(Box::new(prewarmed_session)) } Ok(Err(err)) => { warn!("startup websocket prewarm setup failed: {err:#}"); SessionStartupPrewarmResolution::Unavailable { status: "failed", prewarm_duration: None, } } Err(err) => { warn!("startup websocket prewarm setup join failed: {err}"); SessionStartupPrewarmResolution::Unavailable { status: "join_failed", prewarm_duration: Some(started_at.elapsed()), } } } } } impl Session { pub(crate) async fn schedule_startup_prewarm(self: &Arc, base_instructions: String) { if self.features().enabled(Feature::CodeModePrewarm) && self.services.code_mode_service.is_available() { let session = Arc::clone(self); tokio::spawn(async move { if session.services.code_mode_service.session().await.is_err() { warn!("code-mode host startup prewarm failed"); } }); } if !self.services.model_client.responses_websocket_enabled() { // Without websocket prewarm, resolve auth once so Agent Identity bootstrap can // register or engage this session's bearer fallback before the first user request. let model_client = self.services.model_client.clone(); tokio::spawn(async move { if let Err(err) = model_client.prewarm_auth().await { warn!("startup auth prewarm failed: {err:#}"); } }); return; } let session_telemetry = self.services.session_telemetry.clone(); let websocket_connect_timeout = self.provider().await.websocket_connect_timeout(); let started_at = Instant::now(); let startup_prewarm_session = Arc::clone(self); let startup_prewarm = tokio::spawn( async move { let result = schedule_startup_prewarm_inner(startup_prewarm_session, base_instructions) .await; let status = if result.is_ok() { "ready" } else { "failed" }; session_telemetry.record_startup_phase( "startup_prewarm_total", started_at.elapsed(), Some(status), ); session_telemetry.record_duration( STARTUP_PREWARM_DURATION_METRIC, started_at.elapsed(), &[("status", status)], ); result } .instrument(trace_span!( "startup_prewarm", otel.name = "startup_prewarm", thread.id = %self.thread_id(), )), ); self.set_session_startup_prewarm(SessionStartupPrewarmHandle::new( startup_prewarm, started_at, websocket_connect_timeout, )) .await; } pub(crate) async fn consume_startup_prewarm_for_regular_turn( &self, cancellation_token: &CancellationToken, ) -> SessionStartupPrewarmResolution { let Some(startup_prewarm) = self.take_session_startup_prewarm().await else { return SessionStartupPrewarmResolution::Unavailable { status: "not_scheduled", prewarm_duration: None, }; }; startup_prewarm .resolve(&self.services.session_telemetry, cancellation_token) .await } } async fn schedule_startup_prewarm_inner( session: Arc, base_instructions: String, ) -> CodexResult { let prewarm_started_at = Instant::now(); let startup_turn_context = session .new_startup_prewarm_turn_with_sub_id(INITIAL_SUBMIT_ID.to_owned()) .await; startup_turn_context.session_telemetry.record_startup_phase( "startup_prewarm_create_turn_context", prewarm_started_at.elapsed(), /*status*/ None, ); if routes_approval_to_guardian(&startup_turn_context) { let guardian_session = Arc::clone(&session); let guardian_parent_turn = Arc::clone(&startup_turn_context); drop(tokio::spawn(async move { if let Err(err) = crate::guardian::prewarm_guardian_review_session( guardian_session, guardian_parent_turn, ) .await { warn!("failed to initialize guardian review session: {err:#}"); } })); } let startup_cancellation_token = CancellationToken::new(); let built_tools_started_at = Instant::now(); // Startup prewarm runs before run_turn and needs its own tool-building snapshot. let step_context = session .capture_step_context( Arc::clone(&startup_turn_context), &startup_cancellation_token, ) .await?; startup_turn_context.session_telemetry.record_startup_phase( "startup_prewarm_build_tools", built_tools_started_at.elapsed(), /*status*/ None, ); let build_prompt_started_at = Instant::now(); let startup_prompt = build_prompt( Vec::new(), step_context.as_ref(), BaseInstructions { text: base_instructions, provenance: None, }, ); startup_turn_context.session_telemetry.record_startup_phase( "startup_prewarm_build_prompt", build_prompt_started_at.elapsed(), /*status*/ None, ); let responses_metadata = session .responses_metadata(step_context.as_ref(), CodexResponsesRequestKind::Prewarm) .await; let mut client_session = session.services.model_client.new_session(); let websocket_warmup_started_at = Instant::now(); // Prewarm establishes the request baseline before the first turn can change effort. client_session .prewarm_websocket( &startup_prompt, &step_context.settings.model_info, &step_context.session_telemetry, session .reasoning_effort_for_request(&step_context.settings, RequestEffortUsage::Sampling) .await, step_context.settings.reasoning_summary, step_context.settings.service_tier.clone(), &responses_metadata, ) .await?; startup_turn_context.session_telemetry.record_startup_phase( "startup_prewarm_websocket_warmup", websocket_warmup_started_at.elapsed(), /*status*/ None, ); Ok(client_session) }