diff --git a/codex-rs/app-server-client/src/lib.rs b/codex-rs/app-server-client/src/lib.rs index 4eec8a2348d4..a46feb544aa1 100644 --- a/codex-rs/app-server-client/src/lib.rs +++ b/codex-rs/app-server-client/src/lib.rs @@ -23,7 +23,6 @@ use std::fmt; use std::io::Error as IoError; use std::io::ErrorKind; use std::io::Result as IoResult; -use std::path::Path; use std::sync::Arc; use std::time::Duration; @@ -50,11 +49,8 @@ use codex_config::LoaderOverrides; use codex_config::NoopThreadConfigLoader; use codex_config::RemoteThreadConfigLoader; use codex_config::ThreadConfigLoader; -use codex_config::config_toml::ConfigToml; use codex_core::config::Config; pub use codex_core::otel_init::build_provider as build_otel_provider; -use codex_core::personality_migration::PersonalityMigrationStatus; -use codex_core::personality_migration::maybe_migrate_personality; pub use codex_exec_server::EnvironmentManager; pub use codex_exec_server::ExecServerRuntimePaths; use codex_feedback::CodexFeedback; @@ -89,23 +85,6 @@ pub mod legacy_core { const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5); -/// Runs the embedded app-server personality migration. -/// -/// Returns `true` when the migration changed config and the caller should reload it. -pub async fn migrate_personality_if_needed( - codex_home: &Path, - config_toml: &ConfigToml, - state_db: Option, -) -> IoResult { - let status = maybe_migrate_personality(codex_home, config_toml, state_db).await?; - match status { - PersonalityMigrationStatus::Applied => Ok(true), - PersonalityMigrationStatus::SkippedMarker - | PersonalityMigrationStatus::SkippedExplicitPersonality - | PersonalityMigrationStatus::SkippedNoSessions => Ok(false), - } -} - /// Raw app-server request result for typed in-process requests. /// /// Even on the in-process path, successful responses still travel back through diff --git a/codex-rs/app-server/src/lib.rs b/codex-rs/app-server/src/lib.rs index 91da0c1645c9..8bb24c5ca15d 100644 --- a/codex-rs/app-server/src/lib.rs +++ b/codex-rs/app-server/src/lib.rs @@ -516,11 +516,11 @@ pub async fn run_main_with_transport_options( } }; let mut config_warnings = Vec::new(); - let (mut config, should_run_personality_migration) = match config_manager + let config = match config_manager .load_latest_config(/*fallback_cwd*/ None) .await { - Ok(config) => (config, true), + Ok(config) => config, Err(err) => { if strict_config { return Err(err); @@ -528,15 +528,12 @@ pub async fn run_main_with_transport_options( let message = config_warning_from_error("Invalid configuration; using defaults.", &err); config_warnings.push(message); - ( - config_manager.load_default_config().await.map_err(|e| { - std::io::Error::new( - ErrorKind::InvalidData, - format!("error loading default config after config error: {e}"), - ) - })?, - false, - ) + config_manager.load_default_config().await.map_err(|e| { + std::io::Error::new( + ErrorKind::InvalidData, + format!("error loading default config after config error: {e}"), + ) + })? } }; @@ -582,46 +579,6 @@ pub async fn run_main_with_transport_options( }); } - if should_run_personality_migration { - let effective_toml = config.config_layer_stack.effective_config(); - match effective_toml.try_into() { - Ok(config_toml) => { - match codex_core::personality_migration::maybe_migrate_personality( - &config.codex_home, - &config_toml, - state_db.clone(), - ) - .await - { - Ok(codex_core::personality_migration::PersonalityMigrationStatus::Applied) => { - config = config_manager - .load_latest_config(/*fallback_cwd*/ None) - .await - .map_err(|err| { - std::io::Error::new( - ErrorKind::InvalidData, - format!( - "error reloading config after personality migration: {err}" - ), - ) - })?; - } - Ok( - codex_core::personality_migration::PersonalityMigrationStatus::SkippedMarker - | codex_core::personality_migration::PersonalityMigrationStatus::SkippedExplicitPersonality - | codex_core::personality_migration::PersonalityMigrationStatus::SkippedNoSessions, - ) => {} - Err(err) => { - warn!(error = %err, "Failed to run personality migration"); - } - } - } - Err(err) => { - warn!(error = %err, "Failed to deserialize config for personality migration"); - } - } - } - if let Ok(Some(err)) = check_execpolicy_for_warnings(&config.config_layer_stack).await { config_warnings.push(exec_policy_config_warning(&err)); } diff --git a/codex-rs/app-server/tests/suite/v2/turn_start.rs b/codex-rs/app-server/tests/suite/v2/turn_start.rs index c748c413c23b..10e1edcf699a 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start.rs @@ -3,7 +3,6 @@ use anyhow::Result; use app_test_support::TestAppServer; use app_test_support::create_apply_patch_sse_response; use app_test_support::create_exec_command_sse_response; -use app_test_support::create_fake_rollout; use app_test_support::create_final_assistant_message_sse_response; use app_test_support::create_mock_responses_server_repeating_assistant; use app_test_support::create_mock_responses_server_sequence; @@ -61,8 +60,6 @@ use codex_app_server_protocol::TurnStatus; use codex_app_server_protocol::TurnSteerParams; use codex_app_server_protocol::UserInput as V2UserInput; use codex_app_server_protocol::WarningNotification; -use codex_config::config_toml::ConfigToml; -use codex_core::personality_migration::PERSONALITY_MIGRATION_FILENAME; use codex_core::test_support::all_model_presets; use codex_features::FEATURES; use codex_features::Feature; @@ -100,7 +97,6 @@ const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs #[cfg(not(windows))] const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10); const TEST_ORIGINATOR: &str = "codex_vscode"; -const LOCAL_PRAGMATIC_TEMPLATE: &str = "You are a deeply pragmatic, effective software engineer."; const MULTI_AGENT_V2_NAMESPACE: &str = "collaboration"; const INVALID_REQUEST_ERROR_CODE: i64 = -32600; const TINY_PNG_BYTES: &[u8] = &[ @@ -2090,100 +2086,6 @@ async fn turn_start_change_personality_mid_thread_v2() -> Result<()> { Ok(()) } -#[tokio::test] -async fn turn_start_uses_migrated_pragmatic_personality_without_override_v2() -> Result<()> { - skip_if_no_network!(Ok(())); - - let server = responses::start_mock_server().await; - let body = responses::sse(vec![ - responses::ev_response_created("resp-1"), - responses::ev_assistant_message("msg-1", "Done"), - responses::ev_completed("resp-1"), - ]); - let response_mock = responses::mount_sse_once(&server, body).await; - - let codex_home = TempDir::new()?; - create_config_toml( - codex_home.path(), - &server.uri(), - "never", - &BTreeMap::from([(Feature::Personality, true)]), - )?; - create_fake_rollout( - codex_home.path(), - "2025-01-01T00-00-00", - "2025-01-01T00:00:00Z", - "history user message", - Some("mock_provider"), - /*git_info*/ None, - )?; - - let mut mcp = TestAppServer::builder() - .with_codex_home(codex_home.path()) - .build() - .await?; - timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; - - let persisted_toml: ConfigToml = toml::from_str(&std::fs::read_to_string( - codex_home.path().join("config.toml"), - )?)?; - assert_eq!(persisted_toml.personality, Some(Personality::Pragmatic)); - assert!( - codex_home - .path() - .join(PERSONALITY_MIGRATION_FILENAME) - .exists(), - "expected personality migration marker to be written on startup" - ); - - let thread_req = mcp - .send_thread_start_request_with_auto_env(ThreadStartParams { - model: Some("gpt-5.4".to_string()), - ..Default::default() - }) - .await?; - let thread_resp: JSONRPCResponse = timeout( - DEFAULT_READ_TIMEOUT, - mcp.read_stream_until_response_message(RequestId::Integer(thread_req)), - ) - .await??; - let ThreadStartResponse { thread, .. } = to_response::(thread_resp)?; - - let turn_req = mcp - .send_turn_start_request(TurnStartParams { - thread_id: thread.id, - client_user_message_id: None, - input: vec![V2UserInput::Text { - text: "Hello".to_string(), - text_elements: Vec::new(), - }], - personality: None, - ..Default::default() - }) - .await?; - let turn_resp: JSONRPCResponse = timeout( - DEFAULT_READ_TIMEOUT, - mcp.read_stream_until_response_message(RequestId::Integer(turn_req)), - ) - .await??; - let _turn: TurnStartResponse = to_response::(turn_resp)?; - - timeout( - DEFAULT_READ_TIMEOUT, - mcp.read_stream_until_notification_message("turn/completed"), - ) - .await??; - - let request = response_mock.single_request(); - let instructions_text = request.instructions_text(); - assert!( - instructions_text.contains(LOCAL_PRAGMATIC_TEMPLATE), - "expected startup-migrated pragmatic personality in model instructions, got: {instructions_text:?}" - ); - - Ok(()) -} - #[tokio::test] async fn turn_start_defaults_local_image_detail_to_high() -> Result<()> { let input_images = run_local_image_turn(/*detail*/ None).await?; diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index cfc26c571164..ed9788a2ddf0 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -73,7 +73,6 @@ pub(crate) mod utils; pub use mention_syntax::PLUGIN_TEXT_MENTION_SIGIL; pub use mention_syntax::TOOL_MENTION_SIGIL; pub use utils::path_utils; -pub mod personality_migration; pub(crate) mod plugins; #[doc(hidden)] pub(crate) mod prompt_debug; diff --git a/codex-rs/core/src/personality_migration.rs b/codex-rs/core/src/personality_migration.rs deleted file mode 100644 index b5c4586d9f3d..000000000000 --- a/codex-rs/core/src/personality_migration.rs +++ /dev/null @@ -1,116 +0,0 @@ -use crate::config::edit::ConfigEditsBuilder; -use codex_config::config_toml::ConfigToml; -use codex_protocol::config_types::Personality; -use codex_rollout::state_db::StateDbHandle; -use codex_thread_store::ListThreadsParams; -use codex_thread_store::LocalThreadStore; -use codex_thread_store::LocalThreadStoreConfig; -use codex_thread_store::ThreadSortKey; -use codex_thread_store::ThreadStore; -use std::io; -use std::path::Path; -use tokio::fs::OpenOptions; -use tokio::io::AsyncWriteExt; - -pub const PERSONALITY_MIGRATION_FILENAME: &str = ".personality_migration"; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum PersonalityMigrationStatus { - SkippedMarker, - SkippedExplicitPersonality, - SkippedNoSessions, - Applied, -} - -pub async fn maybe_migrate_personality( - codex_home: &Path, - config_toml: &ConfigToml, - state_db: Option, -) -> io::Result { - let marker_path = codex_home.join(PERSONALITY_MIGRATION_FILENAME); - if tokio::fs::try_exists(&marker_path).await? { - return Ok(PersonalityMigrationStatus::SkippedMarker); - } - - if config_toml.personality.is_some() { - create_marker(&marker_path).await?; - return Ok(PersonalityMigrationStatus::SkippedExplicitPersonality); - } - - let model_provider_id = config_toml - .model_provider - .clone() - .unwrap_or_else(|| "openai".to_string()); - - if !has_recorded_sessions(codex_home, model_provider_id.as_str(), state_db).await? { - create_marker(&marker_path).await?; - return Ok(PersonalityMigrationStatus::SkippedNoSessions); - } - - ConfigEditsBuilder::new(codex_home) - .set_personality(Some(Personality::Pragmatic)) - .apply() - .await - .map_err(|err| { - io::Error::other(format!("failed to persist personality migration: {err}")) - })?; - - create_marker(&marker_path).await?; - Ok(PersonalityMigrationStatus::Applied) -} - -async fn has_recorded_sessions( - codex_home: &Path, - default_provider: &str, - state_db: Option, -) -> io::Result { - let store = LocalThreadStore::new( - LocalThreadStoreConfig { - codex_home: codex_home.to_path_buf(), - sqlite_home: codex_home.to_path_buf(), - default_model_provider_id: default_provider.to_string(), - }, - state_db, - ); - if has_threads(&store, /*archived*/ false).await? { - return Ok(true); - } - has_threads(&store, /*archived*/ true).await -} - -async fn has_threads(store: &LocalThreadStore, archived: bool) -> io::Result { - store - .list_threads(ListThreadsParams { - page_size: 1, - cursor: None, - sort_key: ThreadSortKey::CreatedAt, - sort_direction: codex_thread_store::SortDirection::Desc, - allowed_sources: Vec::new(), - model_providers: None, - cwd_filters: None, - relation_filter: None, - archived, - search_term: None, - use_state_db_only: false, - }) - .await - .map(|page| !page.items.is_empty()) - .map_err(io::Error::other) -} - -async fn create_marker(marker_path: &Path) -> io::Result<()> { - match OpenOptions::new() - .create_new(true) - .write(true) - .open(marker_path) - .await - { - Ok(mut file) => file.write_all(b"v1\n").await, - Err(err) if err.kind() == io::ErrorKind::AlreadyExists => Ok(()), - Err(err) => Err(err), - } -} - -#[cfg(test)] -#[path = "personality_migration_tests.rs"] -mod tests; diff --git a/codex-rs/core/src/personality_migration_tests.rs b/codex-rs/core/src/personality_migration_tests.rs deleted file mode 100644 index 80f5b3c02054..000000000000 --- a/codex-rs/core/src/personality_migration_tests.rs +++ /dev/null @@ -1,171 +0,0 @@ -use super::*; -use codex_protocol::ThreadId; -use codex_protocol::protocol::EventMsg; -use codex_protocol::protocol::RolloutItem; -use codex_protocol::protocol::RolloutLine; -use codex_protocol::protocol::SessionMeta; -use codex_protocol::protocol::SessionMetaLine; -use codex_protocol::protocol::SessionSource; -use codex_protocol::protocol::UserMessageEvent; -use codex_rollout::ARCHIVED_SESSIONS_SUBDIR; -use codex_rollout::SESSIONS_SUBDIR; -use pretty_assertions::assert_eq; -use tempfile::TempDir; -use tokio::io::AsyncWriteExt; - -const TEST_TIMESTAMP: &str = "2025-01-01T00-00-00"; - -async fn read_config_toml(codex_home: &Path) -> io::Result { - let contents = tokio::fs::read_to_string(codex_home.join("config.toml")).await?; - toml::from_str(&contents).map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err)) -} - -async fn write_session_with_user_event(codex_home: &Path) -> io::Result<()> { - let thread_id = ThreadId::new(); - let dir = codex_home - .join(SESSIONS_SUBDIR) - .join("2025") - .join("01") - .join("01"); - write_rollout_with_user_event(&dir, thread_id).await -} - -async fn write_archived_session_with_user_event(codex_home: &Path) -> io::Result<()> { - let thread_id = ThreadId::new(); - let dir = codex_home.join(ARCHIVED_SESSIONS_SUBDIR); - write_rollout_with_user_event(&dir, thread_id).await -} - -async fn write_rollout_with_user_event(dir: &Path, thread_id: ThreadId) -> io::Result<()> { - tokio::fs::create_dir_all(&dir).await?; - let file_path = dir.join(format!("rollout-{TEST_TIMESTAMP}-{thread_id}.jsonl")); - let mut file = tokio::fs::File::create(&file_path).await?; - - let session_meta = SessionMetaLine { - meta: SessionMeta { - session_id: thread_id.into(), - id: thread_id, - forked_from_id: None, - parent_thread_id: None, - timestamp: TEST_TIMESTAMP.to_string(), - cwd: std::path::PathBuf::from("."), - originator: "test_originator".to_string(), - cli_version: "test_version".to_string(), - source: SessionSource::Cli, - thread_source: None, - agent_path: None, - agent_nickname: None, - agent_role: None, - model_provider: None, - base_instructions: None, - dynamic_tools: None, - selected_capability_roots: Vec::new(), - memory_mode: None, - history_mode: Default::default(), - multi_agent_version: None, - context_window: None, - }, - git: None, - }; - let meta_line = RolloutLine { - timestamp: TEST_TIMESTAMP.to_string(), - item: RolloutItem::SessionMeta(session_meta), - }; - let user_event = RolloutLine { - timestamp: TEST_TIMESTAMP.to_string(), - item: RolloutItem::EventMsg(EventMsg::UserMessage(UserMessageEvent { - client_id: None, - message: "hello".to_string(), - images: None, - local_images: Vec::new(), - text_elements: Vec::new(), - ..Default::default() - })), - }; - - file.write_all(format!("{}\n", serde_json::to_string(&meta_line)?).as_bytes()) - .await?; - file.write_all(format!("{}\n", serde_json::to_string(&user_event)?).as_bytes()) - .await?; - Ok(()) -} - -#[tokio::test] -async fn applies_when_sessions_exist_and_no_personality() -> io::Result<()> { - let temp = TempDir::new()?; - write_session_with_user_event(temp.path()).await?; - - let config_toml = ConfigToml::default(); - let status = maybe_migrate_personality(temp.path(), &config_toml, /*state_db*/ None).await?; - - assert_eq!(status, PersonalityMigrationStatus::Applied); - assert!(temp.path().join(PERSONALITY_MIGRATION_FILENAME).exists()); - - let persisted = read_config_toml(temp.path()).await?; - assert_eq!(persisted.personality, Some(Personality::Pragmatic)); - Ok(()) -} - -#[tokio::test] -async fn applies_when_only_archived_sessions_exist_and_no_personality() -> io::Result<()> { - let temp = TempDir::new()?; - write_archived_session_with_user_event(temp.path()).await?; - - let config_toml = ConfigToml::default(); - let status = maybe_migrate_personality(temp.path(), &config_toml, /*state_db*/ None).await?; - - assert_eq!(status, PersonalityMigrationStatus::Applied); - assert!(temp.path().join(PERSONALITY_MIGRATION_FILENAME).exists()); - - let persisted = read_config_toml(temp.path()).await?; - assert_eq!(persisted.personality, Some(Personality::Pragmatic)); - Ok(()) -} - -#[tokio::test] -async fn skips_when_marker_exists() -> io::Result<()> { - let temp = TempDir::new()?; - create_marker(&temp.path().join(PERSONALITY_MIGRATION_FILENAME)).await?; - - let config_toml = ConfigToml::default(); - let status = maybe_migrate_personality(temp.path(), &config_toml, /*state_db*/ None).await?; - - assert_eq!(status, PersonalityMigrationStatus::SkippedMarker); - assert!(!temp.path().join("config.toml").exists()); - Ok(()) -} - -#[tokio::test] -async fn skips_when_personality_explicit() -> io::Result<()> { - let temp = TempDir::new()?; - ConfigEditsBuilder::new(temp.path()) - .set_personality(Some(Personality::Friendly)) - .apply() - .await - .map_err(|err| io::Error::other(format!("failed to write config: {err}")))?; - - let config_toml = read_config_toml(temp.path()).await?; - let status = maybe_migrate_personality(temp.path(), &config_toml, /*state_db*/ None).await?; - - assert_eq!( - status, - PersonalityMigrationStatus::SkippedExplicitPersonality - ); - assert!(temp.path().join(PERSONALITY_MIGRATION_FILENAME).exists()); - - let persisted = read_config_toml(temp.path()).await?; - assert_eq!(persisted.personality, Some(Personality::Friendly)); - Ok(()) -} - -#[tokio::test] -async fn skips_when_no_sessions() -> io::Result<()> { - let temp = TempDir::new()?; - let config_toml = ConfigToml::default(); - let status = maybe_migrate_personality(temp.path(), &config_toml, /*state_db*/ None).await?; - - assert_eq!(status, PersonalityMigrationStatus::SkippedNoSessions); - assert!(temp.path().join(PERSONALITY_MIGRATION_FILENAME).exists()); - assert!(!temp.path().join("config.toml").exists()); - Ok(()) -} diff --git a/codex-rs/core/tests/suite/mod.rs b/codex-rs/core/tests/suite/mod.rs index a916bf20f417..d224c1fde6be 100644 --- a/codex-rs/core/tests/suite/mod.rs +++ b/codex-rs/core/tests/suite/mod.rs @@ -87,7 +87,6 @@ mod override_updates; mod pending_input; mod permissions_messages; mod personality; -mod personality_migration; mod plugins; mod prompt_caching; mod prompt_debug_tests; diff --git a/codex-rs/core/tests/suite/personality_migration.rs b/codex-rs/core/tests/suite/personality_migration.rs deleted file mode 100644 index 0ca3e1b2a9c5..000000000000 --- a/codex-rs/core/tests/suite/personality_migration.rs +++ /dev/null @@ -1,358 +0,0 @@ -use codex_config::config_toml::ConfigToml; -use codex_core::ARCHIVED_SESSIONS_SUBDIR; -use codex_core::SESSIONS_SUBDIR; -use codex_core::personality_migration::PERSONALITY_MIGRATION_FILENAME; -use codex_core::personality_migration::PersonalityMigrationStatus; -use codex_core::personality_migration::maybe_migrate_personality; -use codex_protocol::ThreadId; -use codex_protocol::config_types::Personality; -use codex_protocol::protocol::EventMsg; -use codex_protocol::protocol::RolloutItem; -use codex_protocol::protocol::RolloutLine; -use codex_protocol::protocol::SessionMeta; -use codex_protocol::protocol::SessionMetaLine; -use codex_protocol::protocol::SessionSource; -use codex_protocol::protocol::UserMessageEvent; -use pretty_assertions::assert_eq; -use std::io; -use std::path::Path; -use tempfile::TempDir; -use tokio::io::AsyncWriteExt; - -const TEST_TIMESTAMP: &str = "2025-01-01T00-00-00"; - -async fn read_config_toml(codex_home: &Path) -> io::Result { - let contents = tokio::fs::read_to_string(codex_home.join("config.toml")).await?; - toml::from_str(&contents).map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err)) -} - -async fn write_session_with_user_event(codex_home: &Path) -> io::Result<()> { - let thread_id = ThreadId::new(); - let dir = codex_home - .join(SESSIONS_SUBDIR) - .join("2025") - .join("01") - .join("01"); - write_rollout_with_user_event(&dir, thread_id).await -} - -async fn write_archived_session_with_user_event(codex_home: &Path) -> io::Result<()> { - let thread_id = ThreadId::new(); - let dir = codex_home.join(ARCHIVED_SESSIONS_SUBDIR); - write_rollout_with_user_event(&dir, thread_id).await -} - -async fn write_session_with_meta_only(codex_home: &Path) -> io::Result<()> { - let thread_id = ThreadId::new(); - let dir = codex_home - .join(SESSIONS_SUBDIR) - .join("2025") - .join("01") - .join("01"); - write_rollout_with_meta_only(&dir, thread_id).await -} - -async fn write_rollout_with_user_event(dir: &Path, thread_id: ThreadId) -> io::Result<()> { - tokio::fs::create_dir_all(&dir).await?; - let file_path = dir.join(format!("rollout-{TEST_TIMESTAMP}-{thread_id}.jsonl")); - let mut file = tokio::fs::File::create(&file_path).await?; - - let session_meta = SessionMetaLine { - meta: SessionMeta { - session_id: thread_id.into(), - id: thread_id, - forked_from_id: None, - parent_thread_id: None, - timestamp: TEST_TIMESTAMP.to_string(), - cwd: std::path::PathBuf::from("."), - originator: "test_originator".to_string(), - cli_version: "test_version".to_string(), - source: SessionSource::Cli, - thread_source: None, - agent_path: None, - agent_nickname: None, - agent_role: None, - model_provider: None, - base_instructions: None, - dynamic_tools: None, - selected_capability_roots: Vec::new(), - memory_mode: None, - history_mode: Default::default(), - multi_agent_version: None, - context_window: None, - }, - git: None, - }; - let meta_line = RolloutLine { - timestamp: TEST_TIMESTAMP.to_string(), - item: RolloutItem::SessionMeta(session_meta), - }; - let user_event = RolloutLine { - timestamp: TEST_TIMESTAMP.to_string(), - item: RolloutItem::EventMsg(EventMsg::UserMessage(UserMessageEvent { - client_id: None, - message: "hello".to_string(), - images: None, - local_images: Vec::new(), - text_elements: Vec::new(), - ..Default::default() - })), - }; - - let meta_json = serde_json::to_string(&meta_line)?; - file.write_all(format!("{meta_json}\n").as_bytes()).await?; - let user_json = serde_json::to_string(&user_event)?; - file.write_all(format!("{user_json}\n").as_bytes()).await?; - Ok(()) -} - -async fn write_rollout_with_meta_only(dir: &Path, thread_id: ThreadId) -> io::Result<()> { - tokio::fs::create_dir_all(&dir).await?; - let file_path = dir.join(format!("rollout-{TEST_TIMESTAMP}-{thread_id}.jsonl")); - let mut file = tokio::fs::File::create(&file_path).await?; - - let session_meta = SessionMetaLine { - meta: SessionMeta { - session_id: thread_id.into(), - id: thread_id, - forked_from_id: None, - parent_thread_id: None, - timestamp: TEST_TIMESTAMP.to_string(), - cwd: std::path::PathBuf::from("."), - originator: "test_originator".to_string(), - cli_version: "test_version".to_string(), - source: SessionSource::Cli, - thread_source: None, - agent_path: None, - agent_nickname: None, - agent_role: None, - model_provider: None, - base_instructions: None, - dynamic_tools: None, - selected_capability_roots: Vec::new(), - memory_mode: None, - history_mode: Default::default(), - multi_agent_version: None, - context_window: None, - }, - git: None, - }; - let meta_line = RolloutLine { - timestamp: TEST_TIMESTAMP.to_string(), - item: RolloutItem::SessionMeta(session_meta), - }; - - let meta_json = serde_json::to_string(&meta_line)?; - file.write_all(format!("{meta_json}\n").as_bytes()).await?; - Ok(()) -} - -fn parse_config_toml(contents: &str) -> io::Result { - toml::from_str(contents).map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err)) -} - -#[tokio::test] -async fn migration_marker_exists_no_sessions_no_change() -> io::Result<()> { - let temp = TempDir::new()?; - let marker_path = temp.path().join(PERSONALITY_MIGRATION_FILENAME); - tokio::fs::write(&marker_path, "v1\n").await?; - - let status = - maybe_migrate_personality(temp.path(), &ConfigToml::default(), /*state_db*/ None).await?; - - assert_eq!(status, PersonalityMigrationStatus::SkippedMarker); - assert_eq!( - tokio::fs::try_exists(temp.path().join("config.toml")).await?, - false - ); - Ok(()) -} - -#[tokio::test] -async fn no_marker_no_sessions_no_change() -> io::Result<()> { - let temp = TempDir::new()?; - - let status = - maybe_migrate_personality(temp.path(), &ConfigToml::default(), /*state_db*/ None).await?; - - assert_eq!(status, PersonalityMigrationStatus::SkippedNoSessions); - assert_eq!( - tokio::fs::try_exists(temp.path().join(PERSONALITY_MIGRATION_FILENAME)).await?, - true - ); - assert_eq!( - tokio::fs::try_exists(temp.path().join("config.toml")).await?, - false - ); - Ok(()) -} - -#[tokio::test] -async fn no_marker_sessions_sets_personality() -> io::Result<()> { - let temp = TempDir::new()?; - write_session_with_user_event(temp.path()).await?; - - let status = - maybe_migrate_personality(temp.path(), &ConfigToml::default(), /*state_db*/ None).await?; - - assert_eq!(status, PersonalityMigrationStatus::Applied); - assert_eq!( - tokio::fs::try_exists(temp.path().join(PERSONALITY_MIGRATION_FILENAME)).await?, - true - ); - - let persisted = read_config_toml(temp.path()).await?; - assert_eq!(persisted.personality, Some(Personality::Pragmatic)); - Ok(()) -} - -#[tokio::test] -async fn no_marker_sessions_preserves_existing_config_fields() -> io::Result<()> { - let temp = TempDir::new()?; - write_session_with_user_event(temp.path()).await?; - tokio::fs::write(temp.path().join("config.toml"), "model = \"gpt-5.4\"\n").await?; - let config_toml = read_config_toml(temp.path()).await?; - - let status = maybe_migrate_personality(temp.path(), &config_toml, /*state_db*/ None).await?; - - assert_eq!(status, PersonalityMigrationStatus::Applied); - let persisted = read_config_toml(temp.path()).await?; - assert_eq!(persisted.model, Some("gpt-5.4".to_string())); - assert_eq!(persisted.personality, Some(Personality::Pragmatic)); - Ok(()) -} - -#[tokio::test] -async fn no_marker_meta_only_rollout_is_treated_as_no_sessions() -> io::Result<()> { - let temp = TempDir::new()?; - write_session_with_meta_only(temp.path()).await?; - - let status = - maybe_migrate_personality(temp.path(), &ConfigToml::default(), /*state_db*/ None).await?; - - assert_eq!(status, PersonalityMigrationStatus::SkippedNoSessions); - assert_eq!( - tokio::fs::try_exists(temp.path().join(PERSONALITY_MIGRATION_FILENAME)).await?, - true - ); - assert_eq!( - tokio::fs::try_exists(temp.path().join("config.toml")).await?, - false - ); - Ok(()) -} - -#[tokio::test] -async fn no_marker_explicit_global_personality_skips_migration() -> io::Result<()> { - let temp = TempDir::new()?; - write_session_with_user_event(temp.path()).await?; - let config_toml = parse_config_toml("personality = \"friendly\"\n")?; - - let status = maybe_migrate_personality(temp.path(), &config_toml, /*state_db*/ None).await?; - - assert_eq!( - status, - PersonalityMigrationStatus::SkippedExplicitPersonality - ); - assert_eq!( - tokio::fs::try_exists(temp.path().join(PERSONALITY_MIGRATION_FILENAME)).await?, - true - ); - assert_eq!( - tokio::fs::try_exists(temp.path().join("config.toml")).await?, - false - ); - Ok(()) -} - -#[tokio::test] -async fn no_marker_profile_personality_does_not_skip_migration() -> io::Result<()> { - let temp = TempDir::new()?; - write_session_with_user_event(temp.path()).await?; - let config_toml = parse_config_toml( - r#" -profile = "work" - -[profiles.work] -personality = "friendly" -"#, - )?; - - let status = maybe_migrate_personality(temp.path(), &config_toml, /*state_db*/ None).await?; - - assert_eq!(status, PersonalityMigrationStatus::Applied); - assert_eq!( - tokio::fs::try_exists(temp.path().join(PERSONALITY_MIGRATION_FILENAME)).await?, - true - ); - assert_eq!( - tokio::fs::try_exists(temp.path().join("config.toml")).await?, - true - ); - let persisted = read_config_toml(temp.path()).await?; - assert_eq!(persisted.personality, Some(Personality::Pragmatic)); - Ok(()) -} - -#[tokio::test] -async fn marker_short_circuits_migration_with_legacy_profile() -> io::Result<()> { - let temp = TempDir::new()?; - tokio::fs::write(temp.path().join(PERSONALITY_MIGRATION_FILENAME), "v1\n").await?; - let config_toml = parse_config_toml("profile = \"missing\"\n")?; - - let status = maybe_migrate_personality(temp.path(), &config_toml, /*state_db*/ None).await?; - - assert_eq!(status, PersonalityMigrationStatus::SkippedMarker); - Ok(()) -} - -#[tokio::test] -async fn missing_legacy_profile_does_not_block_migration() -> io::Result<()> { - let temp = TempDir::new()?; - let config_toml = parse_config_toml("profile = \"missing\"\n")?; - - let status = maybe_migrate_personality(temp.path(), &config_toml, /*state_db*/ None).await?; - - assert_eq!(status, PersonalityMigrationStatus::SkippedNoSessions); - assert_eq!( - tokio::fs::try_exists(temp.path().join(PERSONALITY_MIGRATION_FILENAME)).await?, - true - ); - Ok(()) -} - -#[tokio::test] -async fn applied_migration_is_idempotent_on_second_run() -> io::Result<()> { - let temp = TempDir::new()?; - write_session_with_user_event(temp.path()).await?; - - let first_status = - maybe_migrate_personality(temp.path(), &ConfigToml::default(), /*state_db*/ None).await?; - let second_status = - maybe_migrate_personality(temp.path(), &ConfigToml::default(), /*state_db*/ None).await?; - - assert_eq!(first_status, PersonalityMigrationStatus::Applied); - assert_eq!(second_status, PersonalityMigrationStatus::SkippedMarker); - let persisted = read_config_toml(temp.path()).await?; - assert_eq!(persisted.personality, Some(Personality::Pragmatic)); - Ok(()) -} - -#[tokio::test] -async fn no_marker_archived_sessions_sets_personality() -> io::Result<()> { - let temp = TempDir::new()?; - write_archived_session_with_user_event(temp.path()).await?; - - let status = - maybe_migrate_personality(temp.path(), &ConfigToml::default(), /*state_db*/ None).await?; - - assert_eq!(status, PersonalityMigrationStatus::Applied); - assert_eq!( - tokio::fs::try_exists(temp.path().join(PERSONALITY_MIGRATION_FILENAME)).await?, - true - ); - - let persisted = read_config_toml(temp.path()).await?; - assert_eq!(persisted.personality, Some(Personality::Pragmatic)); - Ok(()) -} diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index dc8262f7db30..a26c36f5f01f 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -1059,7 +1059,7 @@ pub async fn run_main( ..Default::default() }; - let mut config = load_config_or_exit( + let config = load_config_or_exit( cli_kv_overrides.clone(), overrides.clone(), loader_overrides.clone(), @@ -1102,37 +1102,6 @@ pub async fn run_main( let _ = codex_state::install_process_db_telemetry(telemetry); } let state_db = init_state_db_for_app_server_target(&config, &app_server_target).await?; - - let effective_toml = config.config_layer_stack.effective_config(); - match effective_toml.try_into() { - Ok(config_toml) => { - match codex_app_server_client::migrate_personality_if_needed( - &config.codex_home, - &config_toml, - state_db.clone(), - ) - .await - { - Ok(true) => { - config = load_config_or_exit( - cli_kv_overrides.clone(), - overrides.clone(), - loader_overrides.clone(), - cloud_config_bundle.clone(), - strict_config, - ) - .await; - } - Ok(false) => {} - Err(err) => { - tracing::warn!(error = %err, "failed to run personality migration"); - } - } - } - Err(err) => { - tracing::warn!(error = %err, "failed to deserialize config for personality migration"); - } - } let config_toml_log_dir_configured = config .config_layer_stack .effective_config()