From 875b8467214ee2b84c8c743d141f095f9eacae5c Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Thu, 14 May 2026 12:43:40 -0700 Subject: [PATCH 1/2] Remove core OverrideTurnContext op --- codex-rs/core/src/session/handlers.rs | 57 ---------------- codex-rs/core/src/session/tests.rs | 42 ++---------- .../tests/suite/collaboration_instructions.rs | 39 +++++++---- codex-rs/core/tests/suite/compact.rs | 3 +- codex-rs/core/tests/suite/compact_remote.rs | 6 +- .../core/tests/suite/compact_resume_fork.rs | 3 +- codex-rs/core/tests/suite/model_overrides.rs | 10 +-- codex-rs/core/tests/suite/model_switching.rs | 9 ++- .../core/tests/suite/model_visible_layout.rs | 3 +- codex-rs/core/tests/suite/override_updates.rs | 17 ++--- .../core/tests/suite/permissions_messages.rs | 12 ++-- codex-rs/core/tests/suite/personality.rs | 12 ++-- codex-rs/core/tests/suite/prompt_caching.rs | 6 +- codex-rs/core/tests/suite/remote_models.rs | 6 +- codex-rs/core/tests/suite/resume.rs | 3 +- codex-rs/core/tests/suite/review.rs | 3 +- codex-rs/docs/protocol_v1.md | 2 +- codex-rs/memories/write/src/startup_tests.rs | 3 +- codex-rs/protocol/src/protocol.rs | 65 ------------------- 19 files changed, 93 insertions(+), 208 deletions(-) diff --git a/codex-rs/core/src/session/handlers.rs b/codex-rs/core/src/session/handlers.rs index 33d4ead581..e2f850d13f 100644 --- a/codex-rs/core/src/session/handlers.rs +++ b/codex-rs/core/src/session/handlers.rs @@ -81,19 +81,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, sub_id: String, op: Op) { user_input_or_turn_inner( sess, @@ -723,50 +710,6 @@ 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 diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index 2c03e75a80..c906ed0f29 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -2256,24 +2256,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 { @@ -2284,7 +2266,11 @@ async fn fork_startup_context_then_first_turn_diff_snapshot() -> anyhow::Result< }], final_output_json_schema: None, responsesapi_client_metadata: None, - turn_context: Default::default(), + turn_context: TurnContextOverrides { + approval_policy: Some(AskForApproval::Never), + collaboration_mode: Some(collaboration_mode), + ..Default::default() + }, }) .await?; wait_for_event(&forked.thread, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; @@ -5206,24 +5192,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, diff --git a/codex-rs/core/tests/suite/collaboration_instructions.rs b/codex-rs/core/tests/suite/collaboration_instructions.rs index 727f5d135e..a18959a393 100644 --- a/codex-rs/core/tests/suite/collaboration_instructions.rs +++ b/codex-rs/core/tests/suite/collaboration_instructions.rs @@ -124,12 +124,13 @@ 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 { + .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { cwd: None, approval_policy: None, approvals_reviewer: None, sandbox_policy: None, permission_profile: None, + active_permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -286,12 +287,13 @@ async fn override_then_next_turn_uses_updated_collaboration_instructions() -> Re let collaboration_mode = collab_mode_with_instructions(Some(collab_text)); test.codex - .submit(Op::OverrideTurnContext { + .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { cwd: None, approval_policy: None, approvals_reviewer: None, sandbox_policy: None, permission_profile: None, + active_permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -342,12 +344,13 @@ async fn user_turn_overrides_collaboration_instructions_after_override() -> Resu let turn_mode = collab_mode_with_instructions(Some(turn_text)); test.codex - .submit(Op::OverrideTurnContext { + .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { cwd: None, approval_policy: None, approvals_reviewer: None, sandbox_policy: None, permission_profile: None, + active_permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -418,12 +421,13 @@ async fn collaboration_mode_update_emits_new_instruction_message() -> Result<()> let second_text = "second instructions"; test.codex - .submit(Op::OverrideTurnContext { + .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { cwd: None, approval_policy: None, approvals_reviewer: None, sandbox_policy: None, permission_profile: None, + active_permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -449,12 +453,13 @@ async fn collaboration_mode_update_emits_new_instruction_message() -> Result<()> wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; test.codex - .submit(Op::OverrideTurnContext { + .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { cwd: None, approval_policy: None, approvals_reviewer: None, sandbox_policy: None, permission_profile: None, + active_permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -509,12 +514,13 @@ async fn collaboration_mode_update_noop_does_not_append() -> Result<()> { let collab_text = "same instructions"; test.codex - .submit(Op::OverrideTurnContext { + .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { cwd: None, approval_policy: None, approvals_reviewer: None, sandbox_policy: None, permission_profile: None, + active_permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -540,12 +546,13 @@ async fn collaboration_mode_update_noop_does_not_append() -> Result<()> { wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; test.codex - .submit(Op::OverrideTurnContext { + .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { cwd: None, approval_policy: None, approvals_reviewer: None, sandbox_policy: None, permission_profile: None, + active_permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -599,12 +606,13 @@ async fn collaboration_mode_update_emits_new_instruction_message_when_mode_chang let plan_text = "plan mode instructions"; test.codex - .submit(Op::OverrideTurnContext { + .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { cwd: None, approval_policy: None, approvals_reviewer: None, sandbox_policy: None, permission_profile: None, + active_permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -633,12 +641,13 @@ async fn collaboration_mode_update_emits_new_instruction_message_when_mode_chang wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; test.codex - .submit(Op::OverrideTurnContext { + .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { cwd: None, approval_policy: None, approvals_reviewer: None, sandbox_policy: None, permission_profile: None, + active_permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -696,12 +705,13 @@ async fn collaboration_mode_update_noop_does_not_append_when_mode_is_unchanged() let collab_text = "mode-stable instructions"; test.codex - .submit(Op::OverrideTurnContext { + .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { cwd: None, approval_policy: None, approvals_reviewer: None, sandbox_policy: None, permission_profile: None, + active_permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -730,12 +740,13 @@ async fn collaboration_mode_update_noop_does_not_append_when_mode_is_unchanged() wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; test.codex - .submit(Op::OverrideTurnContext { + .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { cwd: None, approval_policy: None, approvals_reviewer: None, sandbox_policy: None, permission_profile: None, + active_permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -799,12 +810,13 @@ async fn resume_replays_collaboration_instructions() -> Result<()> { let collab_text = "resume instructions"; initial .codex - .submit(Op::OverrideTurnContext { + .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { cwd: None, approval_policy: None, approvals_reviewer: None, sandbox_policy: None, permission_profile: None, + active_permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -869,12 +881,13 @@ async fn empty_collaboration_instructions_are_ignored() -> Result<()> { let current_model = test.session_configured.model.clone(); test.codex - .submit(Op::OverrideTurnContext { + .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { cwd: None, approval_policy: None, approvals_reviewer: None, sandbox_policy: None, permission_profile: None, + active_permission_profile: None, windows_sandbox_level: None, model: None, effort: None, diff --git a/codex-rs/core/tests/suite/compact.rs b/codex-rs/core/tests/suite/compact.rs index 7fa5cdf40e..08f6eef832 100644 --- a/codex-rs/core/tests/suite/compact.rs +++ b/codex-rs/core/tests/suite/compact.rs @@ -3284,12 +3284,13 @@ async fn snapshot_request_shape_pre_turn_compaction_including_incoming_user_mess wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; } codex - .submit(Op::OverrideTurnContext { + .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { cwd: Some(PathBuf::from(PRETURN_CONTEXT_DIFF_CWD)), approval_policy: None, approvals_reviewer: None, sandbox_policy: None, permission_profile: None, + active_permission_profile: None, windows_sandbox_level: None, model: None, effort: None, diff --git a/codex-rs/core/tests/suite/compact_remote.rs b/codex-rs/core/tests/suite/compact_remote.rs index 1f5f661f05..11ab20a5ad 100644 --- a/codex-rs/core/tests/suite/compact_remote.rs +++ b/codex-rs/core/tests/suite/compact_remote.rs @@ -2774,12 +2774,13 @@ 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 { + .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { cwd: Some(PathBuf::from(PRETURN_CONTEXT_DIFF_CWD)), approval_policy: None, approvals_reviewer: None, sandbox_policy: None, permission_profile: None, + active_permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -2892,12 +2893,13 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_strips_incoming_model wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; codex - .submit(Op::OverrideTurnContext { + .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { cwd: None, approval_policy: None, approvals_reviewer: None, sandbox_policy: None, permission_profile: None, + active_permission_profile: None, windows_sandbox_level: None, model: Some(next_model.to_string()), effort: None, diff --git a/codex-rs/core/tests/suite/compact_resume_fork.rs b/codex-rs/core/tests/suite/compact_resume_fork.rs index 06cf5c357a..49f108d044 100644 --- a/codex-rs/core/tests/suite/compact_resume_fork.rs +++ b/codex-rs/core/tests/suite/compact_resume_fork.rs @@ -549,12 +549,13 @@ 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 { + .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { cwd: Some(override_cwd.to_path_buf()), approval_policy: None, approvals_reviewer: None, sandbox_policy: None, permission_profile: None, + active_permission_profile: None, windows_sandbox_level: None, model: None, effort: None, diff --git a/codex-rs/core/tests/suite/model_overrides.rs b/codex-rs/core/tests/suite/model_overrides.rs index 9f8835f19f..e904654b4b 100644 --- a/codex-rs/core/tests/suite/model_overrides.rs +++ b/codex-rs/core/tests/suite/model_overrides.rs @@ -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 turn_context_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() @@ -25,12 +25,13 @@ async fn override_turn_context_does_not_persist_when_config_exists() { let config_path = test.home.path().join(CONFIG_TOML); codex - .submit(Op::OverrideTurnContext { + .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { cwd: None, approval_policy: None, approvals_reviewer: None, sandbox_policy: None, permission_profile: None, + active_permission_profile: None, windows_sandbox_level: None, model: Some("o3".to_string()), effort: Some(Some(ReasoningEffort::High)), @@ -52,7 +53,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 turn_context_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"); @@ -64,12 +65,13 @@ async fn override_turn_context_does_not_create_config_file() { ); codex - .submit(Op::OverrideTurnContext { + .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { cwd: None, approval_policy: None, approvals_reviewer: None, sandbox_policy: None, permission_profile: None, + active_permission_profile: None, windows_sandbox_level: None, model: Some("o3".to_string()), effort: Some(Some(ReasoningEffort::Medium)), diff --git a/codex-rs/core/tests/suite/model_switching.rs b/codex-rs/core/tests/suite/model_switching.rs index 9c9c4580d7..978308f2cb 100644 --- a/codex-rs/core/tests/suite/model_switching.rs +++ b/codex-rs/core/tests/suite/model_switching.rs @@ -166,12 +166,13 @@ async fn model_change_appends_model_instructions_developer_message() -> Result<( wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; test.codex - .submit(Op::OverrideTurnContext { + .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { cwd: None, approval_policy: None, approvals_reviewer: None, sandbox_policy: None, permission_profile: None, + active_permission_profile: None, windows_sandbox_level: None, model: Some(next_model.to_string()), effort: None, @@ -246,12 +247,13 @@ async fn model_and_personality_change_only_appends_model_instructions() -> Resul wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; test.codex - .submit(Op::OverrideTurnContext { + .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { cwd: None, approval_policy: None, approvals_reviewer: None, sandbox_policy: None, permission_profile: None, + active_permission_profile: None, windows_sandbox_level: None, model: Some(next_model.to_string()), effort: None, @@ -993,12 +995,13 @@ 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 { + .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { cwd: None, approval_policy: None, approvals_reviewer: None, sandbox_policy: None, permission_profile: None, + active_permission_profile: None, windows_sandbox_level: None, model: Some(smaller_model_slug.to_string()), effort: None, diff --git a/codex-rs/core/tests/suite/model_visible_layout.rs b/codex-rs/core/tests/suite/model_visible_layout.rs index 2b275a97f5..664e05fbec 100644 --- a/codex-rs/core/tests/suite/model_visible_layout.rs +++ b/codex-rs/core/tests/suite/model_visible_layout.rs @@ -521,12 +521,13 @@ async fn snapshot_model_visible_layout_resume_override_matches_rollout_model() - fs::create_dir_all(&resume_override_cwd)?; resumed .codex - .submit(Op::OverrideTurnContext { + .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { cwd: Some(resume_override_cwd), approval_policy: None, approvals_reviewer: None, sandbox_policy: None, permission_profile: None, + active_permission_profile: None, windows_sandbox_level: None, model: Some("gpt-5.2".to_string()), effort: None, diff --git a/codex-rs/core/tests/suite/override_updates.rs b/codex-rs/core/tests/suite/override_updates.rs index d0e949947a..bc92b178af 100644 --- a/codex-rs/core/tests/suite/override_updates.rs +++ b/codex-rs/core/tests/suite/override_updates.rs @@ -24,8 +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 turn_context_update_without_user_turn_does_not_record_permissions_update() -> Result<()> { skip_if_no_network!(Ok(())); let server = start_mock_server().await; @@ -35,12 +34,13 @@ async fn override_turn_context_without_user_turn_does_not_record_permissions_upd let test = builder.build(&server).await?; test.codex - .submit(Op::OverrideTurnContext { + .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { cwd: None, approval_policy: Some(AskForApproval::Never), approvals_reviewer: None, sandbox_policy: None, permission_profile: None, + active_permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -64,8 +64,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 turn_context_update_without_user_turn_does_not_record_environment_update() -> Result<()> { skip_if_no_network!(Ok(())); let server = start_mock_server().await; @@ -73,12 +72,13 @@ async fn override_turn_context_without_user_turn_does_not_record_environment_upd let new_cwd = TempDir::new()?; test.codex - .submit(Op::OverrideTurnContext { + .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { cwd: Some(new_cwd.path().to_path_buf()), approval_policy: None, approvals_reviewer: None, sandbox_policy: None, permission_profile: None, + active_permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -102,7 +102,7 @@ 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 turn_context_update_without_user_turn_does_not_record_collaboration_update() -> Result<()> { skip_if_no_network!(Ok(())); @@ -112,12 +112,13 @@ async fn override_turn_context_without_user_turn_does_not_record_collaboration_u let collaboration_mode = collab_mode_with_instructions(Some(collab_text)); test.codex - .submit(Op::OverrideTurnContext { + .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { cwd: None, approval_policy: None, approvals_reviewer: None, sandbox_policy: None, permission_profile: None, + active_permission_profile: None, windows_sandbox_level: None, model: None, effort: None, diff --git a/codex-rs/core/tests/suite/permissions_messages.rs b/codex-rs/core/tests/suite/permissions_messages.rs index d22b37a843..13faecedf1 100644 --- a/codex-rs/core/tests/suite/permissions_messages.rs +++ b/codex-rs/core/tests/suite/permissions_messages.rs @@ -104,12 +104,13 @@ async fn permissions_message_added_on_override_change() -> Result<()> { wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; test.codex - .submit(Op::OverrideTurnContext { + .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { cwd: None, approval_policy: Some(AskForApproval::Never), approvals_reviewer: None, sandbox_policy: None, permission_profile: None, + active_permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -241,12 +242,13 @@ async fn permissions_message_omitted_when_disabled() -> Result<()> { wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; test.codex - .submit(Op::OverrideTurnContext { + .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { cwd: None, approval_policy: Some(AskForApproval::Never), approvals_reviewer: None, sandbox_policy: None, permission_profile: None, + active_permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -332,12 +334,13 @@ async fn resume_replays_permissions_messages() -> Result<()> { initial .codex - .submit(Op::OverrideTurnContext { + .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { cwd: None, approval_policy: Some(AskForApproval::Never), approvals_reviewer: None, sandbox_policy: None, permission_profile: None, + active_permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -441,12 +444,13 @@ async fn resume_and_fork_append_permissions_messages() -> Result<()> { initial .codex - .submit(Op::OverrideTurnContext { + .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { cwd: None, approval_policy: Some(AskForApproval::Never), approvals_reviewer: None, sandbox_policy: None, permission_profile: None, + active_permission_profile: None, windows_sandbox_level: None, model: None, effort: None, diff --git a/codex-rs/core/tests/suite/personality.rs b/codex-rs/core/tests/suite/personality.rs index 5ae0d1bd2a..9e8ddbf4fa 100644 --- a/codex-rs/core/tests/suite/personality.rs +++ b/codex-rs/core/tests/suite/personality.rs @@ -337,12 +337,13 @@ 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 { + .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { cwd: None, approval_policy: None, approvals_reviewer: None, sandbox_policy: None, permission_profile: None, + active_permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -421,12 +422,13 @@ 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 { + .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { cwd: None, approval_policy: None, approvals_reviewer: None, sandbox_policy: None, permission_profile: None, + active_permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -518,12 +520,13 @@ 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 { + .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { cwd: None, approval_policy: None, approvals_reviewer: None, sandbox_policy: None, permission_profile: None, + active_permission_profile: None, windows_sandbox_level: None, model: None, effort: None, @@ -767,12 +770,13 @@ 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 { + .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { cwd: None, approval_policy: None, approvals_reviewer: None, sandbox_policy: None, permission_profile: None, + active_permission_profile: None, windows_sandbox_level: None, model: None, effort: None, diff --git a/codex-rs/core/tests/suite/prompt_caching.rs b/codex-rs/core/tests/suite/prompt_caching.rs index 17c57c39df..0a107b0cf1 100644 --- a/codex-rs/core/tests/suite/prompt_caching.rs +++ b/codex-rs/core/tests/suite/prompt_caching.rs @@ -443,12 +443,13 @@ async fn overrides_turn_context_but_keeps_cached_prefix_and_key_constant() -> an .to_legacy_sandbox_policy(config.cwd.as_path()) .expect("workspace profile should have legacy projection"); codex - .submit(Op::OverrideTurnContext { + .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { cwd: None, approval_policy: Some(AskForApproval::Never), approvals_reviewer: None, sandbox_policy: Some(sandbox_policy), permission_profile: Some(permission_profile), + active_permission_profile: None, windows_sandbox_level: None, model: None, effort: Some(Some(ReasoningEffort::High)), @@ -530,12 +531,13 @@ async fn override_before_first_turn_emits_environment_context() -> anyhow::Resul }; codex - .submit(Op::OverrideTurnContext { + .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { cwd: None, approval_policy: Some(AskForApproval::Never), approvals_reviewer: None, sandbox_policy: None, permission_profile: None, + active_permission_profile: None, windows_sandbox_level: None, model: Some("gpt-5.4".to_string()), effort: Some(Some(ReasoningEffort::Low)), diff --git a/codex-rs/core/tests/suite/remote_models.rs b/codex-rs/core/tests/suite/remote_models.rs index aa976c3aa1..799775fbf3 100644 --- a/codex-rs/core/tests/suite/remote_models.rs +++ b/codex-rs/core/tests/suite/remote_models.rs @@ -639,12 +639,13 @@ async fn remote_models_remote_model_uses_unified_exec() -> Result<()> { assert_eq!(model_info.shell_type, ConfigShellToolType::UnifiedExec); codex - .submit(Op::OverrideTurnContext { + .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { cwd: None, approval_policy: None, approvals_reviewer: None, sandbox_policy: None, permission_profile: None, + active_permission_profile: None, windows_sandbox_level: None, model: Some(REMOTE_MODEL_SLUG.to_string()), effort: None, @@ -899,12 +900,13 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> { wait_for_model_available(&models_manager, model).await; codex - .submit(Op::OverrideTurnContext { + .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { cwd: None, approval_policy: None, approvals_reviewer: None, sandbox_policy: None, permission_profile: None, + active_permission_profile: None, windows_sandbox_level: None, model: Some(model.to_string()), effort: None, diff --git a/codex-rs/core/tests/suite/resume.rs b/codex-rs/core/tests/suite/resume.rs index 29a8f4ff7a..c9d0bb0760 100644 --- a/codex-rs/core/tests/suite/resume.rs +++ b/codex-rs/core/tests/suite/resume.rs @@ -425,12 +425,13 @@ async fn resume_model_switch_is_not_duplicated_after_pre_turn_override() -> Resu let resumed = resume_builder.resume(&server, home, rollout_path).await?; resumed .codex - .submit(Op::OverrideTurnContext { + .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { cwd: None, approval_policy: None, approvals_reviewer: None, sandbox_policy: None, permission_profile: None, + active_permission_profile: None, windows_sandbox_level: None, model: Some("gpt-5.4".to_string()), effort: None, diff --git a/codex-rs/core/tests/suite/review.rs b/codex-rs/core/tests/suite/review.rs index 57467a1007..bdc1fe7540 100644 --- a/codex-rs/core/tests/suite/review.rs +++ b/codex-rs/core/tests/suite/review.rs @@ -790,12 +790,13 @@ async fn review_uses_overridden_cwd_for_base_branch_merge_base() { .await; codex - .submit(Op::OverrideTurnContext { + .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { cwd: Some(repo_path.to_path_buf()), approval_policy: None, approvals_reviewer: None, sandbox_policy: None, permission_profile: None, + active_permission_profile: None, windows_sandbox_level: None, model: None, effort: None, diff --git a/codex-rs/docs/protocol_v1.md b/codex-rs/docs/protocol_v1.md index d18aa669ca..44b767dfa1 100644 --- a/codex-rs/docs/protocol_v1.md +++ b/codex-rs/docs/protocol_v1.md @@ -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 model’s communication style + - `Op::UserTurn` and `Op::UserInputWithTurnContext` accept an optional `personality` override that updates the model’s communication style Valid `personality` values are `friendly`, `pragmatic`, and `none`. When `none` is selected, the personality placeholder is replaced with an empty string. diff --git a/codex-rs/memories/write/src/startup_tests.rs b/codex-rs/memories/write/src/startup_tests.rs index 4fcb1d409b..cd48b6a1d7 100644 --- a/codex-rs/memories/write/src/startup_tests.rs +++ b/codex-rs/memories/write/src/startup_tests.rs @@ -243,12 +243,13 @@ async fn memories_startup_phase1_uses_live_thread_service_tier() -> anyhow::Resu assert_eq!(test.config.service_tier, None); test.codex - .submit(Op::OverrideTurnContext { + .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { cwd: None, approval_policy: None, approvals_reviewer: None, sandbox_policy: None, permission_profile: None, + active_permission_profile: None, windows_sandbox_level: None, model: None, effort: None, diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index c241ceda60..6a4c539385 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -524,70 +524,6 @@ pub enum Op { communication: InterAgentCommunication, }, - /// Override parts of the persistent turn context 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 context (for example, - /// [`Op::UserInput`]). - OverrideTurnContext { - /// Updated `cwd` for sandbox/tool calls. - #[serde(skip_serializing_if = "Option::is_none")] - cwd: Option, - - /// Updated command approval policy. - #[serde(skip_serializing_if = "Option::is_none")] - approval_policy: Option, - - /// Updated approval reviewer for future approval prompts. - #[serde(skip_serializing_if = "Option::is_none")] - approvals_reviewer: Option, - - /// Updated sandbox policy for tool calls. - #[serde(skip_serializing_if = "Option::is_none")] - sandbox_policy: Option, - - /// Updated permissions profile for tool calls. - #[serde(skip_serializing_if = "Option::is_none")] - permission_profile: Option, - - /// Updated Windows sandbox mode for tool execution. - #[serde(skip_serializing_if = "Option::is_none")] - windows_sandbox_level: Option, - - /// Updated model slug. When set, the model info is derived - /// automatically. - #[serde(skip_serializing_if = "Option::is_none")] - model: Option, - - /// 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>, - - /// Updated reasoning summary preference (honored only for reasoning-capable models). - #[serde(skip_serializing_if = "Option::is_none")] - summary: Option, - - /// 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>, - - /// 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, - - /// Updated personality preference. - #[serde(skip_serializing_if = "Option::is_none")] - personality: Option, - }, - /// Approve a command execution ExecApproval { /// The id of the submission we are approving @@ -776,7 +712,6 @@ impl Op { Self::RealtimeConversationListVoices => "realtime_conversation_list_voices", Self::UserInput { .. } => "user_input", Self::InterAgentCommunication { .. } => "inter_agent_communication", - Self::OverrideTurnContext { .. } => "override_turn_context", Self::ExecApproval { .. } => "exec_approval", Self::PatchApproval { .. } => "patch_approval", Self::ResolveElicitation { .. } => "resolve_elicitation", From 9393c2647b1843934ea747972004fbbc4cc272c2 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Sat, 16 May 2026 15:49:05 -0700 Subject: [PATCH 2/2] Add queued core turn context op --- .../thread_processor_tests.rs | 12 ++ codex-rs/core/src/codex_thread.rs | 38 +++- codex-rs/core/src/session/handlers.rs | 164 +++++++++++------ codex-rs/core/src/session/mod.rs | 9 +- codex-rs/core/src/session/session.rs | 2 + codex-rs/core/src/session/tests.rs | 8 + codex-rs/core/src/session/turn.rs | 1 + .../tests/suite/collaboration_instructions.rs | 169 ++---------------- codex-rs/core/tests/suite/compact.rs | 13 +- codex-rs/core/tests/suite/compact_remote.rs | 26 +-- .../core/tests/suite/compact_resume_fork.rs | 12 +- codex-rs/core/tests/suite/model_overrides.rs | 24 +-- codex-rs/core/tests/suite/model_switching.rs | 38 +--- .../core/tests/suite/model_visible_layout.rs | 12 +- codex-rs/core/tests/suite/override_updates.rs | 39 +--- .../core/tests/suite/permissions_messages.rs | 52 +----- codex-rs/core/tests/suite/personality.rs | 52 +----- codex-rs/core/tests/suite/prompt_caching.rs | 19 +- codex-rs/core/tests/suite/remote_models.rs | 26 +-- codex-rs/core/tests/suite/resume.rs | 13 +- codex-rs/core/tests/suite/review.rs | 13 +- codex-rs/mcp-server/src/codex_tool_runner.rs | 1 + codex-rs/memories/write/src/startup_tests.rs | 13 +- codex-rs/protocol/src/protocol.rs | 45 ++++- codex-rs/rollout-trace/src/protocol_event.rs | 2 + codex-rs/rollout/src/policy.rs | 1 + 26 files changed, 263 insertions(+), 541 deletions(-) diff --git a/codex-rs/app-server/src/request_processors/thread_processor_tests.rs b/codex-rs/app-server/src/request_processors/thread_processor_tests.rs index f9841421ca..aa33fc623f 100644 --- a/codex-rs/app-server/src/request_processors/thread_processor_tests.rs +++ b/codex-rs/app-server/src/request_processors/thread_processor_tests.rs @@ -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, }; diff --git a/codex-rs/core/src/codex_thread.rs b/codex-rs/core/src/codex_thread.rs index 1a2d5ed710..dd1a676dbd 100644 --- a/codex-rs/core/src/codex_thread.rs +++ b/codex-rs/core/src/codex_thread.rs @@ -63,7 +63,9 @@ pub struct ThreadConfigSnapshot { pub profile_workspace_roots: Vec, pub ephemeral: bool, pub reasoning_effort: Option, + pub reasoning_summary: Option, pub personality: Option, + pub collaboration_mode: CollaborationMode, pub session_source: SessionSource, pub thread_source: Option, } @@ -255,11 +257,40 @@ impl CodexThread { .await } - /// Validate persistent turn context overrides without committing them. + /// Preview persistent turn context overrides without committing them. + pub async fn preview_turn_context_overrides( + &self, + overrides: CodexThreadTurnContextOverrides, + ) -> ConstraintResult { + let updates = self.turn_context_settings_update(overrides).await; + self.codex.session.preview_settings(&updates).await + } + pub async fn validate_turn_context_overrides( &self, overrides: CodexThreadTurnContextOverrides, ) -> ConstraintResult<()> { + self.preview_turn_context_overrides(overrides).await?; + Ok(()) + } + + /// Apply persistent turn context overrides directly and return the effective state. + /// + /// This bypasses the submission queue; callers that need ordering relative + /// to turns should submit `Op::TurnContext`. + pub async fn update_turn_context_overrides( + &self, + overrides: CodexThreadTurnContextOverrides, + ) -> ConstraintResult { + let updates = self.turn_context_settings_update(overrides).await; + self.codex.session.update_settings(updates).await?; + Ok(self.config_snapshot().await) + } + + async fn turn_context_settings_update( + &self, + overrides: CodexThreadTurnContextOverrides, + ) -> SessionSettingsUpdate { let CodexThreadTurnContextOverrides { cwd, workspace_roots, @@ -287,7 +318,7 @@ impl CodexThread { .with_updates(model, effort, /*developer_instructions*/ None) }; - let updates = SessionSettingsUpdate { + SessionSettingsUpdate { cwd, workspace_roots, profile_workspace_roots, @@ -302,8 +333,7 @@ impl CodexThread { service_tier, personality, ..Default::default() - }; - self.codex.session.validate_settings(&updates).await + } } /// Use sparingly: this is intended to be removed soon. diff --git a/codex-rs/core/src/session/handlers.rs b/codex-rs/core/src/session/handlers.rs index e2f850d13f..d9a6058a91 100644 --- a/codex-rs/core/src/session/handlers.rs +++ b/codex-rs/core/src/session/handlers.rs @@ -43,7 +43,9 @@ use codex_protocol::protocol::RolloutItem; use codex_protocol::protocol::ThreadMemoryMode; use codex_protocol::protocol::ThreadRolledBackEvent; use codex_protocol::protocol::TurnAbortReason; +use codex_protocol::protocol::TurnContextAppliedEvent; use codex_protocol::protocol::TurnContextOverrides; +use codex_protocol::protocol::TurnContextSnapshot; use codex_protocol::protocol::WarningEvent; use codex_protocol::request_permissions::RequestPermissionsResponse; use codex_protocol::request_user_input::RequestUserInputResponse; @@ -91,13 +93,100 @@ pub async fn user_input_or_turn(sess: &Arc, sub_id: String, op: Op) { .await; } +pub async fn update_turn_context(sess: &Arc, sub_id: String, op: Op) { + let Op::TurnContext { turn_context } = op else { + unreachable!(); + }; + let updates = turn_context_settings_update(sess, turn_context).await; + let msg = match sess.update_settings(updates).await { + Ok(()) => turn_context_applied_event(sess).await, + Err(err) => EventMsg::Error(ErrorEvent { + message: format!("invalid turn context override: {err}"), + codex_error_info: Some(CodexErrorInfo::Other), + }), + }; + sess.send_event_raw(Event { id: sub_id, msg }).await; +} + +async fn turn_context_settings_update( + sess: &Session, + turn_context: TurnContextOverrides, +) -> SessionSettingsUpdate { + let TurnContextOverrides { + 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, + } = turn_context; + let collaboration_mode = match collaboration_mode { + Some(collaboration_mode) => collaboration_mode, + None => { + let state = sess.state.lock().await; + 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 turn_context_applied_event(sess: &Session) -> EventMsg { + let snapshot = { + let state = sess.state.lock().await; + state.session_configuration.thread_config_snapshot() + }; + EventMsg::TurnContextApplied(TurnContextAppliedEvent { + turn_context: TurnContextSnapshot { + 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, sub_id: String, op: Op, mirror_user_text_to_realtime: Option<()>, ) { - let (items, updates, responsesapi_client_metadata) = match op { + let (items, updates, responsesapi_client_metadata, emit_turn_context_applied) = match op { Op::UserInput { items, environments, @@ -105,14 +194,20 @@ pub(super) async fn user_input_or_turn_inner( responsesapi_client_metadata, turn_context, } => { - let mut updates = if turn_context == TurnContextOverrides::default() { - SessionSettingsUpdate::default() - } else { + let emit_turn_context_applied = turn_context != TurnContextOverrides::default(); + let mut updates = if emit_turn_context_applied { turn_context_settings_update(sess, turn_context).await + } else { + SessionSettingsUpdate::default() }; updates.final_output_json_schema = Some(final_output_json_schema); updates.environments = environments; - (items, updates, responsesapi_client_metadata) + ( + items, + updates, + responsesapi_client_metadata, + emit_turn_context_applied, + ) } _ => unreachable!(), }; @@ -121,6 +216,13 @@ pub(super) async fn user_input_or_turn_inner( // new_turn_with_sub_id already emits the error event. return; }; + if emit_turn_context_applied { + sess.send_event_raw(Event { + id: sub_id.clone(), + msg: turn_context_applied_event(sess).await, + }) + .await; + } sess.maybe_emit_unknown_model_warning_for_turn(current_context.as_ref()) .await; let accepted_items = match sess @@ -170,54 +272,6 @@ pub(super) async fn user_input_or_turn_inner( } } -async fn turn_context_settings_update( - sess: &Session, - turn_context: TurnContextOverrides, -) -> SessionSettingsUpdate { - let TurnContextOverrides { - 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, - } = turn_context; - let collaboration_mode = if let Some(collaboration_mode) = collaboration_mode { - collaboration_mode - } else { - let state = sess.state.lock().await; - 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, items: &[UserInput]) { let text = UserMessageItem::new(items).message(); if text.is_empty() { @@ -714,6 +768,10 @@ pub(super) async fn submission_loop( user_input_or_turn(&sess, sub.id.clone(), sub.op).await; false } + Op::TurnContext { .. } => { + update_turn_context(&sess, sub.id.clone(), sub.op).await; + false + } Op::InterAgentCommunication { communication } => { inter_agent_communication(&sess, sub.id.clone(), communication).await; false diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index 9a78242222..91f9564814 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -1386,12 +1386,15 @@ impl Session { Ok(()) } - pub(crate) async fn validate_settings( + pub(crate) async fn preview_settings( &self, updates: &SessionSettingsUpdate, - ) -> ConstraintResult<()> { + ) -> ConstraintResult { 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( diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs index 3f8276d04d..ab7020d5aa 100644 --- a/codex-rs/core/src/session/session.rs +++ b/codex-rs/core/src/session/session.rs @@ -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, } diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index c906ed0f29..579b4bc1be 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -123,6 +123,7 @@ use codex_protocol::protocol::TokenUsage; use codex_protocol::protocol::TokenUsageInfo; use codex_protocol::protocol::TurnAbortedEvent; use codex_protocol::protocol::TurnCompleteEvent; +use codex_protocol::protocol::TurnContextOverrides; use codex_protocol::protocol::TurnStartedEvent; use codex_protocol::protocol::UserMessageEvent; use codex_protocol::protocol::W3cTraceContext; @@ -5203,6 +5204,13 @@ fn op_kind_for_input_and_context_ops() { .kind(), "user_input" ); + assert_eq!( + Op::TurnContext { + turn_context: TurnContextOverrides::default(), + } + .kind(), + "turn_context" + ); } #[tokio::test] diff --git a/codex-rs/core/src/session/turn.rs b/codex-rs/core/src/session/turn.rs index aee4bd360f..7ae572e6e2 100644 --- a/codex-rs/core/src/session/turn.rs +++ b/codex-rs/core/src/session/turn.rs @@ -1463,6 +1463,7 @@ pub(super) fn realtime_text_for_event(msg: &EventMsg) -> Option { | EventMsg::ContextCompacted(_) | EventMsg::ThreadRolledBack(_) | EventMsg::TurnStarted(_) + | EventMsg::TurnContextApplied(_) | EventMsg::TurnComplete(_) | EventMsg::TokenCount(_) | EventMsg::UserMessage(_) diff --git a/codex-rs/core/tests/suite/collaboration_instructions.rs b/codex-rs/core/tests/suite/collaboration_instructions.rs index a18959a393..900cf24d8d 100644 --- a/codex-rs/core/tests/suite/collaboration_instructions.rs +++ b/codex-rs/core/tests/suite/collaboration_instructions.rs @@ -125,19 +125,8 @@ async fn user_input_includes_collaboration_instructions_after_override() -> Resu let collaboration_mode = collab_mode_with_instructions(Some(collab_text)); test.codex .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { - cwd: None, - approval_policy: None, - approvals_reviewer: None, - sandbox_policy: None, - permission_profile: None, - active_permission_profile: None, - windows_sandbox_level: None, - model: None, - effort: None, - summary: None, - service_tier: None, collaboration_mode: Some(collaboration_mode), - personality: None, + ..Default::default() }) .await?; @@ -288,19 +277,8 @@ async fn override_then_next_turn_uses_updated_collaboration_instructions() -> Re test.codex .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { - cwd: None, - approval_policy: None, - approvals_reviewer: None, - sandbox_policy: None, - permission_profile: None, - active_permission_profile: None, - windows_sandbox_level: None, - model: None, - effort: None, - summary: None, - service_tier: None, collaboration_mode: Some(collaboration_mode), - personality: None, + ..Default::default() }) .await?; @@ -345,19 +323,8 @@ async fn user_turn_overrides_collaboration_instructions_after_override() -> Resu test.codex .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { - cwd: None, - approval_policy: None, - approvals_reviewer: None, - sandbox_policy: None, - permission_profile: None, - active_permission_profile: None, - windows_sandbox_level: None, - model: None, - effort: None, - summary: None, - service_tier: None, collaboration_mode: Some(base_mode), - personality: None, + ..Default::default() }) .await?; @@ -422,19 +389,8 @@ async fn collaboration_mode_update_emits_new_instruction_message() -> Result<()> test.codex .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { - cwd: None, - approval_policy: None, - approvals_reviewer: None, - sandbox_policy: None, - permission_profile: None, - active_permission_profile: None, - windows_sandbox_level: None, - model: None, - effort: None, - summary: None, - service_tier: None, collaboration_mode: Some(collab_mode_with_instructions(Some(first_text))), - personality: None, + ..Default::default() }) .await?; @@ -454,19 +410,8 @@ async fn collaboration_mode_update_emits_new_instruction_message() -> Result<()> test.codex .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { - cwd: None, - approval_policy: None, - approvals_reviewer: None, - sandbox_policy: None, - permission_profile: None, - active_permission_profile: None, - windows_sandbox_level: None, - model: None, - effort: None, - summary: None, - service_tier: None, collaboration_mode: Some(collab_mode_with_instructions(Some(second_text))), - personality: None, + ..Default::default() }) .await?; @@ -515,19 +460,8 @@ async fn collaboration_mode_update_noop_does_not_append() -> Result<()> { test.codex .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { - cwd: None, - approval_policy: None, - approvals_reviewer: None, - sandbox_policy: None, - permission_profile: None, - active_permission_profile: None, - windows_sandbox_level: None, - model: None, - effort: None, - summary: None, - service_tier: None, collaboration_mode: Some(collab_mode_with_instructions(Some(collab_text))), - personality: None, + ..Default::default() }) .await?; @@ -547,19 +481,8 @@ async fn collaboration_mode_update_noop_does_not_append() -> Result<()> { test.codex .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { - cwd: None, - approval_policy: None, - approvals_reviewer: None, - sandbox_policy: None, - permission_profile: None, - active_permission_profile: None, - windows_sandbox_level: None, - model: None, - effort: None, - summary: None, - service_tier: None, collaboration_mode: Some(collab_mode_with_instructions(Some(collab_text))), - personality: None, + ..Default::default() }) .await?; @@ -607,22 +530,11 @@ async fn collaboration_mode_update_emits_new_instruction_message_when_mode_chang test.codex .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { - cwd: None, - approval_policy: None, - approvals_reviewer: None, - sandbox_policy: None, - permission_profile: None, - active_permission_profile: None, - windows_sandbox_level: None, - model: None, - effort: None, - summary: None, - service_tier: None, collaboration_mode: Some(collab_mode_with_mode_and_instructions( ModeKind::Default, Some(default_text), )), - personality: None, + ..Default::default() }) .await?; @@ -642,22 +554,11 @@ async fn collaboration_mode_update_emits_new_instruction_message_when_mode_chang test.codex .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { - cwd: None, - approval_policy: None, - approvals_reviewer: None, - sandbox_policy: None, - permission_profile: None, - active_permission_profile: None, - windows_sandbox_level: None, - model: None, - effort: None, - summary: None, - service_tier: None, collaboration_mode: Some(collab_mode_with_mode_and_instructions( ModeKind::Plan, Some(plan_text), )), - personality: None, + ..Default::default() }) .await?; @@ -706,22 +607,11 @@ async fn collaboration_mode_update_noop_does_not_append_when_mode_is_unchanged() test.codex .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { - cwd: None, - approval_policy: None, - approvals_reviewer: None, - sandbox_policy: None, - permission_profile: None, - active_permission_profile: None, - windows_sandbox_level: None, - model: None, - effort: None, - summary: None, - service_tier: None, collaboration_mode: Some(collab_mode_with_mode_and_instructions( ModeKind::Default, Some(collab_text), )), - personality: None, + ..Default::default() }) .await?; @@ -741,22 +631,11 @@ async fn collaboration_mode_update_noop_does_not_append_when_mode_is_unchanged() test.codex .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { - cwd: None, - approval_policy: None, - approvals_reviewer: None, - sandbox_policy: None, - permission_profile: None, - active_permission_profile: None, - windows_sandbox_level: None, - model: None, - effort: None, - summary: None, - service_tier: None, collaboration_mode: Some(collab_mode_with_mode_and_instructions( ModeKind::Default, Some(collab_text), )), - personality: None, + ..Default::default() }) .await?; @@ -811,19 +690,8 @@ async fn resume_replays_collaboration_instructions() -> Result<()> { initial .codex .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { - cwd: None, - approval_policy: None, - approvals_reviewer: None, - sandbox_policy: None, - permission_profile: None, - active_permission_profile: None, - windows_sandbox_level: None, - model: None, - effort: None, - summary: None, - service_tier: None, collaboration_mode: Some(collab_mode_with_instructions(Some(collab_text))), - personality: None, + ..Default::default() }) .await?; @@ -882,17 +750,6 @@ async fn empty_collaboration_instructions_are_ignored() -> Result<()> { test.codex .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { - cwd: None, - approval_policy: None, - approvals_reviewer: None, - sandbox_policy: None, - permission_profile: None, - active_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 { @@ -901,7 +758,7 @@ async fn empty_collaboration_instructions_are_ignored() -> Result<()> { developer_instructions: Some("".to_string()), }, }), - personality: None, + ..Default::default() }) .await?; diff --git a/codex-rs/core/tests/suite/compact.rs b/codex-rs/core/tests/suite/compact.rs index 08f6eef832..51e5139749 100644 --- a/codex-rs/core/tests/suite/compact.rs +++ b/codex-rs/core/tests/suite/compact.rs @@ -3286,18 +3286,7 @@ async fn snapshot_request_shape_pre_turn_compaction_including_incoming_user_mess codex .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { cwd: Some(PathBuf::from(PRETURN_CONTEXT_DIFF_CWD)), - approval_policy: None, - approvals_reviewer: None, - sandbox_policy: None, - permission_profile: None, - active_permission_profile: None, - windows_sandbox_level: None, - model: None, - effort: None, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, + ..Default::default() }) .await .expect("override turn context"); diff --git a/codex-rs/core/tests/suite/compact_remote.rs b/codex-rs/core/tests/suite/compact_remote.rs index 11ab20a5ad..a0c2f0b50f 100644 --- a/codex-rs/core/tests/suite/compact_remote.rs +++ b/codex-rs/core/tests/suite/compact_remote.rs @@ -2776,18 +2776,7 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_including_incoming_us codex .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { cwd: Some(PathBuf::from(PRETURN_CONTEXT_DIFF_CWD)), - approval_policy: None, - approvals_reviewer: None, - sandbox_policy: None, - permission_profile: None, - active_permission_profile: None, - windows_sandbox_level: None, - model: None, - effort: None, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, + ..Default::default() }) .await?; } @@ -2894,19 +2883,8 @@ async fn snapshot_request_shape_remote_pre_turn_compaction_strips_incoming_model codex .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { - cwd: None, - approval_policy: None, - approvals_reviewer: None, - sandbox_policy: None, - permission_profile: None, - active_permission_profile: None, - windows_sandbox_level: None, model: Some(next_model.to_string()), - effort: None, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, + ..Default::default() }) .await?; codex diff --git a/codex-rs/core/tests/suite/compact_resume_fork.rs b/codex-rs/core/tests/suite/compact_resume_fork.rs index 49f108d044..d01be5d695 100644 --- a/codex-rs/core/tests/suite/compact_resume_fork.rs +++ b/codex-rs/core/tests/suite/compact_resume_fork.rs @@ -551,16 +551,6 @@ async fn snapshot_rollback_followup_turn_trims_context_updates() -> Result<()> { conversation .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { cwd: Some(override_cwd.to_path_buf()), - approval_policy: None, - approvals_reviewer: None, - sandbox_policy: None, - permission_profile: None, - active_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 { @@ -569,7 +559,7 @@ async fn snapshot_rollback_followup_turn_trims_context_updates() -> Result<()> { developer_instructions: Some(ROLLED_BACK_DEV_INSTRUCTIONS.to_string()), }, }), - personality: None, + ..Default::default() }) .await?; diff --git a/codex-rs/core/tests/suite/model_overrides.rs b/codex-rs/core/tests/suite/model_overrides.rs index e904654b4b..a8d46dd32b 100644 --- a/codex-rs/core/tests/suite/model_overrides.rs +++ b/codex-rs/core/tests/suite/model_overrides.rs @@ -26,19 +26,9 @@ async fn turn_context_update_does_not_persist_when_config_exists() { codex .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { - cwd: None, - approval_policy: None, - approvals_reviewer: None, - sandbox_policy: None, - permission_profile: None, - active_permission_profile: None, - windows_sandbox_level: None, model: Some("o3".to_string()), effort: Some(Some(ReasoningEffort::High)), - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, + ..Default::default() }) .await .expect("submit override"); @@ -66,19 +56,9 @@ async fn turn_context_update_does_not_create_config_file() { codex .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { - cwd: None, - approval_policy: None, - approvals_reviewer: None, - sandbox_policy: None, - permission_profile: None, - active_permission_profile: None, - windows_sandbox_level: None, model: Some("o3".to_string()), effort: Some(Some(ReasoningEffort::Medium)), - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, + ..Default::default() }) .await .expect("submit override"); diff --git a/codex-rs/core/tests/suite/model_switching.rs b/codex-rs/core/tests/suite/model_switching.rs index 978308f2cb..eaf9a5f7dc 100644 --- a/codex-rs/core/tests/suite/model_switching.rs +++ b/codex-rs/core/tests/suite/model_switching.rs @@ -167,19 +167,8 @@ async fn model_change_appends_model_instructions_developer_message() -> Result<( test.codex .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { - cwd: None, - approval_policy: None, - approvals_reviewer: None, - sandbox_policy: None, - permission_profile: None, - active_permission_profile: None, - windows_sandbox_level: None, model: Some(next_model.to_string()), - effort: None, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, + ..Default::default() }) .await?; @@ -248,19 +237,9 @@ async fn model_and_personality_change_only_appends_model_instructions() -> Resul test.codex .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { - cwd: None, - approval_policy: None, - approvals_reviewer: None, - sandbox_policy: None, - permission_profile: None, - active_permission_profile: None, - windows_sandbox_level: None, model: Some(next_model.to_string()), - effort: None, - summary: None, - service_tier: None, - collaboration_mode: None, personality: Some(Personality::Pragmatic), + ..Default::default() }) .await?; @@ -996,19 +975,8 @@ async fn model_switch_to_smaller_model_updates_token_context_window() -> Result< test.codex .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { - cwd: None, - approval_policy: None, - approvals_reviewer: None, - sandbox_policy: None, - permission_profile: None, - active_permission_profile: None, - windows_sandbox_level: None, model: Some(smaller_model_slug.to_string()), - effort: None, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, + ..Default::default() }) .await?; diff --git a/codex-rs/core/tests/suite/model_visible_layout.rs b/codex-rs/core/tests/suite/model_visible_layout.rs index 664e05fbec..85b1c9a99e 100644 --- a/codex-rs/core/tests/suite/model_visible_layout.rs +++ b/codex-rs/core/tests/suite/model_visible_layout.rs @@ -523,18 +523,8 @@ async fn snapshot_model_visible_layout_resume_override_matches_rollout_model() - .codex .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { cwd: Some(resume_override_cwd), - approval_policy: None, - approvals_reviewer: None, - sandbox_policy: None, - permission_profile: None, - active_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, + ..Default::default() }) .await?; resumed diff --git a/codex-rs/core/tests/suite/override_updates.rs b/codex-rs/core/tests/suite/override_updates.rs index bc92b178af..b3cec800ab 100644 --- a/codex-rs/core/tests/suite/override_updates.rs +++ b/codex-rs/core/tests/suite/override_updates.rs @@ -35,19 +35,8 @@ async fn turn_context_update_without_user_turn_does_not_record_permissions_updat test.codex .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { - cwd: None, approval_policy: Some(AskForApproval::Never), - approvals_reviewer: None, - sandbox_policy: None, - permission_profile: None, - active_permission_profile: None, - windows_sandbox_level: None, - model: None, - effort: None, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, + ..Default::default() }) .await?; @@ -74,18 +63,7 @@ async fn turn_context_update_without_user_turn_does_not_record_environment_updat test.codex .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { cwd: Some(new_cwd.path().to_path_buf()), - approval_policy: None, - approvals_reviewer: None, - sandbox_policy: None, - permission_profile: None, - active_permission_profile: None, - windows_sandbox_level: None, - model: None, - effort: None, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, + ..Default::default() }) .await?; @@ -113,19 +91,8 @@ async fn turn_context_update_without_user_turn_does_not_record_collaboration_upd test.codex .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { - cwd: None, - approval_policy: None, - approvals_reviewer: None, - sandbox_policy: None, - permission_profile: None, - active_permission_profile: None, - windows_sandbox_level: None, - model: None, - effort: None, - summary: None, - service_tier: None, collaboration_mode: Some(collaboration_mode), - personality: None, + ..Default::default() }) .await?; diff --git a/codex-rs/core/tests/suite/permissions_messages.rs b/codex-rs/core/tests/suite/permissions_messages.rs index 13faecedf1..94b983be51 100644 --- a/codex-rs/core/tests/suite/permissions_messages.rs +++ b/codex-rs/core/tests/suite/permissions_messages.rs @@ -105,19 +105,8 @@ async fn permissions_message_added_on_override_change() -> Result<()> { test.codex .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { - cwd: None, approval_policy: Some(AskForApproval::Never), - approvals_reviewer: None, - sandbox_policy: None, - permission_profile: None, - active_permission_profile: None, - windows_sandbox_level: None, - model: None, - effort: None, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, + ..Default::default() }) .await?; @@ -243,19 +232,8 @@ async fn permissions_message_omitted_when_disabled() -> Result<()> { test.codex .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { - cwd: None, approval_policy: Some(AskForApproval::Never), - approvals_reviewer: None, - sandbox_policy: None, - permission_profile: None, - active_permission_profile: None, - windows_sandbox_level: None, - model: None, - effort: None, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, + ..Default::default() }) .await?; @@ -335,19 +313,8 @@ async fn resume_replays_permissions_messages() -> Result<()> { initial .codex .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { - cwd: None, approval_policy: Some(AskForApproval::Never), - approvals_reviewer: None, - sandbox_policy: None, - permission_profile: None, - active_permission_profile: None, - windows_sandbox_level: None, - model: None, - effort: None, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, + ..Default::default() }) .await?; @@ -445,19 +412,8 @@ async fn resume_and_fork_append_permissions_messages() -> Result<()> { initial .codex .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { - cwd: None, approval_policy: Some(AskForApproval::Never), - approvals_reviewer: None, - sandbox_policy: None, - permission_profile: None, - active_permission_profile: None, - windows_sandbox_level: None, - model: None, - effort: None, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, + ..Default::default() }) .await?; diff --git a/codex-rs/core/tests/suite/personality.rs b/codex-rs/core/tests/suite/personality.rs index 9e8ddbf4fa..c492c43286 100644 --- a/codex-rs/core/tests/suite/personality.rs +++ b/codex-rs/core/tests/suite/personality.rs @@ -338,19 +338,8 @@ async fn user_turn_personality_some_adds_update_message() -> anyhow::Result<()> test.codex .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { - cwd: None, - approval_policy: None, - approvals_reviewer: None, - sandbox_policy: None, - permission_profile: None, - active_permission_profile: None, - windows_sandbox_level: None, - model: None, - effort: None, - summary: None, - service_tier: None, - collaboration_mode: None, personality: Some(Personality::Friendly), + ..Default::default() }) .await?; @@ -423,19 +412,8 @@ async fn user_turn_personality_same_value_does_not_add_update_message() -> anyho test.codex .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { - cwd: None, - approval_policy: None, - approvals_reviewer: None, - sandbox_policy: None, - permission_profile: None, - active_permission_profile: None, - windows_sandbox_level: None, - model: None, - effort: None, - summary: None, - service_tier: None, - collaboration_mode: None, personality: Some(Personality::Pragmatic), + ..Default::default() }) .await?; @@ -521,19 +499,8 @@ async fn user_turn_personality_skips_if_feature_disabled() -> anyhow::Result<()> test.codex .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { - cwd: None, - approval_policy: None, - approvals_reviewer: None, - sandbox_policy: None, - permission_profile: None, - active_permission_profile: None, - windows_sandbox_level: None, - model: None, - effort: None, - summary: None, - service_tier: None, - collaboration_mode: None, personality: Some(Personality::Pragmatic), + ..Default::default() }) .await?; @@ -771,19 +738,8 @@ async fn user_turn_personality_remote_model_template_includes_update_message() - test.codex .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { - cwd: None, - approval_policy: None, - approvals_reviewer: None, - sandbox_policy: None, - permission_profile: None, - active_permission_profile: None, - windows_sandbox_level: None, - model: None, - effort: None, - summary: None, - service_tier: None, - collaboration_mode: None, personality: Some(Personality::Friendly), + ..Default::default() }) .await?; diff --git a/codex-rs/core/tests/suite/prompt_caching.rs b/codex-rs/core/tests/suite/prompt_caching.rs index 0a107b0cf1..ab9844bfcb 100644 --- a/codex-rs/core/tests/suite/prompt_caching.rs +++ b/codex-rs/core/tests/suite/prompt_caching.rs @@ -444,19 +444,12 @@ async fn overrides_turn_context_but_keeps_cached_prefix_and_key_constant() -> an .expect("workspace profile should have legacy projection"); codex .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { - cwd: None, approval_policy: Some(AskForApproval::Never), - approvals_reviewer: None, sandbox_policy: Some(sandbox_policy), permission_profile: Some(permission_profile), - active_permission_profile: None, - windows_sandbox_level: None, - model: None, effort: Some(Some(ReasoningEffort::High)), summary: Some(ReasoningSummary::Detailed), - service_tier: None, - collaboration_mode: None, - personality: None, + ..Default::default() }) .await?; @@ -532,19 +525,11 @@ async fn override_before_first_turn_emits_environment_context() -> anyhow::Resul codex .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { - cwd: None, approval_policy: Some(AskForApproval::Never), - approvals_reviewer: None, - sandbox_policy: None, - permission_profile: None, - active_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, + ..Default::default() }) .await?; diff --git a/codex-rs/core/tests/suite/remote_models.rs b/codex-rs/core/tests/suite/remote_models.rs index 799775fbf3..42a57691ef 100644 --- a/codex-rs/core/tests/suite/remote_models.rs +++ b/codex-rs/core/tests/suite/remote_models.rs @@ -640,19 +640,8 @@ async fn remote_models_remote_model_uses_unified_exec() -> Result<()> { codex .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { - cwd: None, - approval_policy: None, - approvals_reviewer: None, - sandbox_policy: None, - permission_profile: None, - active_permission_profile: None, - windows_sandbox_level: None, model: Some(REMOTE_MODEL_SLUG.to_string()), - effort: None, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, + ..Default::default() }) .await?; @@ -901,19 +890,8 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> { codex .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { - cwd: None, - approval_policy: None, - approvals_reviewer: None, - sandbox_policy: None, - permission_profile: None, - active_permission_profile: None, - windows_sandbox_level: None, model: Some(model.to_string()), - effort: None, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, + ..Default::default() }) .await?; diff --git a/codex-rs/core/tests/suite/resume.rs b/codex-rs/core/tests/suite/resume.rs index c9d0bb0760..fc5a82d5b8 100644 --- a/codex-rs/core/tests/suite/resume.rs +++ b/codex-rs/core/tests/suite/resume.rs @@ -426,19 +426,8 @@ async fn resume_model_switch_is_not_duplicated_after_pre_turn_override() -> Resu resumed .codex .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { - cwd: None, - approval_policy: None, - approvals_reviewer: None, - sandbox_policy: None, - permission_profile: None, - active_permission_profile: None, - windows_sandbox_level: None, model: Some("gpt-5.4".to_string()), - effort: None, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, + ..Default::default() }) .await?; resumed diff --git a/codex-rs/core/tests/suite/review.rs b/codex-rs/core/tests/suite/review.rs index bdc1fe7540..55e1d1dc61 100644 --- a/codex-rs/core/tests/suite/review.rs +++ b/codex-rs/core/tests/suite/review.rs @@ -792,18 +792,7 @@ async fn review_uses_overridden_cwd_for_base_branch_merge_base() { codex .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { cwd: Some(repo_path.to_path_buf()), - approval_policy: None, - approvals_reviewer: None, - sandbox_policy: None, - permission_profile: None, - active_permission_profile: None, - windows_sandbox_level: None, - model: None, - effort: None, - summary: None, - service_tier: None, - collaboration_mode: None, - personality: None, + ..Default::default() }) .await .unwrap(); diff --git a/codex-rs/mcp-server/src/codex_tool_runner.rs b/codex-rs/mcp-server/src/codex_tool_runner.rs index a38fbf6b55..a33b59bd1d 100644 --- a/codex-rs/mcp-server/src/codex_tool_runner.rs +++ b/codex-rs/mcp-server/src/codex_tool_runner.rs @@ -333,6 +333,7 @@ async fn run_codex_tool_session_inner( } EventMsg::AgentReasoningRawContent(_) | EventMsg::TurnStarted(_) + | EventMsg::TurnContextApplied(_) | EventMsg::TokenCount(_) | EventMsg::AgentReasoning(_) | EventMsg::AgentReasoningSectionBreak(_) diff --git a/codex-rs/memories/write/src/startup_tests.rs b/codex-rs/memories/write/src/startup_tests.rs index cd48b6a1d7..f5a75ed10e 100644 --- a/codex-rs/memories/write/src/startup_tests.rs +++ b/codex-rs/memories/write/src/startup_tests.rs @@ -244,19 +244,8 @@ async fn memories_startup_phase1_uses_live_thread_service_tier() -> anyhow::Resu test.codex .update_turn_context_overrides(codex_core::CodexThreadTurnContextOverrides { - cwd: None, - approval_policy: None, - approvals_reviewer: None, - sandbox_policy: None, - permission_profile: None, - active_permission_profile: None, - windows_sandbox_level: None, - model: None, - effort: None, - summary: None, service_tier: Some(Some(ServiceTier::Fast.request_value().to_string())), - collaboration_mode: None, - personality: None, + ..Default::default() }) .await?; diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 6a4c539385..fda02f5d30 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -396,7 +396,8 @@ pub struct ConversationTextParams { pub text: String, } -/// Persistent turn-context overrides that can be applied before user input. +/// Persistent turn-context overrides that can be applied before user input or +/// on their own. #[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq, JsonSchema)] pub struct TurnContextOverrides { /// Updated `cwd` for sandbox/tool calls. @@ -518,6 +519,16 @@ pub enum Op { turn_context: TurnContextOverrides, }, + /// Apply persistent turn-context 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. + TurnContext { + /// Persistent turn-context overrides to apply. + #[serde(flatten)] + turn_context: TurnContextOverrides, + }, + /// Inter-agent communication that should be recorded as assistant history /// while still using the normal thread submission lifecycle. InterAgentCommunication { @@ -711,6 +722,7 @@ impl Op { Self::RealtimeConversationClose => "realtime_conversation_close", Self::RealtimeConversationListVoices => "realtime_conversation_list_voices", Self::UserInput { .. } => "user_input", + Self::TurnContext { .. } => "turn_context", Self::InterAgentCommunication { .. } => "inter_agent_communication", Self::ExecApproval { .. } => "exec_approval", Self::PatchApproval { .. } => "patch_approval", @@ -1162,6 +1174,10 @@ pub enum EventMsg { #[serde(rename = "task_started", alias = "turn_started")] TurnStarted(TurnStartedEvent), + /// Persistent turn-context overrides from the correlated submission have + /// been applied to the session configuration. + TurnContextApplied(TurnContextAppliedEvent), + /// Agent has completed all actions. /// v1 wire format uses `task_complete`; accept `turn_complete` for v2 interop. #[serde(rename = "task_complete", alias = "turn_complete")] @@ -1842,6 +1858,33 @@ pub struct TurnStartedEvent { pub collaboration_mode_kind: ModeKind, } +#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)] +pub struct TurnContextAppliedEvent { + pub turn_context: TurnContextSnapshot, +} + +#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)] +pub struct TurnContextSnapshot { + pub model: String, + pub model_provider_id: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub service_tier: Option, + 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, + pub cwd: AbsolutePathBuf, + #[serde(skip_serializing_if = "Option::is_none")] + pub reasoning_effort: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub reasoning_summary: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub personality: Option, + pub collaboration_mode: CollaborationMode, +} + #[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq, Eq, JsonSchema, TS)] pub struct TokenUsage { #[ts(type = "number")] diff --git a/codex-rs/rollout-trace/src/protocol_event.rs b/codex-rs/rollout-trace/src/protocol_event.rs index 1e49c82be2..9a7f59842a 100644 --- a/codex-rs/rollout-trace/src/protocol_event.rs +++ b/codex-rs/rollout-trace/src/protocol_event.rs @@ -228,6 +228,7 @@ pub(crate) fn tool_runtime_trace_event(event: &EventMsg) -> Option Option<&'static s | EventMsg::ModelReroute(_) | EventMsg::ModelVerification(_) | EventMsg::ContextCompacted(_) + | EventMsg::TurnContextApplied(_) | EventMsg::TokenCount(_) | EventMsg::AgentMessage(_) | EventMsg::UserMessage(_) diff --git a/codex-rs/rollout/src/policy.rs b/codex-rs/rollout/src/policy.rs index ceb617763f..e9be01df25 100644 --- a/codex-rs/rollout/src/policy.rs +++ b/codex-rs/rollout/src/policy.rs @@ -183,6 +183,7 @@ fn event_msg_persistence_mode(ev: &EventMsg) -> Option { | EventMsg::AgentReasoningSectionBreak(_) | EventMsg::RawResponseItem(_) | EventMsg::SessionConfigured(_) + | EventMsg::TurnContextApplied(_) | EventMsg::McpToolCallBegin(_) | EventMsg::ExecCommandBegin(_) | EventMsg::TerminalInteraction(_)