From a668379abf0f67d81a61dc971ea463c483846fd2 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Mon, 18 May 2026 21:03:51 -0700 Subject: [PATCH] [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) --- .../thread_processor_tests.rs | 12 + .../src/request_processors/turn_processor.rs | 2 +- codex-rs/core/src/codex_thread.rs | 21 +- codex-rs/core/src/session/handlers.rs | 245 ++++++++------- codex-rs/core/src/session/mod.rs | 9 +- codex-rs/core/src/session/session.rs | 2 + codex-rs/core/src/session/tests.rs | 70 ++--- codex-rs/core/src/session/turn.rs | 1 + codex-rs/core/tests/common/lib.rs | 25 ++ .../tests/suite/collaboration_instructions.rs | 287 ++++++------------ codex-rs/core/tests/suite/compact.rs | 24 +- codex-rs/core/tests/suite/compact_remote.rs | 44 +-- .../core/tests/suite/compact_resume_fork.rs | 23 +- codex-rs/core/tests/suite/model_overrides.rs | 50 ++- codex-rs/core/tests/suite/model_switching.rs | 65 ++-- .../core/tests/suite/model_visible_layout.rs | 22 +- codex-rs/core/tests/suite/override_updates.rs | 74 ++--- .../core/tests/suite/permissions_messages.rs | 90 ++---- codex-rs/core/tests/suite/personality.rs | 88 ++---- codex-rs/core/tests/suite/prompt_caching.rs | 39 +-- codex-rs/core/tests/suite/remote_models.rs | 44 +-- codex-rs/core/tests/suite/resume.rs | 23 +- codex-rs/core/tests/suite/review.rs | 24 +- codex-rs/docs/protocol_v1.md | 2 +- codex-rs/mcp-server/src/codex_tool_runner.rs | 1 + codex-rs/memories/write/src/startup_tests.rs | 22 +- codex-rs/protocol/src/protocol.rs | 110 +++---- codex-rs/rollout-trace/src/protocol_event.rs | 2 + codex-rs/rollout/src/policy.rs | 1 + 29 files changed, 553 insertions(+), 869 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 f9841421c..aa33fc623 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/app-server/src/request_processors/turn_processor.rs b/codex-rs/app-server/src/request_processors/turn_processor.rs index d383f64d4..71715e507 100644 --- a/codex-rs/app-server/src/request_processors/turn_processor.rs +++ b/codex-rs/app-server/src/request_processors/turn_processor.rs @@ -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, diff --git a/codex-rs/core/src/codex_thread.rs b/codex-rs/core/src/codex_thread.rs index aff76b8c0..1b40387c3 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, } @@ -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 { + 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. diff --git a/codex-rs/core/src/session/handlers.rs b/codex-rs/core/src/session/handlers.rs index e01ee28f9..3d57bd707 100644 --- a/codex-rs/core/src/session/handlers.rs +++ b/codex-rs/core/src/session/handlers.rs @@ -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, sub_id: String, op: Op) { user_input_or_turn_inner( sess, @@ -104,36 +93,132 @@ pub async fn user_input_or_turn(sess: &Arc, sub_id: String, op: Op) { .await; } +pub async fn update_thread_settings( + sess: &Arc, + 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, 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, 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 diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index 7c3b4803d..cb848286c 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -1384,12 +1384,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 082308f20..2d96cf916 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 4f090a8a3..2363f02cc 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -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(); diff --git a/codex-rs/core/src/session/turn.rs b/codex-rs/core/src/session/turn.rs index 9fe7f93dc..24788d4d9 100644 --- a/codex-rs/core/src/session/turn.rs +++ b/codex-rs/core/src/session/turn.rs @@ -1476,6 +1476,7 @@ pub(super) fn realtime_text_for_event(msg: &EventMsg) -> Option { | EventMsg::ContextCompacted(_) | EventMsg::ThreadRolledBack(_) | EventMsg::TurnStarted(_) + | EventMsg::ThreadSettingsApplied(_) | EventMsg::TurnComplete(_) | EventMsg::TokenCount(_) | EventMsg::UserMessage(_) diff --git a/codex-rs/core/tests/common/lib.rs b/codex-rs/core/tests/common/lib.rs index ad7858f80..194fb2ae0 100644 --- a/codex-rs/core/tests/common/lib.rs +++ b/codex-rs/core/tests/common/lib.rs @@ -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(codex: &CodexThread, matcher: F) -> T where F: Fn(&codex_protocol::protocol::EventMsg) -> Option, diff --git a/codex-rs/core/tests/suite/collaboration_instructions.rs b/codex-rs/core/tests/suite/collaboration_instructions.rs index bd01dc4e7..bf8de53dd 100644 --- a/codex-rs/core/tests/suite/collaboration_instructions.rs +++ b/codex-rs/core/tests/suite/collaboration_instructions.rs @@ -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 { diff --git a/codex-rs/core/tests/suite/compact.rs b/codex-rs/core/tests/suite/compact.rs index 2c789a271..b07adfc61 100644 --- a/codex-rs/core/tests/suite/compact.rs +++ b/codex-rs/core/tests/suite/compact.rs @@ -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 diff --git a/codex-rs/core/tests/suite/compact_remote.rs b/codex-rs/core/tests/suite/compact_remote.rs index 9db94016e..9aac846ff 100644 --- a/codex-rs/core/tests/suite/compact_remote.rs +++ b/codex-rs/core/tests/suite/compact_remote.rs @@ -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, diff --git a/codex-rs/core/tests/suite/compact_resume_fork.rs b/codex-rs/core/tests/suite/compact_resume_fork.rs index 20ae213b2..b2b02f5e6 100644 --- a/codex-rs/core/tests/suite/compact_resume_fork.rs +++ b/codex-rs/core/tests/suite/compact_resume_fork.rs @@ -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; diff --git a/codex-rs/core/tests/suite/model_overrides.rs b/codex-rs/core/tests/suite/model_overrides.rs index 9f8835f19..f70c0ae1c 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 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; diff --git a/codex-rs/core/tests/suite/model_switching.rs b/codex-rs/core/tests/suite/model_switching.rs index 1c9358a8a..009dfda60 100644 --- a/codex-rs/core/tests/suite/model_switching.rs +++ b/codex-rs/core/tests/suite/model_switching.rs @@ -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( diff --git a/codex-rs/core/tests/suite/model_visible_layout.rs b/codex-rs/core/tests/suite/model_visible_layout.rs index 9cf83424c..17bc0f670 100644 --- a/codex-rs/core/tests/suite/model_visible_layout.rs +++ b/codex-rs/core/tests/suite/model_visible_layout.rs @@ -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 { diff --git a/codex-rs/core/tests/suite/override_updates.rs b/codex-rs/core/tests/suite/override_updates.rs index d0e949947..ce7c87a93 100644 --- a/codex-rs/core/tests/suite/override_updates.rs +++ b/codex-rs/core/tests/suite/override_updates.rs @@ -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; diff --git a/codex-rs/core/tests/suite/permissions_messages.rs b/codex-rs/core/tests/suite/permissions_messages.rs index 33d494ae4..b77e65c0f 100644 --- a/codex-rs/core/tests/suite/permissions_messages.rs +++ b/codex-rs/core/tests/suite/permissions_messages.rs @@ -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 diff --git a/codex-rs/core/tests/suite/personality.rs b/codex-rs/core/tests/suite/personality.rs index 8924ca2a2..bbe8178f8 100644 --- a/codex-rs/core/tests/suite/personality.rs +++ b/codex-rs/core/tests/suite/personality.rs @@ -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( diff --git a/codex-rs/core/tests/suite/prompt_caching.rs b/codex-rs/core/tests/suite/prompt_caching.rs index accf3fb78..68b3816ae 100644 --- a/codex-rs/core/tests/suite/prompt_caching.rs +++ b/codex-rs/core/tests/suite/prompt_caching.rs @@ -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 { diff --git a/codex-rs/core/tests/suite/remote_models.rs b/codex-rs/core/tests/suite/remote_models.rs index adc73d8a7..e143cb8e3 100644 --- a/codex-rs/core/tests/suite/remote_models.rs +++ b/codex-rs/core/tests/suite/remote_models.rs @@ -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) = diff --git a/codex-rs/core/tests/suite/resume.rs b/codex-rs/core/tests/suite/resume.rs index db6428935..fc7a23215 100644 --- a/codex-rs/core/tests/suite/resume.rs +++ b/codex-rs/core/tests/suite/resume.rs @@ -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 { diff --git a/codex-rs/core/tests/suite/review.rs b/codex-rs/core/tests/suite/review.rs index e128a189b..05ec967d0 100644 --- a/codex-rs/core/tests/suite/review.rs +++ b/codex-rs/core/tests/suite/review.rs @@ -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 { diff --git a/codex-rs/docs/protocol_v1.md b/codex-rs/docs/protocol_v1.md index d18aa669c..464e8187b 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::UserInput` accepts an optional `personality` turn-context 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/mcp-server/src/codex_tool_runner.rs b/codex-rs/mcp-server/src/codex_tool_runner.rs index 1898bebee..167d56da5 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::ThreadSettingsApplied(_) | 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 4fcb1d409..30240f04c 100644 --- a/codex-rs/memories/write/src/startup_tests.rs +++ b/codex-rs/memories/write/src/startup_tests.rs @@ -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?; diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index cec929b07..d5bf20038 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 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, - - /// 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 @@ -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, + 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 1e49c82be..a862af116 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::ThreadSettingsApplied(_) | EventMsg::TokenCount(_) | EventMsg::AgentMessage(_) | EventMsg::UserMessage(_) diff --git a/codex-rs/rollout/src/policy.rs b/codex-rs/rollout/src/policy.rs index ceb617763..b6dbba98f 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::ThreadSettingsApplied(_) | EventMsg::McpToolCallBegin(_) | EventMsg::ExecCommandBegin(_) | EventMsg::TerminalInteraction(_)