[5 of 7] Replace OverrideTurnContext with ThreadSettings (#22508)

**Stack position:** [5 of 7]

## Summary

This PR adds `Op::ThreadSettings`, a queued settings-only update
mechanism for changing stored thread settings without starting a new
turn. It also removes the legacy `Op::OverrideTurnContext` in the same
layer, so reviewers can see the replacement and deletion together.

## Changes

- Add `Op::ThreadSettings` for settings-only queued updates.
- Emit `ThreadSettingsApplied` with the effective thread settings
snapshot after core applies an update.
- Route settings-only updates through the same submission queue as user
input.
- Migrate remaining `OverrideTurnContext` tests and callers to the
queued `Op::ThreadSettings` path.
- Delete `Op::OverrideTurnContext` from the core protocol and submission
loop.

This stack addresses #20656 and #22090.

## Stack

1. [1 of 7] [Add thread settings to
UserInput](https://github.com/openai/codex/pull/23080)
2. [2 of 7] [Remove
UserInputWithTurnContext](https://github.com/openai/codex/pull/23081)
3. [3 of 7] [Remove
UserTurn](https://github.com/openai/codex/pull/23075)
4. [4 of 7] [Placeholder for OverrideTurnContext
cleanup](https://github.com/openai/codex/pull/23087)
5. [5 of 7] [Replace OverrideTurnContext with
ThreadSettings](https://github.com/openai/codex/pull/22508) (this PR)
6. [6 of 7] [Add app-server thread settings
API](https://github.com/openai/codex/pull/22509)
7. [7 of 7] [Sync TUI thread
settings](https://github.com/openai/codex/pull/22510)
This commit is contained in:
Eric Traut
2026-05-18 21:03:51 -07:00
committed by GitHub
parent d3d38159ed
commit a668379abf
29 changed files with 553 additions and 869 deletions
+44 -66
View File
@@ -396,7 +396,8 @@ pub struct ConversationTextParams {
pub text: String,
}
/// Persistent thread-settings overrides that can be applied before user input.
/// Persistent thread-settings overrides that can be applied before user input or
/// on their own.
#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq, JsonSchema)]
pub struct ThreadSettingsOverrides {
/// Updated `cwd` for sandbox/tool calls.
@@ -518,76 +519,22 @@ pub enum Op {
thread_settings: ThreadSettingsOverrides,
},
/// Apply persistent thread-settings overrides without starting a turn.
///
/// This uses the same submission queue as turn starts so app-server can
/// preserve caller order between both kinds of mutation.
ThreadSettings {
/// Persistent thread-settings overrides to apply.
#[serde(flatten)]
thread_settings: ThreadSettingsOverrides,
},
/// Inter-agent communication that should be recorded as assistant history
/// while still using the normal thread submission lifecycle.
InterAgentCommunication {
communication: InterAgentCommunication,
},
/// Override parts of the persistent thread settings for subsequent turns.
///
/// All fields are optional; when omitted, the existing value is preserved.
/// This does not enqueue any input it only updates defaults used for
/// turns that rely on persistent session-level settings (for example,
/// [`Op::UserInput`]).
OverrideTurnContext {
/// Updated `cwd` for sandbox/tool calls.
#[serde(skip_serializing_if = "Option::is_none")]
cwd: Option<PathBuf>,
/// Updated command approval policy.
#[serde(skip_serializing_if = "Option::is_none")]
approval_policy: Option<AskForApproval>,
/// Updated approval reviewer for future approval prompts.
#[serde(skip_serializing_if = "Option::is_none")]
approvals_reviewer: Option<ApprovalsReviewer>,
/// Updated sandbox policy for tool calls.
#[serde(skip_serializing_if = "Option::is_none")]
sandbox_policy: Option<SandboxPolicy>,
/// Updated permissions profile for tool calls.
#[serde(skip_serializing_if = "Option::is_none")]
permission_profile: Option<PermissionProfile>,
/// Updated Windows sandbox mode for tool execution.
#[serde(skip_serializing_if = "Option::is_none")]
windows_sandbox_level: Option<WindowsSandboxLevel>,
/// Updated model slug. When set, the model info is derived
/// automatically.
#[serde(skip_serializing_if = "Option::is_none")]
model: Option<String>,
/// Updated reasoning effort (honored only for reasoning-capable models).
///
/// Use `Some(Some(_))` to set a specific effort, `Some(None)` to clear
/// the effort, or `None` to leave the existing value unchanged.
#[serde(skip_serializing_if = "Option::is_none")]
effort: Option<Option<ReasoningEffortConfig>>,
/// Updated reasoning summary preference (honored only for reasoning-capable models).
#[serde(skip_serializing_if = "Option::is_none")]
summary: Option<ReasoningSummaryConfig>,
/// Updated service tier preference for future turns.
///
/// Use `Some(Some(_))` to set a specific tier, `Some(None)` to clear the
/// preference, or `None` to leave the existing value unchanged.
#[serde(skip_serializing_if = "Option::is_none")]
service_tier: Option<Option<String>>,
/// EXPERIMENTAL - set a pre-set collaboration mode.
/// Takes precedence over model, effort, and developer instructions if set.
#[serde(skip_serializing_if = "Option::is_none")]
collaboration_mode: Option<CollaborationMode>,
/// Updated personality preference.
#[serde(skip_serializing_if = "Option::is_none")]
personality: Option<Personality>,
},
/// Approve a command execution
ExecApproval {
/// The id of the submission we are approving
@@ -775,8 +722,8 @@ impl Op {
Self::RealtimeConversationClose => "realtime_conversation_close",
Self::RealtimeConversationListVoices => "realtime_conversation_list_voices",
Self::UserInput { .. } => "user_input",
Self::ThreadSettings { .. } => "thread_settings",
Self::InterAgentCommunication { .. } => "inter_agent_communication",
Self::OverrideTurnContext { .. } => "override_turn_context",
Self::ExecApproval { .. } => "exec_approval",
Self::PatchApproval { .. } => "patch_approval",
Self::ResolveElicitation { .. } => "resolve_elicitation",
@@ -1227,6 +1174,10 @@ pub enum EventMsg {
#[serde(rename = "task_started", alias = "turn_started")]
TurnStarted(TurnStartedEvent),
/// Persistent thread-settings overrides from the correlated submission have
/// been applied to the session configuration.
ThreadSettingsApplied(ThreadSettingsAppliedEvent),
/// Agent has completed all actions.
/// v1 wire format uses `task_complete`; accept `turn_complete` for v2 interop.
#[serde(rename = "task_complete", alias = "turn_complete")]
@@ -1907,6 +1858,33 @@ pub struct TurnStartedEvent {
pub collaboration_mode_kind: ModeKind,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct ThreadSettingsAppliedEvent {
pub thread_settings: ThreadSettingsSnapshot,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
pub struct ThreadSettingsSnapshot {
pub model: String,
pub model_provider_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub service_tier: Option<String>,
pub approval_policy: AskForApproval,
pub approvals_reviewer: ApprovalsReviewer,
pub permission_profile: PermissionProfile,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub active_permission_profile: Option<ActivePermissionProfile>,
pub cwd: AbsolutePathBuf,
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning_effort: Option<ReasoningEffortConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reasoning_summary: Option<ReasoningSummaryConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub personality: Option<Personality>,
pub collaboration_mode: CollaborationMode,
}
#[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq, Eq, JsonSchema, TS)]
pub struct TokenUsage {
#[ts(type = "number")]