[1 of 7] Add thread settings to UserInput (#23080)

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

## Summary

The first three PRs in this stack are a cleanup pass before the actual
thread settings API work.

Today, core has several overlapping "user input" ops: `UserInput`,
`UserInputWithTurnContext`, and `UserTurn`. They differ mostly in how
much next-turn state they carry, which makes the later queued thread
settings update harder to reason about and review.

This PR starts that cleanup by adding the shared
`ThreadSettingsOverrides` payload and allowing `Op::UserInput` to carry
it. Existing variants remain in place here, so this layer is mostly a
behavior-preserving API shape change plus mechanical constructor
updates.

## End State After PR3

By the end of PR3, `Op::UserInput` is the only "user input" core op. It
can carry optional thread settings overrides for callers that need to
update stored defaults with a turn, while callers without updates use
empty settings. `Op::UserInputWithTurnContext` and `Op::UserTurn` are
deleted.

## End State After PR5

By the end of PR5, core will have only two ops for this area:

- `Op::UserInput` for user-input-bearing submissions.
- `Op::ThreadSettings` for settings-only updates.

## Stack

1. [1 of 7] [Add thread settings to
UserInput](https://github.com/openai/codex/pull/23080) (this PR)
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)
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 18:48:35 -07:00
committed by GitHub
parent daa11820b0
commit 84d941d07f
43 changed files with 429 additions and 52 deletions
+3
View File
@@ -442,6 +442,7 @@ async fn send_input_submits_user_message() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
thread_settings: Default::default(),
},
);
let captured = harness
@@ -595,6 +596,7 @@ async fn spawn_agent_creates_thread_and_sends_prompt() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
thread_settings: Default::default(),
},
);
let captured = harness
@@ -743,6 +745,7 @@ async fn spawn_agent_can_fork_parent_thread_history_with_sanitized_items() {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
thread_settings: Default::default(),
},
);
let captured = harness
+1
View File
@@ -192,6 +192,7 @@ pub(crate) async fn run_codex_thread_one_shot(
items: input,
final_output_json_schema,
responsesapi_client_metadata: None,
thread_settings: Default::default(),
})
.await?;
+6 -6
View File
@@ -80,9 +80,9 @@ impl ThreadConfigSnapshot {
}
}
/// Turn context overrides that app-server validates before starting a turn.
/// Thread settings overrides that app-server validates before starting a turn.
#[derive(Clone, Default)]
pub struct CodexThreadTurnContextOverrides {
pub struct CodexThreadSettingsOverrides {
pub cwd: Option<PathBuf>,
pub workspace_roots: Option<Vec<AbsolutePathBuf>>,
pub profile_workspace_roots: Option<Vec<AbsolutePathBuf>>,
@@ -257,12 +257,12 @@ impl CodexThread {
.await
}
/// Validate persistent turn context overrides without committing them.
pub async fn validate_turn_context_overrides(
/// Validate persistent thread settings overrides without committing them.
pub async fn validate_thread_settings_overrides(
&self,
overrides: CodexThreadTurnContextOverrides,
overrides: CodexThreadSettingsOverrides,
) -> ConstraintResult<()> {
let CodexThreadTurnContextOverrides {
let CodexThreadSettingsOverrides {
cwd,
workspace_roots,
profile_workspace_roots,
+1 -1
View File
@@ -20,7 +20,7 @@ mod compact_remote;
mod compact_remote_v2;
mod config_lock;
pub use codex_thread::CodexThread;
pub use codex_thread::CodexThreadTurnContextOverrides;
pub use codex_thread::CodexThreadSettingsOverrides;
pub use codex_thread::ThreadConfigSnapshot;
pub use session::turn_context::TurnContext;
mod agent;
+73 -30
View File
@@ -42,6 +42,7 @@ use codex_protocol::protocol::ReviewRequest;
use codex_protocol::protocol::RolloutItem;
use codex_protocol::protocol::ThreadMemoryMode;
use codex_protocol::protocol::ThreadRolledBackEvent;
use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::protocol::TurnAbortReason;
use codex_protocol::protocol::WarningEvent;
use codex_protocol::request_permissions::RequestPermissionsResponse;
@@ -184,20 +185,9 @@ pub(super) async fn user_input_or_turn_inner(
personality,
environments,
} => {
let collaboration_mode = if let Some(collab_mode) = collaboration_mode {
Some(collab_mode)
} else {
let state = sess.state.lock().await;
Some(
state
.session_configuration
.collaboration_mode
.with_updates(model, effort, /*developer_instructions*/ None),
)
};
(
items,
SessionSettingsUpdate {
let mut updates = thread_settings_update(
sess,
ThreadSettingsOverrides {
cwd,
workspace_roots,
profile_workspace_roots,
@@ -207,32 +197,35 @@ pub(super) async fn user_input_or_turn_inner(
permission_profile,
active_permission_profile,
windows_sandbox_level,
collaboration_mode,
reasoning_summary: summary,
model,
effort,
summary,
service_tier,
final_output_json_schema: Some(final_output_json_schema),
environments,
collaboration_mode,
personality,
app_server_client_name: None,
app_server_client_version: None,
},
responsesapi_client_metadata,
)
.await;
updates.final_output_json_schema = Some(final_output_json_schema);
updates.environments = environments;
(items, updates, responsesapi_client_metadata)
}
Op::UserInput {
items,
environments,
final_output_json_schema,
responsesapi_client_metadata,
} => (
items,
SessionSettingsUpdate {
final_output_json_schema: Some(final_output_json_schema),
environments,
..Default::default()
},
responsesapi_client_metadata,
),
thread_settings,
} => {
let mut updates = if thread_settings == ThreadSettingsOverrides::default() {
SessionSettingsUpdate::default()
} else {
thread_settings_update(sess, thread_settings).await
};
updates.final_output_json_schema = Some(final_output_json_schema);
updates.environments = environments;
(items, updates, responsesapi_client_metadata)
}
_ => unreachable!(),
};
@@ -289,6 +282,56 @@ pub(super) async fn user_input_or_turn_inner(
}
}
async fn thread_settings_update(
sess: &Session,
thread_settings: ThreadSettingsOverrides,
) -> SessionSettingsUpdate {
let ThreadSettingsOverrides {
cwd,
workspace_roots,
profile_workspace_roots,
approval_policy,
approvals_reviewer,
sandbox_policy,
permission_profile,
active_permission_profile,
windows_sandbox_level,
model,
effort,
summary,
service_tier,
collaboration_mode,
personality,
} = thread_settings;
let collaboration_mode = if let Some(collaboration_mode) = collaboration_mode {
collaboration_mode
} else {
let state = sess.state.lock().await;
// Model and reasoning effort live in CollaborationMode settings today, so
// partial thread-settings updates refresh those fields on the active mode.
state
.session_configuration
.collaboration_mode
.with_updates(model, effort, /*developer_instructions*/ None)
};
SessionSettingsUpdate {
cwd,
workspace_roots,
profile_workspace_roots,
approval_policy,
approvals_reviewer,
sandbox_policy,
permission_profile,
active_permission_profile,
windows_sandbox_level,
collaboration_mode: Some(collaboration_mode),
reasoning_summary: summary,
service_tier,
personality,
..Default::default()
}
}
async fn mirror_user_text_to_realtime(sess: &Arc<Session>, items: &[UserInput]) {
let text = UserMessageItem::new(items).message();
if text.is_empty() {
+1
View File
@@ -1094,6 +1094,7 @@ impl Session {
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
thread_settings: Default::default(),
},
/*mirror_user_text_to_realtime*/ None,
)
+6
View File
@@ -2221,6 +2221,7 @@ async fn fork_startup_context_then_first_turn_diff_snapshot() -> anyhow::Result<
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
thread_settings: Default::default(),
})
.await?;
wait_for_event(&initial.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -2284,6 +2285,7 @@ async fn fork_startup_context_then_first_turn_diff_snapshot() -> anyhow::Result<
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
thread_settings: Default::default(),
})
.await?;
wait_for_event(&forked.thread, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -5220,6 +5222,7 @@ fn op_kind_distinguishes_turn_ops() {
items: vec![],
final_output_json_schema: None,
responsesapi_client_metadata: None,
thread_settings: Default::default(),
}
.kind(),
"user_input"
@@ -8369,6 +8372,7 @@ async fn active_goal_continuation_runs_again_after_no_tool_turn() -> anyhow::Res
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
thread_settings: Default::default(),
})
.await?;
@@ -8473,6 +8477,7 @@ async fn pending_request_user_input_does_not_spawn_extra_goal_continuation() ->
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
thread_settings: Default::default(),
})
.await?;
@@ -8957,6 +8962,7 @@ async fn completed_goal_accounts_current_turn_tokens_before_tool_response() -> a
}],
final_output_json_schema: None,
responsesapi_client_metadata: None,
thread_settings: Default::default(),
})
.await?;
@@ -2470,6 +2470,7 @@ async fn send_input_accepts_structured_items() {
],
final_output_json_schema: None,
responsesapi_client_metadata: None,
thread_settings: Default::default(),
};
let captured = manager
.captured_ops()