Make automatic turn admission atomic with settings updates (#40648)

## Why

Turn settings can change after an input request previews its overrides. Automatic idle work could therefore make a Plan-mode admission decision from stale settings or apply a sparse override built from an outdated collaboration mode.

## What changed

- Evaluate automatic turn admission against both the current and proposed configuration under the settings publication lock.
- Reject automatic work that would enter or leave Plan mode without committing settings or triggering runtime configuration effects.
- Keep model and reasoning-effort overrides sparse until they are merged with the configuration being committed.
- Continue allowing explicit user and recovery turns to make valid mode transitions.

## Testing

Add coverage for concurrent settings changes, sparse model and effort updates, commit-time constraint validation, atomic Plan-mode rejection, and subsequent explicit user submission.

GitOrigin-RevId: 234f06de116bdce7c3a3fb5cac3d15583304a253
This commit is contained in:
joeflorencio-openai authored and copyberry committed 2026-08-25 17:30:45 +00:00
1 parent 1d64085e67
commit ba6cf9c692
9 files changed
+756 -137

No files matched your search

+23 -1
View File
@@ -1602,6 +1602,24 @@ impl Session {
&self,
updates: SessionSettingsUpdate,
) -> ConstraintResult<SessionSettingsCommit> {
let Some(commit) = self.update_settings_if(updates, |_, _| true).await? else {
unreachable!("unconditional settings updates must commit");
};
Ok(commit)
}
/// Evaluates the caller's synchronous predicate against the current configuration
/// and validated candidate under the same state lock used for publication.
/// This prevents a stale admission decision. Rejection has no settings or runtime
/// effects.
///
/// The predicate must be fast and side-effect-free. It must not block, acquire
/// other locks, or call back into `Session`.
async fn update_settings_if(
&self,
updates: SessionSettingsUpdate,
should_commit: impl FnOnce(&SessionConfiguration, &SessionConfiguration) -> bool + Send,
) -> ConstraintResult<Option<SessionSettingsCommit>> {
let notify_config_contributors = !self.services.extensions.config_contributors().is_empty();
let (commit, previous_config, new_config, permission_profile_changed, mcp_inputs_changed) = {
let mut state = self.state.lock().await;
@@ -1614,6 +1632,10 @@ impl Session {
}
};
if !should_commit(&state.session_configuration, &updated) {
return Ok(None);
}
let previous_config = notify_config_contributors
.then(|| self.build_effective_session_config(&state.session_configuration));
let previous_permission_profile = state.session_configuration.permission_profile();
@@ -1662,7 +1684,7 @@ impl Session {
if mcp_inputs_changed {
self.schedule_mcp_prewarm();
}
Ok(commit)
Ok(Some(commit))
}
pub(crate) async fn preview_settings(