[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
Unverified
parent d3d38159ed
commit a668379abf
29 changed files with 553 additions and 869 deletions
@@ -62,6 +62,9 @@ mod thread_processor_behavior_tests {
use codex_model_provider_info::ModelProviderInfo;
use codex_model_provider_info::WireApi;
use codex_protocol::ThreadId;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Settings;
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS;
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_READ_ONLY;
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE;
@@ -662,7 +665,16 @@ mod thread_processor_behavior_tests {
profile_workspace_roots: Vec::new(),
ephemeral: false,
reasoning_effort: None,
reasoning_summary: None,
personality: None,
collaboration_mode: CollaborationMode {
mode: ModeKind::Default,
settings: Settings {
model: "gpt-5".to_string(),
reasoning_effort: None,
developer_instructions: None,
},
},
session_source: SessionSource::Cli,
thread_source: None,
};
@@ -479,7 +479,7 @@ impl TurnRequestProcessor {
// still queued together with the input below to preserve submission order.
if has_any_overrides {
thread
.validate_thread_settings_overrides(CodexThreadSettingsOverrides {
.preview_thread_settings_overrides(CodexThreadSettingsOverrides {
cwd: cwd.clone(),
workspace_roots: runtime_workspace_roots.clone(),
approval_policy,
+15 -6
View File
@@ -63,7 +63,9 @@ pub struct ThreadConfigSnapshot {
pub profile_workspace_roots: Vec<AbsolutePathBuf>,
pub ephemeral: bool,
pub reasoning_effort: Option<ReasoningEffort>,
pub reasoning_summary: Option<ReasoningSummary>,
pub personality: Option<Personality>,
pub collaboration_mode: CollaborationMode,
pub session_source: SessionSource,
pub thread_source: Option<ThreadSource>,
}
@@ -257,11 +259,19 @@ impl CodexThread {
.await
}
/// Validate persistent thread settings overrides without committing them.
pub async fn validate_thread_settings_overrides(
/// Preview persistent thread settings overrides without committing them.
pub async fn preview_thread_settings_overrides(
&self,
overrides: CodexThreadSettingsOverrides,
) -> ConstraintResult<()> {
) -> ConstraintResult<ThreadConfigSnapshot> {
let updates = self.thread_settings_update(overrides).await;
self.codex.session.preview_settings(&updates).await
}
async fn thread_settings_update(
&self,
overrides: CodexThreadSettingsOverrides,
) -> SessionSettingsUpdate {
let CodexThreadSettingsOverrides {
cwd,
workspace_roots,
@@ -289,7 +299,7 @@ impl CodexThread {
.with_updates(model, effort, /*developer_instructions*/ None)
};
let updates = SessionSettingsUpdate {
SessionSettingsUpdate {
cwd,
workspace_roots,
profile_workspace_roots,
@@ -304,8 +314,7 @@ impl CodexThread {
service_tier,
personality,
..Default::default()
};
self.codex.session.validate_settings(&updates).await
}
}
/// Use sparingly: this is intended to be removed soon.
+120 -125
View File
@@ -42,7 +42,9 @@ use codex_protocol::protocol::ReviewRequest;
use codex_protocol::protocol::RolloutItem;
use codex_protocol::protocol::ThreadMemoryMode;
use codex_protocol::protocol::ThreadRolledBackEvent;
use codex_protocol::protocol::ThreadSettingsAppliedEvent;
use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::protocol::ThreadSettingsSnapshot;
use codex_protocol::protocol::TurnAbortReason;
use codex_protocol::protocol::WarningEvent;
use codex_protocol::request_permissions::RequestPermissionsResponse;
@@ -81,19 +83,6 @@ pub async fn realtime_conversation_list_voices(sess: &Session, sub_id: String) {
.await;
}
pub async fn override_turn_context(sess: &Session, sub_id: String, updates: SessionSettingsUpdate) {
if let Err(err) = sess.update_settings(updates).await {
sess.send_event_raw(Event {
id: sub_id,
msg: EventMsg::Error(ErrorEvent {
message: err.to_string(),
codex_error_info: Some(CodexErrorInfo::BadRequest),
}),
})
.await;
}
}
pub async fn user_input_or_turn(sess: &Arc<Session>, sub_id: String, op: Op) {
user_input_or_turn_inner(
sess,
@@ -104,36 +93,132 @@ pub async fn user_input_or_turn(sess: &Arc<Session>, sub_id: String, op: Op) {
.await;
}
pub async fn update_thread_settings(
sess: &Arc<Session>,
sub_id: String,
thread_settings: ThreadSettingsOverrides,
) {
let updates = thread_settings_update(sess, thread_settings).await;
let msg = match sess.update_settings(updates).await {
Ok(()) => thread_settings_applied_event(sess).await,
Err(err) => EventMsg::Error(ErrorEvent {
message: format!("invalid thread settings override: {err}"),
codex_error_info: Some(CodexErrorInfo::BadRequest),
}),
};
sess.send_event_raw(Event { id: sub_id, msg }).await;
}
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 = match collaboration_mode {
Some(collaboration_mode) => collaboration_mode,
None => {
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 thread_settings_applied_event(sess: &Session) -> EventMsg {
let snapshot = {
let state = sess.state.lock().await;
state.session_configuration.thread_config_snapshot()
};
EventMsg::ThreadSettingsApplied(ThreadSettingsAppliedEvent {
thread_settings: ThreadSettingsSnapshot {
model: snapshot.model,
model_provider_id: snapshot.model_provider_id,
service_tier: snapshot.service_tier,
approval_policy: snapshot.approval_policy,
approvals_reviewer: snapshot.approvals_reviewer,
permission_profile: snapshot.permission_profile,
active_permission_profile: snapshot.active_permission_profile,
cwd: snapshot.cwd,
reasoning_effort: snapshot.reasoning_effort,
reasoning_summary: snapshot.reasoning_summary,
personality: snapshot.personality,
collaboration_mode: snapshot.collaboration_mode,
},
})
}
pub(super) async fn user_input_or_turn_inner(
sess: &Arc<Session>,
sub_id: String,
op: Op,
mirror_user_text_to_realtime: Option<()>,
) {
let (items, updates, responsesapi_client_metadata) = match op {
Op::UserInput {
items,
environments,
final_output_json_schema,
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!(),
let Op::UserInput {
items,
environments,
final_output_json_schema,
responsesapi_client_metadata,
thread_settings,
} = op
else {
unreachable!();
};
let emit_thread_settings_applied = thread_settings != ThreadSettingsOverrides::default();
let mut updates = if emit_thread_settings_applied {
thread_settings_update(sess, thread_settings).await
} else {
SessionSettingsUpdate::default()
};
updates.final_output_json_schema = Some(final_output_json_schema);
updates.environments = environments;
let Ok(current_context) = sess.new_turn_with_sub_id(sub_id.clone(), updates).await else {
// new_turn_with_sub_id already emits the error event.
return;
};
if emit_thread_settings_applied {
sess.send_event_raw(Event {
id: sub_id.clone(),
msg: thread_settings_applied_event(sess).await,
})
.await;
}
sess.maybe_emit_unknown_model_warning_for_turn(current_context.as_ref())
.await;
let accepted_items = match sess
@@ -183,56 +268,6 @@ 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() {
@@ -729,54 +764,14 @@ pub(super) async fn submission_loop(
realtime_conversation_list_voices(&sess, sub.id.clone()).await;
false
}
Op::OverrideTurnContext {
cwd,
approval_policy,
approvals_reviewer,
sandbox_policy,
permission_profile,
windows_sandbox_level,
model,
effort,
summary,
service_tier,
collaboration_mode,
personality,
} => {
let collaboration_mode = if let Some(collab_mode) = collaboration_mode {
collab_mode
} else {
let state = sess.state.lock().await;
state.session_configuration.collaboration_mode.with_updates(
model.clone(),
effort,
/*developer_instructions*/ None,
)
};
override_turn_context(
&sess,
sub.id.clone(),
SessionSettingsUpdate {
cwd,
approval_policy,
approvals_reviewer,
sandbox_policy,
permission_profile,
windows_sandbox_level,
collaboration_mode: Some(collaboration_mode),
reasoning_summary: summary,
service_tier,
personality,
..Default::default()
},
)
.await;
false
}
Op::UserInput { .. } => {
user_input_or_turn(&sess, sub.id.clone(), sub.op).await;
false
}
Op::ThreadSettings { thread_settings } => {
update_thread_settings(&sess, sub.id.clone(), thread_settings).await;
false
}
Op::InterAgentCommunication { communication } => {
inter_agent_communication(&sess, sub.id.clone(), communication).await;
false
+6 -3
View File
@@ -1384,12 +1384,15 @@ impl Session {
Ok(())
}
pub(crate) async fn validate_settings(
pub(crate) async fn preview_settings(
&self,
updates: &SessionSettingsUpdate,
) -> ConstraintResult<()> {
) -> ConstraintResult<ThreadConfigSnapshot> {
let state = self.state.lock().await;
state.session_configuration.apply(updates).map(|_| ())
state
.session_configuration
.apply(updates)
.map(|configuration| configuration.thread_config_snapshot())
}
pub(crate) async fn set_session_startup_prewarm(
+2
View File
@@ -178,7 +178,9 @@ impl SessionConfiguration {
profile_workspace_roots: self.profile_workspace_roots().to_vec(),
ephemeral: self.original_config_do_not_use.ephemeral,
reasoning_effort: self.collaboration_mode.reasoning_effort(),
reasoning_summary: self.model_reasoning_summary,
personality: self.personality,
collaboration_mode: self.collaboration_mode.clone(),
session_source: self.session_source.clone(),
thread_source: self.thread_source,
}
+23 -47
View File
@@ -118,6 +118,7 @@ use codex_protocol::protocol::SkillScope;
use codex_protocol::protocol::Submission;
use codex_protocol::protocol::ThreadGoalStatus;
use codex_protocol::protocol::ThreadRolledBackEvent;
use codex_protocol::protocol::ThreadSettingsOverrides;
use codex_protocol::protocol::TokenCountEvent;
use codex_protocol::protocol::TokenUsage;
use codex_protocol::protocol::TokenUsageInfo;
@@ -2257,24 +2258,6 @@ async fn fork_startup_context_then_first_turn_diff_snapshot() -> anyhow::Result<
developer_instructions: Some("Fork turn collaboration instructions.".to_string()),
},
};
forked
.thread
.submit(Op::OverrideTurnContext {
cwd: None,
approval_policy: Some(AskForApproval::Never),
approvals_reviewer: None,
sandbox_policy: None,
permission_profile: None,
windows_sandbox_level: None,
model: None,
effort: None,
summary: None,
service_tier: None,
collaboration_mode: Some(collaboration_mode),
personality: None,
})
.await?;
forked
.thread
.submit(Op::UserInput {
@@ -2285,7 +2268,11 @@ 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(),
thread_settings: ThreadSettingsOverrides {
approval_policy: Some(AskForApproval::Never),
collaboration_mode: Some(collaboration_mode),
..Default::default()
},
})
.await?;
wait_for_event(&forked.thread, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
@@ -2338,7 +2325,7 @@ async fn record_initial_history_forked_hydrates_previous_turn_settings() {
let turn_id = previous_context_item
.turn_id
.clone()
.expect("turn context should have turn_id");
.expect("thread settings should have turn_id");
let rollout_items = vec![
RolloutItem::EventMsg(EventMsg::TurnStarted(
codex_protocol::protocol::TurnStartedEvent {
@@ -2521,14 +2508,14 @@ async fn thread_rollback_recomputes_previous_turn_settings_and_reference_context
let first_turn_id = first_context_item
.turn_id
.clone()
.expect("turn context should have turn_id");
.expect("thread settings should have turn_id");
let mut rolled_back_context_item = first_context_item.clone();
rolled_back_context_item.turn_id = Some("rolled-back-turn".to_string());
rolled_back_context_item.model = "rolled-back-model".to_string();
let rolled_back_turn_id = rolled_back_context_item
.turn_id
.clone()
.expect("turn context should have turn_id");
.expect("thread settings should have turn_id");
let turn_one_user = user_message("turn 1 user");
let turn_one_assistant = assistant_message("turn 1 assistant");
let turn_two_user = user_message("turn 2 user");
@@ -2637,7 +2624,7 @@ async fn thread_rollback_restores_cleared_reference_context_item_after_compactio
let first_turn_id = first_context_item
.turn_id
.clone()
.expect("turn context should have turn_id");
.expect("thread settings should have turn_id");
let compact_turn_id = "compact-turn".to_string();
let rolled_back_turn_id = "rolled-back-turn".to_string();
let compacted_history = vec![
@@ -4833,7 +4820,7 @@ async fn request_permissions_emits_event_when_granular_policy_allows_requests()
let (session, mut turn_context, rx) = make_session_and_context_with_rx().await;
*session.active_turn.lock().await = Some(ActiveTurn::default());
Arc::get_mut(&mut turn_context)
.expect("single turn context ref")
.expect("single thread settings ref")
.approval_policy
.set(AskForApproval::Granular(GranularApprovalConfig {
sandbox_approval: true,
@@ -4911,7 +4898,7 @@ async fn request_permissions_response_materializes_session_cwd_grants_before_rec
let (session, mut turn_context, rx) = make_session_and_context_with_rx().await;
*session.active_turn.lock().await = Some(ActiveTurn::default());
Arc::get_mut(&mut turn_context)
.expect("single turn context ref")
.expect("single thread settings ref")
.approval_policy
.set(AskForApproval::Granular(GranularApprovalConfig {
sandbox_approval: true,
@@ -5008,7 +4995,7 @@ async fn request_permissions_is_auto_denied_when_granular_policy_blocks_tool_req
let (session, mut turn_context, rx) = make_session_and_context_with_rx().await;
*session.active_turn.lock().await = Some(ActiveTurn::default());
Arc::get_mut(&mut turn_context)
.expect("single turn context ref")
.expect("single thread settings ref")
.approval_policy
.set(AskForApproval::Granular(GranularApprovalConfig {
sandbox_approval: true,
@@ -5198,24 +5185,6 @@ fn submission_dispatch_span_uses_debug_for_realtime_audio() {
#[test]
fn op_kind_for_input_and_context_ops() {
assert_eq!(
Op::OverrideTurnContext {
cwd: None,
approval_policy: None,
approvals_reviewer: None,
sandbox_policy: None,
permission_profile: None,
windows_sandbox_level: None,
model: None,
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
}
.kind(),
"override_turn_context"
);
assert_eq!(
Op::UserInput {
environments: None,
@@ -5227,6 +5196,13 @@ fn op_kind_for_input_and_context_ops() {
.kind(),
"user_input"
);
assert_eq!(
Op::ThreadSettings {
thread_settings: ThreadSettingsOverrides::default(),
}
.kind(),
"thread_settings"
);
}
#[tokio::test]
@@ -6798,7 +6774,7 @@ async fn build_initial_context_adds_multi_agent_v2_subagent_usage_hint_as_develo
.session_configuration
.session_source = session_source.clone();
Arc::get_mut(&mut turn_context)
.expect("turn context should not be shared")
.expect("thread settings should not be shared")
.session_source = session_source;
let initial_context = session.build_initial_context(turn_context.as_ref()).await;
@@ -7064,7 +7040,7 @@ fn emit_thread_start_skill_metrics_records_description_truncated_chars_without_o
#[tokio::test]
async fn build_initial_context_emits_thread_start_skill_warning_on_repeated_builds() {
let (session, turn_context, rx) = make_session_and_context_with_rx().await;
let mut turn_context = Arc::into_inner(turn_context).expect("sole turn context owner");
let mut turn_context = Arc::into_inner(turn_context).expect("sole thread settings owner");
let mut outcome = SkillLoadOutcome::default();
outcome.skills = vec![
SkillMetadata {
@@ -9890,7 +9866,7 @@ async fn rejects_escalated_permissions_when_policy_not_on_request() {
// The rejection should not poison the non-escalated path for the same
// command. Force DangerFullAccess so this check stays focused on approval
// policy rather than platform-specific sandbox behavior.
let turn_context_mut = Arc::get_mut(&mut turn_context).expect("unique turn context Arc");
let turn_context_mut = Arc::get_mut(&mut turn_context).expect("unique thread settings Arc");
turn_context_mut.permission_profile = PermissionProfile::Disabled;
let file_system_sandbox_policy = turn_context.file_system_sandbox_policy();
+1
View File
@@ -1476,6 +1476,7 @@ pub(super) fn realtime_text_for_event(msg: &EventMsg) -> Option<String> {
| EventMsg::ContextCompacted(_)
| EventMsg::ThreadRolledBack(_)
| EventMsg::TurnStarted(_)
| EventMsg::ThreadSettingsApplied(_)
| EventMsg::TurnComplete(_)
| EventMsg::TokenCount(_)
| EventMsg::UserMessage(_)
+25
View File
@@ -248,6 +248,31 @@ where
wait_for_event_with_timeout(codex, predicate, Duration::from_secs(1)).await
}
pub async fn submit_thread_settings(
codex: &CodexThread,
thread_settings: codex_protocol::protocol::ThreadSettingsOverrides,
) -> anyhow::Result<()> {
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op;
use tokio::time::Duration;
use tokio::time::timeout;
let submission_id = codex.submit(Op::ThreadSettings { thread_settings }).await?;
loop {
let ev = timeout(Duration::from_secs(10), codex.next_event())
.await
.expect("timeout waiting for thread settings update")
.expect("stream ended unexpectedly");
if ev.id == submission_id {
match ev.msg {
EventMsg::ThreadSettingsApplied(_) => return Ok(()),
EventMsg::Error(err) => panic!("thread settings update failed: {}", err.message),
other => panic!("unexpected thread settings update event: {other:?}"),
}
}
}
}
pub async fn wait_for_event_match<T, F>(codex: &CodexThread, matcher: F) -> T
where
F: Fn(&codex_protocol::protocol::EventMsg) -> Option<T>,
@@ -123,22 +123,14 @@ async fn user_input_includes_collaboration_instructions_after_override() -> Resu
let collab_text = "collab instructions";
let collaboration_mode = collab_mode_with_instructions(Some(collab_text));
test.codex
.submit(Op::OverrideTurnContext {
cwd: None,
approval_policy: None,
approvals_reviewer: None,
sandbox_policy: None,
permission_profile: None,
windows_sandbox_level: None,
model: None,
effort: None,
summary: None,
service_tier: None,
core_test_support::submit_thread_settings(
&test.codex,
codex_protocol::protocol::ThreadSettingsOverrides {
collaboration_mode: Some(collaboration_mode),
personality: None,
})
.await?;
..Default::default()
},
)
.await?;
test.codex
.submit(Op::UserInput {
@@ -277,22 +269,14 @@ async fn override_then_next_turn_uses_updated_collaboration_instructions() -> Re
let collab_text = "override instructions";
let collaboration_mode = collab_mode_with_instructions(Some(collab_text));
test.codex
.submit(Op::OverrideTurnContext {
cwd: None,
approval_policy: None,
approvals_reviewer: None,
sandbox_policy: None,
permission_profile: None,
windows_sandbox_level: None,
model: None,
effort: None,
summary: None,
service_tier: None,
core_test_support::submit_thread_settings(
&test.codex,
codex_protocol::protocol::ThreadSettingsOverrides {
collaboration_mode: Some(collaboration_mode),
personality: None,
})
.await?;
..Default::default()
},
)
.await?;
test.codex
.submit(Op::UserInput {
@@ -333,22 +317,14 @@ async fn user_turn_overrides_collaboration_instructions_after_override() -> Resu
let turn_text = "turn override";
let turn_mode = collab_mode_with_instructions(Some(turn_text));
test.codex
.submit(Op::OverrideTurnContext {
cwd: None,
approval_policy: None,
approvals_reviewer: None,
sandbox_policy: None,
permission_profile: None,
windows_sandbox_level: None,
model: None,
effort: None,
summary: None,
service_tier: None,
core_test_support::submit_thread_settings(
&test.codex,
codex_protocol::protocol::ThreadSettingsOverrides {
collaboration_mode: Some(base_mode),
personality: None,
})
.await?;
..Default::default()
},
)
.await?;
test.codex
.submit(Op::UserInput {
@@ -405,22 +381,14 @@ async fn collaboration_mode_update_emits_new_instruction_message() -> Result<()>
let first_text = "first instructions";
let second_text = "second instructions";
test.codex
.submit(Op::OverrideTurnContext {
cwd: None,
approval_policy: None,
approvals_reviewer: None,
sandbox_policy: None,
permission_profile: None,
windows_sandbox_level: None,
model: None,
effort: None,
summary: None,
service_tier: None,
core_test_support::submit_thread_settings(
&test.codex,
codex_protocol::protocol::ThreadSettingsOverrides {
collaboration_mode: Some(collab_mode_with_instructions(Some(first_text))),
personality: None,
})
.await?;
..Default::default()
},
)
.await?;
test.codex
.submit(Op::UserInput {
@@ -436,22 +404,14 @@ async fn collaboration_mode_update_emits_new_instruction_message() -> Result<()>
.await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
test.codex
.submit(Op::OverrideTurnContext {
cwd: None,
approval_policy: None,
approvals_reviewer: None,
sandbox_policy: None,
permission_profile: None,
windows_sandbox_level: None,
model: None,
effort: None,
summary: None,
service_tier: None,
core_test_support::submit_thread_settings(
&test.codex,
codex_protocol::protocol::ThreadSettingsOverrides {
collaboration_mode: Some(collab_mode_with_instructions(Some(second_text))),
personality: None,
})
.await?;
..Default::default()
},
)
.await?;
test.codex
.submit(Op::UserInput {
@@ -496,22 +456,14 @@ async fn collaboration_mode_update_noop_does_not_append() -> Result<()> {
let test = test_codex().build(&server).await?;
let collab_text = "same instructions";
test.codex
.submit(Op::OverrideTurnContext {
cwd: None,
approval_policy: None,
approvals_reviewer: None,
sandbox_policy: None,
permission_profile: None,
windows_sandbox_level: None,
model: None,
effort: None,
summary: None,
service_tier: None,
core_test_support::submit_thread_settings(
&test.codex,
codex_protocol::protocol::ThreadSettingsOverrides {
collaboration_mode: Some(collab_mode_with_instructions(Some(collab_text))),
personality: None,
})
.await?;
..Default::default()
},
)
.await?;
test.codex
.submit(Op::UserInput {
@@ -527,22 +479,14 @@ async fn collaboration_mode_update_noop_does_not_append() -> Result<()> {
.await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
test.codex
.submit(Op::OverrideTurnContext {
cwd: None,
approval_policy: None,
approvals_reviewer: None,
sandbox_policy: None,
permission_profile: None,
windows_sandbox_level: None,
model: None,
effort: None,
summary: None,
service_tier: None,
core_test_support::submit_thread_settings(
&test.codex,
codex_protocol::protocol::ThreadSettingsOverrides {
collaboration_mode: Some(collab_mode_with_instructions(Some(collab_text))),
personality: None,
})
.await?;
..Default::default()
},
)
.await?;
test.codex
.submit(Op::UserInput {
@@ -586,25 +530,17 @@ async fn collaboration_mode_update_emits_new_instruction_message_when_mode_chang
let default_text = "default mode instructions";
let plan_text = "plan mode instructions";
test.codex
.submit(Op::OverrideTurnContext {
cwd: None,
approval_policy: None,
approvals_reviewer: None,
sandbox_policy: None,
permission_profile: None,
windows_sandbox_level: None,
model: None,
effort: None,
summary: None,
service_tier: None,
core_test_support::submit_thread_settings(
&test.codex,
codex_protocol::protocol::ThreadSettingsOverrides {
collaboration_mode: Some(collab_mode_with_mode_and_instructions(
ModeKind::Default,
Some(default_text),
)),
personality: None,
})
.await?;
..Default::default()
},
)
.await?;
test.codex
.submit(Op::UserInput {
@@ -620,25 +556,17 @@ async fn collaboration_mode_update_emits_new_instruction_message_when_mode_chang
.await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
test.codex
.submit(Op::OverrideTurnContext {
cwd: None,
approval_policy: None,
approvals_reviewer: None,
sandbox_policy: None,
permission_profile: None,
windows_sandbox_level: None,
model: None,
effort: None,
summary: None,
service_tier: None,
core_test_support::submit_thread_settings(
&test.codex,
codex_protocol::protocol::ThreadSettingsOverrides {
collaboration_mode: Some(collab_mode_with_mode_and_instructions(
ModeKind::Plan,
Some(plan_text),
)),
personality: None,
})
.await?;
..Default::default()
},
)
.await?;
test.codex
.submit(Op::UserInput {
@@ -683,25 +611,17 @@ async fn collaboration_mode_update_noop_does_not_append_when_mode_is_unchanged()
let test = test_codex().build(&server).await?;
let collab_text = "mode-stable instructions";
test.codex
.submit(Op::OverrideTurnContext {
cwd: None,
approval_policy: None,
approvals_reviewer: None,
sandbox_policy: None,
permission_profile: None,
windows_sandbox_level: None,
model: None,
effort: None,
summary: None,
service_tier: None,
core_test_support::submit_thread_settings(
&test.codex,
codex_protocol::protocol::ThreadSettingsOverrides {
collaboration_mode: Some(collab_mode_with_mode_and_instructions(
ModeKind::Default,
Some(collab_text),
)),
personality: None,
})
.await?;
..Default::default()
},
)
.await?;
test.codex
.submit(Op::UserInput {
@@ -717,25 +637,17 @@ async fn collaboration_mode_update_noop_does_not_append_when_mode_is_unchanged()
.await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
test.codex
.submit(Op::OverrideTurnContext {
cwd: None,
approval_policy: None,
approvals_reviewer: None,
sandbox_policy: None,
permission_profile: None,
windows_sandbox_level: None,
model: None,
effort: None,
summary: None,
service_tier: None,
core_test_support::submit_thread_settings(
&test.codex,
codex_protocol::protocol::ThreadSettingsOverrides {
collaboration_mode: Some(collab_mode_with_mode_and_instructions(
ModeKind::Default,
Some(collab_text),
)),
personality: None,
})
.await?;
..Default::default()
},
)
.await?;
test.codex
.submit(Op::UserInput {
@@ -785,23 +697,14 @@ async fn resume_replays_collaboration_instructions() -> Result<()> {
let home = initial.home.clone();
let collab_text = "resume instructions";
initial
.codex
.submit(Op::OverrideTurnContext {
cwd: None,
approval_policy: None,
approvals_reviewer: None,
sandbox_policy: None,
permission_profile: None,
windows_sandbox_level: None,
model: None,
effort: None,
summary: None,
service_tier: None,
core_test_support::submit_thread_settings(
&initial.codex,
codex_protocol::protocol::ThreadSettingsOverrides {
collaboration_mode: Some(collab_mode_with_instructions(Some(collab_text))),
personality: None,
})
.await?;
..Default::default()
},
)
.await?;
initial
.codex
@@ -856,18 +759,9 @@ async fn empty_collaboration_instructions_are_ignored() -> Result<()> {
let test = test_codex().build(&server).await?;
let current_model = test.session_configured.model.clone();
test.codex
.submit(Op::OverrideTurnContext {
cwd: None,
approval_policy: None,
approvals_reviewer: None,
sandbox_policy: None,
permission_profile: None,
windows_sandbox_level: None,
model: None,
effort: None,
summary: None,
service_tier: None,
core_test_support::submit_thread_settings(
&test.codex,
codex_protocol::protocol::ThreadSettingsOverrides {
collaboration_mode: Some(CollaborationMode {
mode: ModeKind::Default,
settings: Settings {
@@ -876,9 +770,10 @@ async fn empty_collaboration_instructions_are_ignored() -> Result<()> {
developer_instructions: Some("".to_string()),
},
}),
personality: None,
})
.await?;
..Default::default()
},
)
.await?;
test.codex
.submit(Op::UserInput {
+8 -16
View File
@@ -3279,23 +3279,15 @@ async fn snapshot_request_shape_pre_turn_compaction_including_incoming_user_mess
.expect("submit user input");
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
}
codex
.submit(Op::OverrideTurnContext {
core_test_support::submit_thread_settings(
&codex,
codex_protocol::protocol::ThreadSettingsOverrides {
cwd: Some(PathBuf::from(PRETURN_CONTEXT_DIFF_CWD)),
approval_policy: None,
approvals_reviewer: None,
sandbox_policy: None,
permission_profile: None,
windows_sandbox_level: None,
model: None,
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.await
.expect("override turn context");
..Default::default()
},
)
.await
.expect("override thread settings");
let image_url = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR4nGNgYAAAAAMAASsJTYQAAAAASUVORK5CYII="
.to_string();
codex
+14 -30
View File
@@ -2773,22 +2773,14 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_including_incoming_us
for user in ["USER_ONE", "USER_TWO", "USER_THREE"] {
if user == "USER_THREE" {
codex
.submit(Op::OverrideTurnContext {
core_test_support::submit_thread_settings(
&codex,
codex_protocol::protocol::ThreadSettingsOverrides {
cwd: Some(PathBuf::from(PRETURN_CONTEXT_DIFF_CWD)),
approval_policy: None,
approvals_reviewer: None,
sandbox_policy: None,
permission_profile: None,
windows_sandbox_level: None,
model: None,
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.await?;
..Default::default()
},
)
.await?;
}
codex
.submit(Op::UserInput {
@@ -2891,22 +2883,14 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_strips_incoming_model
.await?;
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
codex
.submit(Op::OverrideTurnContext {
cwd: None,
approval_policy: None,
approvals_reviewer: None,
sandbox_policy: None,
permission_profile: None,
windows_sandbox_level: None,
core_test_support::submit_thread_settings(
&codex,
codex_protocol::protocol::ThreadSettingsOverrides {
model: Some(next_model.to_string()),
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.await?;
..Default::default()
},
)
.await?;
codex
.submit(Op::UserInput {
environments: None,
@@ -508,7 +508,7 @@ async fn snapshot_rollback_past_compaction_replays_append_only_history() -> Resu
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
/// Scenario: rolling back a turn that introduced persistent thread settings
/// Scenario: rolling back a turn that introduced persistent pre-thread settings
/// diffs should trim those context updates so the next request includes them
/// only once.
async fn snapshot_rollback_followup_turn_trims_context_updates() -> Result<()> {
@@ -548,18 +548,10 @@ async fn snapshot_rollback_followup_turn_trims_context_updates() -> Result<()> {
let override_cwd = config.cwd.join(PRETURN_CONTEXT_DIFF_CWD);
std::fs::create_dir_all(&override_cwd)?;
conversation
.submit(Op::OverrideTurnContext {
core_test_support::submit_thread_settings(
&conversation,
codex_protocol::protocol::ThreadSettingsOverrides {
cwd: Some(override_cwd.to_path_buf()),
approval_policy: None,
approvals_reviewer: None,
sandbox_policy: None,
permission_profile: None,
windows_sandbox_level: None,
model: None,
effort: None,
summary: None,
service_tier: None,
collaboration_mode: Some(CollaborationMode {
mode: ModeKind::Default,
settings: Settings {
@@ -568,9 +560,10 @@ async fn snapshot_rollback_followup_turn_trims_context_updates() -> Result<()> {
developer_instructions: Some(ROLLED_BACK_DEV_INSTRUCTIONS.to_string()),
},
}),
personality: None,
})
.await?;
..Default::default()
},
)
.await?;
user_turn(&conversation, TURN_TWO_USER).await;
+18 -32
View File
@@ -9,7 +9,7 @@ use pretty_assertions::assert_eq;
const CONFIG_TOML: &str = "config.toml";
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn override_turn_context_does_not_persist_when_config_exists() {
async fn thread_settings_update_does_not_persist_when_config_exists() {
let server = start_mock_server().await;
let initial_contents = "model = \"gpt-4o\"\n";
let mut builder = test_codex()
@@ -24,23 +24,16 @@ async fn override_turn_context_does_not_persist_when_config_exists() {
let codex = test.codex.clone();
let config_path = test.home.path().join(CONFIG_TOML);
codex
.submit(Op::OverrideTurnContext {
cwd: None,
approval_policy: None,
approvals_reviewer: None,
sandbox_policy: None,
permission_profile: None,
windows_sandbox_level: None,
core_test_support::submit_thread_settings(
&codex,
codex_protocol::protocol::ThreadSettingsOverrides {
model: Some("o3".to_string()),
effort: Some(Some(ReasoningEffort::High)),
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.await
.expect("submit override");
..Default::default()
},
)
.await
.expect("submit override");
codex.submit(Op::Shutdown).await.expect("request shutdown");
wait_for_event(&codex, |ev| matches!(ev, EventMsg::ShutdownComplete)).await;
@@ -52,7 +45,7 @@ async fn override_turn_context_does_not_persist_when_config_exists() {
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn override_turn_context_does_not_create_config_file() {
async fn thread_settings_update_does_not_create_config_file() {
let server = start_mock_server().await;
let mut builder = test_codex();
let test = builder.build(&server).await.expect("create conversation");
@@ -63,23 +56,16 @@ async fn override_turn_context_does_not_create_config_file() {
"test setup should start without config"
);
codex
.submit(Op::OverrideTurnContext {
cwd: None,
approval_policy: None,
approvals_reviewer: None,
sandbox_policy: None,
permission_profile: None,
windows_sandbox_level: None,
core_test_support::submit_thread_settings(
&codex,
codex_protocol::protocol::ThreadSettingsOverrides {
model: Some("o3".to_string()),
effort: Some(Some(ReasoningEffort::Medium)),
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.await
.expect("submit override");
..Default::default()
},
)
.await
.expect("submit override");
codex.submit(Op::Shutdown).await.expect("request shutdown");
wait_for_event(&codex, |ev| matches!(ev, EventMsg::ShutdownComplete)).await;
+21 -44
View File
@@ -161,22 +161,14 @@ async fn model_change_appends_model_instructions_developer_message() -> Result<(
.await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
test.codex
.submit(Op::OverrideTurnContext {
cwd: None,
approval_policy: None,
approvals_reviewer: None,
sandbox_policy: None,
permission_profile: None,
windows_sandbox_level: None,
core_test_support::submit_thread_settings(
&test.codex,
codex_protocol::protocol::ThreadSettingsOverrides {
model: Some(next_model.to_string()),
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.await?;
..Default::default()
},
)
.await?;
test.codex
.submit(read_only_user_turn(
@@ -241,22 +233,15 @@ async fn model_and_personality_change_only_appends_model_instructions() -> Resul
.await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
test.codex
.submit(Op::OverrideTurnContext {
cwd: None,
approval_policy: None,
approvals_reviewer: None,
sandbox_policy: None,
permission_profile: None,
windows_sandbox_level: None,
core_test_support::submit_thread_settings(
&test.codex,
codex_protocol::protocol::ThreadSettingsOverrides {
model: Some(next_model.to_string()),
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: Some(Personality::Pragmatic),
})
.await?;
..Default::default()
},
)
.await?;
test.codex
.submit(read_only_user_turn(
@@ -988,22 +973,14 @@ async fn model_switch_to_smaller_model_updates_token_context_window() -> Result<
);
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
test.codex
.submit(Op::OverrideTurnContext {
cwd: None,
approval_policy: None,
approvals_reviewer: None,
sandbox_policy: None,
permission_profile: None,
windows_sandbox_level: None,
core_test_support::submit_thread_settings(
&test.codex,
codex_protocol::protocol::ThreadSettingsOverrides {
model: Some(smaller_model_slug.to_string()),
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.await?;
..Default::default()
},
)
.await?;
test.codex
.submit(read_only_user_turn(
@@ -501,23 +501,15 @@ async fn snapshot_model_visible_layout_resume_override_matches_rollout_model() -
let resumed = resume_builder.resume(&server, home, rollout_path).await?;
let resume_override_cwd = resumed.cwd_path().join(PRETURN_CONTEXT_DIFF_CWD);
fs::create_dir_all(&resume_override_cwd)?;
resumed
.codex
.submit(Op::OverrideTurnContext {
core_test_support::submit_thread_settings(
&resumed.codex,
codex_protocol::protocol::ThreadSettingsOverrides {
cwd: Some(resume_override_cwd),
approval_policy: None,
approvals_reviewer: None,
sandbox_policy: None,
permission_profile: None,
windows_sandbox_level: None,
model: Some("gpt-5.2".to_string()),
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.await?;
..Default::default()
},
)
.await?;
resumed
.codex
.submit(Op::UserInput {
+25 -49
View File
@@ -24,7 +24,7 @@ fn collab_mode_with_instructions(instructions: Option<&str>) -> CollaborationMod
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn override_turn_context_without_user_turn_does_not_record_permissions_update() -> Result<()>
async fn thread_settings_update_without_user_turn_does_not_record_permissions_update() -> Result<()>
{
skip_if_no_network!(Ok(()));
@@ -34,22 +34,14 @@ async fn override_turn_context_without_user_turn_does_not_record_permissions_upd
});
let test = builder.build(&server).await?;
test.codex
.submit(Op::OverrideTurnContext {
cwd: None,
core_test_support::submit_thread_settings(
&test.codex,
codex_protocol::protocol::ThreadSettingsOverrides {
approval_policy: Some(AskForApproval::Never),
approvals_reviewer: None,
sandbox_policy: None,
permission_profile: None,
windows_sandbox_level: None,
model: None,
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.await?;
..Default::default()
},
)
.await?;
test.codex.submit(Op::Shutdown).await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::ShutdownComplete)).await;
@@ -64,7 +56,7 @@ async fn override_turn_context_without_user_turn_does_not_record_permissions_upd
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn override_turn_context_without_user_turn_does_not_record_environment_update() -> Result<()>
async fn thread_settings_update_without_user_turn_does_not_record_environment_update() -> Result<()>
{
skip_if_no_network!(Ok(()));
@@ -72,22 +64,14 @@ async fn override_turn_context_without_user_turn_does_not_record_environment_upd
let test = test_codex().build(&server).await?;
let new_cwd = TempDir::new()?;
test.codex
.submit(Op::OverrideTurnContext {
core_test_support::submit_thread_settings(
&test.codex,
codex_protocol::protocol::ThreadSettingsOverrides {
cwd: Some(new_cwd.path().to_path_buf()),
approval_policy: None,
approvals_reviewer: None,
sandbox_policy: None,
permission_profile: None,
windows_sandbox_level: None,
model: None,
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.await?;
..Default::default()
},
)
.await?;
test.codex.submit(Op::Shutdown).await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::ShutdownComplete)).await;
@@ -102,8 +86,8 @@ async fn override_turn_context_without_user_turn_does_not_record_environment_upd
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn override_turn_context_without_user_turn_does_not_record_collaboration_update() -> Result<()>
{
async fn thread_settings_update_without_user_turn_does_not_record_collaboration_update()
-> Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
@@ -111,22 +95,14 @@ async fn override_turn_context_without_user_turn_does_not_record_collaboration_u
let collab_text = "override collaboration instructions";
let collaboration_mode = collab_mode_with_instructions(Some(collab_text));
test.codex
.submit(Op::OverrideTurnContext {
cwd: None,
approval_policy: None,
approvals_reviewer: None,
sandbox_policy: None,
permission_profile: None,
windows_sandbox_level: None,
model: None,
effort: None,
summary: None,
service_tier: None,
core_test_support::submit_thread_settings(
&test.codex,
codex_protocol::protocol::ThreadSettingsOverrides {
collaboration_mode: Some(collaboration_mode),
personality: None,
})
.await?;
..Default::default()
},
)
.await?;
test.codex.submit(Op::Shutdown).await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::ShutdownComplete)).await;
@@ -103,22 +103,14 @@ async fn permissions_message_added_on_override_change() -> Result<()> {
.await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
test.codex
.submit(Op::OverrideTurnContext {
cwd: None,
core_test_support::submit_thread_settings(
&test.codex,
codex_protocol::protocol::ThreadSettingsOverrides {
approval_policy: Some(AskForApproval::Never),
approvals_reviewer: None,
sandbox_policy: None,
permission_profile: None,
windows_sandbox_level: None,
model: None,
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.await?;
..Default::default()
},
)
.await?;
test.codex
.submit(Op::UserInput {
@@ -240,22 +232,14 @@ async fn permissions_message_omitted_when_disabled() -> Result<()> {
.await?;
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
test.codex
.submit(Op::OverrideTurnContext {
cwd: None,
core_test_support::submit_thread_settings(
&test.codex,
codex_protocol::protocol::ThreadSettingsOverrides {
approval_policy: Some(AskForApproval::Never),
approvals_reviewer: None,
sandbox_policy: None,
permission_profile: None,
windows_sandbox_level: None,
model: None,
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.await?;
..Default::default()
},
)
.await?;
test.codex
.submit(Op::UserInput {
@@ -330,23 +314,14 @@ async fn resume_replays_permissions_messages() -> Result<()> {
.await?;
wait_for_event(&initial.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
initial
.codex
.submit(Op::OverrideTurnContext {
cwd: None,
core_test_support::submit_thread_settings(
&initial.codex,
codex_protocol::protocol::ThreadSettingsOverrides {
approval_policy: Some(AskForApproval::Never),
approvals_reviewer: None,
sandbox_policy: None,
permission_profile: None,
windows_sandbox_level: None,
model: None,
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.await?;
..Default::default()
},
)
.await?;
initial
.codex
@@ -439,23 +414,14 @@ async fn resume_and_fork_append_permissions_messages() -> Result<()> {
.await?;
wait_for_event(&initial.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
initial
.codex
.submit(Op::OverrideTurnContext {
cwd: None,
core_test_support::submit_thread_settings(
&initial.codex,
codex_protocol::protocol::ThreadSettingsOverrides {
approval_policy: Some(AskForApproval::Never),
approvals_reviewer: None,
sandbox_policy: None,
permission_profile: None,
windows_sandbox_level: None,
model: None,
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.await?;
..Default::default()
},
)
.await?;
initial
.codex
+28 -60
View File
@@ -333,22 +333,14 @@ async fn user_turn_personality_some_adds_update_message() -> anyhow::Result<()>
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
test.codex
.submit(Op::OverrideTurnContext {
cwd: None,
approval_policy: None,
approvals_reviewer: None,
sandbox_policy: None,
permission_profile: None,
windows_sandbox_level: None,
model: None,
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
core_test_support::submit_thread_settings(
&test.codex,
codex_protocol::protocol::ThreadSettingsOverrides {
personality: Some(Personality::Friendly),
})
.await?;
..Default::default()
},
)
.await?;
test.codex
.submit(read_only_text_turn(
@@ -417,22 +409,14 @@ async fn user_turn_personality_same_value_does_not_add_update_message() -> anyho
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
test.codex
.submit(Op::OverrideTurnContext {
cwd: None,
approval_policy: None,
approvals_reviewer: None,
sandbox_policy: None,
permission_profile: None,
windows_sandbox_level: None,
model: None,
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
core_test_support::submit_thread_settings(
&test.codex,
codex_protocol::protocol::ThreadSettingsOverrides {
personality: Some(Personality::Pragmatic),
})
.await?;
..Default::default()
},
)
.await?;
test.codex
.submit(read_only_text_turn(
@@ -514,22 +498,14 @@ async fn user_turn_personality_skips_if_feature_disabled() -> anyhow::Result<()>
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
test.codex
.submit(Op::OverrideTurnContext {
cwd: None,
approval_policy: None,
approvals_reviewer: None,
sandbox_policy: None,
permission_profile: None,
windows_sandbox_level: None,
model: None,
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
core_test_support::submit_thread_settings(
&test.codex,
codex_protocol::protocol::ThreadSettingsOverrides {
personality: Some(Personality::Pragmatic),
})
.await?;
..Default::default()
},
)
.await?;
test.codex
.submit(read_only_text_turn(
@@ -763,22 +739,14 @@ async fn user_turn_personality_remote_model_template_includes_update_message() -
wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
test.codex
.submit(Op::OverrideTurnContext {
cwd: None,
approval_policy: None,
approvals_reviewer: None,
sandbox_policy: None,
permission_profile: None,
windows_sandbox_level: None,
model: None,
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
core_test_support::submit_thread_settings(
&test.codex,
codex_protocol::protocol::ThreadSettingsOverrides {
personality: Some(Personality::Friendly),
})
.await?;
..Default::default()
},
)
.await?;
test.codex
.submit(read_only_text_turn(
+15 -24
View File
@@ -442,22 +442,18 @@ async fn overrides_turn_context_but_keeps_cached_prefix_and_key_constant() -> an
let sandbox_policy = permission_profile
.to_legacy_sandbox_policy(config.cwd.as_path())
.expect("workspace profile should have legacy projection");
codex
.submit(Op::OverrideTurnContext {
cwd: None,
core_test_support::submit_thread_settings(
&codex,
codex_protocol::protocol::ThreadSettingsOverrides {
approval_policy: Some(AskForApproval::Never),
approvals_reviewer: None,
sandbox_policy: Some(sandbox_policy),
permission_profile: Some(permission_profile),
windows_sandbox_level: None,
model: None,
effort: Some(Some(ReasoningEffort::High)),
summary: Some(ReasoningSummary::Detailed),
service_tier: None,
collaboration_mode: None,
personality: None,
})
.await?;
..Default::default()
},
)
.await?;
// Second turn after overrides
codex
@@ -493,7 +489,7 @@ async fn overrides_turn_context_but_keeps_cached_prefix_and_key_constant() -> an
});
let expected_permissions_msg = body1["input"][0].clone();
let body1_input = body1["input"].as_array().expect("input array");
// After overriding thread settings, emit one updated permissions message.
// After overriding the thread settings, emit one updated permissions message.
let expected_permissions_msg_2 = body2["input"][body1_input.len()].clone();
assert_ne!(
expected_permissions_msg_2, expected_permissions_msg,
@@ -529,22 +525,17 @@ async fn override_before_first_turn_emits_environment_context() -> anyhow::Resul
},
};
codex
.submit(Op::OverrideTurnContext {
cwd: None,
core_test_support::submit_thread_settings(
&codex,
codex_protocol::protocol::ThreadSettingsOverrides {
approval_policy: Some(AskForApproval::Never),
approvals_reviewer: None,
sandbox_policy: None,
permission_profile: None,
windows_sandbox_level: None,
model: Some("gpt-5.4".to_string()),
effort: Some(Some(ReasoningEffort::Low)),
summary: None,
service_tier: None,
collaboration_mode: Some(collaboration_mode),
personality: None,
})
.await?;
..Default::default()
},
)
.await?;
codex
.submit(Op::UserInput {
+14 -30
View File
@@ -534,22 +534,14 @@ async fn remote_models_remote_model_uses_unified_exec() -> Result<()> {
.await;
assert_eq!(model_info.shell_type, ConfigShellToolType::UnifiedExec);
codex
.submit(Op::OverrideTurnContext {
cwd: None,
approval_policy: None,
approvals_reviewer: None,
sandbox_policy: None,
permission_profile: None,
windows_sandbox_level: None,
core_test_support::submit_thread_settings(
&codex,
codex_protocol::protocol::ThreadSettingsOverrides {
model: Some(REMOTE_MODEL_SLUG.to_string()),
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.await?;
..Default::default()
},
)
.await?;
let call_id = "call";
let args = json!({
@@ -783,22 +775,14 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> {
let models_manager = thread_manager.get_models_manager();
wait_for_model_available(&models_manager, model).await;
codex
.submit(Op::OverrideTurnContext {
cwd: None,
approval_policy: None,
approvals_reviewer: None,
sandbox_policy: None,
permission_profile: None,
windows_sandbox_level: None,
core_test_support::submit_thread_settings(
&codex,
codex_protocol::protocol::ThreadSettingsOverrides {
model: Some(model.to_string()),
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.await?;
..Default::default()
},
)
.await?;
let cwd_path = cwd.path().to_path_buf();
let (sandbox_policy, permission_profile) =
+7 -16
View File
@@ -423,23 +423,14 @@ async fn resume_model_switch_is_not_duplicated_after_pre_turn_override() -> Resu
config.model = Some("gpt-5.3-codex".to_string());
});
let resumed = resume_builder.resume(&server, home, rollout_path).await?;
resumed
.codex
.submit(Op::OverrideTurnContext {
cwd: None,
approval_policy: None,
approvals_reviewer: None,
sandbox_policy: None,
permission_profile: None,
windows_sandbox_level: None,
core_test_support::submit_thread_settings(
&resumed.codex,
codex_protocol::protocol::ThreadSettingsOverrides {
model: Some("gpt-5.4".to_string()),
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.await?;
..Default::default()
},
)
.await?;
resumed
.codex
.submit(Op::UserInput {
+8 -16
View File
@@ -789,23 +789,15 @@ async fn review_uses_overridden_cwd_for_base_branch_merge_base() {
})
.await;
codex
.submit(Op::OverrideTurnContext {
core_test_support::submit_thread_settings(
&codex,
codex_protocol::protocol::ThreadSettingsOverrides {
cwd: Some(repo_path.to_path_buf()),
approval_policy: None,
approvals_reviewer: None,
sandbox_policy: None,
permission_profile: None,
windows_sandbox_level: None,
model: None,
effort: None,
summary: None,
service_tier: None,
collaboration_mode: None,
personality: None,
})
.await
.unwrap();
..Default::default()
},
)
.await
.unwrap();
codex
.submit(Op::Review {
+1 -1
View File
@@ -70,7 +70,7 @@ For complete documentation of the `Op` and `EventMsg` variants, refer to [protoc
- `Op::Interrupt` Interrupts a running turn
- `Op::ExecApproval` Approve or deny code execution
- `Op::UserInputAnswer` Provide answers for a `request_user_input` tool call
- `Op::UserTurn` and `Op::OverrideTurnContext` accept an optional `personality` override that updates the models communication style
- `Op::UserInput` accepts an optional `personality` turn-context override that updates the models communication style
Valid `personality` values are `friendly`, `pragmatic`, and `none`. When `none` is selected, the personality placeholder is replaced with an empty string.
@@ -333,6 +333,7 @@ async fn run_codex_tool_session_inner(
}
EventMsg::AgentReasoningRawContent(_)
| EventMsg::TurnStarted(_)
| EventMsg::ThreadSettingsApplied(_)
| EventMsg::TokenCount(_)
| EventMsg::AgentReasoning(_)
| EventMsg::AgentReasoningSectionBreak(_)
+7 -15
View File
@@ -242,22 +242,14 @@ async fn memories_startup_phase1_uses_live_thread_service_tier() -> anyhow::Resu
let test = build_test_codex(&server, home).await?;
assert_eq!(test.config.service_tier, None);
test.codex
.submit(Op::OverrideTurnContext {
cwd: None,
approval_policy: None,
approvals_reviewer: None,
sandbox_policy: None,
permission_profile: None,
windows_sandbox_level: None,
model: None,
effort: None,
summary: None,
core_test_support::submit_thread_settings(
&test.codex,
codex_protocol::protocol::ThreadSettingsOverrides {
service_tier: Some(Some(ServiceTier::Fast.request_value().to_string())),
collaboration_mode: None,
personality: None,
})
.await?;
..Default::default()
},
)
.await?;
let config_snapshot =
wait_for_service_tier(&test, Some(ServiceTier::Fast.request_value().to_string())).await?;
+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")]
@@ -228,6 +228,7 @@ pub(crate) fn tool_runtime_trace_event(event: &EventMsg) -> Option<ToolRuntimeTr
| EventMsg::ThreadRolledBack(_)
| EventMsg::ThreadGoalUpdated(_)
| EventMsg::TurnStarted(_)
| EventMsg::ThreadSettingsApplied(_)
| EventMsg::TurnComplete(_)
| EventMsg::TokenCount(_)
| EventMsg::AgentMessage(_)
@@ -297,6 +298,7 @@ pub(crate) fn wrapped_protocol_event_type(event: &EventMsg) -> Option<&'static s
| EventMsg::ModelReroute(_)
| EventMsg::ModelVerification(_)
| EventMsg::ContextCompacted(_)
| EventMsg::ThreadSettingsApplied(_)
| EventMsg::TokenCount(_)
| EventMsg::AgentMessage(_)
| EventMsg::UserMessage(_)
+1
View File
@@ -183,6 +183,7 @@ fn event_msg_persistence_mode(ev: &EventMsg) -> Option<EventPersistenceMode> {
| EventMsg::AgentReasoningSectionBreak(_)
| EventMsg::RawResponseItem(_)
| EventMsg::SessionConfigured(_)
| EventMsg::ThreadSettingsApplied(_)
| EventMsg::McpToolCallBegin(_)
| EventMsg::ExecCommandBegin(_)
| EventMsg::TerminalInteraction(_)