Pin request reasoning effort while configuration overrides are active (#43795)

## Why

Reasoning-effort changes should preserve the request baseline while trusted `configuration_update` items carry the selected effort.

## What changed

- Pin request-level reasoning effort for the current model when reasoning-effort overrides are available, including during WebSocket prewarm.
- Re-establish the selected effort after rollback, resume, or a model switch, refreshing the trusted override even when the selection is unchanged.
- Continue using the selected request effort when overrides are unavailable.

## Testing

Add regression coverage for WebSocket prewarm and reconnect, rollback, resume, and model switches. Update transition tests to assert that request-level effort stays pinned while configuration updates change.

GitOrigin-RevId: 89cfee6289543230347b0488d1388190b4da7216
This commit is contained in:
felixxia-oai
2026-09-08 10:41:15 +00:00
committed by copyberry
parent df522cae16
commit 31ccaf40c2
7 changed files with 504 additions and 14 deletions
+3
View File
@@ -299,6 +299,7 @@ use crate::state::AcceptedUserInputResponse;
use crate::state::AutoCompactWindowIds;
use crate::state::AutoCompactWindowSnapshot;
use crate::state::PendingRequestPermissions;
use crate::state::ReasoningEffortPin;
use crate::state::SessionServices;
use crate::state::SessionState;
#[cfg(test)]
@@ -1605,6 +1606,8 @@ impl Session {
state
.history
.restore_review_context(Some(&retained_context), guardian_history.as_ref());
// The next send supplies the selected effort. Refresh its trusted override too.
state.reasoning_effort_pin = ReasoningEffortPin::Unset;
if let Some(world_state) = world_state_baseline {
state.history.set_world_state_baseline(world_state);
}
+30 -4
View File
@@ -1,11 +1,12 @@
//! Gated reasoning-effort updates appended after accepted input.
//! Cache-preserving effort updates and the original sampling request effort.
//!
//! Only trusted harness items establish an effort. Updates append to surviving
//! history without replacing the request-level reasoning effort.
//! Only trusted harness items establish overrides. Replay invalidates the runtime
//! pin so the next send re-establishes the currently selected effort.
use super::session::Session;
use super::step_context::StepContext;
use super::step_settings::ResolvedStepSettings;
use crate::state::ReasoningEffortPin;
use codex_features::Feature;
use codex_history::CodexHarnessMetadata;
use codex_history::ResponseItemEnvelope;
@@ -43,7 +44,10 @@ impl Session {
_ => None,
}
});
established_effort == Some(&effort)
state
.reasoning_effort_pin
.get(&settings.model_info.slug)
.is_some_and(|pinned| established_effort.unwrap_or(&pinned) == &effort)
};
if should_skip {
return;
@@ -64,6 +68,28 @@ impl Session {
.await;
}
/// Reuses the original request effort while updates carry the selected effort.
pub(crate) async fn reasoning_effort_for_request(
&self,
settings: &ResolvedStepSettings,
) -> Option<ReasoningEffort> {
let selected_effort = settings.reasoning_effort().cloned();
if !self.enabled(Feature::ReasoningEffortOverride) {
return selected_effort;
}
let effort = self.effort_for_configuration_update(settings).await;
let mut state = self.state.lock().await;
let Some(effort) = effort else {
state.reasoning_effort_pin = ReasoningEffortPin::Unset;
return selected_effort;
};
Some(
state
.reasoning_effort_pin
.pin(&settings.model_info.slug, effort),
)
}
async fn effort_for_configuration_update(
&self,
settings: &ResolvedStepSettings,
+2 -1
View File
@@ -2313,7 +2313,8 @@ async fn try_run_sampling_request(
prompt,
&step_context.settings.model_info,
&step_context.session_telemetry,
step_context.settings.reasoning_effort().cloned(),
sess.reasoning_effort_for_request(&step_context.settings)
.await,
step_context.settings.reasoning_summary,
step_context.settings.service_tier.clone(),
responses_metadata,
+4 -1
View File
@@ -318,12 +318,15 @@ async fn schedule_startup_prewarm_inner(
.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,
step_context.settings.reasoning_effort().cloned(),
session
.reasoning_effort_for_request(&step_context.settings)
.await,
step_context.settings.reasoning_summary,
step_context.settings.service_tier.clone(),
&responses_metadata,
+1
View File
@@ -9,6 +9,7 @@ pub(crate) use additional_context::AdditionalContextStore;
pub(crate) use auto_compact_window::AutoCompactWindowIds;
pub(crate) use auto_compact_window::AutoCompactWindowSnapshot;
pub(crate) use service::SessionServices;
pub(crate) use session::ReasoningEffortPin;
pub(crate) use session::SessionState;
pub(crate) use turn::AcceptedUserInputResponse;
pub(crate) use turn::ActiveTurn;
+36
View File
@@ -3,6 +3,7 @@
use codex_protocol::models::AdditionalPermissionProfile;
use codex_protocol::models::BaseInstructionsProvenance;
use codex_protocol::models::ResponseItem;
use codex_protocol::openai_models::ReasoningEffort;
use codex_sandboxing::policy_transforms::merge_permission_profiles;
use std::collections::HashMap;
use std::collections::HashSet;
@@ -29,6 +30,38 @@ use codex_protocol::protocol::TurnContextItem;
use codex_utils_output_truncation::TruncationPolicy;
use tokio_util::task::AbortOnDropHandle;
/// Runtime request effort; startup and replay must re-establish the selected effort.
pub(crate) enum ReasoningEffortPin {
Unset,
Active {
model: String,
effort: ReasoningEffort,
},
}
impl ReasoningEffortPin {
pub(crate) fn get(&self, model: &str) -> Option<ReasoningEffort> {
match self {
Self::Active {
model: pinned_model,
effort,
} if pinned_model == model => Some(effort.clone()),
Self::Unset | Self::Active { .. } => None,
}
}
pub(crate) fn pin(&mut self, model: &str, effort: ReasoningEffort) -> ReasoningEffort {
if let Some(pinned) = self.get(model) {
return pinned;
}
*self = Self::Active {
model: model.to_owned(),
effort: effort.clone(),
};
effort
}
}
/// Persistent, session-scoped state previously stored directly on `Session`.
pub(crate) struct SessionState {
pub(crate) session_configuration: SessionConfiguration,
@@ -46,6 +79,8 @@ pub(crate) struct SessionState {
previous_turn_settings: Option<PreviousTurnSettings>,
/// Runtime accounting state for the active auto-compaction window.
auto_compact_window: AutoCompactWindow,
/// Original request effort for the current model while configuration updates remain active.
pub(crate) reasoning_effort_pin: ReasoningEffortPin,
/// Startup prewarmed session prepared during session initialization.
pub(crate) startup_prewarm: Option<SessionStartupPrewarmHandle>,
/// Retained after completion so later turns do not repeat speculative captures.
@@ -84,6 +119,7 @@ impl SessionState {
additional_context: AdditionalContextStore::default(),
previous_turn_settings: None,
auto_compact_window: AutoCompactWindow::new_with_ids(auto_compact_window_ids),
reasoning_effort_pin: ReasoningEffortPin::Unset,
startup_prewarm: None,
shell_snapshot_prewarm: None,
current_time_reminder: CurrentTimeReminderState::default(),