diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index e9e71140a..ed8207e24 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -569,7 +569,7 @@ client_notification_definitions! { mod tests { use super::*; use anyhow::Result; - use codex_protocol::ConversationId; + use codex_protocol::ThreadId; use codex_protocol::account::PlanType; use codex_protocol::parse_command::ParsedCommand; use codex_protocol::protocol::AskForApproval; @@ -618,7 +618,7 @@ mod tests { #[test] fn conversation_id_serializes_as_plain_string() -> Result<()> { - let id = ConversationId::from_string("67e55044-10b1-426f-9247-bb680e5fe0c8")?; + let id = ThreadId::from_string("67e55044-10b1-426f-9247-bb680e5fe0c8")?; assert_eq!( json!("67e55044-10b1-426f-9247-bb680e5fe0c8"), @@ -629,11 +629,10 @@ mod tests { #[test] fn conversation_id_deserializes_from_plain_string() -> Result<()> { - let id: ConversationId = - serde_json::from_value(json!("67e55044-10b1-426f-9247-bb680e5fe0c8"))?; + let id: ThreadId = serde_json::from_value(json!("67e55044-10b1-426f-9247-bb680e5fe0c8"))?; assert_eq!( - ConversationId::from_string("67e55044-10b1-426f-9247-bb680e5fe0c8")?, + ThreadId::from_string("67e55044-10b1-426f-9247-bb680e5fe0c8")?, id, ); Ok(()) @@ -654,7 +653,7 @@ mod tests { #[test] fn serialize_server_request() -> Result<()> { - let conversation_id = ConversationId::from_string("67e55044-10b1-426f-9247-bb680e5fe0c8")?; + let conversation_id = ThreadId::from_string("67e55044-10b1-426f-9247-bb680e5fe0c8")?; let params = v1::ExecCommandApprovalParams { conversation_id, call_id: "call-42".to_string(), diff --git a/codex-rs/app-server-protocol/src/protocol/v1.rs b/codex-rs/app-server-protocol/src/protocol/v1.rs index 8aad35e41..981ab28d1 100644 --- a/codex-rs/app-server-protocol/src/protocol/v1.rs +++ b/codex-rs/app-server-protocol/src/protocol/v1.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use std::path::PathBuf; -use codex_protocol::ConversationId; +use codex_protocol::ThreadId; use codex_protocol::config_types::ForcedLoginMethod; use codex_protocol::config_types::ReasoningSummary; use codex_protocol::config_types::SandboxMode; @@ -68,7 +68,7 @@ pub struct NewConversationParams { #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] pub struct NewConversationResponse { - pub conversation_id: ConversationId, + pub conversation_id: ThreadId, pub model: String, pub reasoning_effort: Option, pub rollout_path: PathBuf, @@ -77,7 +77,7 @@ pub struct NewConversationResponse { #[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS)] #[serde(rename_all = "camelCase")] pub struct ResumeConversationResponse { - pub conversation_id: ConversationId, + pub conversation_id: ThreadId, pub model: String, pub initial_messages: Option>, pub rollout_path: PathBuf, @@ -90,9 +90,9 @@ pub enum GetConversationSummaryParams { #[serde(rename = "rolloutPath")] rollout_path: PathBuf, }, - ConversationId { + ThreadId { #[serde(rename = "conversationId")] - conversation_id: ConversationId, + conversation_id: ThreadId, }, } @@ -113,7 +113,7 @@ pub struct ListConversationsParams { #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] pub struct ConversationSummary { - pub conversation_id: ConversationId, + pub conversation_id: ThreadId, pub path: PathBuf, pub preview: String, pub timestamp: Option, @@ -143,7 +143,7 @@ pub struct ListConversationsResponse { #[serde(rename_all = "camelCase")] pub struct ResumeConversationParams { pub path: Option, - pub conversation_id: Option, + pub conversation_id: Option, pub history: Option>, pub overrides: Option, } @@ -158,7 +158,7 @@ pub struct AddConversationSubscriptionResponse { #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] pub struct ArchiveConversationParams { - pub conversation_id: ConversationId, + pub conversation_id: ThreadId, pub rollout_path: PathBuf, } @@ -198,7 +198,7 @@ pub struct GitDiffToRemoteResponse { #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] pub struct ApplyPatchApprovalParams { - pub conversation_id: ConversationId, + pub conversation_id: ThreadId, /// Use to correlate this with [codex_core::protocol::PatchApplyBeginEvent] /// and [codex_core::protocol::PatchApplyEndEvent]. pub call_id: String, @@ -219,7 +219,7 @@ pub struct ApplyPatchApprovalResponse { #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] pub struct ExecCommandApprovalParams { - pub conversation_id: ConversationId, + pub conversation_id: ThreadId, /// Use to correlate this with [codex_core::protocol::ExecCommandBeginEvent] /// and [codex_core::protocol::ExecCommandEndEvent]. pub call_id: String, @@ -369,14 +369,14 @@ pub struct SandboxSettings { #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] pub struct SendUserMessageParams { - pub conversation_id: ConversationId, + pub conversation_id: ThreadId, pub items: Vec, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] pub struct SendUserTurnParams { - pub conversation_id: ConversationId, + pub conversation_id: ThreadId, pub items: Vec, pub cwd: PathBuf, pub approval_policy: AskForApproval, @@ -395,7 +395,7 @@ pub struct SendUserTurnResponse {} #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] pub struct InterruptConversationParams { - pub conversation_id: ConversationId, + pub conversation_id: ThreadId, } #[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS)] @@ -411,7 +411,7 @@ pub struct SendUserMessageResponse {} #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] pub struct AddConversationListenerParams { - pub conversation_id: ConversationId, + pub conversation_id: ThreadId, #[serde(default)] pub experimental_raw_events: bool, } @@ -445,7 +445,7 @@ pub struct LoginChatGptCompleteNotification { #[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS)] #[serde(rename_all = "camelCase")] pub struct SessionConfiguredNotification { - pub session_id: ConversationId, + pub session_id: ThreadId, pub model: String, pub reasoning_effort: Option, pub history_log_id: u64, diff --git a/codex-rs/app-server-test-client/src/main.rs b/codex-rs/app-server-test-client/src/main.rs index 526558250..389961cb0 100644 --- a/codex-rs/app-server-test-client/src/main.rs +++ b/codex-rs/app-server-test-client/src/main.rs @@ -52,7 +52,7 @@ use codex_app_server_protocol::TurnStartParams; use codex_app_server_protocol::TurnStartResponse; use codex_app_server_protocol::TurnStatus; use codex_app_server_protocol::UserInput as V2UserInput; -use codex_protocol::ConversationId; +use codex_protocol::ThreadId; use codex_protocol::protocol::Event; use codex_protocol::protocol::EventMsg; use serde::Serialize; @@ -176,7 +176,7 @@ fn send_message(codex_bin: &str, config_overrides: &[String], user_message: Stri let initialize = client.initialize()?; println!("< initialize response: {initialize:?}"); - let conversation = client.new_conversation()?; + let conversation = client.start_thread()?; println!("< newConversation response: {conversation:?}"); let subscription = client.add_conversation_listener(&conversation.conversation_id)?; @@ -187,7 +187,7 @@ fn send_message(codex_bin: &str, config_overrides: &[String], user_message: Stri client.stream_conversation(&conversation.conversation_id)?; - client.remove_conversation_listener(subscription.subscription_id)?; + client.remove_thread_listener(subscription.subscription_id)?; Ok(()) } @@ -416,7 +416,7 @@ impl CodexClient { self.send_request(request, request_id, "initialize") } - fn new_conversation(&mut self) -> Result { + fn start_thread(&mut self) -> Result { let request_id = self.request_id(); let request = ClientRequest::NewConversation { request_id: request_id.clone(), @@ -428,7 +428,7 @@ impl CodexClient { fn add_conversation_listener( &mut self, - conversation_id: &ConversationId, + conversation_id: &ThreadId, ) -> Result { let request_id = self.request_id(); let request = ClientRequest::AddConversationListener { @@ -442,7 +442,7 @@ impl CodexClient { self.send_request(request, request_id, "addConversationListener") } - fn remove_conversation_listener(&mut self, subscription_id: Uuid) -> Result<()> { + fn remove_thread_listener(&mut self, subscription_id: Uuid) -> Result<()> { let request_id = self.request_id(); let request = ClientRequest::RemoveConversationListener { request_id: request_id.clone(), @@ -460,7 +460,7 @@ impl CodexClient { fn send_user_message( &mut self, - conversation_id: &ConversationId, + conversation_id: &ThreadId, message: &str, ) -> Result { let request_id = self.request_id(); @@ -527,7 +527,7 @@ impl CodexClient { self.send_request(request, request_id, "model/list") } - fn stream_conversation(&mut self, conversation_id: &ConversationId) -> Result<()> { + fn stream_conversation(&mut self, conversation_id: &ThreadId) -> Result<()> { loop { let notification = self.next_notification()?; @@ -664,7 +664,7 @@ impl CodexClient { fn extract_event( &self, notification: JSONRPCNotification, - conversation_id: &ConversationId, + conversation_id: &ThreadId, ) -> Result> { let params = notification .params @@ -678,7 +678,7 @@ impl CodexClient { let conversation_value = map .remove("conversationId") .context("event missing conversationId")?; - let notification_conversation: ConversationId = serde_json::from_value(conversation_value) + let notification_conversation: ThreadId = serde_json::from_value(conversation_value) .context("conversationId was not a valid UUID")?; if ¬ification_conversation != conversation_id { diff --git a/codex-rs/app-server/src/bespoke_event_handling.rs b/codex-rs/app-server/src/bespoke_event_handling.rs index c9b78fe8c..8fb06ca2e 100644 --- a/codex-rs/app-server/src/bespoke_event_handling.rs +++ b/codex-rs/app-server/src/bespoke_event_handling.rs @@ -59,7 +59,7 @@ use codex_app_server_protocol::TurnPlanStep; use codex_app_server_protocol::TurnPlanUpdatedNotification; use codex_app_server_protocol::TurnStatus; use codex_app_server_protocol::build_turns_from_event_msgs; -use codex_core::CodexConversation; +use codex_core::CodexThread; use codex_core::parse_command::shlex_join; use codex_core::protocol::ApplyPatchApprovalRequestEvent; use codex_core::protocol::CodexErrorInfo as CoreCodexErrorInfo; @@ -76,7 +76,7 @@ use codex_core::protocol::TokenCountEvent; use codex_core::protocol::TurnDiffEvent; use codex_core::review_format::format_review_findings_block; use codex_core::review_prompts; -use codex_protocol::ConversationId; +use codex_protocol::ThreadId; use codex_protocol::plan_tool::UpdatePlanArgs; use codex_protocol::protocol::ReviewOutputEvent; use std::collections::HashMap; @@ -91,8 +91,8 @@ type JsonValue = serde_json::Value; #[allow(clippy::too_many_arguments)] pub(crate) async fn apply_bespoke_event_handling( event: Event, - conversation_id: ConversationId, - conversation: Arc, + conversation_id: ThreadId, + conversation: Arc, outgoing: Arc, pending_interrupts: PendingInterrupts, pending_rollbacks: PendingRollbacks, @@ -801,7 +801,7 @@ pub(crate) async fn apply_bespoke_event_handling( } async fn handle_turn_diff( - conversation_id: ConversationId, + conversation_id: ThreadId, event_turn_id: &str, turn_diff_event: TurnDiffEvent, api_version: ApiVersion, @@ -820,7 +820,7 @@ async fn handle_turn_diff( } async fn handle_turn_plan_update( - conversation_id: ConversationId, + conversation_id: ThreadId, event_turn_id: &str, plan_update_event: UpdatePlanArgs, api_version: ApiVersion, @@ -844,7 +844,7 @@ async fn handle_turn_plan_update( } async fn emit_turn_completed_with_status( - conversation_id: ConversationId, + conversation_id: ThreadId, event_turn_id: String, status: TurnStatus, error: Option, @@ -865,7 +865,7 @@ async fn emit_turn_completed_with_status( } async fn complete_file_change_item( - conversation_id: ConversationId, + conversation_id: ThreadId, item_id: String, changes: Vec, status: PatchApplyStatus, @@ -897,7 +897,7 @@ async fn complete_file_change_item( #[allow(clippy::too_many_arguments)] async fn complete_command_execution_item( - conversation_id: ConversationId, + conversation_id: ThreadId, turn_id: String, item_id: String, command: String, @@ -930,7 +930,7 @@ async fn complete_command_execution_item( async fn maybe_emit_raw_response_item_completed( api_version: ApiVersion, - conversation_id: ConversationId, + conversation_id: ThreadId, turn_id: &str, item: codex_protocol::models::ResponseItem, outgoing: &OutgoingMessageSender, @@ -950,7 +950,7 @@ async fn maybe_emit_raw_response_item_completed( } async fn find_and_remove_turn_summary( - conversation_id: ConversationId, + conversation_id: ThreadId, turn_summary_store: &TurnSummaryStore, ) -> TurnSummary { let mut map = turn_summary_store.lock().await; @@ -958,7 +958,7 @@ async fn find_and_remove_turn_summary( } async fn handle_turn_complete( - conversation_id: ConversationId, + conversation_id: ThreadId, event_turn_id: String, outgoing: &OutgoingMessageSender, turn_summary_store: &TurnSummaryStore, @@ -974,7 +974,7 @@ async fn handle_turn_complete( } async fn handle_turn_interrupted( - conversation_id: ConversationId, + conversation_id: ThreadId, event_turn_id: String, outgoing: &OutgoingMessageSender, turn_summary_store: &TurnSummaryStore, @@ -992,7 +992,7 @@ async fn handle_turn_interrupted( } async fn handle_thread_rollback_failed( - conversation_id: ConversationId, + conversation_id: ThreadId, message: String, pending_rollbacks: &PendingRollbacks, outgoing: &OutgoingMessageSender, @@ -1017,7 +1017,7 @@ async fn handle_thread_rollback_failed( } async fn handle_token_count_event( - conversation_id: ConversationId, + conversation_id: ThreadId, turn_id: String, token_count_event: TokenCountEvent, outgoing: &OutgoingMessageSender, @@ -1045,7 +1045,7 @@ async fn handle_token_count_event( } async fn handle_error( - conversation_id: ConversationId, + conversation_id: ThreadId, error: TurnError, turn_summary_store: &TurnSummaryStore, ) { @@ -1056,7 +1056,7 @@ async fn handle_error( async fn on_patch_approval_response( event_turn_id: String, receiver: oneshot::Receiver, - codex: Arc, + codex: Arc, ) { let response = receiver.await; let value = match response { @@ -1098,7 +1098,7 @@ async fn on_patch_approval_response( async fn on_exec_approval_response( event_turn_id: String, receiver: oneshot::Receiver, - conversation: Arc, + conversation: Arc, ) { let response = receiver.await; let value = match response { @@ -1196,11 +1196,11 @@ fn format_file_change_diff(change: &CoreFileChange) -> String { #[allow(clippy::too_many_arguments)] async fn on_file_change_request_approval_response( event_turn_id: String, - conversation_id: ConversationId, + conversation_id: ThreadId, item_id: String, changes: Vec, receiver: oneshot::Receiver, - codex: Arc, + codex: Arc, outgoing: Arc, turn_summary_store: TurnSummaryStore, ) { @@ -1265,13 +1265,13 @@ async fn on_file_change_request_approval_response( #[allow(clippy::too_many_arguments)] async fn on_command_execution_request_approval_response( event_turn_id: String, - conversation_id: ConversationId, + conversation_id: ThreadId, item_id: String, command: String, cwd: PathBuf, command_actions: Vec, receiver: oneshot::Receiver, - conversation: Arc, + conversation: Arc, outgoing: Arc, ) { let response = receiver.await; @@ -1444,7 +1444,7 @@ mod tests { #[tokio::test] async fn test_handle_error_records_message() -> Result<()> { - let conversation_id = ConversationId::new(); + let conversation_id = ThreadId::new(); let turn_summary_store = new_turn_summary_store(); handle_error( @@ -1472,7 +1472,7 @@ mod tests { #[tokio::test] async fn test_handle_turn_complete_emits_completed_without_error() -> Result<()> { - let conversation_id = ConversationId::new(); + let conversation_id = ThreadId::new(); let event_turn_id = "complete1".to_string(); let (tx, mut rx) = mpsc::channel(CHANNEL_CAPACITY); let outgoing = Arc::new(OutgoingMessageSender::new(tx)); @@ -1504,7 +1504,7 @@ mod tests { #[tokio::test] async fn test_handle_turn_interrupted_emits_interrupted_with_error() -> Result<()> { - let conversation_id = ConversationId::new(); + let conversation_id = ThreadId::new(); let event_turn_id = "interrupt1".to_string(); let turn_summary_store = new_turn_summary_store(); handle_error( @@ -1546,7 +1546,7 @@ mod tests { #[tokio::test] async fn test_handle_turn_complete_emits_failed_with_error() -> Result<()> { - let conversation_id = ConversationId::new(); + let conversation_id = ThreadId::new(); let event_turn_id = "complete_err1".to_string(); let turn_summary_store = new_turn_summary_store(); handle_error( @@ -1611,7 +1611,7 @@ mod tests { ], }; - let conversation_id = ConversationId::new(); + let conversation_id = ThreadId::new(); handle_turn_plan_update( conversation_id, @@ -1645,7 +1645,7 @@ mod tests { #[tokio::test] async fn test_handle_token_count_event_emits_usage_and_rate_limits() -> Result<()> { - let conversation_id = ConversationId::new(); + let conversation_id = ThreadId::new(); let turn_id = "turn-123".to_string(); let (tx, mut rx) = mpsc::channel(CHANNEL_CAPACITY); let outgoing = Arc::new(OutgoingMessageSender::new(tx)); @@ -1730,7 +1730,7 @@ mod tests { #[tokio::test] async fn test_handle_token_count_event_without_usage_info() -> Result<()> { - let conversation_id = ConversationId::new(); + let conversation_id = ThreadId::new(); let turn_id = "turn-456".to_string(); let (tx, mut rx) = mpsc::channel(CHANNEL_CAPACITY); let outgoing = Arc::new(OutgoingMessageSender::new(tx)); @@ -1764,7 +1764,7 @@ mod tests { }, }; - let thread_id = ConversationId::new().to_string(); + let thread_id = ThreadId::new().to_string(); let turn_id = "turn_1".to_string(); let notification = construct_mcp_tool_call_notification( begin_event.clone(), @@ -1794,8 +1794,8 @@ mod tests { #[tokio::test] async fn test_handle_turn_complete_emits_error_multiple_turns() -> Result<()> { // Conversation A will have two turns; Conversation B will have one turn. - let conversation_a = ConversationId::new(); - let conversation_b = ConversationId::new(); + let conversation_a = ThreadId::new(); + let conversation_b = ThreadId::new(); let turn_summary_store = new_turn_summary_store(); let (tx, mut rx) = mpsc::channel(CHANNEL_CAPACITY); @@ -1922,7 +1922,7 @@ mod tests { }, }; - let thread_id = ConversationId::new().to_string(); + let thread_id = ThreadId::new().to_string(); let turn_id = "turn_2".to_string(); let notification = construct_mcp_tool_call_notification( begin_event.clone(), @@ -1973,7 +1973,7 @@ mod tests { result: Ok(result), }; - let thread_id = ConversationId::new().to_string(); + let thread_id = ThreadId::new().to_string(); let turn_id = "turn_3".to_string(); let notification = construct_mcp_tool_call_end_notification( end_event.clone(), @@ -2016,7 +2016,7 @@ mod tests { result: Err("boom".to_string()), }; - let thread_id = ConversationId::new().to_string(); + let thread_id = ThreadId::new().to_string(); let turn_id = "turn_4".to_string(); let notification = construct_mcp_tool_call_end_notification( end_event.clone(), @@ -2050,7 +2050,7 @@ mod tests { let (tx, mut rx) = mpsc::channel(CHANNEL_CAPACITY); let outgoing = OutgoingMessageSender::new(tx); let unified_diff = "--- a\n+++ b\n".to_string(); - let conversation_id = ConversationId::new(); + let conversation_id = ThreadId::new(); handle_turn_diff( conversation_id, @@ -2085,7 +2085,7 @@ mod tests { async fn test_handle_turn_diff_is_noop_for_v1() -> Result<()> { let (tx, mut rx) = mpsc::channel(CHANNEL_CAPACITY); let outgoing = OutgoingMessageSender::new(tx); - let conversation_id = ConversationId::new(); + let conversation_id = ThreadId::new(); handle_turn_diff( conversation_id, diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 1cadd5e4f..3b0e4a9db 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -108,14 +108,14 @@ use codex_app_server_protocol::UserSavedConfig; use codex_app_server_protocol::build_turns_from_event_msgs; use codex_backend_client::Client as BackendClient; use codex_core::AuthManager; -use codex_core::CodexConversation; -use codex_core::ConversationManager; +use codex_core::CodexThread; use codex_core::Cursor as RolloutCursor; use codex_core::INTERACTIVE_SESSION_SOURCES; use codex_core::InitialHistory; -use codex_core::NewConversation; +use codex_core::NewThread; use codex_core::RolloutRecorder; use codex_core::SessionMeta; +use codex_core::ThreadManager; use codex_core::auth::CLIENT_ID; use codex_core::auth::login_with_api_key; use codex_core::config::Config; @@ -127,7 +127,7 @@ use codex_core::default_client::get_codex_user_agent; use codex_core::exec::ExecParams; use codex_core::exec_env::create_env; use codex_core::features::Feature; -use codex_core::find_conversation_path_by_id_str; +use codex_core::find_thread_path_by_id_str; use codex_core::git_info::git_diff_to_remote; use codex_core::mcp::collect_mcp_snapshot; use codex_core::mcp::group_tools_by_server; @@ -144,7 +144,7 @@ use codex_feedback::CodexFeedback; use codex_login::ServerOptions as LoginServerOptions; use codex_login::ShutdownHandle; use codex_login::run_login_server; -use codex_protocol::ConversationId; +use codex_protocol::ThreadId; use codex_protocol::config_types::ForcedLoginMethod; use codex_protocol::items::TurnItem; use codex_protocol::models::ResponseItem; @@ -177,9 +177,9 @@ use tracing::warn; use uuid::Uuid; type PendingInterruptQueue = Vec<(RequestId, ApiVersion)>; -pub(crate) type PendingInterrupts = Arc>>; +pub(crate) type PendingInterrupts = Arc>>; -pub(crate) type PendingRollbacks = Arc>>; +pub(crate) type PendingRollbacks = Arc>>; /// Per-conversation accumulation of the latest states e.g. error message while a turn runs. #[derive(Default, Clone)] @@ -188,7 +188,7 @@ pub(crate) struct TurnSummary { pub(crate) last_error: Option, } -pub(crate) type TurnSummaryStore = Arc>>; +pub(crate) type TurnSummaryStore = Arc>>; const THREAD_LIST_DEFAULT_LIMIT: usize = 25; const THREAD_LIST_MAX_LIMIT: usize = 100; @@ -211,10 +211,10 @@ impl Drop for ActiveLogin { } } -/// Handles JSON-RPC messages for Codex conversations. +/// Handles JSON-RPC messages for Codex threads (and legacy conversation APIs). pub(crate) struct CodexMessageProcessor { auth_manager: Arc, - conversation_manager: Arc, + thread_manager: Arc, outgoing: Arc, codex_linux_sandbox_exe: Option, config: Arc, @@ -237,33 +237,32 @@ pub(crate) enum ApiVersion { } impl CodexMessageProcessor { - async fn conversation_from_thread_id( + async fn load_thread( &self, thread_id: &str, - ) -> Result<(ConversationId, Arc), JSONRPCErrorError> { - // Resolve conversation id from v2 thread id string. - let conversation_id = - ConversationId::from_string(thread_id).map_err(|err| JSONRPCErrorError { - code: INVALID_REQUEST_ERROR_CODE, - message: format!("invalid thread id: {err}"), - data: None, - })?; + ) -> Result<(ThreadId, Arc), JSONRPCErrorError> { + // Resolve the core conversation handle from a v2 thread id string. + let thread_id = ThreadId::from_string(thread_id).map_err(|err| JSONRPCErrorError { + code: INVALID_REQUEST_ERROR_CODE, + message: format!("invalid thread id: {err}"), + data: None, + })?; - let conversation = self - .conversation_manager - .get_conversation(conversation_id) + let thread = self + .thread_manager + .get_thread(thread_id) .await .map_err(|_| JSONRPCErrorError { code: INVALID_REQUEST_ERROR_CODE, - message: format!("conversation not found: {conversation_id}"), + message: format!("thread not found: {thread_id}"), data: None, })?; - Ok((conversation_id, conversation)) + Ok((thread_id, thread)) } pub fn new( auth_manager: Arc, - conversation_manager: Arc, + thread_manager: Arc, outgoing: Arc, codex_linux_sandbox_exe: Option, config: Arc, @@ -272,7 +271,7 @@ impl CodexMessageProcessor { ) -> Self { Self { auth_manager, - conversation_manager, + thread_manager, outgoing, codex_linux_sandbox_exe, config, @@ -396,19 +395,18 @@ impl CodexMessageProcessor { self.process_new_conversation(request_id, params).await; } ClientRequest::GetConversationSummary { request_id, params } => { - self.get_conversation_summary(request_id, params).await; + self.get_thread_summary(request_id, params).await; } ClientRequest::ListConversations { request_id, params } => { self.handle_list_conversations(request_id, params).await; } ClientRequest::ModelList { request_id, params } => { let outgoing = self.outgoing.clone(); - let conversation_manager = self.conversation_manager.clone(); + let thread_manager = self.thread_manager.clone(); let config = self.config.clone(); tokio::spawn(async move { - Self::list_models(outgoing, conversation_manager, config, request_id, params) - .await; + Self::list_models(outgoing, thread_manager, config, request_id, params).await; }); } ClientRequest::McpServerOauthLogin { request_id, params } => { @@ -451,7 +449,7 @@ impl CodexMessageProcessor { self.add_conversation_listener(request_id, params).await; } ClientRequest::RemoveConversationListener { request_id, params } => { - self.remove_conversation_listener(request_id, params).await; + self.remove_thread_listener(request_id, params).await; } ClientRequest::GitDiffToRemote { request_id, params } => { self.git_diff_to_origin(request_id, params.cwd).await; @@ -1306,15 +1304,15 @@ impl CodexMessageProcessor { } }; - match self.conversation_manager.new_conversation(config).await { - Ok(conversation_id) => { - let NewConversation { - conversation_id, + match self.thread_manager.start_thread(config).await { + Ok(new_thread) => { + let NewThread { + thread_id, session_configured, .. - } = conversation_id; + } = new_thread; let response = NewConversationResponse { - conversation_id, + conversation_id: thread_id, model: session_configured.model, reasoning_effort: session_configured.reasoning_effort, rollout_path: session_configured.rollout_path, @@ -1359,10 +1357,10 @@ impl CodexMessageProcessor { } }; - match self.conversation_manager.new_conversation(config).await { + match self.thread_manager.start_thread(config).await { Ok(new_conv) => { - let NewConversation { - conversation_id, + let NewThread { + thread_id, session_configured, .. } = new_conv; @@ -1370,7 +1368,7 @@ impl CodexMessageProcessor { let fallback_provider = self.config.model_provider_id.as_str(); // A bit hacky, but the summary contains a lot of useful information for the thread - // that unfortunately does not get returned from conversation_manager.new_conversation(). + // that unfortunately does not get returned from thread_manager.start_thread(). let thread = match read_summary_from_rollout( rollout_path.as_path(), fallback_provider, @@ -1382,7 +1380,7 @@ impl CodexMessageProcessor { self.send_internal_error( request_id, format!( - "failed to load rollout `{}` for conversation {conversation_id}: {err}", + "failed to load rollout `{}` for thread {thread_id}: {err}", rollout_path.display() ), ) @@ -1409,19 +1407,19 @@ impl CodexMessageProcessor { reasoning_effort: session_configured.reasoning_effort, }; - // Auto-attach a conversation listener when starting a thread. + // Auto-attach a thread listener when starting a thread. // Use the same behavior as the v1 API, with opt-in support for raw item events. if let Err(err) = self .attach_conversation_listener( - conversation_id, + thread_id, params.experimental_raw_events, ApiVersion::V2, ) .await { tracing::warn!( - "failed to attach listener for conversation {}: {}", - conversation_id, + "failed to attach listener for thread {}: {}", + thread_id, err.message ); } @@ -1470,7 +1468,7 @@ impl CodexMessageProcessor { } async fn thread_archive(&mut self, request_id: RequestId, params: ThreadArchiveParams) { - let conversation_id = match ConversationId::from_string(¶ms.thread_id) { + let thread_id = match ThreadId::from_string(¶ms.thread_id) { Ok(id) => id, Err(err) => { let error = JSONRPCErrorError { @@ -1483,37 +1481,31 @@ impl CodexMessageProcessor { } }; - let rollout_path = match find_conversation_path_by_id_str( - &self.config.codex_home, - &conversation_id.to_string(), - ) - .await - { - Ok(Some(p)) => p, - Ok(None) => { - let error = JSONRPCErrorError { - code: INVALID_REQUEST_ERROR_CODE, - message: format!("no rollout found for conversation id {conversation_id}"), - data: None, - }; - self.outgoing.send_error(request_id, error).await; - return; - } - Err(err) => { - let error = JSONRPCErrorError { - code: INVALID_REQUEST_ERROR_CODE, - message: format!("failed to locate conversation id {conversation_id}: {err}"), - data: None, - }; - self.outgoing.send_error(request_id, error).await; - return; - } - }; + let rollout_path = + match find_thread_path_by_id_str(&self.config.codex_home, &thread_id.to_string()).await + { + Ok(Some(p)) => p, + Ok(None) => { + let error = JSONRPCErrorError { + code: INVALID_REQUEST_ERROR_CODE, + message: format!("no rollout found for thread id {thread_id}"), + data: None, + }; + self.outgoing.send_error(request_id, error).await; + return; + } + Err(err) => { + let error = JSONRPCErrorError { + code: INVALID_REQUEST_ERROR_CODE, + message: format!("failed to locate thread id {thread_id}: {err}"), + data: None, + }; + self.outgoing.send_error(request_id, error).await; + return; + } + }; - match self - .archive_conversation_common(conversation_id, &rollout_path) - .await - { + match self.archive_thread_common(thread_id, &rollout_path).await { Ok(()) => { let response = ThreadArchiveResponse {}; self.outgoing.send_response(request_id, response).await; @@ -1536,18 +1528,17 @@ impl CodexMessageProcessor { return; } - let (conversation_id, conversation) = - match self.conversation_from_thread_id(&thread_id).await { - Ok(v) => v, - Err(error) => { - self.outgoing.send_error(request_id, error).await; - return; - } - }; + let (thread_id, thread) = match self.load_thread(&thread_id).await { + Ok(v) => v, + Err(error) => { + self.outgoing.send_error(request_id, error).await; + return; + } + }; { let mut map = self.pending_rollbacks.lock().await; - if map.contains_key(&conversation_id) { + if map.contains_key(&thread_id) { self.send_invalid_request_error( request_id, "rollback already in progress for this thread".to_string(), @@ -1556,14 +1547,14 @@ impl CodexMessageProcessor { return; } - map.insert(conversation_id, request_id.clone()); + map.insert(thread_id, request_id.clone()); } - if let Err(err) = conversation.submit(Op::ThreadRollback { num_turns }).await { + if let Err(err) = thread.submit(Op::ThreadRollback { num_turns }).await { // No ThreadRollback event will arrive if an error occurs. // Clean up and reply immediately. let mut map = self.pending_rollbacks.lock().await; - map.remove(&conversation_id); + map.remove(&thread_id); self.send_internal_error(request_id, format!("failed to start rollback: {err}")) .await; @@ -1582,7 +1573,7 @@ impl CodexMessageProcessor { .unwrap_or(THREAD_LIST_DEFAULT_LIMIT) .clamp(1, THREAD_LIST_MAX_LIMIT); let (summaries, next_cursor) = match self - .list_conversations_common(requested_page_size, cursor, model_providers) + .list_threads_common(requested_page_size, cursor, model_providers) .await { Ok(r) => r, @@ -1653,7 +1644,7 @@ impl CodexMessageProcessor { self.config.as_ref().clone() }; - let conversation_history = if let Some(history) = history { + let thread_history = if let Some(history) = history { if history.is_empty() { self.send_invalid_request_error( request_id, @@ -1676,7 +1667,7 @@ impl CodexMessageProcessor { } } } else { - let existing_conversation_id = match ConversationId::from_string(&thread_id) { + let existing_thread_id = match ThreadId::from_string(&thread_id) { Ok(id) => id, Err(err) => { let error = JSONRPCErrorError { @@ -1689,9 +1680,9 @@ impl CodexMessageProcessor { } }; - let path = match find_conversation_path_by_id_str( + let path = match find_thread_path_by_id_str( &self.config.codex_home, - &existing_conversation_id.to_string(), + &existing_thread_id.to_string(), ) .await { @@ -1699,7 +1690,7 @@ impl CodexMessageProcessor { Ok(None) => { self.send_invalid_request_error( request_id, - format!("no rollout found for conversation id {existing_conversation_id}"), + format!("no rollout found for thread id {existing_thread_id}"), ) .await; return; @@ -1707,9 +1698,7 @@ impl CodexMessageProcessor { Err(err) => { self.send_invalid_request_error( request_id, - format!( - "failed to locate conversation id {existing_conversation_id}: {err}" - ), + format!("failed to locate thread id {existing_thread_id}: {err}"), ) .await; return; @@ -1732,16 +1721,12 @@ impl CodexMessageProcessor { let fallback_model_provider = config.model_provider_id.clone(); match self - .conversation_manager - .resume_conversation_with_history( - config, - conversation_history, - self.auth_manager.clone(), - ) + .thread_manager + .resume_thread_with_history(config, thread_history, self.auth_manager.clone()) .await { - Ok(NewConversation { - conversation_id, + Ok(NewThread { + thread_id, session_configured, .. }) => { @@ -1750,14 +1735,14 @@ impl CodexMessageProcessor { initial_messages, .. } = session_configured; - // Auto-attach a conversation listener when resuming a thread. + // Auto-attach a thread listener when resuming a thread. if let Err(err) = self - .attach_conversation_listener(conversation_id, false, ApiVersion::V2) + .attach_conversation_listener(thread_id, false, ApiVersion::V2) .await { tracing::warn!( - "failed to attach listener for conversation {}: {}", - conversation_id, + "failed to attach listener for thread {}: {}", + thread_id, err.message ); } @@ -1773,7 +1758,7 @@ impl CodexMessageProcessor { self.send_internal_error( request_id, format!( - "failed to load rollout `{}` for conversation {conversation_id}: {err}", + "failed to load rollout `{}` for thread {thread_id}: {err}", rollout_path.display() ), ) @@ -1808,7 +1793,7 @@ impl CodexMessageProcessor { } } - async fn get_conversation_summary( + async fn get_thread_summary( &self, request_id: RequestId, params: GetConversationSummaryParams, @@ -1821,8 +1806,8 @@ impl CodexMessageProcessor { rollout_path } } - GetConversationSummaryParams::ConversationId { conversation_id } => { - match codex_core::find_conversation_path_by_id_str( + GetConversationSummaryParams::ThreadId { conversation_id } => { + match codex_core::find_thread_path_by_id_str( &self.config.codex_home, &conversation_id.to_string(), ) @@ -1881,7 +1866,7 @@ impl CodexMessageProcessor { .clamp(1, THREAD_LIST_MAX_LIMIT); match self - .list_conversations_common(requested_page_size, cursor, model_providers) + .list_threads_common(requested_page_size, cursor, model_providers) .await { Ok((items, next_cursor)) => { @@ -1894,7 +1879,7 @@ impl CodexMessageProcessor { }; } - async fn list_conversations_common( + async fn list_threads_common( &self, requested_page_size: usize, cursor: Option, @@ -1920,7 +1905,7 @@ impl CodexMessageProcessor { while remaining > 0 { let page_size = remaining.min(THREAD_LIST_MAX_LIMIT); - let page = RolloutRecorder::list_conversations( + let page = RolloutRecorder::list_threads( &self.config.codex_home, page_size, cursor_obj.as_ref(), @@ -1931,7 +1916,7 @@ impl CodexMessageProcessor { .await .map_err(|err| JSONRPCErrorError { code: INTERNAL_ERROR_CODE, - message: format!("failed to list conversations: {err}"), + message: format!("failed to list threads: {err}"), data: None, })?; @@ -1987,7 +1972,7 @@ impl CodexMessageProcessor { async fn list_models( outgoing: Arc, - conversation_manager: Arc, + thread_manager: Arc, config: Arc, request_id: RequestId, params: ModelListParams, @@ -1995,7 +1980,7 @@ impl CodexMessageProcessor { let ModelListParams { limit, cursor } = params; let mut config = (*config).clone(); config.features.enable(Feature::RemoteModels); - let models = supported_models(conversation_manager, &config).await; + let models = supported_models(thread_manager, &config).await; let total = models.len(); if total == 0 { @@ -2319,7 +2304,7 @@ impl CodexMessageProcessor { } }; - let conversation_history = if let Some(path) = path { + let thread_history = if let Some(path) = path { match RolloutRecorder::get_rollout_history(&path).await { Ok(initial_history) => initial_history, Err(err) => { @@ -2332,11 +2317,8 @@ impl CodexMessageProcessor { } } } else if let Some(conversation_id) = conversation_id { - match find_conversation_path_by_id_str( - &self.config.codex_home, - &conversation_id.to_string(), - ) - .await + match find_thread_path_by_id_str(&self.config.codex_home, &conversation_id.to_string()) + .await { Ok(Some(found_path)) => { match RolloutRecorder::get_rollout_history(&found_path).await { @@ -2388,16 +2370,12 @@ impl CodexMessageProcessor { }; match self - .conversation_manager - .resume_conversation_with_history( - config, - conversation_history, - self.auth_manager.clone(), - ) + .thread_manager + .resume_thread_with_history(config, thread_history, self.auth_manager.clone()) .await { - Ok(NewConversation { - conversation_id, + Ok(NewThread { + thread_id, session_configured, .. }) => { @@ -2418,9 +2396,9 @@ impl CodexMessageProcessor { .initial_messages .map(|msgs| msgs.into_iter().collect()); - // Reply with conversation id + model and initial messages (when present) + // Reply with thread id + model and initial messages (when present) let response = ResumeConversationResponse { - conversation_id, + conversation_id: thread_id, model: session_configured.model.clone(), initial_messages, rollout_path: session_configured.rollout_path.clone(), @@ -2462,32 +2440,26 @@ impl CodexMessageProcessor { params: ArchiveConversationParams, ) { let ArchiveConversationParams { - conversation_id, + conversation_id: thread_id, rollout_path, } = params; - match self - .archive_conversation_common(conversation_id, &rollout_path) - .await - { + match self.archive_thread_common(thread_id, &rollout_path).await { Ok(()) => { - tracing::info!("thread/archive succeeded for {conversation_id}"); + tracing::info!("thread/archive succeeded for {thread_id}"); let response = ArchiveConversationResponse {}; self.outgoing.send_response(request_id, response).await; } Err(err) => { - tracing::warn!( - "thread/archive failed for {conversation_id}: {}", - err.message - ); + tracing::warn!("thread/archive failed for {thread_id}: {}", err.message); self.outgoing.send_error(request_id, err).await; } } } - async fn archive_conversation_common( + async fn archive_thread_common( &mut self, - conversation_id: ConversationId, + thread_id: ThreadId, rollout_path: &Path, ) -> Result<(), JSONRPCErrorError> { // Verify rollout_path is under sessions dir. @@ -2499,7 +2471,7 @@ impl CodexMessageProcessor { return Err(JSONRPCErrorError { code: INTERNAL_ERROR_CODE, message: format!( - "failed to archive conversation: unable to resolve sessions directory: {err}" + "failed to archive thread: unable to resolve sessions directory: {err}" ), data: None, }); @@ -2521,8 +2493,8 @@ impl CodexMessageProcessor { }); }; - // Verify file name matches conversation id. - let required_suffix = format!("{conversation_id}.jsonl"); + // Verify file name matches thread id. + let required_suffix = format!("{thread_id}.jsonl"); let Some(file_name) = canonical_rollout_path.file_name().map(OsStr::to_owned) else { return Err(JSONRPCErrorError { code: INVALID_REQUEST_ERROR_CODE, @@ -2540,20 +2512,16 @@ impl CodexMessageProcessor { return Err(JSONRPCErrorError { code: INVALID_REQUEST_ERROR_CODE, message: format!( - "rollout path `{}` does not match conversation id {conversation_id}", + "rollout path `{}` does not match thread id {thread_id}", rollout_path.display() ), data: None, }); } - // If the conversation is active, request shutdown and wait briefly. - if let Some(conversation) = self - .conversation_manager - .remove_conversation(&conversation_id) - .await - { - info!("conversation {conversation_id} was active; shutting down"); + // If the thread is active, request shutdown and wait briefly. + if let Some(conversation) = self.thread_manager.remove_thread(&thread_id).await { + info!("thread {thread_id} was active; shutting down"); let conversation_clone = conversation.clone(); let notify = Arc::new(tokio::sync::Notify::new()); let notify_clone = notify.clone(); @@ -2588,7 +2556,7 @@ impl CodexMessageProcessor { // Normal shutdown: proceed with archive. } _ = tokio::time::sleep(Duration::from_secs(10)) => { - warn!("conversation {conversation_id} shutdown timed out; proceeding with archive"); + warn!("thread {thread_id} shutdown timed out; proceeding with archive"); // Wake any waiter; use notify_waiters to avoid missing the signal. notify.notify_waiters(); // Perhaps we lost a shutdown race, so let's continue to @@ -2597,7 +2565,7 @@ impl CodexMessageProcessor { } } Err(err) => { - error!("failed to submit Shutdown to conversation {conversation_id}: {err}"); + error!("failed to submit Shutdown to thread {thread_id}: {err}"); notify.notify_waiters(); } } @@ -2617,7 +2585,7 @@ impl CodexMessageProcessor { result.map_err(|err| JSONRPCErrorError { code: INTERNAL_ERROR_CODE, - message: format!("failed to archive conversation: {err}"), + message: format!("failed to archive thread: {err}"), data: None, }) } @@ -2627,11 +2595,7 @@ impl CodexMessageProcessor { conversation_id, items, } = params; - let Ok(conversation) = self - .conversation_manager - .get_conversation(conversation_id) - .await - else { + let Ok(conversation) = self.thread_manager.get_thread(conversation_id).await else { let error = JSONRPCErrorError { code: INVALID_REQUEST_ERROR_CODE, message: format!("conversation not found: {conversation_id}"), @@ -2677,11 +2641,7 @@ impl CodexMessageProcessor { output_schema, } = params; - let Ok(conversation) = self - .conversation_manager - .get_conversation(conversation_id) - .await - else { + let Ok(conversation) = self.thread_manager.get_thread(conversation_id).await else { let error = JSONRPCErrorError { code: INVALID_REQUEST_ERROR_CODE, message: format!("conversation not found: {conversation_id}"), @@ -2726,7 +2686,7 @@ impl CodexMessageProcessor { cwds }; - let skills_manager = self.conversation_manager.skills_manager(); + let skills_manager = self.thread_manager.skills_manager(); let mut data = Vec::new(); for cwd in cwds { let outcome = skills_manager.skills_for_cwd(&cwd, force_reload).await; @@ -2749,11 +2709,7 @@ impl CodexMessageProcessor { params: InterruptConversationParams, ) { let InterruptConversationParams { conversation_id } = params; - let Ok(conversation) = self - .conversation_manager - .get_conversation(conversation_id) - .await - else { + let Ok(conversation) = self.thread_manager.get_thread(conversation_id).await else { let error = JSONRPCErrorError { code: INVALID_REQUEST_ERROR_CODE, message: format!("conversation not found: {conversation_id}"), @@ -2776,7 +2732,7 @@ impl CodexMessageProcessor { } async fn turn_start(&self, request_id: RequestId, params: TurnStartParams) { - let (_, conversation) = match self.conversation_from_thread_id(¶ms.thread_id).await { + let (_, thread) = match self.load_thread(¶ms.thread_id).await { Ok(v) => v, Err(error) => { self.outgoing.send_error(request_id, error).await; @@ -2800,7 +2756,7 @@ impl CodexMessageProcessor { // If any overrides are provided, update the session turn context first. if has_any_overrides { - let _ = conversation + let _ = thread .submit(Op::OverrideTurnContext { cwd: params.cwd, approval_policy: params.approval_policy.map(AskForApproval::to_core), @@ -2813,7 +2769,7 @@ impl CodexMessageProcessor { } // Start the turn by submitting the user input. Return its submission id as turn_id. - let turn_id = conversation + let turn_id = thread .submit(Op::UserInput { items: mapped_items, final_output_json_schema: params.output_schema, @@ -2899,14 +2855,12 @@ impl CodexMessageProcessor { async fn start_inline_review( &self, request_id: &RequestId, - parent_conversation: Arc, + parent_thread: Arc, review_request: ReviewRequest, display_text: &str, parent_thread_id: String, ) -> std::result::Result<(), JSONRPCErrorError> { - let turn_id = parent_conversation - .submit(Op::Review { review_request }) - .await; + let turn_id = parent_thread.submit(Op::Review { review_request }).await; match turn_id { Ok(turn_id) => { @@ -2931,56 +2885,54 @@ impl CodexMessageProcessor { async fn start_detached_review( &mut self, request_id: &RequestId, - parent_conversation_id: ConversationId, + parent_thread_id: ThreadId, review_request: ReviewRequest, display_text: &str, ) -> std::result::Result<(), JSONRPCErrorError> { - let rollout_path = find_conversation_path_by_id_str( - &self.config.codex_home, - &parent_conversation_id.to_string(), - ) - .await - .map_err(|err| JSONRPCErrorError { - code: INTERNAL_ERROR_CODE, - message: format!("failed to locate conversation id {parent_conversation_id}: {err}"), - data: None, - })? - .ok_or_else(|| JSONRPCErrorError { - code: INVALID_REQUEST_ERROR_CODE, - message: format!("no rollout found for conversation id {parent_conversation_id}"), - data: None, - })?; + let rollout_path = + find_thread_path_by_id_str(&self.config.codex_home, &parent_thread_id.to_string()) + .await + .map_err(|err| JSONRPCErrorError { + code: INTERNAL_ERROR_CODE, + message: format!("failed to locate thread id {parent_thread_id}: {err}"), + data: None, + })? + .ok_or_else(|| JSONRPCErrorError { + code: INVALID_REQUEST_ERROR_CODE, + message: format!("no rollout found for thread id {parent_thread_id}"), + data: None, + })?; let mut config = self.config.as_ref().clone(); config.model = Some(self.config.review_model.clone()); - let NewConversation { - conversation_id, - conversation, + let NewThread { + thread_id, + thread: review_thread, session_configured, .. } = self - .conversation_manager - .fork_conversation(usize::MAX, config, rollout_path) + .thread_manager + .fork_thread(usize::MAX, config, rollout_path) .await .map_err(|err| JSONRPCErrorError { code: INTERNAL_ERROR_CODE, - message: format!("error creating detached review conversation: {err}"), + message: format!("error creating detached review thread: {err}"), data: None, })?; if let Err(err) = self - .attach_conversation_listener(conversation_id, false, ApiVersion::V2) + .attach_conversation_listener(thread_id, false, ApiVersion::V2) .await { tracing::warn!( - "failed to attach listener for review conversation {}: {}", - conversation_id, + "failed to attach listener for review thread {}: {}", + thread_id, err.message ); } - let rollout_path = conversation.rollout_path(); + let rollout_path = review_thread.rollout_path(); let fallback_provider = self.config.model_provider_id.as_str(); match read_summary_from_rollout(rollout_path.as_path(), fallback_provider).await { Ok(summary) => { @@ -2992,14 +2944,14 @@ impl CodexMessageProcessor { } Err(err) => { tracing::warn!( - "failed to load summary for review conversation {}: {}", + "failed to load summary for review thread {}: {}", session_configured.session_id, err ); } } - let turn_id = conversation + let turn_id = review_thread .submit(Op::Review { review_request }) .await .map_err(|err| JSONRPCErrorError { @@ -3009,7 +2961,7 @@ impl CodexMessageProcessor { })?; let turn = Self::build_review_turn(turn_id, display_text); - let review_thread_id = conversation_id.to_string(); + let review_thread_id = thread_id.to_string(); self.emit_review_started(request_id, turn, review_thread_id.clone(), review_thread_id) .await; @@ -3022,14 +2974,13 @@ impl CodexMessageProcessor { target, delivery, } = params; - let (parent_conversation_id, parent_conversation) = - match self.conversation_from_thread_id(&thread_id).await { - Ok(v) => v, - Err(error) => { - self.outgoing.send_error(request_id, error).await; - return; - } - }; + let (parent_thread_id, parent_thread) = match self.load_thread(&thread_id).await { + Ok(v) => v, + Err(error) => { + self.outgoing.send_error(request_id, error).await; + return; + } + }; let (review_request, display_text) = match Self::review_request_from_target(target) { Ok(value) => value, @@ -3045,7 +2996,7 @@ impl CodexMessageProcessor { if let Err(err) = self .start_inline_review( &request_id, - parent_conversation, + parent_thread, review_request, display_text.as_str(), thread_id.clone(), @@ -3059,7 +3010,7 @@ impl CodexMessageProcessor { if let Err(err) = self .start_detached_review( &request_id, - parent_conversation_id, + parent_thread_id, review_request, display_text.as_str(), ) @@ -3074,25 +3025,24 @@ impl CodexMessageProcessor { async fn turn_interrupt(&mut self, request_id: RequestId, params: TurnInterruptParams) { let TurnInterruptParams { thread_id, .. } = params; - let (conversation_id, conversation) = - match self.conversation_from_thread_id(&thread_id).await { - Ok(v) => v, - Err(error) => { - self.outgoing.send_error(request_id, error).await; - return; - } - }; + let (thread_uuid, thread) = match self.load_thread(&thread_id).await { + Ok(v) => v, + Err(error) => { + self.outgoing.send_error(request_id, error).await; + return; + } + }; // Record the pending interrupt so we can reply when TurnAborted arrives. { let mut map = self.pending_interrupts.lock().await; - map.entry(conversation_id) + map.entry(thread_uuid) .or_default() .push((request_id, ApiVersion::V2)); } // Submit the interrupt; we'll respond upon TurnAborted. - let _ = conversation.submit(Op::Interrupt).await; + let _ = thread.submit(Op::Interrupt).await; } async fn add_conversation_listener( @@ -3118,7 +3068,7 @@ impl CodexMessageProcessor { } } - async fn remove_conversation_listener( + async fn remove_thread_listener( &mut self, request_id: RequestId, params: RemoveConversationListenerParams, @@ -3144,20 +3094,16 @@ impl CodexMessageProcessor { async fn attach_conversation_listener( &mut self, - conversation_id: ConversationId, + conversation_id: ThreadId, experimental_raw_events: bool, api_version: ApiVersion, ) -> Result { - let conversation = match self - .conversation_manager - .get_conversation(conversation_id) - .await - { + let conversation = match self.thread_manager.get_thread(conversation_id).await { Ok(conv) => conv, Err(_) => { return Err(JSONRPCErrorError { code: INVALID_REQUEST_ERROR_CODE, - message: format!("conversation not found: {conversation_id}"), + message: format!("thread not found: {conversation_id}"), data: None, }); } @@ -3185,7 +3131,7 @@ impl CodexMessageProcessor { let event = match event { Ok(event) => event, Err(err) => { - tracing::warn!("conversation.next_event() failed with: {err}"); + tracing::warn!("thread.next_event() failed with: {err}"); break; } }; @@ -3312,7 +3258,7 @@ impl CodexMessageProcessor { } = params; let conversation_id = match thread_id.as_deref() { - Some(thread_id) => match ConversationId::from_string(thread_id) { + Some(thread_id) => match ThreadId::from_string(thread_id) { Ok(conversation_id) => Some(conversation_id), Err(err) => { let error = JSONRPCErrorError { @@ -3338,7 +3284,7 @@ impl CodexMessageProcessor { } else { None }; - let session_source = self.conversation_manager.session_source(); + let session_source = self.thread_manager.session_source(); let upload_result = tokio::task::spawn_blocking(move || { let rollout_path_ref = validated_rollout_path.as_deref(); @@ -3381,12 +3327,8 @@ impl CodexMessageProcessor { } } - async fn resolve_rollout_path(&self, conversation_id: ConversationId) -> Option { - match self - .conversation_manager - .get_conversation(conversation_id) - .await - { + async fn resolve_rollout_path(&self, conversation_id: ThreadId) -> Option { + match self.thread_manager.get_thread(conversation_id).await { Ok(conv) => Some(conv.rollout_path()), Err(_) => None, } @@ -3425,7 +3367,7 @@ fn errors_to_info( /// Precedence (lowest to highest): /// - `cli_overrides`: process-wide startup `--config` flags. /// - `request_overrides`: per-request dotted-path overrides (`params.config`), converted JSON->TOML. -/// - `typesafe_overrides`: Request objects such as `NewConversationParams` and +/// - `typesafe_overrides`: Request objects such as `NewThreadParams` and /// `ThreadStartParams` support a limited set of _explicit_ config overrides, so /// `typesafe_overrides` is a `ConfigOverrides` derived from the respective request object. /// Because the overrides are defined explicitly in the `*Params`, this takes priority over @@ -3633,7 +3575,7 @@ mod tests { #[test] fn extract_conversation_summary_prefers_plain_user_messages() -> Result<()> { - let conversation_id = ConversationId::from_string("3f941c35-29b3-493b-b0a4-e25800d9aeb0")?; + let conversation_id = ThreadId::from_string("3f941c35-29b3-493b-b0a4-e25800d9aeb0")?; let timestamp = Some("2025-09-05T16:53:11.850Z".to_string()); let path = PathBuf::from("rollout.jsonl"); @@ -3697,7 +3639,7 @@ mod tests { let temp_dir = TempDir::new()?; let path = temp_dir.path().join("rollout.jsonl"); - let conversation_id = ConversationId::from_string("bfd12a78-5900-467b-9bc5-d3d35df08191")?; + let conversation_id = ThreadId::from_string("bfd12a78-5900-467b-9bc5-d3d35df08191")?; let timestamp = "2025-09-05T16:53:11.850Z".to_string(); let session_meta = SessionMeta { diff --git a/codex-rs/app-server/src/message_processor.rs b/codex-rs/app-server/src/message_processor.rs index be57ad397..6b2ea8d0a 100644 --- a/codex-rs/app-server/src/message_processor.rs +++ b/codex-rs/app-server/src/message_processor.rs @@ -18,7 +18,7 @@ use codex_app_server_protocol::JSONRPCRequest; use codex_app_server_protocol::JSONRPCResponse; use codex_app_server_protocol::RequestId; use codex_core::AuthManager; -use codex_core::ConversationManager; +use codex_core::ThreadManager; use codex_core::config::Config; use codex_core::config_loader::LoaderOverrides; use codex_core::default_client::USER_AGENT_SUFFIX; @@ -51,13 +51,13 @@ impl MessageProcessor { false, config.cli_auth_credentials_store_mode, ); - let conversation_manager = Arc::new(ConversationManager::new( + let thread_manager = Arc::new(ThreadManager::new( auth_manager.clone(), SessionSource::VSCode, )); let codex_message_processor = CodexMessageProcessor::new( auth_manager, - conversation_manager, + thread_manager, outgoing.clone(), codex_linux_sandbox_exe, Arc::clone(&config), diff --git a/codex-rs/app-server/src/models.rs b/codex-rs/app-server/src/models.rs index 29a9c9963..906108c50 100644 --- a/codex-rs/app-server/src/models.rs +++ b/codex-rs/app-server/src/models.rs @@ -2,16 +2,13 @@ use std::sync::Arc; use codex_app_server_protocol::Model; use codex_app_server_protocol::ReasoningEffortOption; -use codex_core::ConversationManager; +use codex_core::ThreadManager; use codex_core::config::Config; use codex_protocol::openai_models::ModelPreset; use codex_protocol::openai_models::ReasoningEffortPreset; -pub async fn supported_models( - conversation_manager: Arc, - config: &Config, -) -> Vec { - conversation_manager +pub async fn supported_models(thread_manager: Arc, config: &Config) -> Vec { + thread_manager .list_models(config) .await .into_iter() diff --git a/codex-rs/app-server/tests/common/mcp_process.rs b/codex-rs/app-server/tests/common/mcp_process.rs index 026b01ebb..f3ec682fb 100644 --- a/codex-rs/app-server/tests/common/mcp_process.rs +++ b/codex-rs/app-server/tests/common/mcp_process.rs @@ -198,7 +198,7 @@ impl McpProcess { } /// Send a `removeConversationListener` JSON-RPC request. - pub async fn send_remove_conversation_listener_request( + pub async fn send_remove_thread_listener_request( &mut self, params: RemoveConversationListenerParams, ) -> anyhow::Result { diff --git a/codex-rs/app-server/tests/common/rollout.rs b/codex-rs/app-server/tests/common/rollout.rs index 52035e4ed..b5829716a 100644 --- a/codex-rs/app-server/tests/common/rollout.rs +++ b/codex-rs/app-server/tests/common/rollout.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use codex_protocol::ConversationId; +use codex_protocol::ThreadId; use codex_protocol::protocol::GitInfo; use codex_protocol::protocol::SessionMeta; use codex_protocol::protocol::SessionMetaLine; @@ -28,7 +28,7 @@ pub fn create_fake_rollout( ) -> Result { let uuid = Uuid::new_v4(); let uuid_str = uuid.to_string(); - let conversation_id = ConversationId::from_string(&uuid_str)?; + let conversation_id = ThreadId::from_string(&uuid_str)?; // sessions/YYYY/MM/DD derived from filename_ts (YYYY-MM-DDThh-mm-ss) let year = &filename_ts[0..4]; diff --git a/codex-rs/app-server/tests/suite/archive_conversation.rs b/codex-rs/app-server/tests/suite/archive_thread.rs similarity index 100% rename from codex-rs/app-server/tests/suite/archive_conversation.rs rename to codex-rs/app-server/tests/suite/archive_thread.rs diff --git a/codex-rs/app-server/tests/suite/codex_message_processor_flow.rs b/codex-rs/app-server/tests/suite/codex_message_processor_flow.rs index c044e1c4c..a508bf880 100644 --- a/codex-rs/app-server/tests/suite/codex_message_processor_flow.rs +++ b/codex-rs/app-server/tests/suite/codex_message_processor_flow.rs @@ -145,9 +145,7 @@ async fn test_codex_jsonrpc_conversation_flow() -> Result<()> { // 4) removeConversationListener let remove_listener_id = mcp - .send_remove_conversation_listener_request(RemoveConversationListenerParams { - subscription_id, - }) + .send_remove_thread_listener_request(RemoveConversationListenerParams { subscription_id }) .await?; let remove_listener_resp: JSONRPCResponse = timeout( DEFAULT_READ_TIMEOUT, diff --git a/codex-rs/app-server/tests/suite/create_conversation.rs b/codex-rs/app-server/tests/suite/create_thread.rs similarity index 100% rename from codex-rs/app-server/tests/suite/create_conversation.rs rename to codex-rs/app-server/tests/suite/create_thread.rs diff --git a/codex-rs/app-server/tests/suite/list_resume.rs b/codex-rs/app-server/tests/suite/list_resume.rs index 34e737437..983553e06 100644 --- a/codex-rs/app-server/tests/suite/list_resume.rs +++ b/codex-rs/app-server/tests/suite/list_resume.rs @@ -6,7 +6,7 @@ use codex_app_server_protocol::JSONRPCNotification; use codex_app_server_protocol::JSONRPCResponse; use codex_app_server_protocol::ListConversationsParams; use codex_app_server_protocol::ListConversationsResponse; -use codex_app_server_protocol::NewConversationParams; // reused for overrides shape +use codex_app_server_protocol::NewConversationParams; use codex_app_server_protocol::RequestId; use codex_app_server_protocol::ResumeConversationParams; use codex_app_server_protocol::ResumeConversationResponse; diff --git a/codex-rs/app-server/tests/suite/mod.rs b/codex-rs/app-server/tests/suite/mod.rs index 916a8f5a7..41d6f83b9 100644 --- a/codex-rs/app-server/tests/suite/mod.rs +++ b/codex-rs/app-server/tests/suite/mod.rs @@ -1,8 +1,8 @@ -mod archive_conversation; +mod archive_thread; mod auth; mod codex_message_processor_flow; mod config; -mod create_conversation; +mod create_thread; mod fuzzy_file_search; mod interrupt; mod list_resume; diff --git a/codex-rs/app-server/tests/suite/send_message.rs b/codex-rs/app-server/tests/suite/send_message.rs index 39b3a31a8..ed93f8a7f 100644 --- a/codex-rs/app-server/tests/suite/send_message.rs +++ b/codex-rs/app-server/tests/suite/send_message.rs @@ -13,7 +13,7 @@ use codex_app_server_protocol::NewConversationResponse; use codex_app_server_protocol::RequestId; use codex_app_server_protocol::SendUserMessageParams; use codex_app_server_protocol::SendUserMessageResponse; -use codex_protocol::ConversationId; +use codex_protocol::ThreadId; use codex_protocol::models::ContentItem; use codex_protocol::models::ResponseItem; use codex_protocol::protocol::RawResponseItemEvent; @@ -81,7 +81,7 @@ async fn test_send_message_success() -> Result<()> { #[expect(clippy::expect_used)] async fn send_message( message: &str, - conversation_id: ConversationId, + conversation_id: ThreadId, mcp: &mut McpProcess, ) -> Result<()> { // Now exercise sendUserMessage. @@ -220,7 +220,7 @@ async fn test_send_message_session_not_found() -> Result<()> { let mut mcp = McpProcess::new(codex_home.path()).await?; timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; - let unknown = ConversationId::new(); + let unknown = ThreadId::new(); let req_id = mcp .send_send_user_message_request(SendUserMessageParams { conversation_id: unknown, @@ -268,10 +268,7 @@ stream_max_retries = 0 } #[expect(clippy::expect_used)] -async fn read_raw_response_item( - mcp: &mut McpProcess, - conversation_id: ConversationId, -) -> ResponseItem { +async fn read_raw_response_item(mcp: &mut McpProcess, conversation_id: ThreadId) -> ResponseItem { loop { let raw_notification: JSONRPCNotification = timeout( DEFAULT_READ_TIMEOUT, diff --git a/codex-rs/app-server/tests/suite/v2/thread_archive.rs b/codex-rs/app-server/tests/suite/v2/thread_archive.rs index 88891af77..b8cdd426a 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_archive.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_archive.rs @@ -8,7 +8,7 @@ use codex_app_server_protocol::ThreadArchiveResponse; use codex_app_server_protocol::ThreadStartParams; use codex_app_server_protocol::ThreadStartResponse; use codex_core::ARCHIVED_SESSIONS_SUBDIR; -use codex_core::find_conversation_path_by_id_str; +use codex_core::find_thread_path_by_id_str; use std::path::Path; use tempfile::TempDir; use tokio::time::timeout; @@ -39,7 +39,7 @@ async fn thread_archive_moves_rollout_into_archived_directory() -> Result<()> { assert!(!thread.id.is_empty()); // Locate the rollout path recorded for this thread id. - let rollout_path = find_conversation_path_by_id_str(codex_home.path(), &thread.id) + let rollout_path = find_thread_path_by_id_str(codex_home.path(), &thread.id) .await? .expect("expected rollout path for thread id to exist"); assert!( diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index 5cf4678bf..8c2ff5041 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -283,7 +283,7 @@ struct StdioToUdsCommand { fn format_exit_messages(exit_info: AppExitInfo, color_enabled: bool) -> Vec { let AppExitInfo { token_usage, - conversation_id, + thread_id: conversation_id, .. } = exit_info; @@ -790,7 +790,7 @@ mod tests { use super::*; use assert_matches::assert_matches; use codex_core::protocol::TokenUsage; - use codex_protocol::ConversationId; + use codex_protocol::ThreadId; use pretty_assertions::assert_eq; fn finalize_from_args(args: &[&str]) -> TuiCli { @@ -830,9 +830,7 @@ mod tests { }; AppExitInfo { token_usage, - conversation_id: conversation - .map(ConversationId::from_string) - .map(Result::unwrap), + thread_id: conversation.map(ThreadId::from_string).map(Result::unwrap), update_action: None, } } @@ -841,7 +839,7 @@ mod tests { fn format_exit_messages_skips_zero_usage() { let exit_info = AppExitInfo { token_usage: TokenUsage::default(), - conversation_id: None, + thread_id: None, update_action: None, }; let lines = format_exit_messages(exit_info, false); diff --git a/codex-rs/core/src/agent/control.rs b/codex-rs/core/src/agent/control.rs index 468f580a2..201bb4e0f 100644 --- a/codex-rs/core/src/agent/control.rs +++ b/codex-rs/core/src/agent/control.rs @@ -1,9 +1,9 @@ -use crate::CodexConversation; +use crate::CodexThread; use crate::agent::AgentStatus; -use crate::conversation_manager::ConversationManagerState; use crate::error::CodexErr; use crate::error::Result as CodexResult; -use codex_protocol::ConversationId; +use crate::thread_manager::ThreadManagerState; +use codex_protocol::ThreadId; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; use codex_protocol::user_input::UserInput; @@ -15,47 +15,46 @@ use std::sync::Weak; /// spawn new agents and the inter-agent communication layer. #[derive(Clone, Default)] pub(crate) struct AgentControl { - /// Weak handle back to the global conversation registry/state. + /// Weak handle back to the global thread registry/state. /// This is `Weak` to avoid reference cycles and shadow persistence of the form - /// `ConversationManagerState -> CodexConversation -> Session -> SessionServices -> ConversationManagerState`. - manager: Weak, + /// `ThreadManagerState -> CodexThread -> Session -> SessionServices -> ThreadManagerState`. + manager: Weak, } impl AgentControl { /// Construct a new `AgentControl` that can spawn/message agents via the given manager state. - pub(crate) fn new(manager: Weak) -> Self { + pub(crate) fn new(manager: Weak) -> Self { Self { manager } } #[allow(dead_code)] // Used by upcoming multi-agent tooling. - /// Spawn a new agent conversation and submit the initial prompt. + /// Spawn a new agent thread and submit the initial prompt. /// /// If `headless` is true, a background drain task is spawned to prevent unbounded event growth - /// of the channel queue when there is no client actively reading the conversation events. + /// of the channel queue when there is no client actively reading the thread events. pub(crate) async fn spawn_agent( &self, config: crate::config::Config, prompt: String, headless: bool, - ) -> CodexResult { + ) -> CodexResult { let state = self.upgrade()?; - let new_conversation = state.spawn_new_conversation(config, self.clone()).await?; + let new_thread = state.spawn_new_thread(config, self.clone()).await?; if headless { - spawn_headless_drain(Arc::clone(&new_conversation.conversation)); + spawn_headless_drain(Arc::clone(&new_thread.thread)); } - self.send_prompt(new_conversation.conversation_id, prompt) - .await?; + self.send_prompt(new_thread.thread_id, prompt).await?; - Ok(new_conversation.conversation_id) + Ok(new_thread.thread_id) } #[allow(dead_code)] // Used by upcoming multi-agent tooling. - /// Send a `user` prompt to an existing agent conversation. + /// Send a `user` prompt to an existing agent thread. pub(crate) async fn send_prompt( &self, - agent_id: ConversationId, + agent_id: ThreadId, prompt: String, ) -> CodexResult { let state = self.upgrade()?; @@ -72,32 +71,32 @@ impl AgentControl { #[allow(dead_code)] // Used by upcoming multi-agent tooling. /// Fetch the last known status for `agent_id`, returning `NotFound` when unavailable. - pub(crate) async fn get_status(&self, agent_id: ConversationId) -> AgentStatus { + pub(crate) async fn get_status(&self, agent_id: ThreadId) -> AgentStatus { let Ok(state) = self.upgrade() else { // No agent available if upgrade fails. return AgentStatus::NotFound; }; - let Ok(conversation) = state.get_conversation(agent_id).await else { + let Ok(thread) = state.get_thread(agent_id).await else { return AgentStatus::NotFound; }; - conversation.agent_status().await + thread.agent_status().await } - fn upgrade(&self) -> CodexResult> { - self.manager.upgrade().ok_or_else(|| { - CodexErr::UnsupportedOperation("conversation manager dropped".to_string()) - }) + fn upgrade(&self) -> CodexResult> { + self.manager + .upgrade() + .ok_or_else(|| CodexErr::UnsupportedOperation("thread manager dropped".to_string())) } } /// When an agent is spawned "headless" (no UI/view attached), there may be no consumer polling -/// `CodexConversation::next_event()`. The underlying event channel is unbounded, so the producer can +/// `CodexThread::next_event()`. The underlying event channel is unbounded, so the producer can /// accumulate events indefinitely. This drain task prevents that memory growth by polling and /// discarding events until shutdown. -fn spawn_headless_drain(conversation: Arc) { +fn spawn_headless_drain(thread: Arc) { tokio::spawn(async move { loop { - match conversation.next_event().await { + match thread.next_event().await { Ok(event) => { if matches!(event.msg, EventMsg::ShutdownComplete) { break; @@ -127,19 +126,19 @@ mod tests { async fn send_prompt_errors_when_manager_dropped() { let control = AgentControl::default(); let err = control - .send_prompt(ConversationId::new(), "hello".to_string()) + .send_prompt(ThreadId::new(), "hello".to_string()) .await .expect_err("send_prompt should fail without a manager"); assert_eq!( err.to_string(), - "unsupported operation: conversation manager dropped" + "unsupported operation: thread manager dropped" ); } #[tokio::test] async fn get_status_returns_not_found_without_manager() { let control = AgentControl::default(); - let got = control.get_status(ConversationId::new()).await; + let got = control.get_status(ThreadId::new()).await; assert_eq!(got, AgentStatus::NotFound); } diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index 11a3c5c65..785d0475b 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -19,7 +19,7 @@ use codex_api::create_text_param_for_request; use codex_api::error::ApiError; use codex_app_server_protocol::AuthMode; use codex_otel::otel_manager::OtelManager; -use codex_protocol::ConversationId; +use codex_protocol::ThreadId; use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig; use codex_protocol::models::ResponseItem; use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig; @@ -60,7 +60,7 @@ pub struct ModelClient { model_family: ModelFamily, otel_manager: OtelManager, provider: ModelProviderInfo, - conversation_id: ConversationId, + conversation_id: ThreadId, effort: Option, summary: ReasoningSummaryConfig, session_source: SessionSource, @@ -76,7 +76,7 @@ impl ModelClient { provider: ModelProviderInfo, effort: Option, summary: ReasoningSummaryConfig, - conversation_id: ConversationId, + conversation_id: ThreadId, session_source: SessionSource, ) -> Self { Self { diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index a4ea288e0..828a6bab0 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -32,7 +32,7 @@ use crate::user_notification::UserNotifier; use crate::util::error_or_panic; use async_channel::Receiver; use async_channel::Sender; -use codex_protocol::ConversationId; +use codex_protocol::ThreadId; use codex_protocol::approvals::ExecPolicyAmendment; use codex_protocol::items::TurnItem; use codex_protocol::protocol::FileChange; @@ -179,7 +179,9 @@ pub struct Codex { /// unique session id. pub struct CodexSpawnOk { pub codex: Codex, - pub conversation_id: ConversationId, + pub thread_id: ThreadId, + #[deprecated(note = "use thread_id")] + pub conversation_id: ThreadId, } pub(crate) const INITIAL_SUBMIT_ID: &str = ""; @@ -299,7 +301,7 @@ impl Codex { error!("Failed to create session: {e:#}"); map_session_init_error(&e, &config.codex_home) })?; - let conversation_id = session.conversation_id; + let thread_id = session.conversation_id; // This task will run until Op::Shutdown is received. tokio::spawn(submission_loop(session, config, rx_sub)); @@ -310,9 +312,11 @@ impl Codex { agent_status, }; + #[allow(deprecated)] Ok(CodexSpawnOk { codex, - conversation_id, + thread_id, + conversation_id: thread_id, }) } @@ -356,7 +360,7 @@ impl Codex { /// /// A session has at most 1 running task at a time, and can be interrupted by user input. pub(crate) struct Session { - conversation_id: ConversationId, + conversation_id: ThreadId, tx_event: Sender, agent_status: Arc>, state: Mutex, @@ -368,7 +372,7 @@ pub(crate) struct Session { next_internal_sub_id: AtomicU64, } -/// The context needed for a single turn of the conversation. +/// The context needed for a single turn of the thread. #[derive(Debug)] pub(crate) struct TurnContext { pub(crate) sub_id: String, @@ -505,7 +509,7 @@ impl Session { session_configuration: &SessionConfiguration, per_turn_config: Config, model_family: ModelFamily, - conversation_id: ConversationId, + conversation_id: ThreadId, sub_id: String, ) -> TurnContext { let otel_manager = otel_manager.clone().with_model( @@ -581,7 +585,7 @@ impl Session { let (conversation_id, rollout_params) = match &initial_history { InitialHistory::New | InitialHistory::Forked(_) => { - let conversation_id = ConversationId::default(); + let conversation_id = ThreadId::default(); ( conversation_id, RolloutRecorderParams::new( @@ -2940,7 +2944,7 @@ mod tests { session .record_initial_history(InitialHistory::Resumed(ResumedHistory { - conversation_id: ConversationId::default(), + conversation_id: ThreadId::default(), history: rollout_items, rollout_path: PathBuf::from("/tmp/resume.jsonl"), })) @@ -3017,7 +3021,7 @@ mod tests { session .record_initial_history(InitialHistory::Resumed(ResumedHistory { - conversation_id: ConversationId::default(), + conversation_id: ThreadId::default(), history: rollout_items, rollout_path: PathBuf::from("/tmp/resume.jsonl"), })) @@ -3453,7 +3457,7 @@ mod tests { } fn otel_manager( - conversation_id: ConversationId, + conversation_id: ThreadId, config: &Config, model_family: &ModelFamily, session_source: SessionSource, @@ -3476,7 +3480,7 @@ mod tests { let codex_home = tempfile::tempdir().expect("create temp dir"); let config = build_test_config(codex_home.path()).await; let config = Arc::new(config); - let conversation_id = ConversationId::default(); + let conversation_id = ThreadId::default(); let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key")); let models_manager = Arc::new(ModelsManager::new(auth_manager.clone())); @@ -3567,7 +3571,7 @@ mod tests { let codex_home = tempfile::tempdir().expect("create temp dir"); let config = build_test_config(codex_home.path()).await; let config = Arc::new(config); - let conversation_id = ConversationId::default(); + let conversation_id = ThreadId::default(); let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key")); let models_manager = Arc::new(ModelsManager::new(auth_manager.clone())); diff --git a/codex-rs/core/src/codex_delegate.rs b/codex-rs/core/src/codex_delegate.rs index d7458f1ac..72c2911bf 100644 --- a/codex-rs/core/src/codex_delegate.rs +++ b/codex-rs/core/src/codex_delegate.rs @@ -28,12 +28,12 @@ use crate::error::CodexErr; use crate::models_manager::manager::ModelsManager; use codex_protocol::protocol::InitialHistory; -/// Start an interactive sub-Codex conversation and return IO channels. +/// Start an interactive sub-Codex thread and return IO channels. /// /// The returned `events_rx` yields non-approval events emitted by the sub-agent. /// Approval requests are handled via `parent_session` and are not surfaced. /// The returned `ops_tx` allows the caller to submit additional `Op`s to the sub-agent. -pub(crate) async fn run_codex_conversation_interactive( +pub(crate) async fn run_codex_thread_interactive( config: Config, auth_manager: Arc, models_manager: Arc, @@ -95,7 +95,7 @@ pub(crate) async fn run_codex_conversation_interactive( /// /// Internally calls the interactive variant, then immediately submits the provided input. #[allow(clippy::too_many_arguments)] -pub(crate) async fn run_codex_conversation_one_shot( +pub(crate) async fn run_codex_thread_one_shot( config: Config, auth_manager: Arc, models_manager: Arc, @@ -108,7 +108,7 @@ pub(crate) async fn run_codex_conversation_one_shot( // Use a child token so we can stop the delegate after completion without // requiring the caller to cancel the parent token. let child_cancel = cancel_token.child_token(); - let io = run_codex_conversation_interactive( + let io = run_codex_thread_interactive( config, auth_manager, models_manager, diff --git a/codex-rs/core/src/codex_conversation.rs b/codex-rs/core/src/codex_thread.rs similarity index 91% rename from codex-rs/core/src/codex_conversation.rs rename to codex-rs/core/src/codex_thread.rs index 723a4cf4f..e8a379930 100644 --- a/codex-rs/core/src/codex_conversation.rs +++ b/codex-rs/core/src/codex_thread.rs @@ -6,14 +6,14 @@ use crate::protocol::Op; use crate::protocol::Submission; use std::path::PathBuf; -pub struct CodexConversation { +pub struct CodexThread { codex: Codex, rollout_path: PathBuf, } -/// Conduit for the bidirectional stream of messages that compose a conversation -/// in Codex. -impl CodexConversation { +/// Conduit for the bidirectional stream of messages that compose a thread +/// (formerly called a conversation) in Codex. +impl CodexThread { pub(crate) fn new(codex: Codex, rollout_path: PathBuf) -> Self { Self { codex, diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs index 77b9303e9..b608cfa46 100644 --- a/codex-rs/core/src/compact.rs +++ b/codex-rs/core/src/compact.rs @@ -108,7 +108,7 @@ async fn run_compact_task_inner( sess.notify_background_event( turn_context.as_ref(), format!( - "Trimmed {truncated_count} older conversation item(s) before compacting so the prompt fits the model context window." + "Trimmed {truncated_count} older thread item(s) before compacting so the prompt fits the model context window." ), ) .await; @@ -182,7 +182,7 @@ async fn run_compact_task_inner( sess.send_event(&turn_context, event).await; let warning = EventMsg::Warning(WarningEvent { - message: "Heads up: Long conversations and multiple compactions can cause the model to be less accurate. Start a new conversation when possible to keep conversations small and targeted.".to_string(), + message: "Heads up: Long threads and multiple compactions can cause the model to be less accurate. Start a new thread when possible to keep threads small and targeted.".to_string(), }); sess.send_event(&turn_context, warning).await; } diff --git a/codex-rs/core/src/context_manager/history.rs b/codex-rs/core/src/context_manager/history.rs index e52561c9f..8dc55187a 100644 --- a/codex-rs/core/src/context_manager/history.rs +++ b/codex-rs/core/src/context_manager/history.rs @@ -16,7 +16,7 @@ use codex_protocol::protocol::TokenUsage; use codex_protocol::protocol::TokenUsageInfo; use std::ops::Deref; -/// Transcript of conversation history +/// Transcript of thread history #[derive(Debug, Clone, Default)] pub(crate) struct ContextManager { /// The oldest items are at the beginning of the vector. diff --git a/codex-rs/core/src/error.rs b/codex-rs/core/src/error.rs index e8fa91d26..4ae3e709f 100644 --- a/codex-rs/core/src/error.rs +++ b/codex-rs/core/src/error.rs @@ -8,7 +8,7 @@ use chrono::Datelike; use chrono::Local; use chrono::Utc; use codex_async_utils::CancelErr; -use codex_protocol::ConversationId; +use codex_protocol::ThreadId; use codex_protocol::protocol::CodexErrorInfo; use codex_protocol::protocol::ErrorEvent; use codex_protocol::protocol::RateLimitSnapshot; @@ -71,12 +71,12 @@ pub enum CodexErr { Stream(String, Option), #[error( - "Codex ran out of room in the model's context window. Start a new conversation or clear earlier history before retrying." + "Codex ran out of room in the model's context window. Start a new thread or clear earlier history before retrying." )] ContextWindowExceeded, - #[error("no conversation with id: {0}")] - ConversationNotFound(ConversationId), + #[error("no thread with id: {0}")] + ThreadNotFound(ThreadId), #[error("session configured event was not the first event in the stream")] SessionConfiguredNotFirstEvent, @@ -455,7 +455,7 @@ impl CodexErr { CodexErr::SessionConfiguredNotFirstEvent | CodexErr::InternalServerError | CodexErr::InternalAgentDied => CodexErrorInfo::InternalServerError, - CodexErr::UnsupportedOperation(_) | CodexErr::ConversationNotFound(_) => { + CodexErr::UnsupportedOperation(_) | CodexErr::ThreadNotFound(_) => { CodexErrorInfo::BadRequest } CodexErr::Sandbox(_) => CodexErrorInfo::SandboxError, diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index b9f2645b1..370c1ecb9 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -12,9 +12,9 @@ pub mod bash; mod client; mod client_common; pub mod codex; -mod codex_conversation; +mod codex_thread; mod compact_remote; -pub use codex_conversation::CodexConversation; +pub use codex_thread::CodexThread; mod agent; mod codex_delegate; mod command_safety; @@ -60,13 +60,19 @@ pub use model_provider_info::OLLAMA_OSS_PROVIDER_ID; pub use model_provider_info::WireApi; pub use model_provider_info::built_in_model_providers; pub use model_provider_info::create_oss_provider_with_base_url; -mod conversation_manager; mod event_mapping; pub mod review_format; pub mod review_prompts; +mod thread_manager; pub use codex_protocol::protocol::InitialHistory; -pub use conversation_manager::ConversationManager; -pub use conversation_manager::NewConversation; +pub use thread_manager::NewThread; +pub use thread_manager::ThreadManager; +#[deprecated(note = "use ThreadManager")] +pub type ConversationManager = ThreadManager; +#[deprecated(note = "use NewThread")] +pub type NewConversation = NewThread; +#[deprecated(note = "use CodexThread")] +pub type CodexConversation = CodexThread; // Re-export common auth types for workspace consumers pub use auth::AuthManager; pub use auth::CodexAuth; @@ -87,10 +93,12 @@ pub use rollout::INTERACTIVE_SESSION_SOURCES; pub use rollout::RolloutRecorder; pub use rollout::SESSIONS_SUBDIR; pub use rollout::SessionMeta; +#[deprecated(note = "use find_thread_path_by_id_str")] pub use rollout::find_conversation_path_by_id_str; -pub use rollout::list::ConversationItem; -pub use rollout::list::ConversationsPage; +pub use rollout::find_thread_path_by_id_str; pub use rollout::list::Cursor; +pub use rollout::list::ThreadItem; +pub use rollout::list::ThreadsPage; pub use rollout::list::parse_cursor; pub use rollout::list::read_head_for_summary; mod function_tool; diff --git a/codex-rs/core/src/message_history.rs b/codex-rs/core/src/message_history.rs index 733e8e800..cb3b10098 100644 --- a/codex-rs/core/src/message_history.rs +++ b/codex-rs/core/src/message_history.rs @@ -13,6 +13,8 @@ //! trailing `\n`) and write it with a **single `write(2)` system call** while //! the file descriptor is opened with the `O_APPEND` flag. POSIX guarantees //! that writes up to `PIPE_BUF` bytes are atomic in that case. +//! Note: `conversation_id` stores the thread id; the field name is preserved for +//! backwards compatibility with existing history files. use std::fs::File; use std::fs::OpenOptions; @@ -36,7 +38,7 @@ use tokio::io::AsyncReadExt; use crate::config::Config; use crate::config::types::HistoryPersistence; -use codex_protocol::ConversationId; +use codex_protocol::ThreadId; #[cfg(unix)] use std::os::unix::fs::OpenOptionsExt; #[cfg(unix)] @@ -69,7 +71,7 @@ fn history_filepath(config: &Config) -> PathBuf { /// which entails a small amount of blocking I/O internally. pub(crate) async fn append_entry( text: &str, - conversation_id: &ConversationId, + conversation_id: &ThreadId, config: &Config, ) -> Result<()> { match config.history.persistence { @@ -402,7 +404,7 @@ fn history_log_id(_metadata: &std::fs::Metadata) -> Option { mod tests { use super::*; use crate::config::ConfigBuilder; - use codex_protocol::ConversationId; + use codex_protocol::ThreadId; use pretty_assertions::assert_eq; use std::fs::File; use std::io::Write; @@ -497,7 +499,7 @@ mod tests { .await .expect("load config"); - let conversation_id = ConversationId::new(); + let conversation_id = ThreadId::new(); let entry_one = "a".repeat(200); let entry_two = "b".repeat(200); @@ -544,7 +546,7 @@ mod tests { .await .expect("load config"); - let conversation_id = ConversationId::new(); + let conversation_id = ThreadId::new(); let short_entry = "a".repeat(200); let long_entry = "b".repeat(400); diff --git a/codex-rs/core/src/rollout/error.rs b/codex-rs/core/src/rollout/error.rs index e924dd2d2..ee48bb202 100644 --- a/codex-rs/core/src/rollout/error.rs +++ b/codex-rs/core/src/rollout/error.rs @@ -33,7 +33,7 @@ fn map_rollout_io_error(io_err: &std::io::Error, codex_home: &Path) -> Option format!( - "Session data under {} looks corrupt or unreadable. Clearing the sessions directory may help (this will remove saved conversations).", + "Session data under {} looks corrupt or unreadable. Clearing the sessions directory may help (this will remove saved threads).", sessions_dir.display() ), ErrorKind::IsADirectory | ErrorKind::NotADirectory => format!( diff --git a/codex-rs/core/src/rollout/list.rs b/codex-rs/core/src/rollout/list.rs index e2ef0e883..487304ddc 100644 --- a/codex-rs/core/src/rollout/list.rs +++ b/codex-rs/core/src/rollout/list.rs @@ -20,11 +20,11 @@ use codex_protocol::protocol::RolloutItem; use codex_protocol::protocol::RolloutLine; use codex_protocol::protocol::SessionSource; -/// Returned page of conversation summaries. +/// Returned page of thread (thread) summaries. #[derive(Debug, Default, PartialEq)] -pub struct ConversationsPage { - /// Conversation summaries ordered newest first. - pub items: Vec, +pub struct ThreadsPage { + /// Thread summaries ordered newest first. + pub items: Vec, /// Opaque pagination token to resume after the last item, or `None` if end. pub next_cursor: Option, /// Total number of files touched while scanning this request. @@ -33,9 +33,9 @@ pub struct ConversationsPage { pub reached_scan_cap: bool, } -/// Summary information for a conversation rollout file. +/// Summary information for a thread rollout file. #[derive(Debug, PartialEq)] -pub struct ConversationItem { +pub struct ThreadItem { /// Absolute path to the rollout file. pub path: PathBuf, /// First up to `HEAD_RECORD_LIMIT` JSONL records parsed as JSON (includes meta line). @@ -46,6 +46,13 @@ pub struct ConversationItem { pub updated_at: Option, } +#[allow(dead_code)] +#[deprecated(note = "use ThreadItem")] +pub type ConversationItem = ThreadItem; +#[allow(dead_code)] +#[deprecated(note = "use ThreadsPage")] +pub type ConversationsPage = ThreadsPage; + #[derive(Default)] struct HeadTailSummary { head: Vec, @@ -99,22 +106,22 @@ impl<'de> serde::Deserialize<'de> for Cursor { } } -/// Retrieve recorded conversation file paths with token pagination. The returned `next_cursor` +/// Retrieve recorded thread file paths with token pagination. The returned `next_cursor` /// can be supplied on the next call to resume after the last returned item, resilient to /// concurrent new sessions being appended. Ordering is stable by timestamp desc, then UUID desc. -pub(crate) async fn get_conversations( +pub(crate) async fn get_threads( codex_home: &Path, page_size: usize, cursor: Option<&Cursor>, allowed_sources: &[SessionSource], model_providers: Option<&[String]>, default_provider: &str, -) -> io::Result { +) -> io::Result { let mut root = codex_home.to_path_buf(); root.push(SESSIONS_SUBDIR); if !root.exists() { - return Ok(ConversationsPage { + return Ok(ThreadsPage { items: Vec::new(), next_cursor: None, num_scanned_files: 0, @@ -138,7 +145,7 @@ pub(crate) async fn get_conversations( Ok(result) } -/// Load conversation file paths from disk using directory traversal. +/// Load thread file paths from disk using directory traversal. /// /// Directory layout: `~/.codex/sessions/YYYY/MM/DD/rollout-YYYY-MM-DDThh-mm-ss-.jsonl` /// Returned newest (latest) first. @@ -148,8 +155,8 @@ async fn traverse_directories_for_paths( anchor: Option, allowed_sources: &[SessionSource], provider_matcher: Option<&ProviderMatcher<'_>>, -) -> io::Result { - let mut items: Vec = Vec::with_capacity(page_size); +) -> io::Result { + let mut items: Vec = Vec::with_capacity(page_size); let mut scanned_files = 0usize; let mut anchor_passed = anchor.is_none(); let (anchor_ts, anchor_id) = match anchor { @@ -232,7 +239,7 @@ async fn traverse_directories_for_paths( .unwrap_or(None) .or_else(|| created_at.clone()); } - items.push(ConversationItem { + items.push(ThreadItem { path, head, created_at, @@ -254,7 +261,7 @@ async fn traverse_directories_for_paths( } else { None }; - Ok(ConversationsPage { + Ok(ThreadsPage { items, next_cursor: next, num_scanned_files: scanned_files, @@ -279,7 +286,7 @@ pub fn parse_cursor(token: &str) -> Option { Some(Cursor::new(ts, uuid)) } -fn build_next_cursor(items: &[ConversationItem]) -> Option { +fn build_next_cursor(items: &[ThreadItem]) -> Option { let last = items.last()?; let file_name = last.path.file_name()?.to_string_lossy(); let (ts, id) = parse_timestamp_uuid_from_filename(&file_name)?; @@ -455,10 +462,10 @@ async fn file_modified_rfc3339(path: &Path) -> io::Result> { Ok(dt.format(&Rfc3339).ok()) } -/// Locate a recorded conversation rollout file by its UUID string using the existing +/// Locate a recorded thread rollout file by its UUID string using the existing /// paginated listing implementation. Returns `Ok(Some(path))` if found, `Ok(None)` if not present /// or the id is invalid. -pub async fn find_conversation_path_by_id_str( +pub async fn find_thread_path_by_id_str( codex_home: &Path, id_str: &str, ) -> io::Result> { diff --git a/codex-rs/core/src/rollout/mod.rs b/codex-rs/core/src/rollout/mod.rs index d7e24602f..5b65bada7 100644 --- a/codex-rs/core/src/rollout/mod.rs +++ b/codex-rs/core/src/rollout/mod.rs @@ -15,7 +15,9 @@ pub(crate) mod truncation; pub use codex_protocol::protocol::SessionMeta; pub(crate) use error::map_session_init_error; -pub use list::find_conversation_path_by_id_str; +pub use list::find_thread_path_by_id_str; +#[deprecated(note = "use find_thread_path_by_id_str")] +pub use list::find_thread_path_by_id_str as find_conversation_path_by_id_str; pub use recorder::RolloutRecorder; pub use recorder::RolloutRecorderParams; diff --git a/codex-rs/core/src/rollout/recorder.rs b/codex-rs/core/src/rollout/recorder.rs index a39f85c82..d571ad191 100644 --- a/codex-rs/core/src/rollout/recorder.rs +++ b/codex-rs/core/src/rollout/recorder.rs @@ -6,7 +6,7 @@ use std::io::Error as IoError; use std::path::Path; use std::path::PathBuf; -use codex_protocol::ConversationId; +use codex_protocol::ThreadId; use serde_json::Value; use time::OffsetDateTime; use time::format_description::FormatItem; @@ -19,9 +19,9 @@ use tracing::info; use tracing::warn; use super::SESSIONS_SUBDIR; -use super::list::ConversationsPage; use super::list::Cursor; -use super::list::get_conversations; +use super::list::ThreadsPage; +use super::list::get_threads; use super::policy::is_persisted_response_item; use crate::config::Config; use crate::default_client::originator; @@ -52,7 +52,7 @@ pub struct RolloutRecorder { #[derive(Clone)] pub enum RolloutRecorderParams { Create { - conversation_id: ConversationId, + conversation_id: ThreadId, instructions: Option, source: SessionSource, }, @@ -74,7 +74,7 @@ enum RolloutCmd { impl RolloutRecorderParams { pub fn new( - conversation_id: ConversationId, + conversation_id: ThreadId, instructions: Option, source: SessionSource, ) -> Self { @@ -91,16 +91,16 @@ impl RolloutRecorderParams { } impl RolloutRecorder { - /// List conversations (rollout files) under the provided Codex home directory. - pub async fn list_conversations( + /// List threads (rollout files) under the provided Codex home directory. + pub async fn list_threads( codex_home: &Path, page_size: usize, cursor: Option<&Cursor>, allowed_sources: &[SessionSource], model_providers: Option<&[String]>, default_provider: &str, - ) -> std::io::Result { - get_conversations( + ) -> std::io::Result { + get_threads( codex_home, page_size, cursor, @@ -215,7 +215,7 @@ impl RolloutRecorder { } let mut items: Vec = Vec::new(); - let mut conversation_id: Option = None; + let mut thread_id: Option = None; for line in text.lines() { if line.trim().is_empty() { continue; @@ -233,9 +233,9 @@ impl RolloutRecorder { Ok(rollout_line) => match rollout_line.item { RolloutItem::SessionMeta(session_meta_line) => { // Use the FIRST SessionMeta encountered in the file as the canonical - // conversation id and main session information. Keep all items intact. - if conversation_id.is_none() { - conversation_id = Some(session_meta_line.meta.id); + // thread id and main session information. Keep all items intact. + if thread_id.is_none() { + thread_id = Some(session_meta_line.meta.id); } items.push(RolloutItem::SessionMeta(session_meta_line)); } @@ -259,12 +259,12 @@ impl RolloutRecorder { } info!( - "Resumed rollout with {} items, conversation ID: {:?}", + "Resumed rollout with {} items, thread ID: {:?}", items.len(), - conversation_id + thread_id ); - let conversation_id = conversation_id - .ok_or_else(|| IoError::other("failed to parse conversation ID from rollout file"))?; + let conversation_id = thread_id + .ok_or_else(|| IoError::other("failed to parse thread ID from rollout file"))?; if items.is_empty() { return Ok(InitialHistory::New); @@ -302,16 +302,13 @@ struct LogFileInfo { path: PathBuf, /// Session ID (also embedded in filename). - conversation_id: ConversationId, + conversation_id: ThreadId, /// Timestamp for the start of the session. timestamp: OffsetDateTime, } -fn create_log_file( - config: &Config, - conversation_id: ConversationId, -) -> std::io::Result { +fn create_log_file(config: &Config, conversation_id: ThreadId) -> std::io::Result { // Resolve ~/.codex/sessions/YYYY/MM/DD and create it if missing. let timestamp = OffsetDateTime::now_local() .map_err(|e| IoError::other(format!("failed to get local time: {e}")))?; diff --git a/codex-rs/core/src/rollout/tests.rs b/codex-rs/core/src/rollout/tests.rs index 1df3659ba..f7c13c70f 100644 --- a/codex-rs/core/src/rollout/tests.rs +++ b/codex-rs/core/src/rollout/tests.rs @@ -13,12 +13,12 @@ use time::macros::format_description; use uuid::Uuid; use crate::rollout::INTERACTIVE_SESSION_SOURCES; -use crate::rollout::list::ConversationItem; -use crate::rollout::list::ConversationsPage; use crate::rollout::list::Cursor; -use crate::rollout::list::get_conversations; +use crate::rollout::list::ThreadItem; +use crate::rollout::list::ThreadsPage; +use crate::rollout::list::get_threads; use anyhow::Result; -use codex_protocol::ConversationId; +use codex_protocol::ThreadId; use codex_protocol::models::ContentItem; use codex_protocol::models::ResponseItem; use codex_protocol::protocol::EventMsg; @@ -162,7 +162,7 @@ async fn test_list_conversations_latest_first() { .unwrap(); let provider_filter = provider_vec(&[TEST_PROVIDER]); - let page = get_conversations( + let page = get_threads( home, 10, None, @@ -227,21 +227,21 @@ async fn test_list_conversations_latest_first() { let updated_times: Vec> = page.items.iter().map(|i| i.updated_at.clone()).collect(); - let expected = ConversationsPage { + let expected = ThreadsPage { items: vec![ - ConversationItem { + ThreadItem { path: p1, head: head_3, created_at: Some("2025-01-03T12-00-00".into()), updated_at: updated_times.first().cloned().flatten(), }, - ConversationItem { + ThreadItem { path: p2, head: head_2, created_at: Some("2025-01-02T12-00-00".into()), updated_at: updated_times.get(1).cloned().flatten(), }, - ConversationItem { + ThreadItem { path: p3, head: head_1, created_at: Some("2025-01-01T12-00-00".into()), @@ -311,7 +311,7 @@ async fn test_pagination_cursor() { .unwrap(); let provider_filter = provider_vec(&[TEST_PROVIDER]); - let page1 = get_conversations( + let page1 = get_threads( home, 2, None, @@ -357,15 +357,15 @@ async fn test_pagination_cursor() { page1.items.iter().map(|i| i.updated_at.clone()).collect(); let expected_cursor1: Cursor = serde_json::from_str(&format!("\"2025-03-04T09-00-00|{u4}\"")).unwrap(); - let expected_page1 = ConversationsPage { + let expected_page1 = ThreadsPage { items: vec![ - ConversationItem { + ThreadItem { path: p5, head: head_5, created_at: Some("2025-03-05T09-00-00".into()), updated_at: updated_page1.first().cloned().flatten(), }, - ConversationItem { + ThreadItem { path: p4, head: head_4, created_at: Some("2025-03-04T09-00-00".into()), @@ -378,7 +378,7 @@ async fn test_pagination_cursor() { }; assert_eq!(page1, expected_page1); - let page2 = get_conversations( + let page2 = get_threads( home, 2, page1.next_cursor.as_ref(), @@ -424,15 +424,15 @@ async fn test_pagination_cursor() { page2.items.iter().map(|i| i.updated_at.clone()).collect(); let expected_cursor2: Cursor = serde_json::from_str(&format!("\"2025-03-02T09-00-00|{u2}\"")).unwrap(); - let expected_page2 = ConversationsPage { + let expected_page2 = ThreadsPage { items: vec![ - ConversationItem { + ThreadItem { path: p3, head: head_3, created_at: Some("2025-03-03T09-00-00".into()), updated_at: updated_page2.first().cloned().flatten(), }, - ConversationItem { + ThreadItem { path: p2, head: head_2, created_at: Some("2025-03-02T09-00-00".into()), @@ -445,7 +445,7 @@ async fn test_pagination_cursor() { }; assert_eq!(page2, expected_page2); - let page3 = get_conversations( + let page3 = get_threads( home, 2, page2.next_cursor.as_ref(), @@ -473,8 +473,8 @@ async fn test_pagination_cursor() { })]; let updated_page3: Vec> = page3.items.iter().map(|i| i.updated_at.clone()).collect(); - let expected_page3 = ConversationsPage { - items: vec![ConversationItem { + let expected_page3 = ThreadsPage { + items: vec![ThreadItem { path: p1, head: head_1, created_at: Some("2025-03-01T09-00-00".into()), @@ -488,7 +488,7 @@ async fn test_pagination_cursor() { } #[tokio::test] -async fn test_get_conversation_contents() { +async fn test_get_thread_contents() { let temp = TempDir::new().unwrap(); let home = temp.path(); @@ -497,7 +497,7 @@ async fn test_get_conversation_contents() { write_session_file(home, ts, uuid, 2, Some(SessionSource::VSCode)).unwrap(); let provider_filter = provider_vec(&[TEST_PROVIDER]); - let page = get_conversations( + let page = get_threads( home, 1, None, @@ -528,8 +528,8 @@ async fn test_get_conversation_contents() { "source": "vscode", "model_provider": "test-provider", })]; - let expected_page = ConversationsPage { - items: vec![ConversationItem { + let expected_page = ThreadsPage { + items: vec![ThreadItem { path: expected_path, head: expected_head, created_at: Some(ts.into()), @@ -579,7 +579,7 @@ async fn test_updated_at_uses_file_mtime() -> Result<()> { let file_path = day_dir.join(format!("rollout-{ts}-{uuid}.jsonl")); let mut file = File::create(&file_path)?; - let conversation_id = ConversationId::from_string(&uuid.to_string())?; + let conversation_id = ThreadId::from_string(&uuid.to_string())?; let meta_line = RolloutLine { timestamp: ts.to_string(), item: RolloutItem::SessionMeta(SessionMetaLine { @@ -624,7 +624,7 @@ async fn test_updated_at_uses_file_mtime() -> Result<()> { drop(file); let provider_filter = provider_vec(&[TEST_PROVIDER]); - let page = get_conversations( + let page = get_threads( home, 1, None, @@ -663,7 +663,7 @@ async fn test_stable_ordering_same_second_pagination() { write_session_file(home, ts, u3, 0, Some(SessionSource::VSCode)).unwrap(); let provider_filter = provider_vec(&[TEST_PROVIDER]); - let page1 = get_conversations( + let page1 = get_threads( home, 2, None, @@ -701,15 +701,15 @@ async fn test_stable_ordering_same_second_pagination() { let updated_page1: Vec> = page1.items.iter().map(|i| i.updated_at.clone()).collect(); let expected_cursor1: Cursor = serde_json::from_str(&format!("\"{ts}|{u2}\"")).unwrap(); - let expected_page1 = ConversationsPage { + let expected_page1 = ThreadsPage { items: vec![ - ConversationItem { + ThreadItem { path: p3, head: head(u3), created_at: Some(ts.to_string()), updated_at: updated_page1.first().cloned().flatten(), }, - ConversationItem { + ThreadItem { path: p2, head: head(u2), created_at: Some(ts.to_string()), @@ -722,7 +722,7 @@ async fn test_stable_ordering_same_second_pagination() { }; assert_eq!(page1, expected_page1); - let page2 = get_conversations( + let page2 = get_threads( home, 2, page1.next_cursor.as_ref(), @@ -740,8 +740,8 @@ async fn test_stable_ordering_same_second_pagination() { .join(format!("rollout-2025-07-01T00-00-00-{u1}.jsonl")); let updated_page2: Vec> = page2.items.iter().map(|i| i.updated_at.clone()).collect(); - let expected_page2 = ConversationsPage { - items: vec![ConversationItem { + let expected_page2 = ThreadsPage { + items: vec![ThreadItem { path: p1, head: head(u1), created_at: Some(ts.to_string()), @@ -780,7 +780,7 @@ async fn test_source_filter_excludes_non_matching_sessions() { .unwrap(); let provider_filter = provider_vec(&[TEST_PROVIDER]); - let interactive_only = get_conversations( + let interactive_only = get_threads( home, 10, None, @@ -801,7 +801,7 @@ async fn test_source_filter_excludes_non_matching_sessions() { path.ends_with("rollout-2025-08-02T10-00-00-00000000-0000-0000-0000-00000000002a.jsonl") })); - let all_sessions = get_conversations(home, 10, None, NO_SOURCE_FILTER, None, TEST_PROVIDER) + let all_sessions = get_threads(home, 10, None, NO_SOURCE_FILTER, None, TEST_PROVIDER) .await .unwrap(); let all_paths: Vec<_> = all_sessions @@ -855,7 +855,7 @@ async fn test_model_provider_filter_selects_only_matching_sessions() -> Result<( let openai_id_str = openai_id.to_string(); let none_id_str = none_id.to_string(); let openai_filter = provider_vec(&["openai"]); - let openai_sessions = get_conversations( + let openai_sessions = get_threads( home, 10, None, @@ -880,7 +880,7 @@ async fn test_model_provider_filter_selects_only_matching_sessions() -> Result<( assert!(openai_ids.contains(&none_id_str)); let beta_filter = provider_vec(&["beta"]); - let beta_sessions = get_conversations( + let beta_sessions = get_threads( home, 10, None, @@ -900,7 +900,7 @@ async fn test_model_provider_filter_selects_only_matching_sessions() -> Result<( assert_eq!(beta_head, Some(beta_id_str.as_str())); let unknown_filter = provider_vec(&["unknown"]); - let unknown_sessions = get_conversations( + let unknown_sessions = get_threads( home, 10, None, @@ -911,7 +911,7 @@ async fn test_model_provider_filter_selects_only_matching_sessions() -> Result<( .await?; assert!(unknown_sessions.items.is_empty()); - let all_sessions = get_conversations(home, 10, None, NO_SOURCE_FILTER, None, "openai").await?; + let all_sessions = get_threads(home, 10, None, NO_SOURCE_FILTER, None, "openai").await?; assert_eq!(all_sessions.items.len(), 3); Ok(()) diff --git a/codex-rs/core/src/tasks/review.rs b/codex-rs/core/src/tasks/review.rs index 00dbc51f4..4a2b587af 100644 --- a/codex-rs/core/src/tasks/review.rs +++ b/codex-rs/core/src/tasks/review.rs @@ -15,7 +15,7 @@ use tokio_util::sync::CancellationToken; use crate::codex::Session; use crate::codex::TurnContext; -use crate::codex_delegate::run_codex_conversation_one_shot; +use crate::codex_delegate::run_codex_thread_one_shot; use crate::review_format::format_review_findings_block; use crate::review_format::render_review_output_text; use crate::state::TaskKind; @@ -92,7 +92,7 @@ async fn start_review_conversation( sub_agent_config.base_instructions = Some(crate::REVIEW_PROMPT.to_string()); sub_agent_config.model = Some(config.review_model.clone()); - (run_codex_conversation_one_shot( + (run_codex_thread_one_shot( sub_agent_config, session.auth_manager(), session.models_manager(), diff --git a/codex-rs/core/src/conversation_manager.rs b/codex-rs/core/src/thread_manager.rs similarity index 71% rename from codex-rs/core/src/conversation_manager.rs rename to codex-rs/core/src/thread_manager.rs index cf0f0106d..08f432a59 100644 --- a/codex-rs/core/src/conversation_manager.rs +++ b/codex-rs/core/src/thread_manager.rs @@ -7,7 +7,7 @@ use crate::agent::AgentControl; use crate::codex::Codex; use crate::codex::CodexSpawnOk; use crate::codex::INITIAL_SUBMIT_ID; -use crate::codex_conversation::CodexConversation; +use crate::codex_thread::CodexThread; use crate::config::Config; use crate::error::CodexErr; use crate::error::Result as CodexResult; @@ -18,7 +18,7 @@ use crate::protocol::SessionConfiguredEvent; use crate::rollout::RolloutRecorder; use crate::rollout::truncation; use crate::skills::SkillsManager; -use codex_protocol::ConversationId; +use codex_protocol::ThreadId; use codex_protocol::openai_models::ModelPreset; use codex_protocol::protocol::InitialHistory; use codex_protocol::protocol::Op; @@ -31,38 +31,38 @@ use std::sync::Arc; use tempfile::TempDir; use tokio::sync::RwLock; -/// Represents a newly created Codex conversation, including the first event +/// Represents a newly created Codex thread (formerly called a conversation), including the first event /// (which is [`EventMsg::SessionConfigured`]). -pub struct NewConversation { - pub conversation_id: ConversationId, - pub conversation: Arc, +pub struct NewThread { + pub thread_id: ThreadId, + pub thread: Arc, pub session_configured: SessionConfiguredEvent, } -/// [`ConversationManager`] is responsible for creating conversations and -/// maintaining them in memory. -pub struct ConversationManager { - state: Arc, +/// [`ThreadManager`] is responsible for creating threads and maintaining +/// them in memory. +pub struct ThreadManager { + state: Arc, #[cfg(any(test, feature = "test-support"))] _test_codex_home_guard: Option, } -/// Shared, `Arc`-owned state for [`ConversationManager`]. This `Arc` is required to have a single +/// Shared, `Arc`-owned state for [`ThreadManager`]. This `Arc` is required to have a single /// `Arc` reference that can be downgraded to by `AgentControl` while preventing every single /// function to require an `Arc<&Self>`. -pub(crate) struct ConversationManagerState { - conversations: Arc>>>, +pub(crate) struct ThreadManagerState { + threads: Arc>>>, auth_manager: Arc, models_manager: Arc, skills_manager: Arc, session_source: SessionSource, } -impl ConversationManager { +impl ThreadManager { pub fn new(auth_manager: Arc, session_source: SessionSource) -> Self { Self { - state: Arc::new(ConversationManagerState { - conversations: Arc::new(RwLock::new(HashMap::new())), + state: Arc::new(ThreadManagerState { + threads: Arc::new(RwLock::new(HashMap::new())), models_manager: Arc::new(ModelsManager::new(auth_manager.clone())), skills_manager: Arc::new(SkillsManager::new( auth_manager.codex_home().to_path_buf(), @@ -96,8 +96,8 @@ impl ConversationManager { ) -> Self { let auth_manager = AuthManager::from_auth_for_testing_with_home(auth, codex_home); Self { - state: Arc::new(ConversationManagerState { - conversations: Arc::new(RwLock::new(HashMap::new())), + state: Arc::new(ThreadManagerState { + threads: Arc::new(RwLock::new(HashMap::new())), models_manager: Arc::new(ModelsManager::with_provider( auth_manager.clone(), provider, @@ -128,16 +128,13 @@ impl ConversationManager { self.state.models_manager.list_models(config).await } - pub async fn get_conversation( - &self, - conversation_id: ConversationId, - ) -> CodexResult> { - self.state.get_conversation(conversation_id).await + pub async fn get_thread(&self, thread_id: ThreadId) -> CodexResult> { + self.state.get_thread(thread_id).await } - pub async fn new_conversation(&self, config: Config) -> CodexResult { + pub async fn start_thread(&self, config: Config) -> CodexResult { self.state - .spawn_conversation( + .spawn_thread( config, InitialHistory::New, Arc::clone(&self.state.auth_manager), @@ -146,56 +143,96 @@ impl ConversationManager { .await } + pub async fn resume_thread_from_rollout( + &self, + config: Config, + rollout_path: PathBuf, + auth_manager: Arc, + ) -> CodexResult { + let initial_history = RolloutRecorder::get_rollout_history(&rollout_path).await?; + self.resume_thread_with_history(config, initial_history, auth_manager) + .await + } + + pub async fn resume_thread_with_history( + &self, + config: Config, + initial_history: InitialHistory, + auth_manager: Arc, + ) -> CodexResult { + self.state + .spawn_thread(config, initial_history, auth_manager, self.agent_control()) + .await + } + + #[deprecated(note = "use get_thread")] + pub async fn get_conversation(&self, thread_id: ThreadId) -> CodexResult> { + self.get_thread(thread_id).await + } + + #[deprecated(note = "use start_thread")] + pub async fn new_conversation(&self, config: Config) -> CodexResult { + self.start_thread(config).await + } + + #[deprecated(note = "use resume_thread_from_rollout")] pub async fn resume_conversation_from_rollout( &self, config: Config, rollout_path: PathBuf, auth_manager: Arc, - ) -> CodexResult { - let initial_history = RolloutRecorder::get_rollout_history(&rollout_path).await?; - self.resume_conversation_with_history(config, initial_history, auth_manager) + ) -> CodexResult { + self.resume_thread_from_rollout(config, rollout_path, auth_manager) .await } + #[deprecated(note = "use resume_thread_with_history")] pub async fn resume_conversation_with_history( &self, config: Config, initial_history: InitialHistory, auth_manager: Arc, - ) -> CodexResult { - self.state - .spawn_conversation(config, initial_history, auth_manager, self.agent_control()) + ) -> CodexResult { + self.resume_thread_with_history(config, initial_history, auth_manager) .await } - /// Removes the conversation from the manager's internal map, though the conversation is stored - /// as `Arc`, it is possible that other references to it exist elsewhere. - /// Returns the conversation if the conversation was found and removed. - pub async fn remove_conversation( - &self, - conversation_id: &ConversationId, - ) -> Option> { - self.state - .conversations - .write() - .await - .remove(conversation_id) + #[deprecated(note = "use remove_thread")] + pub async fn remove_conversation(&self, thread_id: &ThreadId) -> Option> { + self.remove_thread(thread_id).await } - /// Fork an existing conversation by taking messages up to the given position (not including - /// the message at the given position) and starting a new conversation with identical - /// configuration (unless overridden by the caller's `config`). The new conversation will have - /// a fresh id. + #[deprecated(note = "use fork_thread")] pub async fn fork_conversation( &self, nth_user_message: usize, config: Config, path: PathBuf, - ) -> CodexResult { + ) -> CodexResult { + self.fork_thread(nth_user_message, config, path).await + } + + /// Removes the thread from the manager's internal map, though the thread is stored + /// as `Arc`, it is possible that other references to it exist elsewhere. + /// Returns the thread if the thread was found and removed. + pub async fn remove_thread(&self, thread_id: &ThreadId) -> Option> { + self.state.threads.write().await.remove(thread_id) + } + + /// Fork an existing thread by taking messages up to the given position (not including + /// the message at the given position) and starting a new thread with identical + /// configuration (unless overridden by the caller's `config`). The new thread will have + /// a fresh id. + pub async fn fork_thread( + &self, + nth_user_message: usize, + config: Config, + path: PathBuf, + ) -> CodexResult { let history = RolloutRecorder::get_rollout_history(&path).await?; let history = truncate_before_nth_user_message(history, nth_user_message); self.state - .spawn_conversation( + .spawn_thread( config, history, Arc::clone(&self.state.auth_manager), @@ -209,36 +246,26 @@ impl ConversationManager { } } -impl ConversationManagerState { - pub(crate) async fn get_conversation( - &self, - conversation_id: ConversationId, - ) -> CodexResult> { - let conversations = self.conversations.read().await; - conversations - .get(&conversation_id) +impl ThreadManagerState { + pub(crate) async fn get_thread(&self, thread_id: ThreadId) -> CodexResult> { + let threads = self.threads.read().await; + threads + .get(&thread_id) .cloned() - .ok_or_else(|| CodexErr::ConversationNotFound(conversation_id)) + .ok_or_else(|| CodexErr::ThreadNotFound(thread_id)) } - pub(crate) async fn send_op( - &self, - conversation_id: ConversationId, - op: Op, - ) -> CodexResult { - self.get_conversation(conversation_id) - .await? - .submit(op) - .await + pub(crate) async fn send_op(&self, thread_id: ThreadId, op: Op) -> CodexResult { + self.get_thread(thread_id).await?.submit(op).await } #[allow(dead_code)] // Used by upcoming multi-agent tooling. - pub(crate) async fn spawn_new_conversation( + pub(crate) async fn spawn_new_thread( &self, config: Config, agent_control: AgentControl, - ) -> CodexResult { - self.spawn_conversation( + ) -> CodexResult { + self.spawn_thread( config, InitialHistory::New, Arc::clone(&self.auth_manager), @@ -247,16 +274,15 @@ impl ConversationManagerState { .await } - pub(crate) async fn spawn_conversation( + pub(crate) async fn spawn_thread( &self, config: Config, initial_history: InitialHistory, auth_manager: Arc, agent_control: AgentControl, - ) -> CodexResult { + ) -> CodexResult { let CodexSpawnOk { - codex, - conversation_id, + codex, thread_id, .. } = Codex::spawn( config, auth_manager, @@ -267,14 +293,14 @@ impl ConversationManagerState { agent_control, ) .await?; - self.finalize_spawn(codex, conversation_id).await + self.finalize_thread_spawn(codex, thread_id).await } - async fn finalize_spawn( + async fn finalize_thread_spawn( &self, codex: Codex, - conversation_id: ConversationId, - ) -> CodexResult { + thread_id: ThreadId, + ) -> CodexResult { let event = codex.next_event().await?; let session_configured = match event { Event { @@ -286,18 +312,16 @@ impl ConversationManagerState { } }; - let conversation = Arc::new(CodexConversation::new( + let thread = Arc::new(CodexThread::new( codex, session_configured.rollout_path.clone(), )); - self.conversations - .write() - .await - .insert(conversation_id, conversation.clone()); + self.threads.write().await.insert(thread_id, thread.clone()); - Ok(NewConversation { - conversation_id, - conversation, + #[allow(deprecated)] + Ok(NewThread { + thread_id, + thread, session_configured, }) } diff --git a/codex-rs/core/src/tools/spec.rs b/codex-rs/core/src/tools/spec.rs index 75756831b..d9e21bc24 100644 --- a/codex-rs/core/src/tools/spec.rs +++ b/codex-rs/core/src/tools/spec.rs @@ -408,7 +408,7 @@ fn create_view_image_tool() -> ToolSpec { ToolSpec::Function(ResponsesApiTool { name: "view_image".to_string(), description: - "Attach a local image (by filesystem path) to the conversation context for this turn." + "Attach a local image (by filesystem path) to the thread context for this turn." .to_string(), strict: false, parameters: JsonSchema::Object { diff --git a/codex-rs/core/tests/chat_completions_payload.rs b/codex-rs/core/tests/chat_completions_payload.rs index 8af5df216..536de44e8 100644 --- a/codex-rs/core/tests/chat_completions_payload.rs +++ b/codex-rs/core/tests/chat_completions_payload.rs @@ -14,7 +14,7 @@ use codex_core::ResponseItem; use codex_core::WireApi; use codex_core::models_manager::manager::ModelsManager; use codex_otel::otel_manager::OtelManager; -use codex_protocol::ConversationId; +use codex_protocol::ThreadId; use codex_protocol::models::ReasoningItemContent; use codex_protocol::protocol::SessionSource; use core_test_support::load_default_config_for_test; @@ -73,7 +73,7 @@ async fn run_request(input: Vec) -> Value { let summary = config.model_reasoning_summary; let config = Arc::new(config); - let conversation_id = ConversationId::new(); + let conversation_id = ThreadId::new(); let model = ModelsManager::get_model_offline(config.model.as_deref()); let model_family = ModelsManager::construct_model_family_offline(model.as_str(), &config); let otel_manager = OtelManager::new( diff --git a/codex-rs/core/tests/chat_completions_sse.rs b/codex-rs/core/tests/chat_completions_sse.rs index 4f0583827..17d4a5a4a 100644 --- a/codex-rs/core/tests/chat_completions_sse.rs +++ b/codex-rs/core/tests/chat_completions_sse.rs @@ -13,7 +13,7 @@ use codex_core::ResponseItem; use codex_core::WireApi; use codex_core::models_manager::manager::ModelsManager; use codex_otel::otel_manager::OtelManager; -use codex_protocol::ConversationId; +use codex_protocol::ThreadId; use codex_protocol::models::ReasoningItemContent; use codex_protocol::protocol::SessionSource; use core_test_support::load_default_config_for_test; @@ -72,7 +72,7 @@ async fn run_stream_with_bytes(sse_body: &[u8]) -> Vec { let summary = config.model_reasoning_summary; let config = Arc::new(config); - let conversation_id = ConversationId::new(); + let conversation_id = ThreadId::new(); let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key")); let auth_mode = auth_manager.get_auth_mode(); let model = ModelsManager::get_model_offline(config.model.as_deref()); diff --git a/codex-rs/core/tests/common/lib.rs b/codex-rs/core/tests/common/lib.rs index 9568ec278..45e8b0b46 100644 --- a/codex-rs/core/tests/common/lib.rs +++ b/codex-rs/core/tests/common/lib.rs @@ -2,7 +2,7 @@ use tempfile::TempDir; -use codex_core::CodexConversation; +use codex_core::CodexThread; use codex_core::config::Config; use codex_core::config::ConfigBuilder; use codex_core::config::ConfigOverrides; @@ -170,10 +170,7 @@ pub fn load_sse_fixture_with_id(path: impl AsRef, id: &str) -> .collect() } -pub async fn wait_for_event( - codex: &CodexConversation, - predicate: F, -) -> codex_core::protocol::EventMsg +pub async fn wait_for_event(codex: &CodexThread, predicate: F) -> codex_core::protocol::EventMsg where F: FnMut(&codex_core::protocol::EventMsg) -> bool, { @@ -181,7 +178,7 @@ where wait_for_event_with_timeout(codex, predicate, Duration::from_secs(1)).await } -pub async fn wait_for_event_match(codex: &CodexConversation, matcher: F) -> T +pub async fn wait_for_event_match(codex: &CodexThread, matcher: F) -> T where F: Fn(&codex_core::protocol::EventMsg) -> Option, { @@ -190,7 +187,7 @@ where } pub async fn wait_for_event_with_timeout( - codex: &CodexConversation, + codex: &CodexThread, mut predicate: F, wait_time: tokio::time::Duration, ) -> codex_core::protocol::EventMsg diff --git a/codex-rs/core/tests/common/test_codex.rs b/codex-rs/core/tests/common/test_codex.rs index 7ae3ea159..24d5dd8bc 100644 --- a/codex-rs/core/tests/common/test_codex.rs +++ b/codex-rs/core/tests/common/test_codex.rs @@ -5,9 +5,9 @@ use std::sync::Arc; use anyhow::Result; use codex_core::CodexAuth; -use codex_core::CodexConversation; -use codex_core::ConversationManager; +use codex_core::CodexThread; use codex_core::ModelProviderInfo; +use codex_core::ThreadManager; use codex_core::built_in_model_providers; use codex_core::config::Config; use codex_core::features::Feature; @@ -138,34 +138,30 @@ impl TestCodexBuilder { resume_from: Option, ) -> anyhow::Result { let auth = self.auth.clone(); - let conversation_manager = ConversationManager::with_models_provider_and_home( + let thread_manager = ThreadManager::with_models_provider_and_home( auth.clone(), config.model_provider.clone(), config.codex_home.clone(), ); - let conversation_manager = Arc::new(conversation_manager); + let thread_manager = Arc::new(thread_manager); let new_conversation = match resume_from { Some(path) => { let auth_manager = codex_core::AuthManager::from_auth_for_testing(auth); - conversation_manager - .resume_conversation_from_rollout(config.clone(), path, auth_manager) - .await? - } - None => { - conversation_manager - .new_conversation(config.clone()) + thread_manager + .resume_thread_from_rollout(config.clone(), path, auth_manager) .await? } + None => thread_manager.start_thread(config.clone()).await?, }; Ok(TestCodex { home, cwd, config, - codex: new_conversation.conversation, + codex: new_conversation.thread, session_configured: new_conversation.session_configured, - conversation_manager, + thread_manager, }) } @@ -208,10 +204,10 @@ impl TestCodexBuilder { pub struct TestCodex { pub home: Arc, pub cwd: Arc, - pub codex: Arc, + pub codex: Arc, pub session_configured: SessionConfiguredEvent, pub config: Config, - pub conversation_manager: Arc, + pub thread_manager: Arc, } impl TestCodex { diff --git a/codex-rs/core/tests/responses_headers.rs b/codex-rs/core/tests/responses_headers.rs index 3b0ffd298..188397fd2 100644 --- a/codex-rs/core/tests/responses_headers.rs +++ b/codex-rs/core/tests/responses_headers.rs @@ -12,7 +12,7 @@ use codex_core::ResponseItem; use codex_core::WireApi; use codex_core::models_manager::manager::ModelsManager; use codex_otel::otel_manager::OtelManager; -use codex_protocol::ConversationId; +use codex_protocol::ThreadId; use codex_protocol::config_types::ReasoningSummary; use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SubAgentSource; @@ -65,7 +65,7 @@ async fn responses_stream_includes_subagent_header_on_review() { config.model = Some(model.clone()); let config = Arc::new(config); - let conversation_id = ConversationId::new(); + let conversation_id = ThreadId::new(); let auth_mode = AuthMode::ChatGPT; let session_source = SessionSource::SubAgent(SubAgentSource::Review); let model_family = ModelsManager::construct_model_family_offline(model.as_str(), &config); @@ -159,7 +159,7 @@ async fn responses_stream_includes_subagent_header_on_other() { config.model = Some(model.clone()); let config = Arc::new(config); - let conversation_id = ConversationId::new(); + let conversation_id = ThreadId::new(); let auth_mode = AuthMode::ChatGPT; let session_source = SessionSource::SubAgent(SubAgentSource::Other("my-task".to_string())); let model_family = ModelsManager::construct_model_family_offline(model.as_str(), &config); @@ -251,7 +251,7 @@ async fn responses_respects_model_family_overrides_from_config() { let model = config.model.clone().expect("model configured"); let config = Arc::new(config); - let conversation_id = ConversationId::new(); + let conversation_id = ThreadId::new(); let auth_mode = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key")).get_auth_mode(); let session_source = diff --git a/codex-rs/core/tests/suite/cli_stream.rs b/codex-rs/core/tests/suite/cli_stream.rs index 8901dd210..d6cf977a4 100644 --- a/codex-rs/core/tests/suite/cli_stream.rs +++ b/codex-rs/core/tests/suite/cli_stream.rs @@ -72,7 +72,7 @@ async fn chat_mode_stream_cli() { // Verify a new session rollout was created and is discoverable via list_conversations let provider_filter = vec!["mock".to_string()]; - let page = RolloutRecorder::list_conversations( + let page = RolloutRecorder::list_threads( home.path(), 10, None, diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index 5ce6e9f2f..7d876ceb1 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -1,16 +1,16 @@ use codex_core::AuthManager; use codex_core::CodexAuth; use codex_core::ContentItem; -use codex_core::ConversationManager; use codex_core::LocalShellAction; use codex_core::LocalShellExecAction; use codex_core::LocalShellStatus; use codex_core::ModelClient; use codex_core::ModelProviderInfo; -use codex_core::NewConversation; +use codex_core::NewThread; use codex_core::Prompt; use codex_core::ResponseEvent; use codex_core::ResponseItem; +use codex_core::ThreadManager; use codex_core::WireApi; use codex_core::auth::AuthCredentialsStoreMode; use codex_core::built_in_model_providers; @@ -21,7 +21,7 @@ use codex_core::protocol::EventMsg; use codex_core::protocol::Op; use codex_core::protocol::SessionSource; use codex_otel::otel_manager::OtelManager; -use codex_protocol::ConversationId; +use codex_protocol::ThreadId; use codex_protocol::config_types::ReasoningSummary; use codex_protocol::config_types::Verbosity; use codex_protocol::models::ReasoningItemContent; @@ -259,19 +259,19 @@ async fn resume_includes_initial_messages_and_sends_prior_items() { // Also configure user instructions to ensure they are NOT delivered on resume. config.user_instructions = Some("be nice".to_string()); - let conversation_manager = ConversationManager::with_models_provider_and_home( + let thread_manager = ThreadManager::with_models_provider_and_home( CodexAuth::from_api_key("Test API Key"), config.model_provider.clone(), config.codex_home.clone(), ); let auth_manager = codex_core::AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key")); - let NewConversation { - conversation: codex, + let NewThread { + thread: codex, session_configured, .. - } = conversation_manager - .resume_conversation_from_rollout(config, session_path.clone(), auth_manager) + } = thread_manager + .resume_thread_from_rollout(config, session_path.clone(), auth_manager) .await .expect("resume conversation"); @@ -347,17 +347,18 @@ async fn includes_conversation_id_and_model_headers_in_request() { let mut config = load_default_config_for_test(&codex_home).await; config.model_provider = model_provider; - let conversation_manager = ConversationManager::with_models_provider_and_home( + let thread_manager = ThreadManager::with_models_provider_and_home( CodexAuth::from_api_key("Test API Key"), config.model_provider.clone(), config.codex_home.clone(), ); - let NewConversation { - conversation: codex, - conversation_id, + let NewThread { + thread: codex, + thread_id: conversation_id, session_configured: _, - } = conversation_manager - .new_conversation(config) + .. + } = thread_manager + .start_thread(config) .await .expect("create new conversation"); @@ -410,16 +411,16 @@ async fn includes_base_instructions_override_in_request() { config.base_instructions = Some("test instructions".to_string()); config.model_provider = model_provider; - let conversation_manager = ConversationManager::with_models_provider_and_home( + let thread_manager = ThreadManager::with_models_provider_and_home( CodexAuth::from_api_key("Test API Key"), config.model_provider.clone(), config.codex_home.clone(), ); - let codex = conversation_manager - .new_conversation(config) + let codex = thread_manager + .start_thread(config) .await .expect("create new conversation") - .conversation; + .thread; codex .submit(Op::UserInput { @@ -472,17 +473,18 @@ async fn chatgpt_auth_sends_correct_request() { let codex_home = TempDir::new().unwrap(); let mut config = load_default_config_for_test(&codex_home).await; config.model_provider = model_provider; - let conversation_manager = ConversationManager::with_models_provider_and_home( + let thread_manager = ThreadManager::with_models_provider_and_home( create_dummy_codex_auth(), config.model_provider.clone(), config.codex_home.clone(), ); - let NewConversation { - conversation: codex, - conversation_id, + let NewThread { + thread: codex, + thread_id: conversation_id, session_configured: _, - } = conversation_manager - .new_conversation(config) + .. + } = thread_manager + .start_thread(config) .await .expect("create new conversation"); @@ -572,12 +574,9 @@ async fn prefers_apikey_when_config_prefers_apikey_even_with_chatgpt_tokens() { Ok(None) => panic!("No CodexAuth found in codex_home"), Err(e) => panic!("Failed to load CodexAuth: {e}"), }; - let conversation_manager = ConversationManager::new(auth_manager, SessionSource::Exec); - let NewConversation { - conversation: codex, - .. - } = conversation_manager - .new_conversation(config) + let thread_manager = ThreadManager::new(auth_manager, SessionSource::Exec); + let NewThread { thread: codex, .. } = thread_manager + .start_thread(config) .await .expect("create new conversation"); @@ -611,16 +610,16 @@ async fn includes_user_instructions_message_in_request() { config.model_provider = model_provider; config.user_instructions = Some("be nice".to_string()); - let conversation_manager = ConversationManager::with_models_provider_and_home( + let thread_manager = ThreadManager::with_models_provider_and_home( CodexAuth::from_api_key("Test API Key"), config.model_provider.clone(), config.codex_home.clone(), ); - let codex = conversation_manager - .new_conversation(config) + let codex = thread_manager + .start_thread(config) .await .expect("create new conversation") - .conversation; + .thread; codex .submit(Op::UserInput { @@ -682,16 +681,16 @@ async fn skills_append_to_instructions() { config.cwd = codex_home.path().to_path_buf(); config.features.enable(Feature::Skills); - let conversation_manager = ConversationManager::with_models_provider_and_home( + let thread_manager = ThreadManager::with_models_provider_and_home( CodexAuth::from_api_key("Test API Key"), config.model_provider.clone(), config.codex_home.clone(), ); - let codex = conversation_manager - .new_conversation(config) + let codex = thread_manager + .start_thread(config) .await .expect("create new conversation") - .conversation; + .thread; codex .submit(Op::UserInput { @@ -1049,16 +1048,16 @@ async fn includes_developer_instructions_message_in_request() { config.user_instructions = Some("be nice".to_string()); config.developer_instructions = Some("be useful".to_string()); - let conversation_manager = ConversationManager::with_models_provider_and_home( + let thread_manager = ThreadManager::with_models_provider_and_home( CodexAuth::from_api_key("Test API Key"), config.model_provider.clone(), config.codex_home.clone(), ); - let codex = conversation_manager - .new_conversation(config) + let codex = thread_manager + .start_thread(config) .await .expect("create new conversation") - .conversation; + .thread; codex .submit(Op::UserInput { @@ -1144,7 +1143,7 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() { config.model = Some(model.clone()); let config = Arc::new(config); let model_family = ModelsManager::construct_model_family_offline(model.as_str(), &config); - let conversation_id = ConversationId::new(); + let conversation_id = ThreadId::new(); let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key")); let otel_manager = OtelManager::new( conversation_id, @@ -1280,16 +1279,16 @@ async fn token_count_includes_rate_limits_snapshot() { let mut config = load_default_config_for_test(&home).await; config.model_provider = provider; - let conversation_manager = ConversationManager::with_models_provider_and_home( + let thread_manager = ThreadManager::with_models_provider_and_home( CodexAuth::from_api_key("test"), config.model_provider.clone(), config.codex_home.clone(), ); - let codex = conversation_manager - .new_conversation(config) + let codex = thread_manager + .start_thread(config) .await .expect("create conversation") - .conversation; + .thread; codex .submit(Op::UserInput { @@ -1639,16 +1638,16 @@ async fn azure_overrides_assign_properties_used_for_responses_url() { let mut config = load_default_config_for_test(&codex_home).await; config.model_provider = provider; - let conversation_manager = ConversationManager::with_models_provider_and_home( + let thread_manager = ThreadManager::with_models_provider_and_home( create_dummy_codex_auth(), config.model_provider.clone(), config.codex_home.clone(), ); - let codex = conversation_manager - .new_conversation(config) + let codex = thread_manager + .start_thread(config) .await .expect("create new conversation") - .conversation; + .thread; codex .submit(Op::UserInput { @@ -1722,16 +1721,16 @@ async fn env_var_overrides_loaded_auth() { let mut config = load_default_config_for_test(&codex_home).await; config.model_provider = provider; - let conversation_manager = ConversationManager::with_models_provider_and_home( + let thread_manager = ThreadManager::with_models_provider_and_home( create_dummy_codex_auth(), config.model_provider.clone(), config.codex_home.clone(), ); - let codex = conversation_manager - .new_conversation(config) + let codex = thread_manager + .start_thread(config) .await .expect("create new conversation") - .conversation; + .thread; codex .submit(Op::UserInput { @@ -1805,16 +1804,13 @@ async fn history_dedupes_streamed_and_final_messages_across_turns() { let mut config = load_default_config_for_test(&codex_home).await; config.model_provider = model_provider; - let conversation_manager = ConversationManager::with_models_provider_and_home( + let thread_manager = ThreadManager::with_models_provider_and_home( CodexAuth::from_api_key("Test API Key"), config.model_provider.clone(), config.codex_home.clone(), ); - let NewConversation { - conversation: codex, - .. - } = conversation_manager - .new_conversation(config) + let NewThread { thread: codex, .. } = thread_manager + .start_thread(config) .await .expect("create new conversation"); diff --git a/codex-rs/core/tests/suite/compact.rs b/codex-rs/core/tests/suite/compact.rs index 09b5fb18b..e0845ef72 100644 --- a/codex-rs/core/tests/suite/compact.rs +++ b/codex-rs/core/tests/suite/compact.rs @@ -1,8 +1,8 @@ #![allow(clippy::expect_used)] use codex_core::CodexAuth; -use codex_core::ConversationManager; use codex_core::ModelProviderInfo; -use codex_core::NewConversation; +use codex_core::NewThread; +use codex_core::ThreadManager; use codex_core::built_in_model_providers; use codex_core::compact::SUMMARIZATION_PROMPT; use codex_core::compact::SUMMARY_PREFIX; @@ -62,7 +62,7 @@ const DUMMY_CALL_ID: &str = "call-multi-auto"; const FUNCTION_CALL_LIMIT_MSG: &str = "function call limit push"; const POST_AUTO_USER_MSG: &str = "post auto follow-up"; -pub(super) const COMPACT_WARNING_MESSAGE: &str = "Heads up: Long conversations and multiple compactions can cause the model to be less accurate. Start a new conversation when possible to keep conversations small and targeted."; +pub(super) const COMPACT_WARNING_MESSAGE: &str = "Heads up: Long threads and multiple compactions can cause the model to be less accurate. Start a new thread when possible to keep threads small and targeted."; fn auto_summary(summary: &str) -> String { summary.to_string() @@ -144,15 +144,15 @@ async fn summarize_context_three_requests_and_instructions() { config.model_provider = model_provider; set_test_compact_prompt(&mut config); config.model_auto_compact_token_limit = Some(200_000); - let conversation_manager = ConversationManager::with_models_provider( + let thread_manager = ThreadManager::with_models_provider( CodexAuth::from_api_key("dummy"), config.model_provider.clone(), ); - let NewConversation { - conversation: codex, + let NewThread { + thread: codex, session_configured, .. - } = conversation_manager.new_conversation(config).await.unwrap(); + } = thread_manager.start_thread(config).await.unwrap(); let rollout_path = session_configured.rollout_path; // 1) Normal user input – should hit server once. @@ -340,15 +340,15 @@ async fn manual_compact_uses_custom_prompt() { config.model_provider = model_provider; config.compact_prompt = Some(custom_prompt.to_string()); - let conversation_manager = ConversationManager::with_models_provider( + let thread_manager = ThreadManager::with_models_provider( CodexAuth::from_api_key("dummy"), config.model_provider.clone(), ); - let codex = conversation_manager - .new_conversation(config) + let codex = thread_manager + .start_thread(config) .await .expect("create conversation") - .conversation; + .thread; codex.submit(Op::Compact).await.expect("trigger compact"); let warning_event = wait_for_event(&codex, |ev| matches!(ev, EventMsg::Warning(_))).await; @@ -420,14 +420,11 @@ async fn manual_compact_emits_api_and_local_token_usage_events() { config.model_provider = model_provider; set_test_compact_prompt(&mut config); - let conversation_manager = ConversationManager::with_models_provider( + let thread_manager = ThreadManager::with_models_provider( CodexAuth::from_api_key("dummy"), config.model_provider.clone(), ); - let NewConversation { - conversation: codex, - .. - } = conversation_manager.new_conversation(config).await.unwrap(); + let NewThread { thread: codex, .. } = thread_manager.start_thread(config).await.unwrap(); // Trigger manual compact and collect TokenCount events for the compact turn. codex.submit(Op::Compact).await.unwrap(); @@ -1072,15 +1069,11 @@ async fn auto_compact_runs_after_token_limit_hit() { config.model_provider = model_provider; set_test_compact_prompt(&mut config); config.model_auto_compact_token_limit = Some(200_000); - let conversation_manager = ConversationManager::with_models_provider( + let thread_manager = ThreadManager::with_models_provider( CodexAuth::from_api_key("dummy"), config.model_provider.clone(), ); - let codex = conversation_manager - .new_conversation(config) - .await - .unwrap() - .conversation; + let codex = thread_manager.start_thread(config).await.unwrap().thread; codex .submit(Op::UserInput { @@ -1409,15 +1402,15 @@ async fn auto_compact_persists_rollout_entries() { config.model_provider = model_provider; set_test_compact_prompt(&mut config); config.model_auto_compact_token_limit = Some(200_000); - let conversation_manager = ConversationManager::with_models_provider( + let thread_manager = ThreadManager::with_models_provider( CodexAuth::from_api_key("dummy"), config.model_provider.clone(), ); - let NewConversation { - conversation: codex, + let NewThread { + thread: codex, session_configured, .. - } = conversation_manager.new_conversation(config).await.unwrap(); + } = thread_manager.start_thread(config).await.unwrap(); codex .submit(Op::UserInput { @@ -1524,14 +1517,14 @@ async fn manual_compact_retries_after_context_window_error() { config.model_provider = model_provider; set_test_compact_prompt(&mut config); config.model_auto_compact_token_limit = Some(200_000); - let codex = ConversationManager::with_models_provider( + let codex = ThreadManager::with_models_provider( CodexAuth::from_api_key("dummy"), config.model_provider.clone(), ) - .new_conversation(config) + .start_thread(config) .await .unwrap() - .conversation; + .thread; codex .submit(Op::UserInput { @@ -1551,7 +1544,7 @@ async fn manual_compact_retries_after_context_window_error() { panic!("expected background event after compact retry"); }; assert!( - event.message.contains("Trimmed 1 older conversation item"), + event.message.contains("Trimmed 1 older thread item"), "background event should mention trimmed item count: {}", event.message ); @@ -1657,14 +1650,14 @@ async fn manual_compact_twice_preserves_latest_user_messages() { let mut config = load_default_config_for_test(&home).await; config.model_provider = model_provider; set_test_compact_prompt(&mut config); - let codex = ConversationManager::with_models_provider( + let codex = ThreadManager::with_models_provider( CodexAuth::from_api_key("dummy"), config.model_provider.clone(), ) - .new_conversation(config) + .start_thread(config) .await .unwrap() - .conversation; + .thread; codex .submit(Op::UserInput { @@ -1864,15 +1857,11 @@ async fn auto_compact_allows_multiple_attempts_when_interleaved_with_other_turn_ config.model_provider = model_provider; set_test_compact_prompt(&mut config); config.model_auto_compact_token_limit = Some(200); - let conversation_manager = ConversationManager::with_models_provider( + let thread_manager = ThreadManager::with_models_provider( CodexAuth::from_api_key("dummy"), config.model_provider.clone(), ); - let codex = conversation_manager - .new_conversation(config) - .await - .unwrap() - .conversation; + let codex = thread_manager.start_thread(config).await.unwrap().thread; let mut auto_compact_lifecycle_events = Vec::new(); for user in [MULTI_AUTO_MSG, follow_up_user, final_user] { @@ -1978,14 +1967,14 @@ async fn auto_compact_triggers_after_function_call_over_95_percent_usage() { config.model_context_window = Some(context_window); config.model_auto_compact_token_limit = Some(limit); - let codex = ConversationManager::with_models_provider( + let codex = ThreadManager::with_models_provider( CodexAuth::from_api_key("dummy"), config.model_provider.clone(), ) - .new_conversation(config) + .start_thread(config) .await .unwrap() - .conversation; + .thread; codex .submit(Op::UserInput { diff --git a/codex-rs/core/tests/suite/compact_resume_fork.rs b/codex-rs/core/tests/suite/compact_resume_fork.rs index 3e38c89b3..4ad829f07 100644 --- a/codex-rs/core/tests/suite/compact_resume_fork.rs +++ b/codex-rs/core/tests/suite/compact_resume_fork.rs @@ -11,10 +11,10 @@ use super::compact::COMPACT_WARNING_MESSAGE; use super::compact::FIRST_REPLY; use super::compact::SUMMARY_TEXT; use codex_core::CodexAuth; -use codex_core::CodexConversation; -use codex_core::ConversationManager; +use codex_core::CodexThread; use codex_core::ModelProviderInfo; -use codex_core::NewConversation; +use codex_core::NewThread; +use codex_core::ThreadManager; use codex_core::built_in_model_providers; use codex_core::compact::SUMMARIZATION_PROMPT; use codex_core::config::Config; @@ -171,7 +171,7 @@ async fn compact_resume_and_fork_preserve_model_history_view() { "compact+resume test expects resumed path {resumed_path:?} to exist", ); - let forked = fork_conversation(&manager, &config, resumed_path, 2).await; + let forked = fork_thread(&manager, &config, resumed_path, 2).await; user_turn(&forked, "AFTER_FORK").await; // 3. Capture the requests to the model and validate the history slices. @@ -623,7 +623,7 @@ async fn compact_resume_after_second_compaction_preserves_history() { "second compact test expects resumed path {resumed_path:?} to exist", ); - let forked = fork_conversation(&manager, &config, resumed_path, 3).await; + let forked = fork_thread(&manager, &config, resumed_path, 3).await; user_turn(&forked, "AFTER_FORK").await; compact_conversation(&forked).await; @@ -855,7 +855,7 @@ async fn mount_second_compact_flow(server: &MockServer) { async fn start_test_conversation( server: &MockServer, model: Option<&str>, -) -> (TempDir, Config, ConversationManager, Arc) { +) -> (TempDir, Config, ThreadManager, Arc) { let model_provider = ModelProviderInfo { name: "Non-OpenAI Model provider".into(), base_url: Some(format!("{}/v1", server.uri())), @@ -868,19 +868,19 @@ async fn start_test_conversation( if let Some(model) = model { config.model = Some(model.to_string()); } - let manager = ConversationManager::with_models_provider( + let manager = ThreadManager::with_models_provider( CodexAuth::from_api_key("dummy"), config.model_provider.clone(), ); - let NewConversation { conversation, .. } = manager - .new_conversation(config.clone()) + let NewThread { thread, .. } = manager + .start_thread(config.clone()) .await .expect("create conversation"); - (home, config, manager, conversation) + (home, config, manager, thread) } -async fn user_turn(conversation: &Arc, text: &str) { +async fn user_turn(conversation: &Arc, text: &str) { conversation .submit(Op::UserInput { items: vec![UserInput::Text { text: text.into() }], @@ -891,7 +891,7 @@ async fn user_turn(conversation: &Arc, text: &str) { wait_for_event(conversation, |ev| matches!(ev, EventMsg::TaskComplete(_))).await; } -async fn compact_conversation(conversation: &Arc) { +async fn compact_conversation(conversation: &Arc) { conversation .submit(Op::Compact) .await @@ -904,34 +904,34 @@ async fn compact_conversation(conversation: &Arc) { wait_for_event(conversation, |ev| matches!(ev, EventMsg::TaskComplete(_))).await; } -async fn fetch_conversation_path(conversation: &Arc) -> std::path::PathBuf { +async fn fetch_conversation_path(conversation: &Arc) -> std::path::PathBuf { conversation.rollout_path() } async fn resume_conversation( - manager: &ConversationManager, + manager: &ThreadManager, config: &Config, path: std::path::PathBuf, -) -> Arc { +) -> Arc { let auth_manager = codex_core::AuthManager::from_auth_for_testing(CodexAuth::from_api_key("dummy")); - let NewConversation { conversation, .. } = manager - .resume_conversation_from_rollout(config.clone(), path, auth_manager) + let NewThread { thread, .. } = manager + .resume_thread_from_rollout(config.clone(), path, auth_manager) .await .expect("resume conversation"); - conversation + thread } #[cfg(test)] -async fn fork_conversation( - manager: &ConversationManager, +async fn fork_thread( + manager: &ThreadManager, config: &Config, path: std::path::PathBuf, nth_user_message: usize, -) -> Arc { - let NewConversation { conversation, .. } = manager - .fork_conversation(nth_user_message, config.clone(), path) +) -> Arc { + let NewThread { thread, .. } = manager + .fork_thread(nth_user_message, config.clone(), path) .await .expect("fork conversation"); - conversation + thread } diff --git a/codex-rs/core/tests/suite/fork_conversation.rs b/codex-rs/core/tests/suite/fork_thread.rs similarity index 89% rename from codex-rs/core/tests/suite/fork_conversation.rs rename to codex-rs/core/tests/suite/fork_thread.rs index dab856c80..50a6dba1f 100644 --- a/codex-rs/core/tests/suite/fork_conversation.rs +++ b/codex-rs/core/tests/suite/fork_thread.rs @@ -1,7 +1,7 @@ use codex_core::CodexAuth; -use codex_core::ConversationManager; use codex_core::ModelProviderInfo; -use codex_core::NewConversation; +use codex_core::NewThread; +use codex_core::ThreadManager; use codex_core::built_in_model_providers; use codex_core::parse_turn_item; use codex_core::protocol::EventMsg; @@ -26,7 +26,7 @@ fn sse_completed(id: &str) -> String { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn fork_conversation_twice_drops_to_first_message() { +async fn fork_thread_twice_drops_to_first_message() { skip_if_no_network!(); // Start a mock server that completes three turns. @@ -55,15 +55,12 @@ async fn fork_conversation_twice_drops_to_first_message() { config.model_provider = model_provider.clone(); let config_for_fork = config.clone(); - let conversation_manager = ConversationManager::with_models_provider( + let thread_manager = ThreadManager::with_models_provider( CodexAuth::from_api_key("dummy"), config.model_provider.clone(), ); - let NewConversation { - conversation: codex, - .. - } = conversation_manager - .new_conversation(config) + let NewThread { thread: codex, .. } = thread_manager + .start_thread(config) .await .expect("create conversation"); @@ -129,11 +126,11 @@ async fn fork_conversation_twice_drops_to_first_message() { // After dropping again (n=1 on fork1), compute expected relative to fork1's rollout. // Fork once with n=1 → drops the last user input and everything after. - let NewConversation { - conversation: codex_fork1, + let NewThread { + thread: codex_fork1, .. - } = conversation_manager - .fork_conversation(1, config_for_fork.clone(), base_path.clone()) + } = thread_manager + .fork_thread(1, config_for_fork.clone(), base_path.clone()) .await .expect("fork 1"); @@ -147,11 +144,11 @@ async fn fork_conversation_twice_drops_to_first_message() { ); // Fork again with n=0 → drops the (new) last user message, leaving only the first. - let NewConversation { - conversation: codex_fork2, + let NewThread { + thread: codex_fork2, .. - } = conversation_manager - .fork_conversation(0, config_for_fork.clone(), fork1_path.clone()) + } = thread_manager + .fork_thread(0, config_for_fork.clone(), fork1_path.clone()) .await .expect("fork 2"); diff --git a/codex-rs/core/tests/suite/list_models.rs b/codex-rs/core/tests/suite/list_models.rs index 30593a8f6..b81ebcb72 100644 --- a/codex-rs/core/tests/suite/list_models.rs +++ b/codex-rs/core/tests/suite/list_models.rs @@ -1,6 +1,6 @@ use anyhow::Result; use codex_core::CodexAuth; -use codex_core::ConversationManager; +use codex_core::ThreadManager; use codex_core::built_in_model_providers; use codex_protocol::openai_models::ModelPreset; use codex_protocol::openai_models::ReasoningEffort; @@ -13,7 +13,7 @@ use tempfile::tempdir; async fn list_models_returns_api_key_models() -> Result<()> { let codex_home = tempdir()?; let config = load_default_config_for_test(&codex_home).await; - let manager = ConversationManager::with_models_provider( + let manager = ThreadManager::with_models_provider( CodexAuth::from_api_key("sk-test"), built_in_model_providers()["openai"].clone(), ); @@ -29,7 +29,7 @@ async fn list_models_returns_api_key_models() -> Result<()> { async fn list_models_returns_chatgpt_models() -> Result<()> { let codex_home = tempdir()?; let config = load_default_config_for_test(&codex_home).await; - let manager = ConversationManager::with_models_provider( + let manager = ThreadManager::with_models_provider( CodexAuth::create_dummy_chatgpt_auth_for_testing(), built_in_model_providers()["openai"].clone(), ); diff --git a/codex-rs/core/tests/suite/mod.rs b/codex-rs/core/tests/suite/mod.rs index e5b809ca3..dde687a39 100644 --- a/codex-rs/core/tests/suite/mod.rs +++ b/codex-rs/core/tests/suite/mod.rs @@ -28,7 +28,7 @@ mod compact_resume_fork; mod deprecation_notice; mod exec; mod exec_policy; -mod fork_conversation; +mod fork_thread; mod grep_files; mod items; mod json_result; diff --git a/codex-rs/core/tests/suite/model_overrides.rs b/codex-rs/core/tests/suite/model_overrides.rs index f7cdac67c..a418e35a3 100644 --- a/codex-rs/core/tests/suite/model_overrides.rs +++ b/codex-rs/core/tests/suite/model_overrides.rs @@ -1,5 +1,5 @@ use codex_core::CodexAuth; -use codex_core::ConversationManager; +use codex_core::ThreadManager; use codex_core::protocol::EventMsg; use codex_core::protocol::Op; use codex_protocol::openai_models::ReasoningEffort; @@ -22,15 +22,15 @@ async fn override_turn_context_does_not_persist_when_config_exists() { let mut config = load_default_config_for_test(&codex_home).await; config.model = Some("gpt-4o".to_string()); - let conversation_manager = ConversationManager::with_models_provider( + let thread_manager = ThreadManager::with_models_provider( CodexAuth::from_api_key("Test API Key"), config.model_provider.clone(), ); - let codex = conversation_manager - .new_conversation(config) + let codex = thread_manager + .start_thread(config) .await .expect("create conversation") - .conversation; + .thread; codex .submit(Op::OverrideTurnContext { @@ -64,15 +64,15 @@ async fn override_turn_context_does_not_create_config_file() { let config = load_default_config_for_test(&codex_home).await; - let conversation_manager = ConversationManager::with_models_provider( + let thread_manager = ThreadManager::with_models_provider( CodexAuth::from_api_key("Test API Key"), config.model_provider.clone(), ); - let codex = conversation_manager - .new_conversation(config) + let codex = thread_manager + .start_thread(config) .await .expect("create conversation") - .conversation; + .thread; codex .submit(Op::OverrideTurnContext { diff --git a/codex-rs/core/tests/suite/prompt_caching.rs b/codex-rs/core/tests/suite/prompt_caching.rs index f815e2e8a..01c590893 100644 --- a/codex-rs/core/tests/suite/prompt_caching.rs +++ b/codex-rs/core/tests/suite/prompt_caching.rs @@ -75,7 +75,7 @@ async fn prompt_tools_are_consistent_across_requests() -> anyhow::Result<()> { let TestCodex { codex, config, - conversation_manager, + thread_manager, .. } = test_codex() .with_config(|config| { @@ -84,7 +84,7 @@ async fn prompt_tools_are_consistent_across_requests() -> anyhow::Result<()> { }) .build(&server) .await?; - let base_instructions = conversation_manager + let base_instructions = thread_manager .get_models_manager() .construct_model_family( config diff --git a/codex-rs/core/tests/suite/remote_models.rs b/codex-rs/core/tests/suite/remote_models.rs index 94641f1a4..696828531 100644 --- a/codex-rs/core/tests/suite/remote_models.rs +++ b/codex-rs/core/tests/suite/remote_models.rs @@ -4,9 +4,9 @@ use std::sync::Arc; use anyhow::Result; use codex_core::CodexAuth; -use codex_core::CodexConversation; -use codex_core::ConversationManager; +use codex_core::CodexThread; use codex_core::ModelProviderInfo; +use codex_core::ThreadManager; use codex_core::built_in_model_providers; use codex_core::config::Config; use codex_core::features::Feature; @@ -103,11 +103,11 @@ async fn remote_models_remote_model_uses_unified_exec() -> Result<()> { codex, cwd, config, - conversation_manager, + thread_manager, .. } = harness; - let models_manager = conversation_manager.get_models_manager(); + let models_manager = thread_manager.get_models_manager(); let available_model = wait_for_model_available(&models_manager, REMOTE_MODEL_SLUG, &config).await; @@ -249,11 +249,11 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> { codex, cwd, config, - conversation_manager, + thread_manager, .. } = harness; - let models_manager = conversation_manager.get_models_manager(); + let models_manager = thread_manager.get_models_manager(); wait_for_model_available(&models_manager, model, &config).await; codex @@ -411,10 +411,10 @@ async fn wait_for_model_available( } struct RemoteModelsHarness { - codex: Arc, + codex: Arc, cwd: Arc, config: Config, - conversation_manager: Arc, + thread_manager: Arc, } // todo(aibrahim): move this to with_model_provier in test_codex @@ -441,18 +441,16 @@ where mutate_config(&mut config); - let conversation_manager = ConversationManager::with_models_provider(auth, provider); - let conversation_manager = Arc::new(conversation_manager); + let thread_manager = ThreadManager::with_models_provider(auth, provider); + let thread_manager = Arc::new(thread_manager); - let new_conversation = conversation_manager - .new_conversation(config.clone()) - .await?; + let new_conversation = thread_manager.start_thread(config.clone()).await?; Ok(RemoteModelsHarness { - codex: new_conversation.conversation, + codex: new_conversation.thread, cwd, config, - conversation_manager, + thread_manager, }) } diff --git a/codex-rs/core/tests/suite/resume_warning.rs b/codex-rs/core/tests/suite/resume_warning.rs index 92acf7de0..5b38ce4b8 100644 --- a/codex-rs/core/tests/suite/resume_warning.rs +++ b/codex-rs/core/tests/suite/resume_warning.rs @@ -2,15 +2,15 @@ use codex_core::AuthManager; use codex_core::CodexAuth; -use codex_core::ConversationManager; -use codex_core::NewConversation; +use codex_core::NewThread; +use codex_core::ThreadManager; use codex_core::protocol::EventMsg; use codex_core::protocol::InitialHistory; use codex_core::protocol::ResumedHistory; use codex_core::protocol::RolloutItem; use codex_core::protocol::TurnContextItem; use codex_core::protocol::WarningEvent; -use codex_protocol::ConversationId; +use codex_protocol::ThreadId; use core::time::Duration; use core_test_support::load_default_config_for_test; use core_test_support::wait_for_event; @@ -36,7 +36,7 @@ fn resume_history( }; InitialHistory::Resumed(ResumedHistory { - conversation_id: ConversationId::default(), + conversation_id: ThreadId::default(), history: vec![RolloutItem::TurnContext(turn_ctx)], rollout_path: rollout_path.to_path_buf(), }) @@ -56,15 +56,18 @@ async fn emits_warning_when_resumed_model_differs() { let initial_history = resume_history(&config, "previous-model", &rollout_path); - let conversation_manager = ConversationManager::with_models_provider( + let thread_manager = ThreadManager::with_models_provider( CodexAuth::from_api_key("test"), config.model_provider.clone(), ); let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("test")); // Act: resume the conversation. - let NewConversation { conversation, .. } = conversation_manager - .resume_conversation_with_history(config, initial_history, auth_manager) + let NewThread { + thread: conversation, + .. + } = thread_manager + .resume_thread_with_history(config, initial_history, auth_manager) .await .expect("resume conversation"); diff --git a/codex-rs/core/tests/suite/review.rs b/codex-rs/core/tests/suite/review.rs index b35213f7e..763b6109d 100644 --- a/codex-rs/core/tests/suite/review.rs +++ b/codex-rs/core/tests/suite/review.rs @@ -1,10 +1,10 @@ use codex_core::CodexAuth; -use codex_core::CodexConversation; +use codex_core::CodexThread; use codex_core::ContentItem; -use codex_core::ConversationManager; use codex_core::ModelProviderInfo; use codex_core::REVIEW_PROMPT; use codex_core::ResponseItem; +use codex_core::ThreadManager; use codex_core::built_in_model_providers; use codex_core::config::Config; use codex_core::protocol::ENVIRONMENT_CONTEXT_OPEN_TAG; @@ -832,7 +832,7 @@ async fn new_conversation_for_server( server: &MockServer, codex_home: &TempDir, mutator: F, -) -> Arc +) -> Arc where F: FnOnce(&mut Config), { @@ -843,15 +843,15 @@ where let mut config = load_default_config_for_test(codex_home).await; config.model_provider = model_provider; mutator(&mut config); - let conversation_manager = ConversationManager::with_models_provider( + let thread_manager = ThreadManager::with_models_provider( CodexAuth::from_api_key("Test API Key"), config.model_provider.clone(), ); - conversation_manager - .new_conversation(config) + thread_manager + .start_thread(config) .await .expect("create conversation") - .conversation + .thread } /// Create a conversation resuming from a rollout file, configured to talk to the provided mock server. @@ -861,7 +861,7 @@ async fn resume_conversation_for_server( codex_home: &TempDir, resume_path: std::path::PathBuf, mutator: F, -) -> Arc +) -> Arc where F: FnOnce(&mut Config), { @@ -872,15 +872,15 @@ where let mut config = load_default_config_for_test(codex_home).await; config.model_provider = model_provider; mutator(&mut config); - let conversation_manager = ConversationManager::with_models_provider( + let thread_manager = ThreadManager::with_models_provider( CodexAuth::from_api_key("Test API Key"), config.model_provider.clone(), ); let auth_manager = codex_core::AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key")); - conversation_manager - .resume_conversation_from_rollout(config, resume_path, auth_manager) + thread_manager + .resume_thread_from_rollout(config, resume_path, auth_manager) .await .expect("resume conversation") - .conversation + .thread } diff --git a/codex-rs/core/tests/suite/rollout_list_find.rs b/codex-rs/core/tests/suite/rollout_list_find.rs index 1d40718d4..518f26c56 100644 --- a/codex-rs/core/tests/suite/rollout_list_find.rs +++ b/codex-rs/core/tests/suite/rollout_list_find.rs @@ -3,7 +3,7 @@ use std::io::Write; use std::path::Path; use std::path::PathBuf; -use codex_core::find_conversation_path_by_id_str; +use codex_core::find_thread_path_by_id_str; use tempfile::TempDir; use uuid::Uuid; @@ -44,7 +44,7 @@ async fn find_locates_rollout_file_by_id() { let id = Uuid::new_v4(); let expected = write_minimal_rollout_with_id(home.path(), id); - let found = find_conversation_path_by_id_str(home.path(), &id.to_string()) + let found = find_thread_path_by_id_str(home.path(), &id.to_string()) .await .unwrap(); @@ -60,7 +60,7 @@ async fn find_handles_gitignore_covering_codex_home_directory() { let id = Uuid::new_v4(); let expected = write_minimal_rollout_with_id(&codex_home, id); - let found = find_conversation_path_by_id_str(&codex_home, &id.to_string()) + let found = find_thread_path_by_id_str(&codex_home, &id.to_string()) .await .unwrap(); @@ -74,7 +74,7 @@ async fn find_ignores_granular_gitignore_rules() { let expected = write_minimal_rollout_with_id(home.path(), id); std::fs::write(home.path().join("sessions/.gitignore"), "*.jsonl\n").unwrap(); - let found = find_conversation_path_by_id_str(home.path(), &id.to_string()) + let found = find_thread_path_by_id_str(home.path(), &id.to_string()) .await .unwrap(); diff --git a/codex-rs/core/tests/suite/undo.rs b/codex-rs/core/tests/suite/undo.rs index 9fca27282..61bc3b4fd 100644 --- a/codex-rs/core/tests/suite/undo.rs +++ b/codex-rs/core/tests/suite/undo.rs @@ -8,7 +8,7 @@ use std::sync::Arc; use anyhow::Context; use anyhow::Result; use anyhow::bail; -use codex_core::CodexConversation; +use codex_core::CodexThread; use codex_core::features::Feature; use codex_core::protocol::EventMsg; use codex_core::protocol::Op; @@ -108,7 +108,7 @@ async fn run_apply_patch_turn( harness.submit(prompt).await } -async fn invoke_undo(codex: &Arc) -> Result { +async fn invoke_undo(codex: &Arc) -> Result { codex.submit(Op::Undo).await?; let event = wait_for_event_match(codex, |msg| match msg { EventMsg::UndoCompleted(done) => Some(done.clone()), @@ -118,7 +118,7 @@ async fn invoke_undo(codex: &Arc) -> Result) -> Result { +async fn expect_successful_undo(codex: &Arc) -> Result { let event = invoke_undo(codex).await?; assert!( event.success, @@ -128,7 +128,7 @@ async fn expect_successful_undo(codex: &Arc) -> Result) -> Result { +async fn expect_failed_undo(codex: &Arc) -> Result { let event = invoke_undo(codex).await?; assert!( !event.success, diff --git a/codex-rs/core/tests/suite/user_shell_cmd.rs b/codex-rs/core/tests/suite/user_shell_cmd.rs index 270cb8048..d6818c0c5 100644 --- a/codex-rs/core/tests/suite/user_shell_cmd.rs +++ b/codex-rs/core/tests/suite/user_shell_cmd.rs @@ -1,6 +1,6 @@ use anyhow::Context; -use codex_core::ConversationManager; -use codex_core::NewConversation; +use codex_core::NewThread; +use codex_core::ThreadManager; use codex_core::protocol::EventMsg; use codex_core::protocol::ExecCommandEndEvent; use codex_core::protocol::ExecCommandSource; @@ -42,15 +42,12 @@ async fn user_shell_cmd_ls_and_cat_in_temp_dir() { let mut config = load_default_config_for_test(&codex_home).await; config.cwd = cwd.path().to_path_buf(); - let conversation_manager = ConversationManager::with_models_provider( + let thread_manager = ThreadManager::with_models_provider( codex_core::CodexAuth::from_api_key("dummy"), config.model_provider.clone(), ); - let NewConversation { - conversation: codex, - .. - } = conversation_manager - .new_conversation(config) + let NewThread { thread: codex, .. } = thread_manager + .start_thread(config) .await .expect("create new conversation"); @@ -101,15 +98,12 @@ async fn user_shell_cmd_can_be_interrupted() { // Set up isolated config and conversation. let codex_home = TempDir::new().unwrap(); let config = load_default_config_for_test(&codex_home).await; - let conversation_manager = ConversationManager::with_models_provider( + let thread_manager = ThreadManager::with_models_provider( codex_core::CodexAuth::from_api_key("dummy"), config.model_provider.clone(), ); - let NewConversation { - conversation: codex, - .. - } = conversation_manager - .new_conversation(config) + let NewThread { thread: codex, .. } = thread_manager + .start_thread(config) .await .expect("create new conversation"); diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index 701c7b797..b82e371d2 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -16,10 +16,10 @@ pub use cli::ReviewArgs; use codex_common::oss::ensure_oss_provider_ready; use codex_common::oss::get_default_model_for_oss_provider; use codex_core::AuthManager; -use codex_core::ConversationManager; use codex_core::LMSTUDIO_OSS_PROVIDER_ID; -use codex_core::NewConversation; +use codex_core::NewThread; use codex_core::OLLAMA_OSS_PROVIDER_ID; +use codex_core::ThreadManager; use codex_core::auth::enforce_login_restrictions; use codex_core::config::Config; use codex_core::config::ConfigOverrides; @@ -55,7 +55,7 @@ use crate::cli::Command as ExecCommand; use crate::event_processor::CodexStatus; use crate::event_processor::EventProcessor; use codex_core::default_client::set_default_originator; -use codex_core::find_conversation_path_by_id_str; +use codex_core::find_thread_path_by_id_str; enum InitialOperation { UserTurn { @@ -286,33 +286,29 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any true, config.cli_auth_credentials_store_mode, ); - let conversation_manager = ConversationManager::new(auth_manager.clone(), SessionSource::Exec); - let default_model = conversation_manager + let thread_manager = ThreadManager::new(auth_manager.clone(), SessionSource::Exec); + let default_model = thread_manager .get_models_manager() .get_model(&config.model, &config) .await; // Handle resume subcommand by resolving a rollout path and using explicit resume API. - let NewConversation { - conversation_id: _, - conversation, + let NewThread { + thread_id: _, + thread, session_configured, } = if let Some(ExecCommand::Resume(args)) = command.as_ref() { let resume_path = resolve_resume_path(&config, args).await?; if let Some(path) = resume_path { - conversation_manager - .resume_conversation_from_rollout(config.clone(), path, auth_manager.clone()) + thread_manager + .resume_thread_from_rollout(config.clone(), path, auth_manager.clone()) .await? } else { - conversation_manager - .new_conversation(config.clone()) - .await? + thread_manager.start_thread(config.clone()).await? } } else { - conversation_manager - .new_conversation(config.clone()) - .await? + thread_manager.start_thread(config.clone()).await? }; let (initial_operation, prompt_summary) = match (command, prompt, images) { (Some(ExecCommand::Review(review_cli)), _, _) => { @@ -378,20 +374,20 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::(); { - let conversation = conversation.clone(); + let thread = thread.clone(); tokio::spawn(async move { loop { tokio::select! { _ = tokio::signal::ctrl_c() => { tracing::debug!("Keyboard interrupt"); // Immediately notify Codex to abort any in‑flight task. - conversation.submit(Op::Interrupt).await.ok(); + thread.submit(Op::Interrupt).await.ok(); // Exit the inner loop and return to the main input prompt. The codex // will emit a `TurnInterrupted` (Error) event which is drained later. break; } - res = conversation.next_event() => match res { + res = thread.next_event() => match res { Ok(event) => { debug!("Received event: {event:?}"); @@ -420,7 +416,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any items, output_schema, } => { - let task_id = conversation + let task_id = thread .submit(Op::UserTurn { items, cwd: default_cwd, @@ -436,7 +432,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any task_id } InitialOperation::Review { review_request } => { - let task_id = conversation.submit(Op::Review { review_request }).await?; + let task_id = thread.submit(Op::Review { review_request }).await?; info!("Sent review request with event ID: {task_id}"); task_id } @@ -449,7 +445,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any while let Some(event) = rx.recv().await { if let EventMsg::ElicitationRequest(ev) = &event.msg { // Automatically cancel elicitation requests in exec mode. - conversation + thread .submit(Op::ResolveElicitation { server_name: ev.server_name.clone(), request_id: ev.id.clone(), @@ -464,7 +460,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any match shutdown { CodexStatus::Running => continue, CodexStatus::InitiateShutdown => { - conversation.submit(Op::Shutdown).await?; + thread.submit(Op::Shutdown).await?; } CodexStatus::Shutdown => { break; @@ -485,7 +481,7 @@ async fn resolve_resume_path( ) -> anyhow::Result> { if args.last { let default_provider_filter = vec![config.model_provider_id.clone()]; - match codex_core::RolloutRecorder::list_conversations( + match codex_core::RolloutRecorder::list_threads( &config.codex_home, 1, None, @@ -497,12 +493,12 @@ async fn resolve_resume_path( { Ok(page) => Ok(page.items.first().map(|it| it.path.clone())), Err(e) => { - error!("Error listing conversations: {e}"); + error!("Error listing threads: {e}"); Ok(None) } } } else if let Some(id_str) = args.session_id.as_deref() { - let path = find_conversation_path_by_id_str(&config.codex_home, id_str).await?; + let path = find_thread_path_by_id_str(&config.codex_home, id_str).await?; Ok(path) } else { Ok(None) diff --git a/codex-rs/exec/tests/event_processor_with_json_output.rs b/codex-rs/exec/tests/event_processor_with_json_output.rs index d288f568e..a3e231816 100644 --- a/codex-rs/exec/tests/event_processor_with_json_output.rs +++ b/codex-rs/exec/tests/event_processor_with_json_output.rs @@ -69,8 +69,7 @@ fn event(id: &str, msg: EventMsg) -> Event { fn session_configured_produces_thread_started_event() { let mut ep = EventProcessorWithJsonOutput::new(None); let session_id = - codex_protocol::ConversationId::from_string("67e55044-10b1-426f-9247-bb680e5fe0c8") - .unwrap(); + codex_protocol::ThreadId::from_string("67e55044-10b1-426f-9247-bb680e5fe0c8").unwrap(); let rollout_path = PathBuf::from("/tmp/rollout.json"); let ev = event( "e1", diff --git a/codex-rs/feedback/src/lib.rs b/codex-rs/feedback/src/lib.rs index 2096f4505..4a227fe09 100644 --- a/codex-rs/feedback/src/lib.rs +++ b/codex-rs/feedback/src/lib.rs @@ -11,7 +11,7 @@ use std::time::Duration; use anyhow::Result; use anyhow::anyhow; -use codex_protocol::ConversationId; +use codex_protocol::ThreadId; use codex_protocol::protocol::SessionSource; use tracing::Event; use tracing::Level; @@ -88,7 +88,7 @@ impl CodexFeedback { .with_filter(Targets::new().with_target(FEEDBACK_TAGS_TARGET, Level::TRACE)) } - pub fn snapshot(&self, session_id: Option) -> CodexLogSnapshot { + pub fn snapshot(&self, session_id: Option) -> CodexLogSnapshot { let bytes = { let guard = self.inner.ring.lock().expect("mutex poisoned"); guard.snapshot_bytes() @@ -102,7 +102,7 @@ impl CodexFeedback { tags, thread_id: session_id .map(|id| id.to_string()) - .unwrap_or("no-active-thread-".to_string() + &ConversationId::new().to_string()), + .unwrap_or("no-active-thread-".to_string() + &ThreadId::new().to_string()), } } } diff --git a/codex-rs/mcp-server/src/codex_tool_runner.rs b/codex-rs/mcp-server/src/codex_tool_runner.rs index 629f4cc49..73b75dcbf 100644 --- a/codex-rs/mcp-server/src/codex_tool_runner.rs +++ b/codex-rs/mcp-server/src/codex_tool_runner.rs @@ -9,9 +9,9 @@ use crate::exec_approval::handle_exec_approval_request; use crate::outgoing_message::OutgoingMessageSender; use crate::outgoing_message::OutgoingNotificationMeta; use crate::patch_approval::handle_patch_approval_request; -use codex_core::CodexConversation; -use codex_core::ConversationManager; -use codex_core::NewConversation; +use codex_core::CodexThread; +use codex_core::NewThread; +use codex_core::ThreadManager; use codex_core::config::Config as CodexConfig; use codex_core::protocol::AgentMessageEvent; use codex_core::protocol::ApplyPatchApprovalRequestEvent; @@ -21,7 +21,7 @@ use codex_core::protocol::ExecApprovalRequestEvent; use codex_core::protocol::Op; use codex_core::protocol::Submission; use codex_core::protocol::TaskCompleteEvent; -use codex_protocol::ConversationId; +use codex_protocol::ThreadId; use codex_protocol::user_input::UserInput; use mcp_types::CallToolResult; use mcp_types::ContentBlock; @@ -41,14 +41,14 @@ pub async fn run_codex_tool_session( initial_prompt: String, config: CodexConfig, outgoing: Arc, - conversation_manager: Arc, - running_requests_id_to_codex_uuid: Arc>>, + thread_manager: Arc, + running_requests_id_to_codex_uuid: Arc>>, ) { - let NewConversation { - conversation_id, - conversation, + let NewThread { + thread_id, + thread, session_configured, - } = match conversation_manager.new_conversation(config).await { + } = match thread_manager.start_thread(config).await { Ok(res) => res, Err(e) => { let result = CallToolResult { @@ -87,7 +87,7 @@ pub async fn run_codex_tool_session( running_requests_id_to_codex_uuid .lock() .await - .insert(id.clone(), conversation_id); + .insert(id.clone(), thread_id); let submission = Submission { id: sub_id.clone(), op: Op::UserInput { @@ -98,29 +98,23 @@ pub async fn run_codex_tool_session( }, }; - if let Err(e) = conversation.submit_with_id(submission).await { + if let Err(e) = thread.submit_with_id(submission).await { tracing::error!("Failed to submit initial prompt: {e}"); // unregister the id so we don't keep it in the map running_requests_id_to_codex_uuid.lock().await.remove(&id); return; } - run_codex_tool_session_inner( - conversation, - outgoing, - id, - running_requests_id_to_codex_uuid, - ) - .await; + run_codex_tool_session_inner(thread, outgoing, id, running_requests_id_to_codex_uuid).await; } pub async fn run_codex_tool_session_reply( - conversation: Arc, + conversation: Arc, outgoing: Arc, request_id: RequestId, prompt: String, - running_requests_id_to_codex_uuid: Arc>>, - conversation_id: ConversationId, + running_requests_id_to_codex_uuid: Arc>>, + conversation_id: ThreadId, ) { running_requests_id_to_codex_uuid .lock() @@ -152,10 +146,10 @@ pub async fn run_codex_tool_session_reply( } async fn run_codex_tool_session_inner( - codex: Arc, + codex: Arc, outgoing: Arc, request_id: RequestId, - running_requests_id_to_codex_uuid: Arc>>, + running_requests_id_to_codex_uuid: Arc>>, ) { let request_id_str = match &request_id { RequestId::String(s) => s.clone(), diff --git a/codex-rs/mcp-server/src/exec_approval.rs b/codex-rs/mcp-server/src/exec_approval.rs index 44607b754..47f52caf7 100644 --- a/codex-rs/mcp-server/src/exec_approval.rs +++ b/codex-rs/mcp-server/src/exec_approval.rs @@ -1,7 +1,7 @@ use std::path::PathBuf; use std::sync::Arc; -use codex_core::CodexConversation; +use codex_core::CodexThread; use codex_core::protocol::Op; use codex_core::protocol::ReviewDecision; use codex_protocol::parse_command::ParsedCommand; @@ -53,7 +53,7 @@ pub(crate) async fn handle_exec_approval_request( command: Vec, cwd: PathBuf, outgoing: Arc, - codex: Arc, + codex: Arc, request_id: RequestId, tool_call_id: String, event_id: String, @@ -120,7 +120,7 @@ pub(crate) async fn handle_exec_approval_request( async fn on_exec_approval_response( event_id: String, receiver: tokio::sync::oneshot::Receiver, - codex: Arc, + codex: Arc, ) { let response = receiver.await; let value = match response { diff --git a/codex-rs/mcp-server/src/message_processor.rs b/codex-rs/mcp-server/src/message_processor.rs index 81eb80764..955dc0603 100644 --- a/codex-rs/mcp-server/src/message_processor.rs +++ b/codex-rs/mcp-server/src/message_processor.rs @@ -7,11 +7,11 @@ use crate::codex_tool_config::create_tool_for_codex_tool_call_param; use crate::codex_tool_config::create_tool_for_codex_tool_call_reply_param; use crate::error_code::INVALID_REQUEST_ERROR_CODE; use crate::outgoing_message::OutgoingMessageSender; -use codex_protocol::ConversationId; +use codex_protocol::ThreadId; use codex_protocol::protocol::SessionSource; use codex_core::AuthManager; -use codex_core::ConversationManager; +use codex_core::ThreadManager; use codex_core::config::Config; use codex_core::default_client::USER_AGENT_SUFFIX; use codex_core::default_client::get_codex_user_agent; @@ -40,8 +40,8 @@ pub(crate) struct MessageProcessor { outgoing: Arc, initialized: bool, codex_linux_sandbox_exe: Option, - conversation_manager: Arc, - running_requests_id_to_codex_uuid: Arc>>, + thread_manager: Arc, + running_requests_id_to_codex_uuid: Arc>>, } impl MessageProcessor { @@ -58,13 +58,12 @@ impl MessageProcessor { false, config.cli_auth_credentials_store_mode, ); - let conversation_manager = - Arc::new(ConversationManager::new(auth_manager, SessionSource::Mcp)); + let thread_manager = Arc::new(ThreadManager::new(auth_manager, SessionSource::Mcp)); Self { outgoing, initialized: false, codex_linux_sandbox_exe, - conversation_manager, + thread_manager, running_requests_id_to_codex_uuid: Arc::new(Mutex::new(HashMap::new())), } } @@ -403,7 +402,7 @@ impl MessageProcessor { // Clone outgoing and server to move into async task. let outgoing = self.outgoing.clone(); - let conversation_manager = self.conversation_manager.clone(); + let thread_manager = self.thread_manager.clone(); let running_requests_id_to_codex_uuid = self.running_requests_id_to_codex_uuid.clone(); // Spawn an async task to handle the Codex session so that we do not @@ -415,7 +414,7 @@ impl MessageProcessor { initial_prompt, config, outgoing, - conversation_manager, + thread_manager, running_requests_id_to_codex_uuid, ) .await; @@ -470,7 +469,7 @@ impl MessageProcessor { return; } }; - let conversation_id = match ConversationId::from_string(&conversation_id) { + let conversation_id = match ThreadId::from_string(&conversation_id) { Ok(id) => id, Err(e) => { tracing::error!("Failed to parse conversation_id: {e}"); @@ -493,11 +492,7 @@ impl MessageProcessor { let outgoing = self.outgoing.clone(); let running_requests_id_to_codex_uuid = self.running_requests_id_to_codex_uuid.clone(); - let codex = match self - .conversation_manager - .get_conversation(conversation_id) - .await - { + let codex = match self.thread_manager.get_thread(conversation_id).await { Ok(c) => c, Err(_) => { tracing::warn!("Session not found for conversation_id: {conversation_id}"); @@ -578,11 +573,7 @@ impl MessageProcessor { tracing::info!("conversation_id: {conversation_id}"); // Obtain the Codex conversation from the server. - let codex_arc = match self - .conversation_manager - .get_conversation(conversation_id) - .await - { + let codex_arc = match self.thread_manager.get_thread(conversation_id).await { Ok(c) => c, Err(_) => { tracing::warn!("Session not found for conversation_id: {conversation_id}"); diff --git a/codex-rs/mcp-server/src/outgoing_message.rs b/codex-rs/mcp-server/src/outgoing_message.rs index 83ac25fdf..fef5c8bac 100644 --- a/codex-rs/mcp-server/src/outgoing_message.rs +++ b/codex-rs/mcp-server/src/outgoing_message.rs @@ -238,7 +238,7 @@ mod tests { use codex_core::protocol::EventMsg; use codex_core::protocol::SandboxPolicy; use codex_core::protocol::SessionConfiguredEvent; - use codex_protocol::ConversationId; + use codex_protocol::ThreadId; use codex_protocol::openai_models::ReasoningEffort; use pretty_assertions::assert_eq; use serde_json::json; @@ -251,7 +251,7 @@ mod tests { let (outgoing_tx, mut outgoing_rx) = mpsc::unbounded_channel::(); let outgoing_message_sender = OutgoingMessageSender::new(outgoing_tx); - let conversation_id = ConversationId::new(); + let conversation_id = ThreadId::new(); let rollout_file = NamedTempFile::new()?; let event = Event { id: "1".to_string(), @@ -292,7 +292,7 @@ mod tests { let (outgoing_tx, mut outgoing_rx) = mpsc::unbounded_channel::(); let outgoing_message_sender = OutgoingMessageSender::new(outgoing_tx); - let conversation_id = ConversationId::new(); + let conversation_id = ThreadId::new(); let rollout_file = NamedTempFile::new()?; let session_configured_event = SessionConfiguredEvent { session_id: conversation_id, diff --git a/codex-rs/mcp-server/src/patch_approval.rs b/codex-rs/mcp-server/src/patch_approval.rs index 3c614ab33..00e4f204a 100644 --- a/codex-rs/mcp-server/src/patch_approval.rs +++ b/codex-rs/mcp-server/src/patch_approval.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use std::path::PathBuf; use std::sync::Arc; -use codex_core::CodexConversation; +use codex_core::CodexThread; use codex_core::protocol::FileChange; use codex_core::protocol::Op; use codex_core::protocol::ReviewDecision; @@ -47,7 +47,7 @@ pub(crate) async fn handle_patch_approval_request( grant_root: Option, changes: HashMap, outgoing: Arc, - codex: Arc, + codex: Arc, request_id: RequestId, tool_call_id: String, event_id: String, @@ -111,7 +111,7 @@ pub(crate) async fn handle_patch_approval_request( pub(crate) async fn on_patch_approval_response( event_id: String, receiver: tokio::sync::oneshot::Receiver, - codex: Arc, + codex: Arc, ) { let response = receiver.await; let value = match response { diff --git a/codex-rs/otel/src/otel_manager.rs b/codex-rs/otel/src/otel_manager.rs index fbdd32272..c59d3a168 100644 --- a/codex-rs/otel/src/otel_manager.rs +++ b/codex-rs/otel/src/otel_manager.rs @@ -3,7 +3,7 @@ use chrono::SecondsFormat; use chrono::Utc; use codex_api::ResponseEvent; use codex_app_server_protocol::AuthMode; -use codex_protocol::ConversationId; +use codex_protocol::ThreadId; use codex_protocol::config_types::ReasoningSummary; use codex_protocol::models::ResponseItem; use codex_protocol::openai_models::ReasoningEffort; @@ -37,7 +37,7 @@ pub enum ToolDecisionSource { #[derive(Debug, Clone)] pub struct OtelEventMetadata { - conversation_id: ConversationId, + conversation_id: ThreadId, auth_mode: Option, account_id: Option, account_email: Option, @@ -57,7 +57,7 @@ pub struct OtelManager { impl OtelManager { #[allow(clippy::too_many_arguments)] pub fn new( - conversation_id: ConversationId, + conversation_id: ThreadId, model: &str, slug: &str, account_id: Option, diff --git a/codex-rs/protocol/src/lib.rs b/codex-rs/protocol/src/lib.rs index 0d6a0594f..513743c97 100644 --- a/codex-rs/protocol/src/lib.rs +++ b/codex-rs/protocol/src/lib.rs @@ -1,6 +1,8 @@ pub mod account; -mod conversation_id; -pub use conversation_id::ConversationId; +mod thread_id; +#[allow(deprecated)] +pub use thread_id::ConversationId; +pub use thread_id::ThreadId; pub mod approvals; pub mod config_types; pub mod custom_prompts; diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 4dbd7902c..f5351576f 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -10,7 +10,7 @@ use std::path::PathBuf; use std::str::FromStr; use std::time::Duration; -use crate::ConversationId; +use crate::ThreadId; use crate::approvals::ElicitationRequestEvent; use crate::config_types::ReasoningSummary as ReasoningSummaryConfig; use crate::custom_prompts::CustomPrompt; @@ -80,7 +80,7 @@ pub enum Op { }, /// Similar to [`Op::UserInput`], but contains additional context required - /// for a turn of a [`crate::codex_conversation::CodexConversation`]. + /// for a turn of a [`crate::codex_thread::CodexThread`]. UserTurn { /// User input items, see `InputItem` items: Vec, @@ -738,7 +738,7 @@ pub struct RawResponseItemEvent { #[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)] pub struct ItemStartedEvent { - pub thread_id: ConversationId, + pub thread_id: ThreadId, pub turn_id: String, pub item: TurnItem, } @@ -756,7 +756,7 @@ impl HasLegacyEvent for ItemStartedEvent { #[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)] pub struct ItemCompletedEvent { - pub thread_id: ConversationId, + pub thread_id: ThreadId, pub turn_id: String, pub item: TurnItem, } @@ -1180,17 +1180,18 @@ pub struct WebSearchEndEvent { pub query: String, } +// Conversation kept for backward compatibility. /// Response payload for `Op::GetHistory` containing the current session's /// in-memory transcript. #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)] pub struct ConversationPathResponseEvent { - pub conversation_id: ConversationId, + pub conversation_id: ThreadId, pub path: PathBuf, } #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)] pub struct ResumedHistory { - pub conversation_id: ConversationId, + pub conversation_id: ThreadId, pub history: Vec, pub rollout_path: PathBuf, } @@ -1285,7 +1286,7 @@ impl fmt::Display for SubAgentSource { #[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, TS)] pub struct SessionMeta { - pub id: ConversationId, + pub id: ThreadId, pub timestamp: String, pub cwd: PathBuf, pub originator: String, @@ -1299,7 +1300,7 @@ pub struct SessionMeta { impl Default for SessionMeta { fn default() -> Self { SessionMeta { - id: ConversationId::default(), + id: ThreadId::default(), timestamp: String::new(), cwd: PathBuf::new(), originator: String::new(), @@ -1811,8 +1812,8 @@ pub struct SkillsListEntry { #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)] pub struct SessionConfiguredEvent { - /// Name left as session_id instead of conversation_id for backwards compatibility. - pub session_id: ConversationId, + /// Name left as session_id instead of thread_id for backwards compatibility. + pub session_id: ThreadId, /// Tell the client what model is being queried. pub model: String, @@ -1940,7 +1941,7 @@ mod tests { #[test] fn item_started_event_from_web_search_emits_begin_event() { let event = ItemStartedEvent { - thread_id: ConversationId::new(), + thread_id: ThreadId::new(), turn_id: "turn-1".into(), item: TurnItem::WebSearch(WebSearchItem { id: "search-1".into(), @@ -1959,7 +1960,7 @@ mod tests { #[test] fn item_started_event_from_non_web_search_emits_no_legacy_events() { let event = ItemStartedEvent { - thread_id: ConversationId::new(), + thread_id: ThreadId::new(), turn_id: "turn-1".into(), item: TurnItem::UserMessage(UserMessageItem::new(&[])), }; @@ -2027,7 +2028,7 @@ mod tests { /// amount of nesting. #[test] fn serialize_event() -> Result<()> { - let conversation_id = ConversationId::from_string("67e55044-10b1-426f-9247-bb680e5fe0c8")?; + let conversation_id = ThreadId::from_string("67e55044-10b1-426f-9247-bb680e5fe0c8")?; let rollout_file = NamedTempFile::new()?; let event = Event { id: "1234".to_string(), diff --git a/codex-rs/protocol/src/conversation_id.rs b/codex-rs/protocol/src/thread_id.rs similarity index 75% rename from codex-rs/protocol/src/conversation_id.rs rename to codex-rs/protocol/src/thread_id.rs index db104d453..8589566a2 100644 --- a/codex-rs/protocol/src/conversation_id.rs +++ b/codex-rs/protocol/src/thread_id.rs @@ -10,11 +10,11 @@ use uuid::Uuid; #[derive(Debug, Clone, Copy, PartialEq, Eq, TS, Hash)] #[ts(type = "string")] -pub struct ConversationId { +pub struct ThreadId { uuid: Uuid, } -impl ConversationId { +impl ThreadId { pub fn new() -> Self { Self { uuid: Uuid::now_v7(), @@ -28,19 +28,19 @@ impl ConversationId { } } -impl Default for ConversationId { +impl Default for ThreadId { fn default() -> Self { Self::new() } } -impl Display for ConversationId { +impl Display for ThreadId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.uuid) } } -impl Serialize for ConversationId { +impl Serialize for ThreadId { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, @@ -49,7 +49,7 @@ impl Serialize for ConversationId { } } -impl<'de> Deserialize<'de> for ConversationId { +impl<'de> Deserialize<'de> for ThreadId { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, @@ -60,9 +60,9 @@ impl<'de> Deserialize<'de> for ConversationId { } } -impl JsonSchema for ConversationId { +impl JsonSchema for ThreadId { fn schema_name() -> String { - "ConversationId".to_string() + "ThreadId".to_string() } fn json_schema(generator: &mut SchemaGenerator) -> Schema { @@ -70,12 +70,16 @@ impl JsonSchema for ConversationId { } } +/// Backward-compatible alias for the previous name. +#[deprecated(note = "use ThreadId instead")] +pub type ConversationId = ThreadId; + #[cfg(test)] mod tests { use super::*; #[test] - fn test_conversation_id_default_is_not_zeroes() { - let id = ConversationId::default(); + fn test_thread_id_default_is_not_zeroes() { + let id = ThreadId::default(); assert_ne!(id.uuid, Uuid::nil()); } } diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index d76a5b403..370d3d3f6 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -22,7 +22,7 @@ use crate::tui::TuiEvent; use crate::update_action::UpdateAction; use codex_ansi_escape::ansi_escape_line; use codex_core::AuthManager; -use codex_core::ConversationManager; +use codex_core::ThreadManager; use codex_core::config::Config; use codex_core::config::edit::ConfigEdit; use codex_core::config::edit::ConfigEditsBuilder; @@ -38,7 +38,7 @@ use codex_core::protocol::Op; use codex_core::protocol::SessionSource; use codex_core::protocol::SkillErrorInfo; use codex_core::protocol::TokenUsage; -use codex_protocol::ConversationId; +use codex_protocol::ThreadId; use codex_protocol::openai_models::ModelPreset; use codex_protocol::openai_models::ModelUpgrade; use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig; @@ -70,21 +70,17 @@ const EXTERNAL_EDITOR_HINT: &str = "Save and close external editor to continue." #[derive(Debug, Clone)] pub struct AppExitInfo { pub token_usage: TokenUsage, - pub conversation_id: Option, + pub thread_id: Option, pub update_action: Option, } -fn session_summary( - token_usage: TokenUsage, - conversation_id: Option, -) -> Option { +fn session_summary(token_usage: TokenUsage, thread_id: Option) -> Option { if token_usage.is_zero() { return None; } let usage_line = FinalOutput::from(token_usage).to_string(); - let resume_command = - conversation_id.map(|conversation_id| format!("codex resume {conversation_id}")); + let resume_command = thread_id.map(|thread_id| format!("codex resume {thread_id}")); Some(SessionSummary { usage_line, resume_command, @@ -275,7 +271,7 @@ async fn handle_model_migration_prompt_if_needed( ModelMigrationOutcome::Exit => { return Some(AppExitInfo { token_usage: TokenUsage::default(), - conversation_id: None, + thread_id: None, update_action: None, }); } @@ -286,7 +282,7 @@ async fn handle_model_migration_prompt_if_needed( } pub(crate) struct App { - pub(crate) server: Arc, + pub(crate) server: Arc, pub(crate) app_event_tx: AppEventSender, pub(crate) chat_widget: ChatWidget, pub(crate) auth_manager: Arc, @@ -316,7 +312,7 @@ pub(crate) struct App { pub(crate) pending_update_action: Option, /// Ignore the next ShutdownComplete event when we're intentionally - /// stopping a conversation (e.g., before starting a new one). + /// stopping a thread (e.g., before starting a new one). suppress_shutdown_complete: bool, // One-shot suppression of the next world-writable scan after user confirmation. @@ -324,11 +320,11 @@ pub(crate) struct App { } impl App { - async fn shutdown_current_conversation(&mut self) { - if let Some(conversation_id) = self.chat_widget.conversation_id() { + async fn shutdown_current_thread(&mut self) { + if let Some(thread_id) = self.chat_widget.thread_id() { self.suppress_shutdown_complete = true; self.chat_widget.submit_op(Op::Shutdown); - self.server.remove_conversation(&conversation_id).await; + self.server.remove_thread(&thread_id).await; } } @@ -348,11 +344,8 @@ impl App { let (app_event_tx, mut app_event_rx) = unbounded_channel(); let app_event_tx = AppEventSender::new(app_event_tx); - let conversation_manager = Arc::new(ConversationManager::new( - auth_manager.clone(), - SessionSource::Cli, - )); - let mut model = conversation_manager + let thread_manager = Arc::new(ThreadManager::new(auth_manager.clone(), SessionSource::Cli)); + let mut model = thread_manager .get_models_manager() .get_model(&config.model, &config) .await; @@ -361,7 +354,7 @@ impl App { &mut config, model.as_str(), &app_event_tx, - conversation_manager.get_models_manager(), + thread_manager.get_models_manager(), ) .await; if let Some(exit_info) = exit_info { @@ -382,20 +375,16 @@ impl App { initial_images: initial_images.clone(), enhanced_keys_supported, auth_manager: auth_manager.clone(), - models_manager: conversation_manager.get_models_manager(), + models_manager: thread_manager.get_models_manager(), feedback: feedback.clone(), is_first_run, model: model.clone(), }; - ChatWidget::new(init, conversation_manager.clone()) + ChatWidget::new(init, thread_manager.clone()) } ResumeSelection::Resume(path) => { - let resumed = conversation_manager - .resume_conversation_from_rollout( - config.clone(), - path.clone(), - auth_manager.clone(), - ) + let resumed = thread_manager + .resume_thread_from_rollout(config.clone(), path.clone(), auth_manager.clone()) .await .wrap_err_with(|| { format!("Failed to resume session from {}", path.display()) @@ -408,16 +397,12 @@ impl App { initial_images: initial_images.clone(), enhanced_keys_supported, auth_manager: auth_manager.clone(), - models_manager: conversation_manager.get_models_manager(), + models_manager: thread_manager.get_models_manager(), feedback: feedback.clone(), is_first_run, model: model.clone(), }; - ChatWidget::new_from_existing( - init, - resumed.conversation, - resumed.session_configured, - ) + ChatWidget::new_from_existing(init, resumed.thread, resumed.session_configured) } }; @@ -428,7 +413,7 @@ impl App { let upgrade_version = crate::updates::get_upgrade_version(&config); let mut app = Self { - server: conversation_manager.clone(), + server: thread_manager.clone(), app_event_tx, chat_widget, auth_manager: auth_manager.clone(), @@ -501,7 +486,7 @@ impl App { tui.terminal.clear()?; Ok(AppExitInfo { token_usage: app.token_usage(), - conversation_id: app.chat_widget.conversation_id(), + thread_id: app.chat_widget.thread_id(), update_action: app.pending_update_action, }) } @@ -562,11 +547,9 @@ impl App { .await; match event { AppEvent::NewSession => { - let summary = session_summary( - self.chat_widget.token_usage(), - self.chat_widget.conversation_id(), - ); - self.shutdown_current_conversation().await; + let summary = + session_summary(self.chat_widget.token_usage(), self.chat_widget.thread_id()); + self.shutdown_current_thread().await; let init = crate::chatwidget::ChatWidgetInit { config: self.config.clone(), frame_requester: tui.frame_requester(), @@ -604,11 +587,11 @@ impl App { ResumeSelection::Resume(path) => { let summary = session_summary( self.chat_widget.token_usage(), - self.chat_widget.conversation_id(), + self.chat_widget.thread_id(), ); match self .server - .resume_conversation_from_rollout( + .resume_thread_from_rollout( self.config.clone(), path.clone(), self.auth_manager.clone(), @@ -616,7 +599,7 @@ impl App { .await { Ok(resumed) => { - self.shutdown_current_conversation().await; + self.shutdown_current_thread().await; let init = crate::chatwidget::ChatWidgetInit { config: self.config.clone(), frame_requester: tui.frame_requester(), @@ -632,7 +615,7 @@ impl App { }; self.chat_widget = ChatWidget::new_from_existing( init, - resumed.conversation, + resumed.thread, resumed.session_configured, ); self.current_model = model_family.get_model_slug().to_string(); @@ -1333,14 +1316,14 @@ mod tests { use crate::history_cell::new_session_info; use codex_core::AuthManager; use codex_core::CodexAuth; - use codex_core::ConversationManager; + use codex_core::ThreadManager; use codex_core::config::ConfigBuilder; use codex_core::protocol::AskForApproval; use codex_core::protocol::Event; use codex_core::protocol::EventMsg; use codex_core::protocol::SandboxPolicy; use codex_core::protocol::SessionConfiguredEvent; - use codex_protocol::ConversationId; + use codex_protocol::ThreadId; use insta::assert_snapshot; use ratatui::prelude::Line; use std::path::PathBuf; @@ -1352,7 +1335,7 @@ mod tests { let (chat_widget, app_event_tx, _rx, _op_rx) = make_chatwidget_manual_with_sender().await; let config = chat_widget.config_ref().clone(); let current_model = "gpt-5.2-codex".to_string(); - let server = Arc::new(ConversationManager::with_models_provider( + let server = Arc::new(ThreadManager::with_models_provider( CodexAuth::from_api_key("Test API Key"), config.model_provider.clone(), )); @@ -1391,7 +1374,7 @@ mod tests { let (chat_widget, app_event_tx, rx, op_rx) = make_chatwidget_manual_with_sender().await; let config = chat_widget.config_ref().clone(); let current_model = "gpt-5.2-codex".to_string(); - let server = Arc::new(ConversationManager::with_models_provider( + let server = Arc::new(ThreadManager::with_models_provider( CodexAuth::from_api_key("Test API Key"), config.model_provider.clone(), )); @@ -1619,7 +1602,7 @@ mod tests { let make_header = |is_first| { let event = SessionConfiguredEvent { - session_id: ConversationId::new(), + session_id: ThreadId::new(), model: "gpt-test".to_string(), model_provider_id: "test-provider".to_string(), approval_policy: AskForApproval::Never, @@ -1641,7 +1624,7 @@ mod tests { // Simulate the transcript after trimming for a fork, replaying history, and // appending the edited turn. The session header separates the retained history - // from the forked conversation's replayed turns. + // from the forked thread's replayed turns. app.transcript_cells = vec![ make_header(true), user_cell("first question"), @@ -1657,7 +1640,7 @@ mod tests { assert_eq!(user_count(&app.transcript_cells), 2); - app.backtrack.base_id = Some(ConversationId::new()); + app.backtrack.base_id = Some(ThreadId::new()); app.backtrack.primed = true; app.backtrack.nth_user_message = user_count(&app.transcript_cells).saturating_sub(1); @@ -1672,9 +1655,9 @@ mod tests { async fn new_session_requests_shutdown_for_previous_conversation() { let (mut app, mut app_event_rx, mut op_rx) = make_test_app_with_channels().await; - let conversation_id = ConversationId::new(); + let thread_id = ThreadId::new(); let event = SessionConfiguredEvent { - session_id: conversation_id, + session_id: thread_id, model: "gpt-test".to_string(), model_provider_id: "test-provider".to_string(), approval_policy: AskForApproval::Never, @@ -1695,7 +1678,7 @@ mod tests { while app_event_rx.try_recv().is_ok() {} while op_rx.try_recv().is_ok() {} - app.shutdown_current_conversation().await; + app.shutdown_current_thread().await; match op_rx.try_recv() { Ok(Op::Shutdown) => {} @@ -1717,8 +1700,7 @@ mod tests { total_tokens: 12, ..Default::default() }; - let conversation = - ConversationId::from_string("123e4567-e89b-12d3-a456-426614174000").unwrap(); + let conversation = ThreadId::from_string("123e4567-e89b-12d3-a456-426614174000").unwrap(); let summary = session_summary(usage, Some(conversation)).expect("summary"); assert_eq!( diff --git a/codex-rs/tui/src/app_backtrack.rs b/codex-rs/tui/src/app_backtrack.rs index ce5dff2ed..c28680dd9 100644 --- a/codex-rs/tui/src/app_backtrack.rs +++ b/codex-rs/tui/src/app_backtrack.rs @@ -9,7 +9,7 @@ use crate::pager_overlay::Overlay; use crate::tui; use crate::tui::TuiEvent; use codex_core::protocol::ConversationPathResponseEvent; -use codex_protocol::ConversationId; +use codex_protocol::ThreadId; use color_eyre::eyre::Result; use crossterm::event::KeyCode; use crossterm::event::KeyEvent; @@ -20,14 +20,14 @@ use crossterm::event::KeyEventKind; pub(crate) struct BacktrackState { /// True when Esc has primed backtrack mode in the main view. pub(crate) primed: bool, - /// Session id of the base conversation to fork from. - pub(crate) base_id: Option, + /// Session id of the base thread to fork from. + pub(crate) base_id: Option, /// Index in the transcript of the last user message. pub(crate) nth_user_message: usize, /// True when the transcript overlay is showing a backtrack preview. pub(crate) overlay_preview_active: bool, /// Pending fork request: (base_id, nth_user_message, prefill). - pub(crate) pending: Option<(ConversationId, usize, String)>, + pub(crate) pending: Option<(ThreadId, usize, String)>, } impl App { @@ -95,11 +95,11 @@ impl App { } } - /// Stage a backtrack and request conversation history from the agent. + /// Stage a backtrack and request thread history from the agent. pub(crate) fn request_backtrack( &mut self, prefill: String, - base_id: ConversationId, + base_id: ThreadId, nth_user_message: usize, ) { self.backtrack.pending = Some((base_id, nth_user_message, prefill)); @@ -153,7 +153,7 @@ impl App { fn prime_backtrack(&mut self) { self.backtrack.primed = true; self.backtrack.nth_user_message = usize::MAX; - self.backtrack.base_id = self.chat_widget.conversation_id(); + self.backtrack.base_id = self.chat_widget.thread_id(); self.chat_widget.show_esc_backtrack_hint(); } @@ -169,7 +169,7 @@ impl App { /// When overlay is already open, begin preview mode and select latest user message. fn begin_overlay_backtrack_preview(&mut self, tui: &mut tui::Tui) { self.backtrack.primed = true; - self.backtrack.base_id = self.chat_widget.conversation_id(); + self.backtrack.base_id = self.chat_widget.thread_id(); self.backtrack.overlay_preview_active = true; let count = user_count(&self.transcript_cells); if let Some(last) = count.checked_sub(1) { @@ -315,28 +315,26 @@ impl App { } } - /// Thin wrapper around ConversationManager::fork_conversation. + /// Thin wrapper around ThreadManager::fork_thread. async fn perform_fork( &self, path: PathBuf, nth_user_message: usize, cfg: codex_core::config::Config, - ) -> codex_core::error::Result { - self.server - .fork_conversation(nth_user_message, cfg, path) - .await + ) -> codex_core::error::Result { + self.server.fork_thread(nth_user_message, cfg, path).await } - /// Install a forked conversation into the ChatWidget and update UI to reflect selection. + /// Install a forked thread into the ChatWidget and update UI to reflect selection. fn install_forked_conversation( &mut self, tui: &mut tui::Tui, cfg: codex_core::config::Config, - new_conv: codex_core::NewConversation, + new_conv: codex_core::NewThread, nth_user_message: usize, prefill: &str, ) { - let conv = new_conv.conversation; + let thread = new_conv.thread; let session_configured = new_conv.session_configured; let init = crate::chatwidget::ChatWidgetInit { config: cfg, @@ -352,7 +350,7 @@ impl App { is_first_run: false, }; self.chat_widget = - crate::chatwidget::ChatWidget::new_from_existing(init, conv, session_configured); + crate::chatwidget::ChatWidget::new_from_existing(init, thread, session_configured); // Trim transcript up to the selected user message and re-render it. self.trim_transcript_for_backtrack(nth_user_message); self.render_transcript_once(tui); diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 0894fada6..b4ae8b76a 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -63,7 +63,7 @@ use codex_core::protocol::WarningEvent; use codex_core::protocol::WebSearchBeginEvent; use codex_core::protocol::WebSearchEndEvent; use codex_core::skills::model::SkillMetadata; -use codex_protocol::ConversationId; +use codex_protocol::ThreadId; use codex_protocol::account::PlanType; use codex_protocol::approvals::ElicitationRequestEvent; use codex_protocol::parse_command::ParsedCommand; @@ -136,7 +136,7 @@ use codex_common::approval_presets::ApprovalPreset; use codex_common::approval_presets::builtin_approval_presets; use codex_core::AuthManager; use codex_core::CodexAuth; -use codex_core::ConversationManager; +use codex_core::ThreadManager; use codex_core::protocol::AskForApproval; use codex_core::protocol::SandboxPolicy; use codex_file_search::FileMatch; @@ -344,7 +344,7 @@ pub(crate) struct ChatWidget { current_status_header: String, // Previous status header to restore after a transient stream retry. retry_status_header: Option, - conversation_id: Option, + thread_id: Option, frame_requester: FrameRequester, // Whether to include the initial welcome banner on session configured show_welcome_banner: bool, @@ -435,7 +435,7 @@ impl ChatWidget { self.bottom_pane .set_history_metadata(event.history_log_id, event.history_entry_count); self.set_skills(None); - self.conversation_id = Some(event.session_id); + self.thread_id = Some(event.session_id); self.current_rollout_path = Some(event.rollout_path.clone()); let initial_messages = event.initial_messages.clone(); let model_for_header = event.model.clone(); @@ -478,7 +478,7 @@ impl ChatWidget { include_logs: bool, ) { // Build a fresh snapshot at the time of opening the note overlay. - let snapshot = self.feedback.snapshot(self.conversation_id); + let snapshot = self.feedback.snapshot(self.thread_id); let rollout = if include_logs { self.current_rollout_path.clone() } else { @@ -1401,10 +1401,7 @@ impl ChatWidget { } } - pub(crate) fn new( - common: ChatWidgetInit, - conversation_manager: Arc, - ) -> Self { + pub(crate) fn new(common: ChatWidgetInit, thread_manager: Arc) -> Self { let ChatWidgetInit { config, frame_requester, @@ -1422,7 +1419,7 @@ impl ChatWidget { config.model = Some(model.clone()); let mut rng = rand::rng(); let placeholder = EXAMPLE_PROMPTS[rng.random_range(0..EXAMPLE_PROMPTS.len())].to_string(); - let codex_op_tx = spawn_agent(config.clone(), app_event_tx.clone(), conversation_manager); + let codex_op_tx = spawn_agent(config.clone(), app_event_tx.clone(), thread_manager); let mut widget = Self { app_event_tx: app_event_tx.clone(), @@ -1466,7 +1463,7 @@ impl ChatWidget { full_reasoning_buffer: String::new(), current_status_header: String::from("Working"), retry_status_header: None, - conversation_id: None, + thread_id: None, queued_user_messages: VecDeque::new(), show_welcome_banner: is_first_run, suppress_session_configured_redraw: false, @@ -1488,7 +1485,7 @@ impl ChatWidget { /// Create a ChatWidget attached to an existing conversation (e.g., a fork). pub(crate) fn new_from_existing( common: ChatWidgetInit, - conversation: std::sync::Arc, + conversation: std::sync::Arc, session_configured: codex_core::protocol::SessionConfiguredEvent, ) -> Self { let ChatWidgetInit { @@ -1552,7 +1549,7 @@ impl ChatWidget { full_reasoning_buffer: String::new(), current_status_header: String::from("Working"), retry_status_header: None, - conversation_id: None, + thread_id: None, queued_user_messages: VecDeque::new(), show_welcome_banner: false, suppress_session_configured_redraw: true, @@ -2286,7 +2283,7 @@ impl ChatWidget { self.auth_manager.as_ref(), token_info, total_usage, - &self.conversation_id, + &self.thread_id, self.rate_limit_snapshot.as_ref(), self.plan_type, Local::now(), @@ -3549,8 +3546,8 @@ impl ChatWidget { .unwrap_or_default() } - pub(crate) fn conversation_id(&self) -> Option { - self.conversation_id + pub(crate) fn thread_id(&self) -> Option { + self.thread_id } pub(crate) fn rollout_path(&self) -> Option { diff --git a/codex-rs/tui/src/chatwidget/agent.rs b/codex-rs/tui/src/chatwidget/agent.rs index 240972347..d8428b221 100644 --- a/codex-rs/tui/src/chatwidget/agent.rs +++ b/codex-rs/tui/src/chatwidget/agent.rs @@ -1,8 +1,8 @@ use std::sync::Arc; -use codex_core::CodexConversation; -use codex_core::ConversationManager; -use codex_core::NewConversation; +use codex_core::CodexThread; +use codex_core::NewThread; +use codex_core::ThreadManager; use codex_core::config::Config; use codex_core::protocol::Event; use codex_core::protocol::EventMsg; @@ -18,17 +18,17 @@ use crate::app_event_sender::AppEventSender; pub(crate) fn spawn_agent( config: Config, app_event_tx: AppEventSender, - server: Arc, + server: Arc, ) -> UnboundedSender { let (codex_op_tx, mut codex_op_rx) = unbounded_channel::(); let app_event_tx_clone = app_event_tx; tokio::spawn(async move { - let NewConversation { - conversation_id: _, - conversation, + let NewThread { + thread, session_configured, - } = match server.new_conversation(config).await { + .. + } = match server.start_thread(config).await { Ok(v) => v, #[allow(clippy::print_stderr)] Err(err) => { @@ -52,17 +52,17 @@ pub(crate) fn spawn_agent( }; app_event_tx_clone.send(AppEvent::CodexEvent(ev)); - let conversation_clone = conversation.clone(); + let thread_clone = thread.clone(); tokio::spawn(async move { while let Some(op) = codex_op_rx.recv().await { - let id = conversation_clone.submit(op).await; + let id = thread_clone.submit(op).await; if let Err(e) = id { tracing::error!("failed to submit op: {e}"); } } }); - while let Ok(event) = conversation.next_event().await { + while let Ok(event) = thread.next_event().await { app_event_tx_clone.send(AppEvent::CodexEvent(event)); } }); @@ -70,11 +70,11 @@ pub(crate) fn spawn_agent( codex_op_tx } -/// Spawn agent loops for an existing conversation (e.g., a forked conversation). +/// Spawn agent loops for an existing thread (e.g., a forked thread). /// Sends the provided `SessionConfiguredEvent` immediately, then forwards subsequent /// events and accepts Ops for submission. pub(crate) fn spawn_agent_from_existing( - conversation: std::sync::Arc, + thread: std::sync::Arc, session_configured: codex_core::protocol::SessionConfiguredEvent, app_event_tx: AppEventSender, ) -> UnboundedSender { @@ -89,17 +89,17 @@ pub(crate) fn spawn_agent_from_existing( }; app_event_tx_clone.send(AppEvent::CodexEvent(ev)); - let conversation_clone = conversation.clone(); + let thread_clone = thread.clone(); tokio::spawn(async move { while let Some(op) = codex_op_rx.recv().await { - let id = conversation_clone.submit(op).await; + let id = thread_clone.submit(op).await; if let Err(e) = id { tracing::error!("failed to submit op: {e}"); } } }); - while let Ok(event) = conversation.next_event().await { + while let Ok(event) = thread.next_event().await { app_event_tx_clone.send(AppEvent::CodexEvent(event)); } }); diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index c5b0bfd55..03f11c050 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -48,7 +48,7 @@ use codex_core::protocol::UndoCompletedEvent; use codex_core::protocol::UndoStartedEvent; use codex_core::protocol::ViewImageToolCallEvent; use codex_core::protocol::WarningEvent; -use codex_protocol::ConversationId; +use codex_protocol::ThreadId; use codex_protocol::account::PlanType; use codex_protocol::openai_models::ModelPreset; use codex_protocol::openai_models::ReasoningEffortPreset; @@ -102,7 +102,7 @@ fn snapshot(percent: f64) -> RateLimitSnapshot { async fn resumed_initial_messages_render_history() { let (mut chat, mut rx, _ops) = make_chatwidget_manual(None).await; - let conversation_id = ConversationId::new(); + let conversation_id = ThreadId::new(); let rollout_file = NamedTempFile::new().unwrap(); let configured = codex_core::protocol::SessionConfiguredEvent { session_id: conversation_id, @@ -314,7 +314,7 @@ async fn helpers_are_available_and_do_not_panic() { let tx = AppEventSender::new(tx_raw); let cfg = test_config().await; let resolved_model = ModelsManager::get_model_offline(cfg.model.as_deref()); - let conversation_manager = Arc::new(ConversationManager::with_models_provider( + let thread_manager = Arc::new(ThreadManager::with_models_provider( CodexAuth::from_api_key("test"), cfg.model_provider.clone(), )); @@ -327,12 +327,12 @@ async fn helpers_are_available_and_do_not_panic() { initial_images: Vec::new(), enhanced_keys_supported: false, auth_manager, - models_manager: conversation_manager.get_models_manager(), + models_manager: thread_manager.get_models_manager(), feedback: codex_feedback::CodexFeedback::new(), is_first_run: true, model: resolved_model, }; - let mut w = ChatWidget::new(init, conversation_manager); + let mut w = ChatWidget::new(init, thread_manager); // Basic construction sanity. let _ = &mut w; } @@ -395,7 +395,7 @@ async fn make_chatwidget_manual( full_reasoning_buffer: String::new(), current_status_header: String::from("Working"), retry_status_header: None, - conversation_id: None, + thread_id: None, frame_requester: FrameRequester::test_dummy(), show_welcome_banner: true, queued_user_messages: VecDeque::new(), diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 4eb487f1d..6f4faaad6 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -19,7 +19,7 @@ use codex_core::config::ConfigOverrides; use codex_core::config::find_codex_home; use codex_core::config::load_config_as_toml_with_cli_overrides; use codex_core::config::resolve_oss_provider; -use codex_core::find_conversation_path_by_id_str; +use codex_core::find_thread_path_by_id_str; use codex_core::get_platform_sandbox; use codex_core::protocol::AskForApproval; use codex_protocol::config_types::SandboxMode; @@ -371,7 +371,7 @@ async fn run_ratatui_app( crate::tui::restore()?; return Ok(AppExitInfo { token_usage: codex_core::protocol::TokenUsage::default(), - conversation_id: None, + thread_id: None, update_action: Some(action), }); } @@ -410,7 +410,7 @@ async fn run_ratatui_app( let _ = tui.terminal.clear(); return Ok(AppExitInfo { token_usage: codex_core::protocol::TokenUsage::default(), - conversation_id: None, + thread_id: None, update_action: None, }); } @@ -430,7 +430,7 @@ async fn run_ratatui_app( // Determine resume behavior: explicit id, then resume last, then picker. let resume_selection = if let Some(id_str) = cli.resume_session_id.as_deref() { - match find_conversation_path_by_id_str(&config.codex_home, id_str).await? { + match find_thread_path_by_id_str(&config.codex_home, id_str).await? { Some(path) => resume_picker::ResumeSelection::Resume(path), None => { error!("Error finding conversation path: {id_str}"); @@ -445,14 +445,14 @@ async fn run_ratatui_app( } return Ok(AppExitInfo { token_usage: codex_core::protocol::TokenUsage::default(), - conversation_id: None, + thread_id: None, update_action: None, }); } } } else if cli.resume_last { let provider_filter = vec![config.model_provider_id.clone()]; - match RolloutRecorder::list_conversations( + match RolloutRecorder::list_threads( &config.codex_home, 1, None, @@ -483,7 +483,7 @@ async fn run_ratatui_app( session_log::log_session_end(); return Ok(AppExitInfo { token_usage: codex_core::protocol::TokenUsage::default(), - conversation_id: None, + thread_id: None, update_action: None, }); } diff --git a/codex-rs/tui/src/resume_picker.rs b/codex-rs/tui/src/resume_picker.rs index 0f55bb5e0..13f7b9db7 100644 --- a/codex-rs/tui/src/resume_picker.rs +++ b/codex-rs/tui/src/resume_picker.rs @@ -5,11 +5,11 @@ use std::sync::Arc; use chrono::DateTime; use chrono::Utc; -use codex_core::ConversationItem; -use codex_core::ConversationsPage; use codex_core::Cursor; use codex_core::INTERACTIVE_SESSION_SOURCES; use codex_core::RolloutRecorder; +use codex_core::ThreadItem; +use codex_core::ThreadsPage; use codex_core::path_utils; use codex_protocol::items::TurnItem; use color_eyre::eyre::Result; @@ -61,7 +61,7 @@ enum BackgroundEvent { PageLoaded { request_token: usize, search_token: Option, - page: std::io::Result, + page: std::io::Result, }, } @@ -89,7 +89,7 @@ pub async fn run_resume_picker( let tx = loader_tx.clone(); tokio::spawn(async move { let provider_filter = vec![request.default_provider.clone()]; - let page = RolloutRecorder::list_conversations( + let page = RolloutRecorder::list_threads( &request.codex_home, PAGE_SIZE, request.cursor.as_ref(), @@ -415,7 +415,7 @@ impl PickerState { self.pagination.loading = LoadingState::Idle; } - fn ingest_page(&mut self, page: ConversationsPage) { + fn ingest_page(&mut self, page: ThreadsPage) { if let Some(cursor) = page.next_cursor.clone() { self.pagination.next_cursor = Some(cursor); } else { @@ -627,11 +627,11 @@ impl PickerState { } } -fn rows_from_items(items: Vec) -> Vec { +fn rows_from_items(items: Vec) -> Vec { items.into_iter().map(|item| head_to_row(&item)).collect() } -fn head_to_row(item: &ConversationItem) -> Row { +fn head_to_row(item: &ThreadItem) -> Row { let created_at = item .created_at .as_deref() @@ -1077,8 +1077,8 @@ mod tests { ] } - fn make_item(path: &str, ts: &str, preview: &str) -> ConversationItem { - ConversationItem { + fn make_item(path: &str, ts: &str, preview: &str) -> ThreadItem { + ThreadItem { path: PathBuf::from(path), head: head_with_ts_and_user_text(ts, &[preview]), created_at: Some(ts.to_string()), @@ -1092,12 +1092,12 @@ mod tests { } fn page( - items: Vec, + items: Vec, next_cursor: Option, num_scanned_files: usize, reached_scan_cap: bool, - ) -> ConversationsPage { - ConversationsPage { + ) -> ThreadsPage { + ThreadsPage { items, next_cursor, num_scanned_files, @@ -1144,13 +1144,13 @@ mod tests { #[test] fn rows_from_items_preserves_backend_order() { // Construct two items with different timestamps and real user text. - let a = ConversationItem { + let a = ThreadItem { path: PathBuf::from("/tmp/a.jsonl"), head: head_with_ts_and_user_text("2025-01-01T00:00:00Z", &["A"]), created_at: Some("2025-01-01T00:00:00Z".into()), updated_at: Some("2025-01-01T00:00:00Z".into()), }; - let b = ConversationItem { + let b = ThreadItem { path: PathBuf::from("/tmp/b.jsonl"), head: head_with_ts_and_user_text("2025-01-02T00:00:00Z", &["B"]), created_at: Some("2025-01-02T00:00:00Z".into()), @@ -1166,7 +1166,7 @@ mod tests { #[test] fn row_uses_tail_timestamp_for_updated_at() { let head = head_with_ts_and_user_text("2025-01-01T00:00:00Z", &["Hello"]); - let item = ConversationItem { + let item = ThreadItem { path: PathBuf::from("/tmp/a.jsonl"), head, created_at: Some("2025-01-01T00:00:00Z".into()), @@ -1351,7 +1351,7 @@ mod tests { None, ); - let page = RolloutRecorder::list_conversations( + let page = RolloutRecorder::list_threads( &state.codex_home, PAGE_SIZE, None, diff --git a/codex-rs/tui/src/status/card.rs b/codex-rs/tui/src/status/card.rs index 07cd5a198..7bf066eeb 100644 --- a/codex-rs/tui/src/status/card.rs +++ b/codex-rs/tui/src/status/card.rs @@ -11,7 +11,7 @@ use codex_core::protocol::NetworkAccess; use codex_core::protocol::SandboxPolicy; use codex_core::protocol::TokenUsage; use codex_core::protocol::TokenUsageInfo; -use codex_protocol::ConversationId; +use codex_protocol::ThreadId; use codex_protocol::account::PlanType; use ratatui::prelude::*; use ratatui::style::Stylize; @@ -74,7 +74,7 @@ pub(crate) fn new_status_output( auth_manager: &AuthManager, token_info: Option<&TokenUsageInfo>, total_usage: &TokenUsage, - session_id: &Option, + session_id: &Option, rate_limits: Option<&RateLimitSnapshotDisplay>, plan_type: Option, now: DateTime, @@ -103,7 +103,7 @@ impl StatusHistoryCell { auth_manager: &AuthManager, token_info: Option<&TokenUsageInfo>, total_usage: &TokenUsage, - session_id: &Option, + session_id: &Option, rate_limits: Option<&RateLimitSnapshotDisplay>, plan_type: Option, now: DateTime, diff --git a/codex-rs/tui2/src/app.rs b/codex-rs/tui2/src/app.rs index 49bb005a7..5bea13a97 100644 --- a/codex-rs/tui2/src/app.rs +++ b/codex-rs/tui2/src/app.rs @@ -40,7 +40,7 @@ use crate::tui::scrolling::TranscriptScroll; use crate::update_action::UpdateAction; use codex_ansi_escape::ansi_escape_line; use codex_core::AuthManager; -use codex_core::ConversationManager; +use codex_core::ThreadManager; use codex_core::config::Config; use codex_core::config::edit::ConfigEditsBuilder; #[cfg(target_os = "windows")] @@ -56,7 +56,7 @@ use codex_core::protocol::SessionSource; use codex_core::protocol::SkillErrorInfo; use codex_core::protocol::TokenUsage; use codex_core::terminal::terminal_info; -use codex_protocol::ConversationId; +use codex_protocol::ThreadId; use codex_protocol::openai_models::ModelPreset; use codex_protocol::openai_models::ModelUpgrade; use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig; @@ -91,7 +91,7 @@ use crate::history_cell::UpdateAvailableHistoryCell; #[derive(Debug, Clone)] pub struct AppExitInfo { pub token_usage: TokenUsage, - pub conversation_id: Option, + pub conversation_id: Option, pub update_action: Option, /// ANSI-styled transcript lines to print after the TUI exits. /// @@ -105,7 +105,7 @@ impl From for codex_tui::AppExitInfo { fn from(info: AppExitInfo) -> Self { codex_tui::AppExitInfo { token_usage: info.token_usage, - conversation_id: info.conversation_id, + thread_id: info.conversation_id, update_action: info.update_action.map(Into::into), } } @@ -113,7 +113,7 @@ impl From for codex_tui::AppExitInfo { fn session_summary( token_usage: TokenUsage, - conversation_id: Option, + conversation_id: Option, ) -> Option { if token_usage.is_zero() { return None; @@ -320,7 +320,7 @@ async fn handle_model_migration_prompt_if_needed( } pub(crate) struct App { - pub(crate) server: Arc, + pub(crate) server: Arc, pub(crate) app_event_tx: AppEventSender, pub(crate) chat_widget: ChatWidget, pub(crate) auth_manager: Arc, @@ -387,7 +387,7 @@ impl App { if let Some(conversation_id) = self.chat_widget.conversation_id() { self.suppress_shutdown_complete = true; self.chat_widget.submit_op(Op::Shutdown); - self.server.remove_conversation(&conversation_id).await; + self.server.remove_thread(&conversation_id).await; } } @@ -407,11 +407,8 @@ impl App { let (app_event_tx, mut app_event_rx) = unbounded_channel(); let app_event_tx = AppEventSender::new(app_event_tx); - let conversation_manager = Arc::new(ConversationManager::new( - auth_manager.clone(), - SessionSource::Cli, - )); - let mut model = conversation_manager + let thread_manager = Arc::new(ThreadManager::new(auth_manager.clone(), SessionSource::Cli)); + let mut model = thread_manager .get_models_manager() .get_model(&config.model, &config) .await; @@ -420,7 +417,7 @@ impl App { &mut config, model.as_str(), &app_event_tx, - conversation_manager.get_models_manager(), + thread_manager.get_models_manager(), ) .await; if let Some(exit_info) = exit_info { @@ -441,20 +438,16 @@ impl App { initial_images: initial_images.clone(), enhanced_keys_supported, auth_manager: auth_manager.clone(), - models_manager: conversation_manager.get_models_manager(), + models_manager: thread_manager.get_models_manager(), feedback: feedback.clone(), is_first_run, model: model.clone(), }; - ChatWidget::new(init, conversation_manager.clone()) + ChatWidget::new(init, thread_manager.clone()) } ResumeSelection::Resume(path) => { - let resumed = conversation_manager - .resume_conversation_from_rollout( - config.clone(), - path.clone(), - auth_manager.clone(), - ) + let resumed = thread_manager + .resume_thread_from_rollout(config.clone(), path.clone(), auth_manager.clone()) .await .wrap_err_with(|| { format!("Failed to resume session from {}", path.display()) @@ -467,16 +460,12 @@ impl App { initial_images: initial_images.clone(), enhanced_keys_supported, auth_manager: auth_manager.clone(), - models_manager: conversation_manager.get_models_manager(), + models_manager: thread_manager.get_models_manager(), feedback: feedback.clone(), is_first_run, model: model.clone(), }; - ChatWidget::new_from_existing( - init, - resumed.conversation, - resumed.session_configured, - ) + ChatWidget::new_from_existing(init, resumed.thread, resumed.session_configured) } }; @@ -503,7 +492,7 @@ impl App { let copy_selection_shortcut = crate::transcript_copy_ui::detect_copy_selection_shortcut(); let mut app = Self { - server: conversation_manager.clone(), + server: thread_manager.clone(), app_event_tx, chat_widget, auth_manager: auth_manager.clone(), @@ -1400,7 +1389,7 @@ impl App { ); match self .server - .resume_conversation_from_rollout( + .resume_thread_from_rollout( self.config.clone(), path.clone(), self.auth_manager.clone(), @@ -1424,7 +1413,7 @@ impl App { }; self.chat_widget = ChatWidget::new_from_existing( init, - resumed.conversation, + resumed.thread, resumed.session_configured, ); if let Some(summary) = summary { @@ -2087,14 +2076,14 @@ mod tests { use crate::tui::scrolling::TranscriptLineMeta; use codex_core::AuthManager; use codex_core::CodexAuth; - use codex_core::ConversationManager; + use codex_core::ThreadManager; use codex_core::config::ConfigBuilder; use codex_core::protocol::AskForApproval; use codex_core::protocol::Event; use codex_core::protocol::EventMsg; use codex_core::protocol::SandboxPolicy; use codex_core::protocol::SessionConfiguredEvent; - use codex_protocol::ConversationId; + use codex_protocol::ThreadId; use insta::assert_snapshot; use pretty_assertions::assert_eq; use ratatui::prelude::Line; @@ -2107,7 +2096,7 @@ mod tests { let (chat_widget, app_event_tx, _rx, _op_rx) = make_chatwidget_manual_with_sender().await; let config = chat_widget.config_ref().clone(); let current_model = "gpt-5.2-codex".to_string(); - let server = Arc::new(ConversationManager::with_models_provider( + let server = Arc::new(ThreadManager::with_models_provider( CodexAuth::from_api_key("Test API Key"), config.model_provider.clone(), )); @@ -2159,7 +2148,7 @@ mod tests { let (chat_widget, app_event_tx, rx, op_rx) = make_chatwidget_manual_with_sender().await; let config = chat_widget.config_ref().clone(); let current_model = "gpt-5.2-codex".to_string(); - let server = Arc::new(ConversationManager::with_models_provider( + let server = Arc::new(ThreadManager::with_models_provider( CodexAuth::from_api_key("Test API Key"), config.model_provider.clone(), )); @@ -2404,7 +2393,7 @@ mod tests { let make_header = |is_first| { let event = SessionConfiguredEvent { - session_id: ConversationId::new(), + session_id: ThreadId::new(), model: "gpt-test".to_string(), model_provider_id: "test-provider".to_string(), approval_policy: AskForApproval::Never, @@ -2442,7 +2431,7 @@ mod tests { assert_eq!(user_count(&app.transcript_cells), 2); - app.backtrack.base_id = Some(ConversationId::new()); + app.backtrack.base_id = Some(ThreadId::new()); app.backtrack.primed = true; app.backtrack.nth_user_message = user_count(&app.transcript_cells).saturating_sub(1); @@ -2697,7 +2686,7 @@ mod tests { async fn new_session_requests_shutdown_for_previous_conversation() { let (mut app, mut app_event_rx, mut op_rx) = make_test_app_with_channels().await; - let conversation_id = ConversationId::new(); + let conversation_id = ThreadId::new(); let event = SessionConfiguredEvent { session_id: conversation_id, model: "gpt-test".to_string(), @@ -2763,8 +2752,7 @@ mod tests { total_tokens: 12, ..Default::default() }; - let conversation = - ConversationId::from_string("123e4567-e89b-12d3-a456-426614174000").unwrap(); + let conversation = ThreadId::from_string("123e4567-e89b-12d3-a456-426614174000").unwrap(); let summary = session_summary(usage, Some(conversation)).expect("summary"); assert_eq!( diff --git a/codex-rs/tui2/src/app_backtrack.rs b/codex-rs/tui2/src/app_backtrack.rs index 01cb64fe5..c5c2f0e95 100644 --- a/codex-rs/tui2/src/app_backtrack.rs +++ b/codex-rs/tui2/src/app_backtrack.rs @@ -9,7 +9,7 @@ use crate::pager_overlay::Overlay; use crate::tui; use crate::tui::TuiEvent; use codex_core::protocol::ConversationPathResponseEvent; -use codex_protocol::ConversationId; +use codex_protocol::ThreadId; use color_eyre::eyre::Result; use crossterm::event::KeyCode; use crossterm::event::KeyEvent; @@ -21,13 +21,13 @@ pub(crate) struct BacktrackState { /// True when Esc has primed backtrack mode in the main view. pub(crate) primed: bool, /// Session id of the base conversation to fork from. - pub(crate) base_id: Option, + pub(crate) base_id: Option, /// Index in the transcript of the last user message. pub(crate) nth_user_message: usize, /// True when the transcript overlay is showing a backtrack preview. pub(crate) overlay_preview_active: bool, /// Pending fork request: (base_id, nth_user_message, prefill). - pub(crate) pending: Option<(ConversationId, usize, String)>, + pub(crate) pending: Option<(ThreadId, usize, String)>, } impl App { @@ -99,7 +99,7 @@ impl App { pub(crate) fn request_backtrack( &mut self, prefill: String, - base_id: ConversationId, + base_id: ThreadId, nth_user_message: usize, ) { self.backtrack.pending = Some((base_id, nth_user_message, prefill)); @@ -308,7 +308,7 @@ impl App { } /// Handle a ConversationHistory response while a backtrack is pending. - /// If it matches the primed base session, fork and switch to the new conversation. + /// If it matches the primed base session, fork and switch to the new thread. pub(crate) async fn on_conversation_history_for_backtrack( &mut self, tui: &mut tui::Tui, @@ -324,7 +324,7 @@ impl App { Ok(()) } - /// Fork the conversation using provided history and switch UI/state accordingly. + /// Fork the thread using provided history and switch UI/state accordingly. async fn fork_and_switch_to_new_conversation( &mut self, tui: &mut tui::Tui, @@ -345,28 +345,26 @@ impl App { } } - /// Thin wrapper around ConversationManager::fork_conversation. + /// Thin wrapper around ThreadManager::fork_thread. async fn perform_fork( &self, path: PathBuf, nth_user_message: usize, cfg: codex_core::config::Config, - ) -> codex_core::error::Result { - self.server - .fork_conversation(nth_user_message, cfg, path) - .await + ) -> codex_core::error::Result { + self.server.fork_thread(nth_user_message, cfg, path).await } - /// Install a forked conversation into the ChatWidget and update UI to reflect selection. + /// Install a forked thread into the ChatWidget and update UI to reflect selection. fn install_forked_conversation( &mut self, tui: &mut tui::Tui, cfg: codex_core::config::Config, - new_conv: codex_core::NewConversation, + new_conv: codex_core::NewThread, nth_user_message: usize, prefill: &str, ) { - let conv = new_conv.conversation; + let thread = new_conv.thread; let session_configured = new_conv.session_configured; let init = crate::chatwidget::ChatWidgetInit { config: cfg, @@ -382,7 +380,7 @@ impl App { is_first_run: false, }; self.chat_widget = - crate::chatwidget::ChatWidget::new_from_existing(init, conv, session_configured); + crate::chatwidget::ChatWidget::new_from_existing(init, thread, session_configured); // Trim transcript up to the selected user message and re-render it. self.trim_transcript_for_backtrack(nth_user_message); self.render_transcript_once(tui); diff --git a/codex-rs/tui2/src/chatwidget.rs b/codex-rs/tui2/src/chatwidget.rs index 053cd8530..05b66d3ae 100644 --- a/codex-rs/tui2/src/chatwidget.rs +++ b/codex-rs/tui2/src/chatwidget.rs @@ -61,7 +61,7 @@ use codex_core::protocol::WarningEvent; use codex_core::protocol::WebSearchBeginEvent; use codex_core::protocol::WebSearchEndEvent; use codex_core::skills::model::SkillMetadata; -use codex_protocol::ConversationId; +use codex_protocol::ThreadId; use codex_protocol::account::PlanType; use codex_protocol::approvals::ElicitationRequestEvent; use codex_protocol::parse_command::ParsedCommand; @@ -131,7 +131,7 @@ use codex_common::approval_presets::ApprovalPreset; use codex_common::approval_presets::builtin_approval_presets; use codex_core::AuthManager; use codex_core::CodexAuth; -use codex_core::ConversationManager; +use codex_core::ThreadManager; use codex_core::protocol::AskForApproval; use codex_core::protocol::SandboxPolicy; use codex_file_search::FileMatch; @@ -311,7 +311,7 @@ pub(crate) struct ChatWidget { current_status_header: String, // Previous status header to restore after a transient stream retry. retry_status_header: Option, - conversation_id: Option, + conversation_id: Option, frame_requester: FrameRequester, // Whether to include the initial welcome banner on session configured show_welcome_banner: bool, @@ -1264,10 +1264,7 @@ impl ChatWidget { } } - pub(crate) fn new( - common: ChatWidgetInit, - conversation_manager: Arc, - ) -> Self { + pub(crate) fn new(common: ChatWidgetInit, thread_manager: Arc) -> Self { let ChatWidgetInit { config, frame_requester, @@ -1285,7 +1282,7 @@ impl ChatWidget { config.model = Some(model.clone()); let mut rng = rand::rng(); let placeholder = EXAMPLE_PROMPTS[rng.random_range(0..EXAMPLE_PROMPTS.len())].to_string(); - let codex_op_tx = spawn_agent(config.clone(), app_event_tx.clone(), conversation_manager); + let codex_op_tx = spawn_agent(config.clone(), app_event_tx.clone(), thread_manager); let mut widget = Self { app_event_tx: app_event_tx.clone(), @@ -1349,7 +1346,7 @@ impl ChatWidget { /// Create a ChatWidget attached to an existing conversation (e.g., a fork). pub(crate) fn new_from_existing( common: ChatWidgetInit, - conversation: std::sync::Arc, + conversation: std::sync::Arc, session_configured: codex_core::protocol::SessionConfiguredEvent, ) -> Self { let ChatWidgetInit { @@ -3345,7 +3342,7 @@ impl ChatWidget { .unwrap_or_default() } - pub(crate) fn conversation_id(&self) -> Option { + pub(crate) fn conversation_id(&self) -> Option { self.conversation_id } diff --git a/codex-rs/tui2/src/chatwidget/agent.rs b/codex-rs/tui2/src/chatwidget/agent.rs index 240972347..0e6fa2712 100644 --- a/codex-rs/tui2/src/chatwidget/agent.rs +++ b/codex-rs/tui2/src/chatwidget/agent.rs @@ -1,8 +1,8 @@ use std::sync::Arc; -use codex_core::CodexConversation; -use codex_core::ConversationManager; -use codex_core::NewConversation; +use codex_core::CodexThread; +use codex_core::NewThread; +use codex_core::ThreadManager; use codex_core::config::Config; use codex_core::protocol::Event; use codex_core::protocol::EventMsg; @@ -18,17 +18,17 @@ use crate::app_event_sender::AppEventSender; pub(crate) fn spawn_agent( config: Config, app_event_tx: AppEventSender, - server: Arc, + server: Arc, ) -> UnboundedSender { let (codex_op_tx, mut codex_op_rx) = unbounded_channel::(); let app_event_tx_clone = app_event_tx; tokio::spawn(async move { - let NewConversation { - conversation_id: _, - conversation, + let NewThread { + thread, session_configured, - } = match server.new_conversation(config).await { + thread_id: _, + } = match server.start_thread(config).await { Ok(v) => v, #[allow(clippy::print_stderr)] Err(err) => { @@ -52,17 +52,17 @@ pub(crate) fn spawn_agent( }; app_event_tx_clone.send(AppEvent::CodexEvent(ev)); - let conversation_clone = conversation.clone(); + let thread_clone = thread.clone(); tokio::spawn(async move { while let Some(op) = codex_op_rx.recv().await { - let id = conversation_clone.submit(op).await; + let id = thread_clone.submit(op).await; if let Err(e) = id { tracing::error!("failed to submit op: {e}"); } } }); - while let Ok(event) = conversation.next_event().await { + while let Ok(event) = thread.next_event().await { app_event_tx_clone.send(AppEvent::CodexEvent(event)); } }); @@ -70,11 +70,11 @@ pub(crate) fn spawn_agent( codex_op_tx } -/// Spawn agent loops for an existing conversation (e.g., a forked conversation). +/// Spawn agent loops for an existing thread (e.g., a forked thread). /// Sends the provided `SessionConfiguredEvent` immediately, then forwards subsequent /// events and accepts Ops for submission. pub(crate) fn spawn_agent_from_existing( - conversation: std::sync::Arc, + thread: std::sync::Arc, session_configured: codex_core::protocol::SessionConfiguredEvent, app_event_tx: AppEventSender, ) -> UnboundedSender { @@ -89,17 +89,17 @@ pub(crate) fn spawn_agent_from_existing( }; app_event_tx_clone.send(AppEvent::CodexEvent(ev)); - let conversation_clone = conversation.clone(); + let thread_clone = thread.clone(); tokio::spawn(async move { while let Some(op) = codex_op_rx.recv().await { - let id = conversation_clone.submit(op).await; + let id = thread_clone.submit(op).await; if let Err(e) = id { tracing::error!("failed to submit op: {e}"); } } }); - while let Ok(event) = conversation.next_event().await { + while let Ok(event) = thread.next_event().await { app_event_tx_clone.send(AppEvent::CodexEvent(event)); } }); diff --git a/codex-rs/tui2/src/chatwidget/tests.rs b/codex-rs/tui2/src/chatwidget/tests.rs index cbece2dba..01b6e0ed5 100644 --- a/codex-rs/tui2/src/chatwidget/tests.rs +++ b/codex-rs/tui2/src/chatwidget/tests.rs @@ -45,7 +45,7 @@ use codex_core::protocol::UndoCompletedEvent; use codex_core::protocol::UndoStartedEvent; use codex_core::protocol::ViewImageToolCallEvent; use codex_core::protocol::WarningEvent; -use codex_protocol::ConversationId; +use codex_protocol::ThreadId; use codex_protocol::account::PlanType; use codex_protocol::openai_models::ModelPreset; use codex_protocol::openai_models::ReasoningEffortPreset; @@ -99,7 +99,7 @@ fn snapshot(percent: f64) -> RateLimitSnapshot { async fn resumed_initial_messages_render_history() { let (mut chat, mut rx, _ops) = make_chatwidget_manual(None).await; - let conversation_id = ConversationId::new(); + let conversation_id = ThreadId::new(); let rollout_file = NamedTempFile::new().unwrap(); let configured = codex_core::protocol::SessionConfiguredEvent { session_id: conversation_id, @@ -311,7 +311,7 @@ async fn helpers_are_available_and_do_not_panic() { let tx = AppEventSender::new(tx_raw); let cfg = test_config().await; let resolved_model = ModelsManager::get_model_offline(cfg.model.as_deref()); - let conversation_manager = Arc::new(ConversationManager::with_models_provider( + let thread_manager = Arc::new(ThreadManager::with_models_provider( CodexAuth::from_api_key("test"), cfg.model_provider.clone(), )); @@ -324,12 +324,12 @@ async fn helpers_are_available_and_do_not_panic() { initial_images: Vec::new(), enhanced_keys_supported: false, auth_manager, - models_manager: conversation_manager.get_models_manager(), + models_manager: thread_manager.get_models_manager(), feedback: codex_feedback::CodexFeedback::new(), is_first_run: true, model: resolved_model, }; - let mut w = ChatWidget::new(init, conversation_manager); + let mut w = ChatWidget::new(init, thread_manager); // Basic construction sanity. let _ = &mut w; } diff --git a/codex-rs/tui2/src/lib.rs b/codex-rs/tui2/src/lib.rs index c8106e087..1c161bf62 100644 --- a/codex-rs/tui2/src/lib.rs +++ b/codex-rs/tui2/src/lib.rs @@ -19,7 +19,7 @@ use codex_core::config::ConfigOverrides; use codex_core::config::find_codex_home; use codex_core::config::load_config_as_toml_with_cli_overrides; use codex_core::config::resolve_oss_provider; -use codex_core::find_conversation_path_by_id_str; +use codex_core::find_thread_path_by_id_str; use codex_core::get_platform_sandbox; use codex_core::protocol::AskForApproval; use codex_protocol::config_types::SandboxMode; @@ -450,7 +450,7 @@ async fn run_ratatui_app( // Determine resume behavior: explicit id, then resume last, then picker. let resume_selection = if let Some(id_str) = cli.resume_session_id.as_deref() { - match find_conversation_path_by_id_str(&config.codex_home, id_str).await? { + match find_thread_path_by_id_str(&config.codex_home, id_str).await? { Some(path) => resume_picker::ResumeSelection::Resume(path), None => { error!("Error finding conversation path: {id_str}"); @@ -473,7 +473,7 @@ async fn run_ratatui_app( } } else if cli.resume_last { let provider_filter = vec![config.model_provider_id.clone()]; - match RolloutRecorder::list_conversations( + match RolloutRecorder::list_threads( &config.codex_home, 1, None, diff --git a/codex-rs/tui2/src/resume_picker.rs b/codex-rs/tui2/src/resume_picker.rs index 0f55bb5e0..13f7b9db7 100644 --- a/codex-rs/tui2/src/resume_picker.rs +++ b/codex-rs/tui2/src/resume_picker.rs @@ -5,11 +5,11 @@ use std::sync::Arc; use chrono::DateTime; use chrono::Utc; -use codex_core::ConversationItem; -use codex_core::ConversationsPage; use codex_core::Cursor; use codex_core::INTERACTIVE_SESSION_SOURCES; use codex_core::RolloutRecorder; +use codex_core::ThreadItem; +use codex_core::ThreadsPage; use codex_core::path_utils; use codex_protocol::items::TurnItem; use color_eyre::eyre::Result; @@ -61,7 +61,7 @@ enum BackgroundEvent { PageLoaded { request_token: usize, search_token: Option, - page: std::io::Result, + page: std::io::Result, }, } @@ -89,7 +89,7 @@ pub async fn run_resume_picker( let tx = loader_tx.clone(); tokio::spawn(async move { let provider_filter = vec![request.default_provider.clone()]; - let page = RolloutRecorder::list_conversations( + let page = RolloutRecorder::list_threads( &request.codex_home, PAGE_SIZE, request.cursor.as_ref(), @@ -415,7 +415,7 @@ impl PickerState { self.pagination.loading = LoadingState::Idle; } - fn ingest_page(&mut self, page: ConversationsPage) { + fn ingest_page(&mut self, page: ThreadsPage) { if let Some(cursor) = page.next_cursor.clone() { self.pagination.next_cursor = Some(cursor); } else { @@ -627,11 +627,11 @@ impl PickerState { } } -fn rows_from_items(items: Vec) -> Vec { +fn rows_from_items(items: Vec) -> Vec { items.into_iter().map(|item| head_to_row(&item)).collect() } -fn head_to_row(item: &ConversationItem) -> Row { +fn head_to_row(item: &ThreadItem) -> Row { let created_at = item .created_at .as_deref() @@ -1077,8 +1077,8 @@ mod tests { ] } - fn make_item(path: &str, ts: &str, preview: &str) -> ConversationItem { - ConversationItem { + fn make_item(path: &str, ts: &str, preview: &str) -> ThreadItem { + ThreadItem { path: PathBuf::from(path), head: head_with_ts_and_user_text(ts, &[preview]), created_at: Some(ts.to_string()), @@ -1092,12 +1092,12 @@ mod tests { } fn page( - items: Vec, + items: Vec, next_cursor: Option, num_scanned_files: usize, reached_scan_cap: bool, - ) -> ConversationsPage { - ConversationsPage { + ) -> ThreadsPage { + ThreadsPage { items, next_cursor, num_scanned_files, @@ -1144,13 +1144,13 @@ mod tests { #[test] fn rows_from_items_preserves_backend_order() { // Construct two items with different timestamps and real user text. - let a = ConversationItem { + let a = ThreadItem { path: PathBuf::from("/tmp/a.jsonl"), head: head_with_ts_and_user_text("2025-01-01T00:00:00Z", &["A"]), created_at: Some("2025-01-01T00:00:00Z".into()), updated_at: Some("2025-01-01T00:00:00Z".into()), }; - let b = ConversationItem { + let b = ThreadItem { path: PathBuf::from("/tmp/b.jsonl"), head: head_with_ts_and_user_text("2025-01-02T00:00:00Z", &["B"]), created_at: Some("2025-01-02T00:00:00Z".into()), @@ -1166,7 +1166,7 @@ mod tests { #[test] fn row_uses_tail_timestamp_for_updated_at() { let head = head_with_ts_and_user_text("2025-01-01T00:00:00Z", &["Hello"]); - let item = ConversationItem { + let item = ThreadItem { path: PathBuf::from("/tmp/a.jsonl"), head, created_at: Some("2025-01-01T00:00:00Z".into()), @@ -1351,7 +1351,7 @@ mod tests { None, ); - let page = RolloutRecorder::list_conversations( + let page = RolloutRecorder::list_threads( &state.codex_home, PAGE_SIZE, None, diff --git a/codex-rs/tui2/src/status/card.rs b/codex-rs/tui2/src/status/card.rs index 3e7a626e4..1cffb7efe 100644 --- a/codex-rs/tui2/src/status/card.rs +++ b/codex-rs/tui2/src/status/card.rs @@ -11,7 +11,7 @@ use codex_core::protocol::NetworkAccess; use codex_core::protocol::SandboxPolicy; use codex_core::protocol::TokenUsage; use codex_core::protocol::TokenUsageInfo; -use codex_protocol::ConversationId; +use codex_protocol::ThreadId; use codex_protocol::account::PlanType; use ratatui::prelude::*; use ratatui::style::Stylize; @@ -74,7 +74,7 @@ pub(crate) fn new_status_output( auth_manager: &AuthManager, token_info: Option<&TokenUsageInfo>, total_usage: &TokenUsage, - session_id: &Option, + session_id: &Option, rate_limits: Option<&RateLimitSnapshotDisplay>, plan_type: Option, now: DateTime, @@ -103,7 +103,7 @@ impl StatusHistoryCell { auth_manager: &AuthManager, token_info: Option<&TokenUsageInfo>, total_usage: &TokenUsage, - session_id: &Option, + session_id: &Option, rate_limits: Option<&RateLimitSnapshotDisplay>, plan_type: Option, now: DateTime,