use crate::model::ThreadMetadata; use codex_history::RolloutItem; use codex_protocol::items::TurnItem; use codex_protocol::models::ResponseItem; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::SessionMetaLine; use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SubAgentSource; use codex_protocol::protocol::TurnContextItem; use codex_protocol::protocol::UserMessageEvent; use codex_protocol::protocol::strip_user_message_prefix; use codex_protocol::protocol::user_message_preview; use serde::Serialize; use serde_json::Value; pub const GUARDIAN_THREAD_TITLE: &str = "Guardian review"; pub const GUARDIAN_THREAD_PREVIEW: &str = "Approval review"; /// Identifies internal review threads whose user messages are synthetic approval prompts. pub fn is_guardian_review_source(source: &SessionSource) -> bool { matches!( source, SessionSource::SubAgent(SubAgentSource::Other(name)) if name == "guardian" ) } /// Apply a rollout item to the metadata structure. pub fn apply_rollout_item( metadata: &mut ThreadMetadata, item: &RolloutItem, default_provider: &str, ) { match item { RolloutItem::SessionMeta(meta_line) => apply_session_meta_from_item(metadata, meta_line), RolloutItem::TurnContext(turn_ctx) => apply_turn_context(metadata, turn_ctx), RolloutItem::EventMsg(event) => apply_event_msg(metadata, event), RolloutItem::ResponseItem(item) => apply_response_item(metadata, &item.item), RolloutItem::InterAgentCommunication(_) | RolloutItem::InterAgentCommunicationMetadata { .. } => {} RolloutItem::Compacted(_) => {} RolloutItem::WorldState(_) => {} RolloutItem::RetainedContext(_) | RolloutItem::SecurityRiskScore(_) => {} RolloutItem::RealtimeItem(_) => {} RolloutItem::TokenUsageRecord(_) => {} } if metadata.model_provider.is_empty() { metadata.model_provider = default_provider.to_string(); } } /// Return whether this rollout item can mutate thread metadata stored in SQLite. pub fn rollout_item_affects_thread_metadata(item: &RolloutItem) -> bool { match item { RolloutItem::SessionMeta(_) | RolloutItem::TurnContext(_) => true, RolloutItem::EventMsg( EventMsg::TokenCount(_) | EventMsg::UserMessage(_) | EventMsg::ThreadGoalUpdated(_) | EventMsg::ThreadSettingsApplied(_), ) => true, RolloutItem::EventMsg(EventMsg::ItemCompleted(event)) if matches!( event.item, TurnItem::UserMessage(_) | TurnItem::FunctionCallOutput(_) ) => { true } RolloutItem::EventMsg(_) | RolloutItem::ResponseItem(_) | RolloutItem::InterAgentCommunication(_) | RolloutItem::InterAgentCommunicationMetadata { .. } | RolloutItem::Compacted(_) | RolloutItem::RealtimeItem(_) | RolloutItem::RetainedContext(_) | RolloutItem::SecurityRiskScore(_) | RolloutItem::TokenUsageRecord(_) | RolloutItem::WorldState(_) => false, } } fn apply_session_meta_from_item(metadata: &mut ThreadMetadata, meta_line: &SessionMetaLine) { if metadata.id != meta_line.meta.id { // Ignore session_meta lines that don't match the canonical thread ID, // e.g., forked rollouts that embed the source session metadata. return; } metadata.creator_user_id = metadata .creator_user_id .take() .or_else(|| meta_line.meta.creator_user_id.clone()); metadata.creator_account_id = metadata .creator_account_id .take() .or_else(|| meta_line.meta.creator_account_id.clone()); metadata.id = meta_line.meta.id; metadata.attached_thread_parent_id = meta_line .meta .forked_from_id .filter(|_| meta_line.meta.attached_thread); metadata.source = enum_to_string(&meta_line.meta.source); if metadata.originator.is_none() && !meta_line.meta.originator.is_empty() { metadata.originator = Some(meta_line.meta.originator.clone()); } // Later SessionMeta lines do not redefine the canonical history_mode. metadata.thread_source = meta_line.meta.thread_source.clone(); metadata.agent_nickname = meta_line.meta.agent_nickname.clone(); metadata.agent_role = meta_line.meta.agent_role.clone(); metadata.agent_path = meta_line.meta.agent_path.clone(); if let Some(provider) = meta_line.meta.model_provider.as_deref() { metadata.model_provider = provider.to_string(); } if !meta_line.meta.cli_version.is_empty() { metadata.cli_version = meta_line.meta.cli_version.clone(); } if !meta_line.meta.cwd.as_os_str().is_empty() { metadata.cwd = meta_line.meta.cwd.clone(); } if let Some(git) = meta_line.git.as_ref() { metadata.git_sha = git.commit_hash.as_ref().map(|sha| sha.0.clone()); metadata.git_branch = git.branch.clone(); metadata.git_origin_url = git.repository_url.clone(); } } fn apply_turn_context(metadata: &mut ThreadMetadata, turn_ctx: &TurnContextItem) { if metadata.cwd.as_os_str().is_empty() { metadata.cwd = turn_ctx.cwd.clone().into_path_buf(); } metadata.model = Some(turn_ctx.model.clone()); metadata.reasoning_effort = turn_ctx.effort.clone(); metadata.sandbox_policy = serde_json::to_string(&turn_ctx.permission_profile()).unwrap_or_default(); metadata.approval_mode = enum_to_string(&turn_ctx.approval_policy); } fn apply_event_msg(metadata: &mut ThreadMetadata, event: &EventMsg) { match event { EventMsg::TokenCount(token_count) => { if let Some(info) = token_count.info.as_ref() { metadata.tokens_used = info.total_token_usage.total_tokens.max(0); } } EventMsg::UserMessage(user) if !metadata_is_guardian_review(metadata) => { apply_user_message(metadata, user); } EventMsg::ItemCompleted(event) => { if let TurnItem::UserMessage(user) = &event.item && !metadata_is_guardian_review(metadata) { apply_user_message(metadata, &user.as_legacy_user_message_event()); } else if let TurnItem::FunctionCallOutput(output) = &event.item && !metadata_is_guardian_review(metadata) { // Delegated turns have no user message, but must still be discoverable. let preview = crate::delegated_output_preview(output); set_preview_if_empty(metadata, preview); } } EventMsg::ThreadGoalUpdated(event) if !metadata_is_guardian_review(metadata) => { let objective = event.goal.objective.trim(); if !objective.is_empty() { set_preview_if_empty(metadata, Some(objective.to_string())); } } EventMsg::ThreadSettingsApplied(event) => { let settings = &event.thread_settings; metadata.model = Some(settings.model.clone()); metadata.model_provider = settings.model_provider_id.clone(); metadata.reasoning_effort = settings.reasoning_effort.clone(); metadata.cwd = settings.cwd.clone().into_path_buf(); metadata.sandbox_policy = serde_json::to_string(&settings.permission_profile).unwrap_or_default(); metadata.approval_mode = enum_to_string(&settings.approval_policy); } _ => {} } } fn apply_response_item(_metadata: &mut ThreadMetadata, _item: &ResponseItem) {} pub(crate) fn metadata_is_guardian_review(metadata: &ThreadMetadata) -> bool { serde_json::from_str::(metadata.source.as_str()) .as_ref() .is_ok_and(is_guardian_review_source) } fn apply_user_message(metadata: &mut ThreadMetadata, user: &UserMessageEvent) { let preview = user_message_preview(user); if metadata.first_user_message.is_none() { metadata.first_user_message = preview.clone(); } set_preview_if_empty(metadata, preview); if metadata.title.is_empty() { let title = strip_user_message_prefix(user.message.as_str()); if !title.is_empty() { metadata.title = title.to_string(); } } } fn set_preview_if_empty(metadata: &mut ThreadMetadata, preview: Option) { if metadata.preview.is_none() { metadata.preview = preview; } } pub(crate) fn enum_to_string(value: &T) -> String { match serde_json::to_value(value) { Ok(Value::String(s)) => s, Ok(other) => other.to_string(), Err(_) => String::new(), } } #[cfg(test)] mod tests { use super::apply_rollout_item; use super::rollout_item_affects_thread_metadata; use crate::ThreadMetadataBuilder; use crate::model::ThreadMetadata; use chrono::DateTime; use chrono::Utc; use codex_history::RolloutItem; use codex_protocol::ThreadId; use codex_protocol::config_types::ApprovalsReviewer; use codex_protocol::config_types::CollaborationMode; use codex_protocol::config_types::ModeKind; use codex_protocol::config_types::ReasoningSummary; use codex_protocol::config_types::Settings; use codex_protocol::items::TurnItem; use codex_protocol::items::UserMessageItem; use codex_protocol::models::ContentItem; use codex_protocol::models::PermissionProfile; use codex_protocol::models::ResponseItem; use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::ItemCompletedEvent; use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::SessionMeta; use codex_protocol::protocol::SessionMetaLine; use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SubAgentSource; use codex_protocol::protocol::ThreadGoal; use codex_protocol::protocol::ThreadGoalStatus; use codex_protocol::protocol::ThreadGoalUpdatedEvent; use codex_protocol::protocol::ThreadHistoryMode; use codex_protocol::protocol::ThreadSettingsAppliedEvent; use codex_protocol::protocol::ThreadSettingsSnapshot; use codex_protocol::protocol::TurnContextItem; use codex_protocol::protocol::USER_MESSAGE_BEGIN; use codex_protocol::protocol::UserMessageEvent; use codex_protocol::user_input::UserInput; use pretty_assertions::assert_eq; use std::path::PathBuf; use uuid::Uuid; #[test] fn response_item_user_messages_do_not_set_title_or_first_user_message() { let mut metadata = metadata_for_test(); let item = RolloutItem::ResponseItem( ResponseItem::Message { status: None, encrypted_content: None, id: None, role: "user".to_string(), content: vec![ContentItem::InputText { text: "hello from response item".to_string(), }], phase: None, internal_chat_message_metadata_passthrough: None, } .into(), ); apply_rollout_item(&mut metadata, &item, "test-provider"); assert_eq!(metadata.first_user_message, None); assert_eq!(metadata.preview, None); assert_eq!(metadata.title, ""); } #[test] fn event_msg_user_messages_set_title_and_first_user_message() { let mut metadata = metadata_for_test(); let item = RolloutItem::EventMsg(EventMsg::UserMessage(UserMessageEvent { client_id: None, message: format!("{USER_MESSAGE_BEGIN} actual user request"), images: Some(vec![]), local_images: vec![], text_elements: vec![], ..Default::default() })); apply_rollout_item(&mut metadata, &item, "test-provider"); assert_eq!( metadata.first_user_message.as_deref(), Some("actual user request") ); assert_eq!(metadata.preview.as_deref(), Some("actual user request")); assert_eq!(metadata.title, "actual user request"); } #[test] fn delegated_turn_preview_preserves_user_message_metadata() { let mut metadata = metadata_for_test(); let mut expected = metadata.clone(); for text in [" ", "delegated task", "later output"] { let item = RolloutItem::EventMsg(EventMsg::ItemCompleted(ItemCompletedEvent { thread_id: metadata.id, turn_id: "turn-1".to_string(), item: TurnItem::FunctionCallOutput(codex_protocol::items::FunctionCallOutputItem { id: "output-1".to_string(), name: "create_thread".to_string(), namespace: Some("codex_app".to_string()), output: codex_protocol::models::FunctionCallOutputBody::Text(text.to_string()), }), started_at_ms: Some(0), completed_at_ms: 0, })); assert!(rollout_item_affects_thread_metadata(&item)); apply_rollout_item(&mut metadata, &item, "test-provider"); if text == "delegated task" { expected.preview = Some(text.to_string()); } assert_eq!(metadata, expected); } let user_item = RolloutItem::EventMsg(EventMsg::UserMessage(UserMessageEvent { message: "actual user follow-up".to_string(), ..Default::default() })); apply_rollout_item(&mut metadata, &user_item, "test-provider"); expected.first_user_message = Some("actual user follow-up".to_string()); expected.title = "actual user follow-up".to_string(); assert_eq!(metadata, expected); } #[test] fn completed_user_message_items_set_title_and_first_user_message() { let mut metadata = metadata_for_test(); let item = RolloutItem::EventMsg(EventMsg::ItemCompleted(ItemCompletedEvent { thread_id: ThreadId::default(), turn_id: "turn-1".to_string(), item: TurnItem::UserMessage(UserMessageItem::new(&[UserInput::Text { text: format!("{USER_MESSAGE_BEGIN} actual user request"), text_elements: Vec::new(), }])), started_at_ms: Some(0), completed_at_ms: 0, })); apply_rollout_item(&mut metadata, &item, "test-provider"); assert_eq!( metadata.first_user_message.as_deref(), Some("actual user request") ); assert_eq!(metadata.preview.as_deref(), Some("actual user request")); assert_eq!(metadata.title, "actual user request"); } #[test] fn guardian_projection_skips_prompts_and_preserves_custom_names() { let thread_id = ThreadId::new(); let source = SessionSource::SubAgent(SubAgentSource::Other("guardian".to_string())); let mut metadata = ThreadMetadataBuilder::new(thread_id, PathBuf::new(), Utc::now(), source.clone()) .build("test-provider"); let session_meta = RolloutItem::SessionMeta(Box::new(SessionMetaLine { meta: SessionMeta { session_id: thread_id.into(), id: thread_id, source, ..Default::default() }, git: None, })); apply_rollout_item(&mut metadata, &session_meta, "test-provider"); let mut expected = metadata.clone(); expected.title = super::GUARDIAN_THREAD_TITLE.to_string(); expected.name = None; expected.preview = Some(super::GUARDIAN_THREAD_PREVIEW.to_string()); expected.first_user_message = None; let user = UserMessageItem::new(&[UserInput::Text { text: "large synthetic guardian prompt".to_string(), text_elements: Vec::new(), }]); for event in [ EventMsg::UserMessage(user.as_legacy_user_message_event()), EventMsg::ItemCompleted(ItemCompletedEvent { thread_id, turn_id: "turn-1".to_string(), item: TurnItem::UserMessage(user), started_at_ms: Some(0), completed_at_ms: 0, }), ] { apply_rollout_item( &mut metadata, &RolloutItem::EventMsg(event), "test-provider", ); assert_eq!(metadata, expected); } let mut existing = metadata.clone(); existing.title = "Named Guardian review".to_string(); metadata.prefer_existing_explicit_title(&existing); assert_eq!(metadata, existing); metadata.name = Some("Named Guardian review".to_string()); let expected = metadata.clone(); apply_rollout_item(&mut metadata, &session_meta, "test-provider"); assert_eq!(metadata, expected); } #[test] fn event_msg_image_only_user_message_sets_image_placeholder_preview() { let mut metadata = metadata_for_test(); let item = RolloutItem::EventMsg(EventMsg::UserMessage(UserMessageEvent { client_id: None, message: String::new(), images: Some(vec!["https://example.com/image.png".to_string()]), local_images: vec![], text_elements: vec![], ..Default::default() })); apply_rollout_item(&mut metadata, &item, "test-provider"); assert_eq!(metadata.first_user_message.as_deref(), Some("[Image]")); assert_eq!(metadata.preview.as_deref(), Some("[Image]")); assert_eq!(metadata.title, ""); } #[test] fn event_msg_audio_only_user_message_sets_audio_placeholder_preview() { let mut metadata = metadata_for_test(); let item = RolloutItem::EventMsg(EventMsg::UserMessage(UserMessageEvent { client_id: None, message: String::new(), images: None, local_images: vec![], audio: Some(vec!["https://example.com/audio.mp3".to_string()]), local_audio: vec![], text_elements: vec![], ..Default::default() })); apply_rollout_item(&mut metadata, &item, "test-provider"); assert_eq!(metadata.first_user_message.as_deref(), Some("[Audio]")); assert_eq!(metadata.preview.as_deref(), Some("[Audio]")); assert_eq!(metadata.title, ""); } #[test] fn event_msg_blank_user_message_without_images_keeps_first_user_message_empty() { let mut metadata = metadata_for_test(); let item = RolloutItem::EventMsg(EventMsg::UserMessage(UserMessageEvent { client_id: None, message: " ".to_string(), images: Some(vec![]), local_images: vec![], text_elements: vec![], ..Default::default() })); apply_rollout_item(&mut metadata, &item, "test-provider"); assert_eq!(metadata.first_user_message, None); assert_eq!(metadata.preview, None); assert_eq!(metadata.title, ""); } #[test] fn event_msg_thread_goal_sets_preview_only_and_later_user_sets_message_title() { let mut metadata = metadata_for_test(); let goal_item = RolloutItem::EventMsg(EventMsg::ThreadGoalUpdated(ThreadGoalUpdatedEvent { thread_id: metadata.id, turn_id: None, goal: ThreadGoal { thread_id: metadata.id, objective: "optimize the benchmark".to_string(), status: ThreadGoalStatus::Active, token_budget: None, tokens_used: 0, time_used_seconds: 0, created_at: 1, updated_at: 1, }, })); apply_rollout_item(&mut metadata, &goal_item, "test-provider"); assert_eq!(metadata.preview.as_deref(), Some("optimize the benchmark")); assert_eq!(metadata.first_user_message, None); assert_eq!(metadata.title, ""); let user_item = RolloutItem::EventMsg(EventMsg::UserMessage(UserMessageEvent { client_id: None, message: format!("{USER_MESSAGE_BEGIN} next normal prompt"), images: Some(vec![]), local_images: vec![], text_elements: vec![], ..Default::default() })); apply_rollout_item(&mut metadata, &user_item, "test-provider"); assert_eq!(metadata.preview.as_deref(), Some("optimize the benchmark")); assert_eq!( metadata.first_user_message.as_deref(), Some("next normal prompt") ); assert_eq!(metadata.title, "next normal prompt"); } #[test] fn turn_context_does_not_override_session_cwd() { let mut metadata = metadata_for_test(); metadata.cwd = PathBuf::new(); let thread_id = metadata.id; apply_rollout_item( &mut metadata, &RolloutItem::SessionMeta(Box::new(SessionMetaLine { meta: SessionMeta { creator_user_id: None, attached_thread: false, creator_account_id: None, session_id: thread_id.into(), id: thread_id, forked_from_id: Some( ThreadId::from_string(&Uuid::now_v7().to_string()).expect("thread id"), ), forked_from_ordinal_exclusive: None, parent_thread_id: None, timestamp: "2026-02-26T00:00:00.000Z".to_string(), cwd: PathBuf::from("/child/worktree"), runtime_workspace_roots: None, originator: "codex_cli_rs".to_string(), cli_version: "0.0.0".to_string(), source: SessionSource::Cli, thread_source: None, agent_path: None, agent_nickname: None, agent_role: None, model_provider: Some("openai".to_string()), base_instructions: None, dynamic_tools: None, selected_capability_roots: Vec::new(), memory_mode: None, history_mode: Default::default(), history_base: None, subagent_history_start_ordinal: None, multi_agent_version: None, context_window: None, }, git: None, })), "test-provider", ); apply_rollout_item( &mut metadata, &RolloutItem::TurnContext(TurnContextItem { turn_id: Some("turn-1".to_string()), root_turn_id: None, disabled_plugin_ids: None, cwd: serde_json::from_value(serde_json::json!( std::env::current_dir() .expect("current directory") .join("parent/workspace") )) .expect("absolute parent cwd"), approval_policy: AskForApproval::Never, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, permission_profile: None, active_permission_profile: None, file_system_sandbox_policy: None, model: "gpt-5".to_string(), comp_hash: None, collaboration_mode: None, multi_agent_version: None, realtime_active: None, cyber_access_program: None, effort: None, summary: Some(codex_protocol::config_types::ReasoningSummary::Auto), }), "test-provider", ); assert_eq!(metadata.cwd, PathBuf::from("/child/worktree")); let permission_profile: PermissionProfile = PermissionProfile::Disabled; assert_eq!( metadata.sandbox_policy, serde_json::to_string(&permission_profile).expect("serialize permission profile") ); assert_eq!(metadata.approval_mode, "never"); } #[test] fn turn_context_sets_permission_profile_metadata() { let mut metadata = metadata_for_test(); let permission_profile = PermissionProfile::workspace_write(); apply_rollout_item( &mut metadata, &RolloutItem::TurnContext(TurnContextItem { turn_id: Some("turn-1".to_string()), root_turn_id: None, disabled_plugin_ids: None, cwd: serde_json::from_value(serde_json::json!( std::env::current_dir() .expect("current directory") .join("workspace") )) .expect("absolute workspace cwd"), approval_policy: AskForApproval::OnRequest, approvals_reviewer: None, sandbox_policy: SandboxPolicy::DangerFullAccess, permission_profile: Some(permission_profile.clone()), active_permission_profile: None, file_system_sandbox_policy: None, model: "gpt-5".to_string(), comp_hash: None, collaboration_mode: None, multi_agent_version: None, realtime_active: None, cyber_access_program: None, effort: None, summary: Some(codex_protocol::config_types::ReasoningSummary::Auto), }), "test-provider", ); assert_eq!( metadata.sandbox_policy, serde_json::to_string(&permission_profile).expect("serialize permission profile") ); } #[test] fn turn_context_sets_cwd_when_session_cwd_missing() { let mut metadata = metadata_for_test(); metadata.cwd = PathBuf::new(); let fallback_cwd = std::env::current_dir() .expect("current directory") .join("fallback/workspace"); apply_rollout_item( &mut metadata, &RolloutItem::TurnContext(TurnContextItem { turn_id: Some("turn-1".to_string()), root_turn_id: None, disabled_plugin_ids: None, cwd: serde_json::from_value(serde_json::json!(&fallback_cwd)) .expect("absolute fallback cwd"), approval_policy: AskForApproval::OnRequest, approvals_reviewer: None, sandbox_policy: SandboxPolicy::new_read_only_policy(), permission_profile: None, active_permission_profile: None, file_system_sandbox_policy: None, model: "gpt-5".to_string(), comp_hash: None, collaboration_mode: None, multi_agent_version: None, realtime_active: None, cyber_access_program: None, effort: Some(ReasoningEffort::High), summary: Some(codex_protocol::config_types::ReasoningSummary::Auto), }), "test-provider", ); assert_eq!(metadata.cwd, fallback_cwd); } #[test] fn turn_context_sets_model_and_reasoning_effort() { let mut metadata = metadata_for_test(); apply_rollout_item( &mut metadata, &RolloutItem::TurnContext(TurnContextItem { turn_id: Some("turn-1".to_string()), root_turn_id: None, disabled_plugin_ids: None, cwd: serde_json::from_value(serde_json::json!( std::env::current_dir() .expect("current directory") .join("fallback/workspace") )) .expect("absolute fallback cwd"), approval_policy: AskForApproval::OnRequest, approvals_reviewer: None, sandbox_policy: SandboxPolicy::new_read_only_policy(), permission_profile: None, active_permission_profile: None, file_system_sandbox_policy: None, model: "gpt-5".to_string(), comp_hash: None, collaboration_mode: None, multi_agent_version: None, realtime_active: None, cyber_access_program: None, effort: Some(ReasoningEffort::High), summary: Some(codex_protocol::config_types::ReasoningSummary::Auto), }), "test-provider", ); assert_eq!(metadata.model.as_deref(), Some("gpt-5")); assert_eq!(metadata.reasoning_effort, Some(ReasoningEffort::High)); } #[test] fn thread_settings_applied_updates_resume_metadata() { let mut metadata = metadata_for_test(); let permission_profile = PermissionProfile::workspace_write(); let cwd = std::env::current_dir() .expect("current directory") .join("updated/workspace"); let item = RolloutItem::EventMsg(EventMsg::ThreadSettingsApplied( ThreadSettingsAppliedEvent { thread_id: None, thread_settings: ThreadSettingsSnapshot { disabled_plugin_ids: Vec::new(), model: "gpt-5.2-codex".to_string(), model_provider_id: "updated-provider".to_string(), service_tier: None, approval_policy: AskForApproval::Never, approvals_reviewer: ApprovalsReviewer::User, permission_profile: permission_profile.clone(), active_permission_profile: None, cwd: cwd.clone().try_into().expect("absolute settings cwd"), runtime_workspace_roots: None, reasoning_effort: Some(ReasoningEffort::Ultra), reasoning_summary: Some(ReasoningSummary::Auto), personality: None, collaboration_mode: CollaborationMode { mode: ModeKind::Default, settings: Settings { model: "gpt-5.2-codex".to_string(), reasoning_effort: Some(ReasoningEffort::Ultra), developer_instructions: None, }, }, }, }, )); assert!(rollout_item_affects_thread_metadata(&item)); apply_rollout_item(&mut metadata, &item, "test-provider"); assert_eq!(metadata.model.as_deref(), Some("gpt-5.2-codex")); assert_eq!(metadata.model_provider, "updated-provider"); assert_eq!(metadata.reasoning_effort, Some(ReasoningEffort::Ultra)); assert_eq!(metadata.cwd, cwd); assert_eq!(metadata.approval_mode, "never"); assert_eq!( metadata.sandbox_policy, serde_json::to_string(&permission_profile) .expect("permission profile should serialize") ); } #[test] fn session_meta_does_not_set_model_or_reasoning_effort() { let mut metadata = metadata_for_test(); metadata.history_mode = ThreadHistoryMode::Paginated; let thread_id = metadata.id; apply_rollout_item( &mut metadata, &RolloutItem::SessionMeta(Box::new(SessionMetaLine { meta: SessionMeta { creator_user_id: None, attached_thread: false, creator_account_id: None, session_id: thread_id.into(), id: thread_id, forked_from_id: None, forked_from_ordinal_exclusive: None, parent_thread_id: None, timestamp: "2026-02-26T00:00:00.000Z".to_string(), cwd: PathBuf::from("/workspace"), runtime_workspace_roots: None, originator: "codex_cli_rs".to_string(), cli_version: "0.0.0".to_string(), source: SessionSource::Cli, thread_source: None, agent_path: None, agent_nickname: None, agent_role: None, model_provider: Some("openai".to_string()), base_instructions: None, dynamic_tools: None, selected_capability_roots: Vec::new(), memory_mode: None, history_mode: ThreadHistoryMode::Legacy, history_base: None, subagent_history_start_ordinal: None, multi_agent_version: None, context_window: None, }, git: None, })), "test-provider", ); assert_eq!(metadata.model, None); assert_eq!(metadata.reasoning_effort, None); assert_eq!(metadata.history_mode, ThreadHistoryMode::Paginated); } fn metadata_for_test() -> ThreadMetadata { let id = ThreadId::from_string(&Uuid::from_u128(42).to_string()).expect("thread id"); let created_at = DateTime::::from_timestamp(1_735_689_600, 0).expect("timestamp"); ThreadMetadata { creator_user_id: None, attached_thread_parent_id: None, creator_account_id: None, originator: None, id, rollout_path: PathBuf::from("/tmp/a.jsonl"), created_at, updated_at: created_at, recency_at: created_at, source: "cli".to_string(), history_mode: Default::default(), thread_source: None, agent_path: None, agent_nickname: None, agent_role: None, model_provider: "openai".to_string(), model: None, reasoning_effort: None, cwd: PathBuf::from("/tmp"), cli_version: "0.0.0".to_string(), title: String::new(), name: None, preview: None, sandbox_policy: "read-only".to_string(), approval_mode: "on-request".to_string(), tokens_used: 1, first_user_message: None, archived_at: None, section: None, section_position: None, section_entered_at: None, project_id: None, daybreak_enabled: None, git_sha: None, git_branch: None, git_origin_url: None, } } #[test] fn diff_fields_detects_changes() { let mut base = metadata_for_test(); base.id = ThreadId::from_string(&Uuid::now_v7().to_string()).expect("thread id"); base.title = "hello".to_string(); let mut other = base.clone(); other.tokens_used = 2; other.title = "world".to_string(); let diffs = base.diff_fields(&other); assert_eq!(diffs, vec!["title", "tokens_used"]); } }