[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
+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?;