From 0d788e6263be56698595f3f12e08392954775a62 Mon Sep 17 00:00:00 2001 From: Thibault Sottiaux Date: Wed, 7 Jan 2026 08:54:27 -0800 Subject: [PATCH 1/8] fix: handle early codex exec exit (#8825) Fixes CodexExec to avoid missing early process exits by registering the exit handler up front and deferring the error until after stdout is drained, and adds a regression test that simulates a fast-exit child while still producing output so hangs are caught. --- sdk/typescript/src/exec.ts | 28 ++++++------- sdk/typescript/tests/exec.test.ts | 70 +++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 14 deletions(-) create mode 100644 sdk/typescript/tests/exec.test.ts diff --git a/sdk/typescript/src/exec.ts b/sdk/typescript/src/exec.ts index fb7ed54ad1..582fc0bc96 100644 --- a/sdk/typescript/src/exec.ts +++ b/sdk/typescript/src/exec.ts @@ -153,6 +153,14 @@ export class CodexExec { }); } + const exitPromise = new Promise<{ code: number | null; signal: NodeJS.Signals | null }>( + (resolve) => { + child.once("exit", (code, signal) => { + resolve({ code, signal }); + }); + }, + ); + const rl = readline.createInterface({ input: child.stdout, crlfDelay: Infinity, @@ -164,21 +172,13 @@ export class CodexExec { yield line as string; } - const exitCode = new Promise((resolve, reject) => { - child.once("exit", (code) => { - if (code === 0) { - resolve(code); - } else { - const stderrBuffer = Buffer.concat(stderrChunks); - reject( - new Error(`Codex Exec exited with code ${code}: ${stderrBuffer.toString("utf8")}`), - ); - } - }); - }); - if (spawnError) throw spawnError; - await exitCode; + const { code, signal } = await exitPromise; + if (code !== 0 || signal) { + const stderrBuffer = Buffer.concat(stderrChunks); + const detail = signal ? `signal ${signal}` : `code ${code ?? 1}`; + throw new Error(`Codex Exec exited with ${detail}: ${stderrBuffer.toString("utf8")}`); + } } finally { rl.close(); child.removeAllListeners(); diff --git a/sdk/typescript/tests/exec.test.ts b/sdk/typescript/tests/exec.test.ts new file mode 100644 index 0000000000..9c4b6c2530 --- /dev/null +++ b/sdk/typescript/tests/exec.test.ts @@ -0,0 +1,70 @@ +import * as child_process from "node:child_process"; +import { EventEmitter } from "node:events"; +import { PassThrough } from "node:stream"; + +import { describe, expect, it } from "@jest/globals"; + +jest.mock("node:child_process", () => { + const actual = jest.requireActual("node:child_process"); + return { ...actual, spawn: jest.fn() }; +}); + +const _actualChildProcess = + jest.requireActual("node:child_process"); +const spawnMock = child_process.spawn as jest.MockedFunction; + +class FakeChildProcess extends EventEmitter { + stdin = new PassThrough(); + stdout = new PassThrough(); + stderr = new PassThrough(); + killed = false; + + kill(): boolean { + this.killed = true; + return true; + } +} + +function createEarlyExitChild(exitCode = 2): FakeChildProcess { + const child = new FakeChildProcess(); + setImmediate(() => { + child.stderr.write("boom"); + child.emit("exit", exitCode, null); + setImmediate(() => { + child.stdout.end(); + child.stderr.end(); + }); + }); + return child; +} + +const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +describe("CodexExec", () => { + it("rejects when exit happens before stdout closes", async () => { + const { CodexExec } = await import("../src/exec"); + const child = createEarlyExitChild(); + spawnMock.mockReturnValue(child as unknown as child_process.ChildProcess); + + const exec = new CodexExec("codex"); + const runPromise = (async () => { + for await (const _ of exec.run({ input: "hi" })) { + // no-op + } + })().then( + () => ({ status: "resolved" as const }), + (error) => ({ status: "rejected" as const, error }), + ); + + const result = await Promise.race([ + runPromise, + delay(500).then(() => ({ status: "timeout" as const })), + ]); + + expect(result.status).toBe("rejected"); + if (result.status === "rejected") { + expect(result.error).toBeInstanceOf(Error); + expect(result.error.message).toMatch(/Codex Exec exited/); + } + }); +}); From 116059c3a0ac76a1b521394ee967a7f4b4cb1713 Mon Sep 17 00:00:00 2001 From: jif-oai Date: Wed, 7 Jan 2026 17:04:53 +0000 Subject: [PATCH 2/8] chore: unify conversation with thread name (#8830) Done and verified by Codex + refactor feature of RustRover --- .../src/protocol/common.rs | 11 +- .../app-server-protocol/src/protocol/v1.rs | 30 +- codex-rs/app-server-test-client/src/main.rs | 20 +- .../app-server/src/bespoke_event_handling.rs | 74 +-- .../app-server/src/codex_message_processor.rs | 450 ++++++++---------- codex-rs/app-server/src/message_processor.rs | 6 +- codex-rs/app-server/src/models.rs | 9 +- .../app-server/tests/common/mcp_process.rs | 2 +- codex-rs/app-server/tests/common/rollout.rs | 4 +- ...hive_conversation.rs => archive_thread.rs} | 0 .../suite/codex_message_processor_flow.rs | 4 +- ...reate_conversation.rs => create_thread.rs} | 0 .../app-server/tests/suite/list_resume.rs | 2 +- codex-rs/app-server/tests/suite/mod.rs | 4 +- .../app-server/tests/suite/send_message.rs | 11 +- .../tests/suite/v2/thread_archive.rs | 4 +- codex-rs/cli/src/main.rs | 10 +- codex-rs/core/src/agent/control.rs | 59 ++- codex-rs/core/src/client.rs | 6 +- codex-rs/core/src/codex.rs | 30 +- codex-rs/core/src/codex_delegate.rs | 8 +- ...{codex_conversation.rs => codex_thread.rs} | 8 +- codex-rs/core/src/compact.rs | 4 +- codex-rs/core/src/context_manager/history.rs | 2 +- codex-rs/core/src/error.rs | 10 +- codex-rs/core/src/lib.rs | 22 +- codex-rs/core/src/message_history.rs | 12 +- codex-rs/core/src/rollout/error.rs | 2 +- codex-rs/core/src/rollout/list.rs | 43 +- codex-rs/core/src/rollout/mod.rs | 4 +- codex-rs/core/src/rollout/recorder.rs | 41 +- codex-rs/core/src/rollout/tests.rs | 78 +-- codex-rs/core/src/tasks/review.rs | 4 +- ...versation_manager.rs => thread_manager.rs} | 196 ++++---- codex-rs/core/src/tools/spec.rs | 2 +- .../core/tests/chat_completions_payload.rs | 4 +- codex-rs/core/tests/chat_completions_sse.rs | 4 +- codex-rs/core/tests/common/lib.rs | 11 +- codex-rs/core/tests/common/test_codex.rs | 26 +- codex-rs/core/tests/responses_headers.rs | 8 +- codex-rs/core/tests/suite/cli_stream.rs | 2 +- codex-rs/core/tests/suite/client.rs | 118 +++-- codex-rs/core/tests/suite/compact.rs | 73 ++- .../core/tests/suite/compact_resume_fork.rs | 48 +- .../{fork_conversation.rs => fork_thread.rs} | 31 +- codex-rs/core/tests/suite/list_models.rs | 6 +- codex-rs/core/tests/suite/mod.rs | 2 +- codex-rs/core/tests/suite/model_overrides.rs | 18 +- codex-rs/core/tests/suite/prompt_caching.rs | 4 +- codex-rs/core/tests/suite/remote_models.rs | 28 +- codex-rs/core/tests/suite/resume_warning.rs | 17 +- codex-rs/core/tests/suite/review.rs | 24 +- .../core/tests/suite/rollout_list_find.rs | 8 +- codex-rs/core/tests/suite/undo.rs | 8 +- codex-rs/core/tests/suite/user_shell_cmd.rs | 22 +- codex-rs/exec/src/lib.rs | 48 +- .../tests/event_processor_with_json_output.rs | 3 +- codex-rs/feedback/src/lib.rs | 6 +- codex-rs/mcp-server/src/codex_tool_runner.rs | 42 +- codex-rs/mcp-server/src/exec_approval.rs | 6 +- codex-rs/mcp-server/src/message_processor.rs | 31 +- codex-rs/mcp-server/src/outgoing_message.rs | 6 +- codex-rs/mcp-server/src/patch_approval.rs | 6 +- codex-rs/otel/src/otel_manager.rs | 6 +- codex-rs/protocol/src/lib.rs | 6 +- codex-rs/protocol/src/protocol.rs | 27 +- .../src/{conversation_id.rs => thread_id.rs} | 24 +- codex-rs/tui/src/app.rs | 98 ++-- codex-rs/tui/src/app_backtrack.rs | 32 +- codex-rs/tui/src/chatwidget.rs | 29 +- codex-rs/tui/src/chatwidget/agent.rs | 32 +- codex-rs/tui/src/chatwidget/tests.rs | 12 +- codex-rs/tui/src/lib.rs | 14 +- codex-rs/tui/src/resume_picker.rs | 32 +- codex-rs/tui/src/status/card.rs | 6 +- codex-rs/tui2/src/app.rs | 66 ++- codex-rs/tui2/src/app_backtrack.rs | 28 +- codex-rs/tui2/src/chatwidget.rs | 17 +- codex-rs/tui2/src/chatwidget/agent.rs | 32 +- codex-rs/tui2/src/chatwidget/tests.rs | 10 +- codex-rs/tui2/src/lib.rs | 6 +- codex-rs/tui2/src/resume_picker.rs | 32 +- codex-rs/tui2/src/status/card.rs | 6 +- 83 files changed, 1094 insertions(+), 1203 deletions(-) rename codex-rs/app-server/tests/suite/{archive_conversation.rs => archive_thread.rs} (100%) rename codex-rs/app-server/tests/suite/{create_conversation.rs => create_thread.rs} (100%) rename codex-rs/core/src/{codex_conversation.rs => codex_thread.rs} (91%) rename codex-rs/core/src/{conversation_manager.rs => thread_manager.rs} (71%) rename codex-rs/core/tests/suite/{fork_conversation.rs => fork_thread.rs} (89%) rename codex-rs/protocol/src/{conversation_id.rs => thread_id.rs} (75%) diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index e9e71140ad..ed8207e24e 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 8aad35e41c..981ab28d1b 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 526558250c..389961cb0d 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 c9b78fe8c8..8fb06ca2ed 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 1cadd5e4f0..3b0e4a9db1 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 be57ad3970..6b2ea8d0ab 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 29a9c99636..906108c50b 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 026b01ebba..f3ec682fb2 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 52035e4ede..b5829716af 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 c044e1c4cf..a508bf8805 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 34e737437c..983553e06e 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 916a8f5a72..41d6f83b95 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 39b3a31a8a..ed93f8a7f3 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 88891af77d..b8cdd426af 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 5cf4678bfc..8c2ff50417 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 468f580a21..201bb4e0ff 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 11a3c5c65f..785d0475b7 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 a4ea288e0d..828a6bab01 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 d7458f1acb..72c2911bfe 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 723a4cf4f0..e8a3799303 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 77b9303e92..b608cfa467 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 e52561c9fe..8dc55187a5 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 e8fa91d26e..4ae3e709fa 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 b9f2645b13..370c1ecb97 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 733e8e8008..cb3b10098c 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 e924dd2d28..ee48bb2029 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 e2ef0e883c..487304ddc8 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 d7e24602fd..5b65bada7c 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 a39f85c823..d571ad191e 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 1df3659ba0..f7c13c70f8 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 00dbc51f40..4a2b587af4 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 cf0f0106db..08f432a596 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 75756831be..d9e21bc24b 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 8af5df2169..536de44e83 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 4f05838279..17d4a5a4ab 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 9568ec2786..45e8b0b46f 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 7ae3ea159f..24d5dd8bc7 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 3b0ffd2983..188397fd24 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 8901dd210e..d6cf977a48 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 5ce6e9f2ff..7d876ceb1f 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 09b5fb18b8..e0845ef72b 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 3e38c89b33..4ad829f07a 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 dab856c80b..50a6dba1fb 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 30593a8f60..b81ebcb72d 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 e5b809ca38..dde687a39d 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 f7cdac67c1..a418e35a39 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 f815e2e8a3..01c5908930 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 94641f1a4c..6968285319 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 92acf7de0f..5b38ce4b8d 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 b35213f7e6..763b6109df 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 1d40718d4b..518f26c562 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 9fca272821..61bc3b4fdf 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 270cb80487..d6818c0c54 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 701c7b7972..b82e371d23 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 d288f568e8..a3e2318165 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 2096f4505b..4a227fe093 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 629f4cc493..73b75dcbf8 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 44607b754d..47f52caf7f 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 81eb80764b..955dc06030 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 83ac25fdfd..fef5c8bac7 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 3c614ab331..00e4f204af 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 fbdd322722..c59d3a168f 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 0d6a0594fc..513743c97f 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 4dbd7902c5..f5351576f9 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 db104d4531..8589566a25 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 d76a5b4039..370d3d3f6a 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 ce5dff2ed8..c28680dd93 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 0894fada63..b4ae8b76a5 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 240972347f..d8428b221f 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 c5b0bfd55e..03f11c050d 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 4eb487f1da..6f4faaad65 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 0f55bb5e0d..13f7b9db7a 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 07cd5a1988..7bf066eebf 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 49bb005a7a..5bea13a973 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 01cb64fe5f..c5c2f0e95f 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 053cd85307..05b66d3aea 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 240972347f..0e6fa2712b 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 cbece2dbae..01b6e0ed56 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 c8106e0877..1c161bf627 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 0f55bb5e0d..13f7b9db7a 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 3e7a626e40..1cffb7efeb 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, From fedcb8f63c734abfbba2561088ac17eb5608c08b Mon Sep 17 00:00:00 2001 From: pakrym-oai Date: Wed, 7 Jan 2026 09:36:44 -0800 Subject: [PATCH 3/8] Move tests below auth manager (#8840) To simplify future diffs --- codex-rs/core/src/auth.rs | 318 +++++++++++++++++++------------------- 1 file changed, 159 insertions(+), 159 deletions(-) diff --git a/codex-rs/core/src/auth.rs b/codex-rs/core/src/auth.rs index 96714e3f74..7d7e7b0bc2 100644 --- a/codex-rs/core/src/auth.rs +++ b/codex-rs/core/src/auth.rs @@ -630,6 +630,165 @@ struct CachedAuth { auth: Option, } +/// Central manager providing a single source of truth for auth.json derived +/// authentication data. It loads once (or on preference change) and then +/// hands out cloned `CodexAuth` values so the rest of the program has a +/// consistent snapshot. +/// +/// External modifications to `auth.json` will NOT be observed until +/// `reload()` is called explicitly. This matches the design goal of avoiding +/// different parts of the program seeing inconsistent auth data mid‑run. +#[derive(Debug)] +pub struct AuthManager { + codex_home: PathBuf, + inner: RwLock, + enable_codex_api_key_env: bool, + auth_credentials_store_mode: AuthCredentialsStoreMode, +} + +impl AuthManager { + /// Create a new manager loading the initial auth using the provided + /// preferred auth method. Errors loading auth are swallowed; `auth()` will + /// simply return `None` in that case so callers can treat it as an + /// unauthenticated state. + pub fn new( + codex_home: PathBuf, + enable_codex_api_key_env: bool, + auth_credentials_store_mode: AuthCredentialsStoreMode, + ) -> Self { + let auth = load_auth( + &codex_home, + enable_codex_api_key_env, + auth_credentials_store_mode, + ) + .ok() + .flatten(); + Self { + codex_home, + inner: RwLock::new(CachedAuth { auth }), + enable_codex_api_key_env, + auth_credentials_store_mode, + } + } + + #[cfg(any(test, feature = "test-support"))] + #[expect(clippy::expect_used)] + /// Create an AuthManager with a specific CodexAuth, for testing only. + pub fn from_auth_for_testing(auth: CodexAuth) -> Arc { + let cached = CachedAuth { auth: Some(auth) }; + let temp_dir = tempfile::tempdir().expect("temp codex home"); + let codex_home = temp_dir.path().to_path_buf(); + TEST_AUTH_TEMP_DIRS + .lock() + .expect("lock test codex homes") + .push(temp_dir); + Arc::new(Self { + codex_home, + inner: RwLock::new(cached), + enable_codex_api_key_env: false, + auth_credentials_store_mode: AuthCredentialsStoreMode::File, + }) + } + + #[cfg(any(test, feature = "test-support"))] + /// Create an AuthManager with a specific CodexAuth and codex home, for testing only. + pub fn from_auth_for_testing_with_home(auth: CodexAuth, codex_home: PathBuf) -> Arc { + let cached = CachedAuth { auth: Some(auth) }; + Arc::new(Self { + codex_home, + inner: RwLock::new(cached), + enable_codex_api_key_env: false, + auth_credentials_store_mode: AuthCredentialsStoreMode::File, + }) + } + + /// Current cached auth (clone). May be `None` if not logged in or load failed. + pub fn auth(&self) -> Option { + self.inner.read().ok().and_then(|c| c.auth.clone()) + } + + pub fn codex_home(&self) -> &Path { + &self.codex_home + } + + /// Force a reload of the auth information from auth.json. Returns + /// whether the auth value changed. + pub fn reload(&self) -> bool { + let new_auth = load_auth( + &self.codex_home, + self.enable_codex_api_key_env, + self.auth_credentials_store_mode, + ) + .ok() + .flatten(); + if let Ok(mut guard) = self.inner.write() { + let changed = !AuthManager::auths_equal(&guard.auth, &new_auth); + guard.auth = new_auth; + changed + } else { + false + } + } + + fn auths_equal(a: &Option, b: &Option) -> bool { + match (a, b) { + (None, None) => true, + (Some(a), Some(b)) => a == b, + _ => false, + } + } + + /// Convenience constructor returning an `Arc` wrapper. + pub fn shared( + codex_home: PathBuf, + enable_codex_api_key_env: bool, + auth_credentials_store_mode: AuthCredentialsStoreMode, + ) -> Arc { + Arc::new(Self::new( + codex_home, + enable_codex_api_key_env, + auth_credentials_store_mode, + )) + } + + /// Attempt to refresh the current auth token (if any). On success, reload + /// the auth state from disk so other components observe refreshed token. + /// If the token refresh fails in a permanent (non‑transient) way, logs out + /// to clear invalid auth state. + pub async fn refresh_token(&self) -> Result, RefreshTokenError> { + let auth = match self.auth() { + Some(a) => a, + None => return Ok(None), + }; + match auth.refresh_token().await { + Ok(token) => { + // Reload to pick up persisted changes. + self.reload(); + Ok(Some(token)) + } + Err(e) => { + tracing::error!("Failed to refresh token: {}", e); + Err(e) + } + } + } + + /// Log out by deleting the on‑disk auth.json (if present). Returns Ok(true) + /// if a file was removed, Ok(false) if no auth file existed. On success, + /// reloads the in‑memory auth cache so callers immediately observe the + /// unauthenticated state. + pub fn logout(&self) -> std::io::Result { + let removed = super::auth::logout(&self.codex_home, self.auth_credentials_store_mode)?; + // Always reload to clear any cached auth (even if file absent). + self.reload(); + Ok(removed) + } + + pub fn get_auth_mode(&self) -> Option { + self.auth().map(|a| a.mode) + } +} + #[cfg(test)] mod tests { use super::*; @@ -1051,162 +1210,3 @@ mod tests { pretty_assertions::assert_eq!(auth.account_plan_type(), Some(AccountPlanType::Unknown)); } } - -/// Central manager providing a single source of truth for auth.json derived -/// authentication data. It loads once (or on preference change) and then -/// hands out cloned `CodexAuth` values so the rest of the program has a -/// consistent snapshot. -/// -/// External modifications to `auth.json` will NOT be observed until -/// `reload()` is called explicitly. This matches the design goal of avoiding -/// different parts of the program seeing inconsistent auth data mid‑run. -#[derive(Debug)] -pub struct AuthManager { - codex_home: PathBuf, - inner: RwLock, - enable_codex_api_key_env: bool, - auth_credentials_store_mode: AuthCredentialsStoreMode, -} - -impl AuthManager { - /// Create a new manager loading the initial auth using the provided - /// preferred auth method. Errors loading auth are swallowed; `auth()` will - /// simply return `None` in that case so callers can treat it as an - /// unauthenticated state. - pub fn new( - codex_home: PathBuf, - enable_codex_api_key_env: bool, - auth_credentials_store_mode: AuthCredentialsStoreMode, - ) -> Self { - let auth = load_auth( - &codex_home, - enable_codex_api_key_env, - auth_credentials_store_mode, - ) - .ok() - .flatten(); - Self { - codex_home, - inner: RwLock::new(CachedAuth { auth }), - enable_codex_api_key_env, - auth_credentials_store_mode, - } - } - - #[cfg(any(test, feature = "test-support"))] - #[expect(clippy::expect_used)] - /// Create an AuthManager with a specific CodexAuth, for testing only. - pub fn from_auth_for_testing(auth: CodexAuth) -> Arc { - let cached = CachedAuth { auth: Some(auth) }; - let temp_dir = tempfile::tempdir().expect("temp codex home"); - let codex_home = temp_dir.path().to_path_buf(); - TEST_AUTH_TEMP_DIRS - .lock() - .expect("lock test codex homes") - .push(temp_dir); - Arc::new(Self { - codex_home, - inner: RwLock::new(cached), - enable_codex_api_key_env: false, - auth_credentials_store_mode: AuthCredentialsStoreMode::File, - }) - } - - #[cfg(any(test, feature = "test-support"))] - /// Create an AuthManager with a specific CodexAuth and codex home, for testing only. - pub fn from_auth_for_testing_with_home(auth: CodexAuth, codex_home: PathBuf) -> Arc { - let cached = CachedAuth { auth: Some(auth) }; - Arc::new(Self { - codex_home, - inner: RwLock::new(cached), - enable_codex_api_key_env: false, - auth_credentials_store_mode: AuthCredentialsStoreMode::File, - }) - } - - /// Current cached auth (clone). May be `None` if not logged in or load failed. - pub fn auth(&self) -> Option { - self.inner.read().ok().and_then(|c| c.auth.clone()) - } - - pub fn codex_home(&self) -> &Path { - &self.codex_home - } - - /// Force a reload of the auth information from auth.json. Returns - /// whether the auth value changed. - pub fn reload(&self) -> bool { - let new_auth = load_auth( - &self.codex_home, - self.enable_codex_api_key_env, - self.auth_credentials_store_mode, - ) - .ok() - .flatten(); - if let Ok(mut guard) = self.inner.write() { - let changed = !AuthManager::auths_equal(&guard.auth, &new_auth); - guard.auth = new_auth; - changed - } else { - false - } - } - - fn auths_equal(a: &Option, b: &Option) -> bool { - match (a, b) { - (None, None) => true, - (Some(a), Some(b)) => a == b, - _ => false, - } - } - - /// Convenience constructor returning an `Arc` wrapper. - pub fn shared( - codex_home: PathBuf, - enable_codex_api_key_env: bool, - auth_credentials_store_mode: AuthCredentialsStoreMode, - ) -> Arc { - Arc::new(Self::new( - codex_home, - enable_codex_api_key_env, - auth_credentials_store_mode, - )) - } - - /// Attempt to refresh the current auth token (if any). On success, reload - /// the auth state from disk so other components observe refreshed token. - /// If the token refresh fails in a permanent (non‑transient) way, logs out - /// to clear invalid auth state. - pub async fn refresh_token(&self) -> Result, RefreshTokenError> { - let auth = match self.auth() { - Some(a) => a, - None => return Ok(None), - }; - match auth.refresh_token().await { - Ok(token) => { - // Reload to pick up persisted changes. - self.reload(); - Ok(Some(token)) - } - Err(e) => { - tracing::error!("Failed to refresh token: {}", e); - Err(e) - } - } - } - - /// Log out by deleting the on‑disk auth.json (if present). Returns Ok(true) - /// if a file was removed, Ok(false) if no auth file existed. On success, - /// reloads the in‑memory auth cache so callers immediately observe the - /// unauthenticated state. - pub fn logout(&self) -> std::io::Result { - let removed = super::auth::logout(&self.codex_home, self.auth_credentials_store_mode)?; - // Always reload to clear any cached auth (even if file absent). - self.reload(); - Ok(removed) - } - - pub fn get_auth_mode(&self) -> Option { - self.auth().map(|a| a.mode) - } -} From a1e81180f854c3458c48ae2d6ef7c527dfcce8f2 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Wed, 7 Jan 2026 10:11:27 -0800 Subject: [PATCH 4/8] fix: upgrade lru crate to 0.16.3 (#8845) See https://rustsec.org/advisories/RUSTSEC-2026-0002. Though our `ratatui` fork has a transitive dep on an older version of the `lru` crate, so to get CI green ASAP, this PR also adds an exception to `deny.toml` for `RUSTSEC-2026-0002`, but hopefully this will be short-lived. --- codex-rs/Cargo.lock | 24 ++++++++++++------------ codex-rs/Cargo.toml | 2 +- codex-rs/deny.toml | 2 ++ 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 90acb1e772..bf68ff043f 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -360,7 +360,7 @@ dependencies = [ "objc2-foundation", "parking_lot", "percent-encoding", - "windows-sys 0.52.0", + "windows-sys 0.60.2", "wl-clipboard-rs", "x11rb", ] @@ -1866,7 +1866,7 @@ dependencies = [ name = "codex-utils-cache" version = "0.0.0" dependencies = [ - "lru 0.16.2", + "lru 0.16.3", "sha1", "tokio", ] @@ -2781,7 +2781,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.60.2", ] [[package]] @@ -2889,7 +2889,7 @@ checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" dependencies = [ "cfg-if", "rustix 1.0.8", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3830,7 +3830,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -4151,9 +4151,9 @@ dependencies = [ [[package]] name = "lru" -version = "0.16.2" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96051b46fc183dc9cd4a223960ef37b9af631b55191852a8274bfef064cda20f" +checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" dependencies = [ "hashbrown 0.16.0", ] @@ -5331,7 +5331,7 @@ dependencies = [ "once_cell", "socket2 0.6.1", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.60.2", ] [[package]] @@ -5459,7 +5459,7 @@ dependencies = [ "indoc", "itertools 0.14.0", "kasuari", - "lru 0.16.2", + "lru 0.16.3", "strum 0.27.2", "thiserror 2.0.17", "unicode-segmentation", @@ -5710,7 +5710,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -5723,7 +5723,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.9.4", - "windows-sys 0.52.0", + "windows-sys 0.60.2", ] [[package]] @@ -8011,7 +8011,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index 645d5aa210..2d0b5eec08 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -152,7 +152,7 @@ landlock = "0.4.4" lazy_static = "1" libc = "0.2.177" log = "0.4" -lru = "0.16.2" +lru = "0.16.3" maplit = "1.0.2" mime_guess = "2.0.5" multimap = "0.10.0" diff --git a/codex-rs/deny.toml b/codex-rs/deny.toml index f9d411f841..0a4a08bd89 100644 --- a/codex-rs/deny.toml +++ b/codex-rs/deny.toml @@ -73,6 +73,8 @@ ignore = [ { id = "RUSTSEC-2024-0388", reason = "derivative is unmaintained; pulled in via starlark v0.13.0 used by execpolicy/cli/core; no fixed release yet" }, { id = "RUSTSEC-2025-0057", reason = "fxhash is unmaintained; pulled in via starlark_map/starlark v0.13.0 used by execpolicy/cli/core; no fixed release yet" }, { id = "RUSTSEC-2024-0436", reason = "paste is unmaintained; pulled in via ratatui/rmcp/starlark used by tui/execpolicy; no fixed release yet" }, + # TODO(joshka, nornagon): remove this exception when once we update the ratatui fork to a version that uses lru 0.13+. + { id = "RUSTSEC-2026-0002", reason = "lru 0.12.5 is pulled in via ratatui fork; cannot upgrade until the fork is updated" }, ] # If this is true, then cargo deny will use the git executable to fetch advisory database. # If this is false, then it uses a built-in git library. From 9179c9deac9230f1ace631c58cc21d404e00e16a Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Wed, 7 Jan 2026 10:35:09 -0800 Subject: [PATCH 5/8] Merge Modelfamily into modelinfo (#8763) - Merge ModelFamily into ModelInfo - Remove logic for adding instructions to apply patch - Add compaction limit and visible context window to `ModelInfo` --- .../app-server-protocol/src/protocol/v2.rs | 1 + .../app-server/tests/common/models_cache.rs | 8 +- codex-rs/codex-api/src/endpoint/models.rs | 4 +- .../codex-api/tests/models_integration.rs | 8 +- .../prompt_with_apply_patch_instructions.md | 386 ++++++++++++ codex-rs/core/src/client.rs | 48 +- codex-rs/core/src/client_common.rs | 55 +- codex-rs/core/src/codex.rs | 71 ++- codex-rs/core/src/config/mod.rs | 4 +- codex-rs/core/src/context_manager/history.rs | 7 +- codex-rs/core/src/models_manager/manager.rs | 35 +- codex-rs/core/src/models_manager/mod.rs | 2 +- .../core/src/models_manager/model_family.rs | 557 ------------------ .../core/src/models_manager/model_info.rs | 348 +++++++++++ codex-rs/core/src/tools/spec.rs | 71 ++- .../core/tests/chat_completions_payload.rs | 6 +- codex-rs/core/tests/chat_completions_sse.rs | 6 +- codex-rs/core/tests/responses_headers.rs | 20 +- codex-rs/core/tests/suite/client.rs | 8 +- codex-rs/core/tests/suite/prompt_caching.rs | 7 +- codex-rs/core/tests/suite/remote_models.rs | 30 +- .../mcp-types/schema/2025-03-26/schema.json | 3 +- .../mcp-types/schema/2025-06-18/schema.json | 3 +- codex-rs/protocol/src/openai_models.rs | 31 +- codex-rs/protocol/src/protocol.rs | 4 +- codex-rs/tui/src/app.rs | 8 +- codex-rs/tui/src/status/tests.rs | 5 +- codex-rs/tui2/src/status/tests.rs | 5 +- 28 files changed, 964 insertions(+), 777 deletions(-) create mode 100644 codex-rs/core/prompt_with_apply_patch_instructions.md delete mode 100644 codex-rs/core/src/models_manager/model_family.rs create mode 100644 codex-rs/core/src/models_manager/model_info.rs diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index dedf51ea1b..6d0c009b42 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -1247,6 +1247,7 @@ pub struct ThreadTokenUsageUpdatedNotification { pub struct ThreadTokenUsage { pub total: TokenUsageBreakdown, pub last: TokenUsageBreakdown, + // TODO(aibrahim): make this not optional #[ts(type = "number | null")] pub model_context_window: Option, } diff --git a/codex-rs/app-server/tests/common/models_cache.rs b/codex-rs/app-server/tests/common/models_cache.rs index 0a977aa7c2..f6ac1fad7e 100644 --- a/codex-rs/app-server/tests/common/models_cache.rs +++ b/codex-rs/app-server/tests/common/models_cache.rs @@ -15,7 +15,7 @@ fn preset_to_info(preset: &ModelPreset, priority: i32) -> ModelInfo { slug: preset.id.clone(), display_name: preset.display_name.clone(), description: Some(preset.description.clone()), - default_reasoning_level: preset.default_reasoning_effort, + default_reasoning_level: Some(preset.default_reasoning_effort), supported_reasoning_levels: preset.supported_reasoning_efforts.clone(), shell_type: ConfigShellToolType::ShellCommand, visibility: if preset.show_in_picker { @@ -26,14 +26,16 @@ fn preset_to_info(preset: &ModelPreset, priority: i32) -> ModelInfo { supported_in_api: true, priority, upgrade: preset.upgrade.as_ref().map(|u| u.id.clone()), - base_instructions: None, + base_instructions: "base instructions".to_string(), supports_reasoning_summaries: false, support_verbosity: false, default_verbosity: None, apply_patch_tool_type: None, truncation_policy: TruncationPolicyConfig::bytes(10_000), supports_parallel_tool_calls: false, - context_window: None, + context_window: Some(272_000), + auto_compact_token_limit: None, + effective_context_window_percent: 95, experimental_supported_tools: Vec::new(), } } diff --git a/codex-rs/codex-api/src/endpoint/models.rs b/codex-rs/codex-api/src/endpoint/models.rs index 74e109bf2a..9f6083dc89 100644 --- a/codex-rs/codex-api/src/endpoint/models.rs +++ b/codex-rs/codex-api/src/endpoint/models.rs @@ -215,14 +215,14 @@ mod tests { "supported_in_api": true, "priority": 1, "upgrade": null, - "base_instructions": null, + "base_instructions": "base instructions", "supports_reasoning_summaries": false, "support_verbosity": false, "default_verbosity": null, "apply_patch_tool_type": null, "truncation_policy": {"mode": "bytes", "limit": 10_000}, "supports_parallel_tool_calls": false, - "context_window": null, + "context_window": 272_000, "experimental_supported_tools": [], })) .unwrap(), diff --git a/codex-rs/codex-api/tests/models_integration.rs b/codex-rs/codex-api/tests/models_integration.rs index 8ed6ce0d6d..46e1773098 100644 --- a/codex-rs/codex-api/tests/models_integration.rs +++ b/codex-rs/codex-api/tests/models_integration.rs @@ -56,7 +56,7 @@ async fn models_client_hits_models_endpoint() { slug: "gpt-test".to_string(), display_name: "gpt-test".to_string(), description: Some("desc".to_string()), - default_reasoning_level: ReasoningEffort::Medium, + default_reasoning_level: Some(ReasoningEffort::Medium), supported_reasoning_levels: vec![ ReasoningEffortPreset { effort: ReasoningEffort::Low, @@ -76,14 +76,16 @@ async fn models_client_hits_models_endpoint() { supported_in_api: true, priority: 1, upgrade: None, - base_instructions: None, + base_instructions: "base instructions".to_string(), supports_reasoning_summaries: false, support_verbosity: false, default_verbosity: None, apply_patch_tool_type: None, truncation_policy: TruncationPolicyConfig::bytes(10_000), supports_parallel_tool_calls: false, - context_window: None, + context_window: Some(272_000), + auto_compact_token_limit: None, + effective_context_window_percent: 95, experimental_supported_tools: Vec::new(), }], }; diff --git a/codex-rs/core/prompt_with_apply_patch_instructions.md b/codex-rs/core/prompt_with_apply_patch_instructions.md new file mode 100644 index 0000000000..af5537c924 --- /dev/null +++ b/codex-rs/core/prompt_with_apply_patch_instructions.md @@ -0,0 +1,386 @@ +You are a coding agent running in the Codex CLI, a terminal-based coding assistant. Codex CLI is an open source project led by OpenAI. You are expected to be precise, safe, and helpful. + +Your capabilities: + +- Receive user prompts and other context provided by the harness, such as files in the workspace. +- Communicate with the user by streaming thinking & responses, and by making & updating plans. +- Emit function calls to run terminal commands and apply patches. Depending on how this specific run is configured, you can request that these function calls be escalated to the user for approval before running. More on this in the "Sandbox and approvals" section. + +Within this context, Codex refers to the open-source agentic coding interface (not the old Codex language model built by OpenAI). + +# How you work + +## Personality + +Your default personality and tone is concise, direct, and friendly. You communicate efficiently, always keeping the user clearly informed about ongoing actions without unnecessary detail. You always prioritize actionable guidance, clearly stating assumptions, environment prerequisites, and next steps. Unless explicitly asked, you avoid excessively verbose explanations about your work. + +# AGENTS.md spec +- Repos often contain AGENTS.md files. These files can appear anywhere within the repository. +- These files are a way for humans to give you (the agent) instructions or tips for working within the container. +- Some examples might be: coding conventions, info about how code is organized, or instructions for how to run or test code. +- Instructions in AGENTS.md files: + - The scope of an AGENTS.md file is the entire directory tree rooted at the folder that contains it. + - For every file you touch in the final patch, you must obey instructions in any AGENTS.md file whose scope includes that file. + - Instructions about code style, structure, naming, etc. apply only to code within the AGENTS.md file's scope, unless the file states otherwise. + - More-deeply-nested AGENTS.md files take precedence in the case of conflicting instructions. + - Direct system/developer/user instructions (as part of a prompt) take precedence over AGENTS.md instructions. +- The contents of the AGENTS.md file at the root of the repo and any directories from the CWD up to the root are included with the developer message and don't need to be re-read. When working in a subdirectory of CWD, or a directory outside the CWD, check for any AGENTS.md files that may be applicable. + +## Responsiveness + +### Preamble messages + +Before making tool calls, send a brief preamble to the user explaining what you’re about to do. When sending preamble messages, follow these principles and examples: + +- **Logically group related actions**: if you’re about to run several related commands, describe them together in one preamble rather than sending a separate note for each. +- **Keep it concise**: be no more than 1-2 sentences, focused on immediate, tangible next steps. (8–12 words for quick updates). +- **Build on prior context**: if this is not your first tool call, use the preamble message to connect the dots with what’s been done so far and create a sense of momentum and clarity for the user to understand your next actions. +- **Keep your tone light, friendly and curious**: add small touches of personality in preambles feel collaborative and engaging. +- **Exception**: Avoid adding a preamble for every trivial read (e.g., `cat` a single file) unless it’s part of a larger grouped action. + +**Examples:** + +- “I’ve explored the repo; now checking the API route definitions.” +- “Next, I’ll patch the config and update the related tests.” +- “I’m about to scaffold the CLI commands and helper functions.” +- “Ok cool, so I’ve wrapped my head around the repo. Now digging into the API routes.” +- “Config’s looking tidy. Next up is patching helpers to keep things in sync.” +- “Finished poking at the DB gateway. I will now chase down error handling.” +- “Alright, build pipeline order is interesting. Checking how it reports failures.” +- “Spotted a clever caching util; now hunting where it gets used.” + +## Planning + +You have access to an `update_plan` tool which tracks steps and progress and renders them to the user. Using the tool helps demonstrate that you've understood the task and convey how you're approaching it. Plans can help to make complex, ambiguous, or multi-phase work clearer and more collaborative for the user. A good plan should break the task into meaningful, logically ordered steps that are easy to verify as you go. + +Note that plans are not for padding out simple work with filler steps or stating the obvious. The content of your plan should not involve doing anything that you aren't capable of doing (i.e. don't try to test things that you can't test). Do not use plans for simple or single-step queries that you can just do or answer immediately. + +Do not repeat the full contents of the plan after an `update_plan` call — the harness already displays it. Instead, summarize the change made and highlight any important context or next step. + +Before running a command, consider whether or not you have completed the previous step, and make sure to mark it as completed before moving on to the next step. It may be the case that you complete all steps in your plan after a single pass of implementation. If this is the case, you can simply mark all the planned steps as completed. Sometimes, you may need to change plans in the middle of a task: call `update_plan` with the updated plan and make sure to provide an `explanation` of the rationale when doing so. + +Use a plan when: + +- The task is non-trivial and will require multiple actions over a long time horizon. +- There are logical phases or dependencies where sequencing matters. +- The work has ambiguity that benefits from outlining high-level goals. +- You want intermediate checkpoints for feedback and validation. +- When the user asked you to do more than one thing in a single prompt +- The user has asked you to use the plan tool (aka "TODOs") +- You generate additional steps while working, and plan to do them before yielding to the user + +### Examples + +**High-quality plans** + +Example 1: + +1. Add CLI entry with file args +2. Parse Markdown via CommonMark library +3. Apply semantic HTML template +4. Handle code blocks, images, links +5. Add error handling for invalid files + +Example 2: + +1. Define CSS variables for colors +2. Add toggle with localStorage state +3. Refactor components to use variables +4. Verify all views for readability +5. Add smooth theme-change transition + +Example 3: + +1. Set up Node.js + WebSocket server +2. Add join/leave broadcast events +3. Implement messaging with timestamps +4. Add usernames + mention highlighting +5. Persist messages in lightweight DB +6. Add typing indicators + unread count + +**Low-quality plans** + +Example 1: + +1. Create CLI tool +2. Add Markdown parser +3. Convert to HTML + +Example 2: + +1. Add dark mode toggle +2. Save preference +3. Make styles look good + +Example 3: + +1. Create single-file HTML game +2. Run quick sanity check +3. Summarize usage instructions + +If you need to write a plan, only write high quality plans, not low quality ones. + +## Task execution + +You are a coding agent. Please keep going until the query is completely resolved, before ending your turn and yielding back to the user. Only terminate your turn when you are sure that the problem is solved. Autonomously resolve the query to the best of your ability, using the tools available to you, before coming back to the user. Do NOT guess or make up an answer. + +You MUST adhere to the following criteria when solving queries: + +- Working on the repo(s) in the current environment is allowed, even if they are proprietary. +- Analyzing code for vulnerabilities is allowed. +- Showing user code and tool call details is allowed. +- Use the `apply_patch` tool to edit files (NEVER try `applypatch` or `apply-patch`, only `apply_patch`): {"command":["apply_patch","*** Begin Patch\\n*** Update File: path/to/file.py\\n@@ def example():\\n- pass\\n+ return 123\\n*** End Patch"]} + +If completing the user's task requires writing or modifying files, your code and final answer should follow these coding guidelines, though user instructions (i.e. AGENTS.md) may override these guidelines: + +- Fix the problem at the root cause rather than applying surface-level patches, when possible. +- Avoid unneeded complexity in your solution. +- Do not attempt to fix unrelated bugs or broken tests. It is not your responsibility to fix them. (You may mention them to the user in your final message though.) +- Update documentation as necessary. +- Keep changes consistent with the style of the existing codebase. Changes should be minimal and focused on the task. +- Use `git log` and `git blame` to search the history of the codebase if additional context is required. +- NEVER add copyright or license headers unless specifically requested. +- Do not waste tokens by re-reading files after calling `apply_patch` on them. The tool call will fail if it didn't work. The same goes for making folders, deleting folders, etc. +- Do not `git commit` your changes or create new git branches unless explicitly requested. +- Do not add inline comments within code unless explicitly requested. +- Do not use one-letter variable names unless explicitly requested. +- NEVER output inline citations like "【F:README.md†L5-L14】" in your outputs. The CLI is not able to render these so they will just be broken in the UI. Instead, if you output valid filepaths, users will be able to click on them to open the files in their editor. + +## Sandbox and approvals + +The Codex CLI harness supports several different sandboxing, and approval configurations that the user can choose from. + +Filesystem sandboxing prevents you from editing files without user approval. The options are: + +- **read-only**: You can only read files. +- **workspace-write**: You can read files. You can write to files in your workspace folder, but not outside it. +- **danger-full-access**: No filesystem sandboxing. + +Network sandboxing prevents you from accessing network without approval. Options are + +- **restricted** +- **enabled** + +Approvals are your mechanism to get user consent to perform more privileged actions. Although they introduce friction to the user because your work is paused until the user responds, you should leverage them to accomplish your important work. Do not let these settings or the sandbox deter you from attempting to accomplish the user's task. Approval options are + +- **untrusted**: The harness will escalate most commands for user approval, apart from a limited allowlist of safe "read" commands. +- **on-failure**: The harness will allow all commands to run in the sandbox (if enabled), and failures will be escalated to the user for approval to run again without the sandbox. +- **on-request**: Commands will be run in the sandbox by default, and you can specify in your tool call if you want to escalate a command to run without sandboxing. (Note that this mode is not always available. If it is, you'll see parameters for it in the `shell` command description.) +- **never**: This is a non-interactive mode where you may NEVER ask the user for approval to run commands. Instead, you must always persist and work around constraints to solve the task for the user. You MUST do your utmost best to finish the task and validate your work before yielding. If this mode is pared with `danger-full-access`, take advantage of it to deliver the best outcome for the user. Further, in this mode, your default testing philosophy is overridden: Even if you don't see local patterns for testing, you may add tests and scripts to validate your work. Just remove them before yielding. + +When you are running with approvals `on-request`, and sandboxing enabled, here are scenarios where you'll need to request approval: + +- You need to run a command that writes to a directory that requires it (e.g. running tests that write to /tmp) +- You need to run a GUI app (e.g., open/xdg-open/osascript) to open browsers or files. +- You are running sandboxed and need to run a command that requires network access (e.g. installing packages) +- If you run a command that is important to solving the user's query, but it fails because of sandboxing, rerun the command with approval. +- You are about to take a potentially destructive action such as an `rm` or `git reset` that the user did not explicitly ask for +- (For all of these, you should weigh alternative paths that do not require approval.) + +Note that when sandboxing is set to read-only, you'll need to request approval for any command that isn't a read. + +You will be told what filesystem sandboxing, network sandboxing, and approval mode are active in a developer or user message. If you are not told about this, assume that you are running with workspace-write, network sandboxing ON, and approval on-failure. + +## Validating your work + +If the codebase has tests or the ability to build or run, consider using them to verify that your work is complete. + +When testing, your philosophy should be to start as specific as possible to the code you changed so that you can catch issues efficiently, then make your way to broader tests as you build confidence. If there's no test for the code you changed, and if the adjacent patterns in the codebases show that there's a logical place for you to add a test, you may do so. However, do not add tests to codebases with no tests. + +Similarly, once you're confident in correctness, you can suggest or use formatting commands to ensure that your code is well formatted. If there are issues you can iterate up to 3 times to get formatting right, but if you still can't manage it's better to save the user time and present them a correct solution where you call out the formatting in your final message. If the codebase does not have a formatter configured, do not add one. + +For all of testing, running, building, and formatting, do not attempt to fix unrelated bugs. It is not your responsibility to fix them. (You may mention them to the user in your final message though.) + +Be mindful of whether to run validation commands proactively. In the absence of behavioral guidance: + +- When running in non-interactive approval modes like **never** or **on-failure**, proactively run tests, lint and do whatever you need to ensure you've completed the task. +- When working in interactive approval modes like **untrusted**, or **on-request**, hold off on running tests or lint commands until the user is ready for you to finalize your output, because these commands take time to run and slow down iteration. Instead suggest what you want to do next, and let the user confirm first. +- When working on test-related tasks, such as adding tests, fixing tests, or reproducing a bug to verify behavior, you may proactively run tests regardless of approval mode. Use your judgement to decide whether this is a test-related task. + +## Ambition vs. precision + +For tasks that have no prior context (i.e. the user is starting something brand new), you should feel free to be ambitious and demonstrate creativity with your implementation. + +If you're operating in an existing codebase, you should make sure you do exactly what the user asks with surgical precision. Treat the surrounding codebase with respect, and don't overstep (i.e. changing filenames or variables unnecessarily). You should balance being sufficiently ambitious and proactive when completing tasks of this nature. + +You should use judicious initiative to decide on the right level of detail and complexity to deliver based on the user's needs. This means showing good judgment that you're capable of doing the right extras without gold-plating. This might be demonstrated by high-value, creative touches when scope of the task is vague; while being surgical and targeted when scope is tightly specified. + +## Sharing progress updates + +For especially longer tasks that you work on (i.e. requiring many tool calls, or a plan with multiple steps), you should provide progress updates back to the user at reasonable intervals. These updates should be structured as a concise sentence or two (no more than 8-10 words long) recapping progress so far in plain language: this update demonstrates your understanding of what needs to be done, progress so far (i.e. files explores, subtasks complete), and where you're going next. + +Before doing large chunks of work that may incur latency as experienced by the user (i.e. writing a new file), you should send a concise message to the user with an update indicating what you're about to do to ensure they know what you're spending time on. Don't start editing or writing large files before informing the user what you are doing and why. + +The messages you send before tool calls should describe what is immediately about to be done next in very concise language. If there was previous work done, this preamble message should also include a note about the work done so far to bring the user along. + +## Presenting your work and final message + +Your final message should read naturally, like an update from a concise teammate. For casual conversation, brainstorming tasks, or quick questions from the user, respond in a friendly, conversational tone. You should ask questions, suggest ideas, and adapt to the user’s style. If you've finished a large amount of work, when describing what you've done to the user, you should follow the final answer formatting guidelines to communicate substantive changes. You don't need to add structured formatting for one-word answers, greetings, or purely conversational exchanges. + +You can skip heavy formatting for single, simple actions or confirmations. In these cases, respond in plain sentences with any relevant next step or quick option. Reserve multi-section structured responses for results that need grouping or explanation. + +The user is working on the same computer as you, and has access to your work. As such there's no need to show the full contents of large files you have already written unless the user explicitly asks for them. Similarly, if you've created or modified files using `apply_patch`, there's no need to tell users to "save the file" or "copy the code into a file"—just reference the file path. + +If there's something that you think you could help with as a logical next step, concisely ask the user if they want you to do so. Good examples of this are running tests, committing changes, or building out the next logical component. If there’s something that you couldn't do (even with approval) but that the user might want to do (such as verifying changes by running the app), include those instructions succinctly. + +Brevity is very important as a default. You should be very concise (i.e. no more than 10 lines), but can relax this requirement for tasks where additional detail and comprehensiveness is important for the user's understanding. + +### Final answer structure and style guidelines + +You are producing plain text that will later be styled by the CLI. Follow these rules exactly. Formatting should make results easy to scan, but not feel mechanical. Use judgment to decide how much structure adds value. + +**Section Headers** + +- Use only when they improve clarity — they are not mandatory for every answer. +- Choose descriptive names that fit the content +- Keep headers short (1–3 words) and in `**Title Case**`. Always start headers with `**` and end with `**` +- Leave no blank line before the first bullet under a header. +- Section headers should only be used where they genuinely improve scanability; avoid fragmenting the answer. + +**Bullets** + +- Use `-` followed by a space for every bullet. +- Merge related points when possible; avoid a bullet for every trivial detail. +- Keep bullets to one line unless breaking for clarity is unavoidable. +- Group into short lists (4–6 bullets) ordered by importance. +- Use consistent keyword phrasing and formatting across sections. + +**Monospace** + +- Wrap all commands, file paths, env vars, and code identifiers in backticks (`` `...` ``). +- Apply to inline examples and to bullet keywords if the keyword itself is a literal file/command. +- Never mix monospace and bold markers; choose one based on whether it’s a keyword (`**`) or inline code/path (`` ` ``). + +**File References** +When referencing files in your response, make sure to include the relevant start line and always follow the below rules: + * Use inline code to make file paths clickable. + * Each reference should have a stand alone path. Even if it's the same file. + * Accepted: absolute, workspace‑relative, a/ or b/ diff prefixes, or bare filename/suffix. + * Line/column (1‑based, optional): :line[:column] or #Lline[Ccolumn] (column defaults to 1). + * Do not use URIs like file://, vscode://, or https://. + * Do not provide range of lines + * Examples: src/app.ts, src/app.ts:42, b/server/index.js#L10, C:\repo\project\main.rs:12:5 + +**Structure** + +- Place related bullets together; don’t mix unrelated concepts in the same section. +- Order sections from general → specific → supporting info. +- For subsections (e.g., “Binaries” under “Rust Workspace”), introduce with a bolded keyword bullet, then list items under it. +- Match structure to complexity: + - Multi-part or detailed results → use clear headers and grouped bullets. + - Simple results → minimal headers, possibly just a short list or paragraph. + +**Tone** + +- Keep the voice collaborative and natural, like a coding partner handing off work. +- Be concise and factual — no filler or conversational commentary and avoid unnecessary repetition +- Use present tense and active voice (e.g., “Runs tests” not “This will run tests”). +- Keep descriptions self-contained; don’t refer to “above” or “below”. +- Use parallel structure in lists for consistency. + +**Don’t** + +- Don’t use literal words “bold” or “monospace” in the content. +- Don’t nest bullets or create deep hierarchies. +- Don’t output ANSI escape codes directly — the CLI renderer applies them. +- Don’t cram unrelated keywords into a single bullet; split for clarity. +- Don’t let keyword lists run long — wrap or reformat for scanability. + +Generally, ensure your final answers adapt their shape and depth to the request. For example, answers to code explanations should have a precise, structured explanation with code references that answer the question directly. For tasks with a simple implementation, lead with the outcome and supplement only with what’s needed for clarity. Larger changes can be presented as a logical walkthrough of your approach, grouping related steps, explaining rationale where it adds value, and highlighting next actions to accelerate the user. Your answers should provide the right level of detail while being easily scannable. + +For casual greetings, acknowledgements, or other one-off conversational messages that are not delivering substantive information or structured results, respond naturally without section headers or bullet formatting. + +# Tool Guidelines + +## Shell commands + +When using the shell, you must adhere to the following guidelines: + +- When searching for text or files, prefer using `rg` or `rg --files` respectively because `rg` is much faster than alternatives like `grep`. (If the `rg` command is not found, then use alternatives.) +- Do not use python scripts to attempt to output larger chunks of a file. + +## `update_plan` + +A tool named `update_plan` is available to you. You can use it to keep an up‑to‑date, step‑by‑step plan for the task. + +To create a new plan, call `update_plan` with a short list of 1‑sentence steps (no more than 5-7 words each) with a `status` for each step (`pending`, `in_progress`, or `completed`). + +When steps have been completed, use `update_plan` to mark each finished step as `completed` and the next step you are working on as `in_progress`. There should always be exactly one `in_progress` step until everything is done. You can mark multiple items as complete in a single `update_plan` call. + +If all steps are complete, ensure you call `update_plan` to mark all steps as `completed`. + +## `apply_patch` + +Use the `apply_patch` shell command to edit files. +Your patch language is a stripped‑down, file‑oriented diff format designed to be easy to parse and safe to apply. You can think of it as a high‑level envelope: + +*** Begin Patch +[ one or more file sections ] +*** End Patch + +Within that envelope, you get a sequence of file operations. +You MUST include a header to specify the action you are taking. +Each operation starts with one of three headers: + +*** Add File: - create a new file. Every following line is a + line (the initial contents). +*** Delete File: - remove an existing file. Nothing follows. +*** Update File: - patch an existing file in place (optionally with a rename). + +May be immediately followed by *** Move to: if you want to rename the file. +Then one or more “hunks”, each introduced by @@ (optionally followed by a hunk header). +Within a hunk each line starts with: + +For instructions on [context_before] and [context_after]: +- By default, show 3 lines of code immediately above and 3 lines immediately below each change. If a change is within 3 lines of a previous change, do NOT duplicate the first change’s [context_after] lines in the second change’s [context_before] lines. +- If 3 lines of context is insufficient to uniquely identify the snippet of code within the file, use the @@ operator to indicate the class or function to which the snippet belongs. For instance, we might have: +@@ class BaseClass +[3 lines of pre-context] +- [old_code] ++ [new_code] +[3 lines of post-context] + +- If a code block is repeated so many times in a class or function such that even a single `@@` statement and 3 lines of context cannot uniquely identify the snippet of code, you can use multiple `@@` statements to jump to the right context. For instance: + +@@ class BaseClass +@@ def method(): +[3 lines of pre-context] +- [old_code] ++ [new_code] +[3 lines of post-context] + +The full grammar definition is below: +Patch := Begin { FileOp } End +Begin := "*** Begin Patch" NEWLINE +End := "*** End Patch" NEWLINE +FileOp := AddFile | DeleteFile | UpdateFile +AddFile := "*** Add File: " path NEWLINE { "+" line NEWLINE } +DeleteFile := "*** Delete File: " path NEWLINE +UpdateFile := "*** Update File: " path NEWLINE [ MoveTo ] { Hunk } +MoveTo := "*** Move to: " newPath NEWLINE +Hunk := "@@" [ header ] NEWLINE { HunkLine } [ "*** End of File" NEWLINE ] +HunkLine := (" " | "-" | "+") text NEWLINE + +A full patch can combine several operations: + +*** Begin Patch +*** Add File: hello.txt ++Hello world +*** Update File: src/app.py +*** Move to: src/main.py +@@ def greet(): +-print("Hi") ++print("Hello, world!") +*** Delete File: obsolete.txt +*** End Patch + +It is important to remember: + +- You must include a header with your intended action (Add/Delete/Update) +- You must prefix new lines with `+` even when creating a new file +- File references can only be relative, NEVER ABSOLUTE. + +You can invoke apply_patch like: + +``` +shell {"command":["apply_patch","*** Begin Patch\n*** Add File: hello.txt\n+Hello, world!\n*** End Patch\n"]} +``` diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index 785d0475b7..c43c074c44 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -22,6 +22,7 @@ use codex_otel::otel_manager::OtelManager; use codex_protocol::ThreadId; use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig; use codex_protocol::models::ResponseItem; +use codex_protocol::openai_models::ModelInfo; use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig; use codex_protocol::protocol::SessionSource; use eventsource_stream::Event; @@ -49,7 +50,6 @@ use crate::features::FEATURES; use crate::flags::CODEX_RS_SSE_FIXTURE; use crate::model_provider_info::ModelProviderInfo; use crate::model_provider_info::WireApi; -use crate::models_manager::model_family::ModelFamily; use crate::tools::spec::create_tools_json_for_chat_completions_api; use crate::tools::spec::create_tools_json_for_responses_api; @@ -57,7 +57,7 @@ use crate::tools::spec::create_tools_json_for_responses_api; pub struct ModelClient { config: Arc, auth_manager: Option>, - model_family: ModelFamily, + model_info: ModelInfo, otel_manager: OtelManager, provider: ModelProviderInfo, conversation_id: ThreadId, @@ -71,7 +71,7 @@ impl ModelClient { pub fn new( config: Arc, auth_manager: Option>, - model_family: ModelFamily, + model_info: ModelInfo, otel_manager: OtelManager, provider: ModelProviderInfo, effort: Option, @@ -82,7 +82,7 @@ impl ModelClient { Self { config, auth_manager, - model_family, + model_info, otel_manager, provider, conversation_id, @@ -93,11 +93,11 @@ impl ModelClient { } pub fn get_model_context_window(&self) -> Option { - let model_family = self.get_model_family(); - let effective_context_window_percent = model_family.effective_context_window_percent; - model_family - .context_window - .map(|w| w.saturating_mul(effective_context_window_percent) / 100) + let model_info = self.get_model_info(); + let effective_context_window_percent = model_info.effective_context_window_percent; + model_info.context_window.map(|context_window| { + context_window.saturating_mul(effective_context_window_percent) / 100 + }) } pub fn config(&self) -> Arc { @@ -146,8 +146,8 @@ impl ModelClient { } let auth_manager = self.auth_manager.clone(); - let model_family = self.get_model_family(); - let instructions = prompt.get_full_instructions(&model_family).into_owned(); + let model_info = self.get_model_info(); + let instructions = prompt.get_full_instructions(&model_info).into_owned(); let tools_json = create_tools_json_for_chat_completions_api(&prompt.tools)?; let api_prompt = build_api_prompt(prompt, instructions, tools_json); let conversation_id = self.conversation_id.to_string(); @@ -200,13 +200,14 @@ impl ModelClient { } let auth_manager = self.auth_manager.clone(); - let model_family = self.get_model_family(); - let instructions = prompt.get_full_instructions(&model_family).into_owned(); + let model_info = self.get_model_info(); + let instructions = prompt.get_full_instructions(&model_info).into_owned(); let tools_json: Vec = create_tools_json_for_responses_api(&prompt.tools)?; - let reasoning = if model_family.supports_reasoning_summaries { + let default_reasoning_effort = model_info.default_reasoning_level; + let reasoning = if model_info.supports_reasoning_summaries { Some(Reasoning { - effort: self.effort.or(model_family.default_reasoning_effort), + effort: self.effort.or(default_reasoning_effort), summary: if self.summary == ReasoningSummaryConfig::None { None } else { @@ -223,15 +224,13 @@ impl ModelClient { vec![] }; - let verbosity = if model_family.support_verbosity { - self.config - .model_verbosity - .or(model_family.default_verbosity) + let verbosity = if model_info.support_verbosity { + self.config.model_verbosity.or(model_info.default_verbosity) } else { if self.config.model_verbosity.is_some() { warn!( "model_verbosity is set but ignored as the model does not support verbosity: {}", - model_family.family + model_info.slug ); } None @@ -298,12 +297,11 @@ impl ModelClient { /// Returns the currently configured model slug. pub fn get_model(&self) -> String { - self.get_model_family().get_model_slug().to_string() + self.model_info.slug.clone() } - /// Returns the currently configured model family. - pub fn get_model_family(&self) -> ModelFamily { - self.model_family.clone() + pub fn get_model_info(&self) -> ModelInfo { + self.model_info.clone() } /// Returns the current reasoning effort setting. @@ -340,7 +338,7 @@ impl ModelClient { .with_telemetry(Some(request_telemetry)); let instructions = prompt - .get_full_instructions(&self.get_model_family()) + .get_full_instructions(&self.get_model_info()) .into_owned(); let payload = ApiCompactionInput { model: &self.get_model(), diff --git a/codex-rs/core/src/client_common.rs b/codex-rs/core/src/client_common.rs index 6972ff7cec..7d7cabcfa6 100644 --- a/codex-rs/core/src/client_common.rs +++ b/codex-rs/core/src/client_common.rs @@ -1,15 +1,13 @@ use crate::client_common::tools::ToolSpec; use crate::error::Result; -use crate::models_manager::model_family::ModelFamily; pub use codex_api::common::ResponseEvent; -use codex_apply_patch::APPLY_PATCH_TOOL_INSTRUCTIONS; use codex_protocol::models::ResponseItem; +use codex_protocol::openai_models::ModelInfo; use futures::Stream; use serde::Deserialize; use serde_json::Value; use std::borrow::Cow; use std::collections::HashSet; -use std::ops::Deref; use std::pin::Pin; use std::task::Context; use std::task::Poll; @@ -44,28 +42,12 @@ pub struct Prompt { } impl Prompt { - pub(crate) fn get_full_instructions<'a>(&'a self, model: &'a ModelFamily) -> Cow<'a, str> { - let base = self - .base_instructions_override - .as_deref() - .unwrap_or(model.base_instructions.deref()); - // When there are no custom instructions, add apply_patch_tool_instructions if: - // - the model needs special instructions (4.1) - // AND - // - there is no apply_patch tool present - let is_apply_patch_tool_present = self.tools.iter().any(|tool| match tool { - ToolSpec::Function(f) => f.name == "apply_patch", - ToolSpec::Freeform(f) => f.name == "apply_patch", - _ => false, - }); - if self.base_instructions_override.is_none() - && model.needs_special_apply_patch_instructions - && !is_apply_patch_tool_present - { - Cow::Owned(format!("{base}\n{APPLY_PATCH_TOOL_INSTRUCTIONS}")) - } else { - Cow::Borrowed(base) - } + pub(crate) fn get_full_instructions<'a>(&'a self, model: &'a ModelInfo) -> Cow<'a, str> { + Cow::Borrowed( + self.base_instructions_override + .as_deref() + .unwrap_or(model.base_instructions.as_str()), + ) } pub(crate) fn get_formatted_input(&self) -> Vec { @@ -277,6 +259,8 @@ mod tests { let prompt = Prompt { ..Default::default() }; + let prompt_with_apply_patch_instructions = + include_str!("../prompt_with_apply_patch_instructions.md"); let test_cases = vec![ InstructionsTestCase { slug: "gpt-3.5", @@ -317,19 +301,16 @@ mod tests { ]; for test_case in test_cases { let config = test_config(); - let model_family = - ModelsManager::construct_model_family_offline(test_case.slug, &config); - let expected = if test_case.expects_apply_patch_instructions { - format!( - "{}\n{}", - model_family.clone().base_instructions, - APPLY_PATCH_TOOL_INSTRUCTIONS - ) - } else { - model_family.clone().base_instructions - }; + let model_info = ModelsManager::construct_model_info_offline(test_case.slug, &config); + if test_case.expects_apply_patch_instructions { + assert_eq!( + model_info.base_instructions.as_str(), + prompt_with_apply_patch_instructions + ); + } - let full = prompt.get_full_instructions(&model_family); + let expected = model_info.base_instructions.as_str(); + let full = prompt.get_full_instructions(&model_info); assert_eq!(full, expected); } } diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 828a6bab01..a496d246eb 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -20,7 +20,6 @@ use crate::exec_policy::ExecPolicyManager; use crate::features::Feature; use crate::features::Features; use crate::models_manager::manager::ModelsManager; -use crate::models_manager::model_family::ModelFamily; use crate::parse_command::parse_command; use crate::parse_turn_item; use crate::stream_events_utils::HandleOutputCtx; @@ -35,6 +34,7 @@ use async_channel::Sender; use codex_protocol::ThreadId; use codex_protocol::approvals::ExecPolicyAmendment; use codex_protocol::items::TurnItem; +use codex_protocol::openai_models::ModelInfo; use codex_protocol::protocol::FileChange; use codex_protocol::protocol::HasLegacyEvent; use codex_protocol::protocol::ItemCompletedEvent; @@ -508,20 +508,20 @@ impl Session { provider: ModelProviderInfo, session_configuration: &SessionConfiguration, per_turn_config: Config, - model_family: ModelFamily, + model_info: ModelInfo, conversation_id: ThreadId, sub_id: String, ) -> TurnContext { let otel_manager = otel_manager.clone().with_model( session_configuration.model.as_str(), - model_family.get_model_slug(), + model_info.slug.as_str(), ); let per_turn_config = Arc::new(per_turn_config); let client = ModelClient::new( per_turn_config.clone(), auth_manager, - model_family.clone(), + model_info.clone(), otel_manager, provider, session_configuration.model_reasoning_effort, @@ -531,7 +531,7 @@ impl Session { ); let tools_config = ToolsConfig::new(&ToolsConfigParams { - model_family: &model_family, + model_info: &model_info, features: &per_turn_config.features, }); @@ -553,7 +553,7 @@ impl Session { tool_call_gate: Arc::new(ReadinessFlag::new()), truncation_policy: TruncationPolicy::new( per_turn_config.as_ref(), - model_family.truncation_policy, + model_info.truncation_policy.into(), ), } } @@ -944,10 +944,10 @@ impl Session { } } - let model_family = self + let model_info = self .services .models_manager - .construct_model_family(session_configuration.model.as_str(), &per_turn_config) + .construct_model_info(session_configuration.model.as_str(), &per_turn_config) .await; let mut turn_context: TurnContext = Self::make_turn_context( Some(Arc::clone(&self.services.auth_manager)), @@ -955,7 +955,7 @@ impl Session { session_configuration.provider.clone(), &session_configuration, per_turn_config, - model_family, + model_info, self.conversation_id, sub_id, ); @@ -1448,14 +1448,11 @@ impl Session { } pub(crate) async fn set_total_tokens_full(&self, turn_context: &TurnContext) { - let context_window = turn_context.client.get_model_context_window(); - if let Some(context_window) = context_window { - { - let mut state = self.state.lock().await; - state.set_token_usage_full(context_window); - } - self.send_token_count_event(turn_context).await; + if let Some(context_window) = turn_context.client.get_model_context_window() { + let mut state = self.state.lock().await; + state.set_token_usage_full(context_window); } + self.send_token_count_event(turn_context).await; } pub(crate) async fn record_response_item_and_emit_turn_item( @@ -2207,10 +2204,10 @@ async fn spawn_review_thread( resolved: crate::review_prompts::ResolvedReviewRequest, ) { let model = config.review_model.clone(); - let review_model_family = sess + let review_model_info = sess .services .models_manager - .construct_model_family(&model, &config) + .construct_model_info(&model, &config) .await; // For reviews, disable web_search and view_image regardless of global settings. let mut review_features = sess.features.clone(); @@ -2219,7 +2216,7 @@ async fn spawn_review_thread( .disable(crate::features::Feature::WebSearchCached) .disable(crate::features::Feature::ViewImageTool); let tools_config = ToolsConfig::new(&ToolsConfigParams { - model_family: &review_model_family, + model_info: &review_model_info, features: &review_features, }); @@ -2227,7 +2224,7 @@ async fn spawn_review_thread( let review_prompt = resolved.prompt.clone(); let provider = parent_turn_context.client.get_provider(); let auth_manager = parent_turn_context.client.get_auth_manager(); - let model_family = review_model_family.clone(); + let model_info = review_model_info.clone(); // Build per‑turn client with the requested model/family. let mut per_turn_config = (*config).clone(); @@ -2237,14 +2234,14 @@ async fn spawn_review_thread( let otel_manager = parent_turn_context.client.get_otel_manager().with_model( config.review_model.as_str(), - review_model_family.slug.as_str(), + review_model_info.slug.as_str(), ); let per_turn_config = Arc::new(per_turn_config); let client = ModelClient::new( per_turn_config.clone(), auth_manager, - model_family.clone(), + model_info.clone(), otel_manager, provider, per_turn_config.model_reasoning_effort, @@ -2269,7 +2266,10 @@ async fn spawn_review_thread( final_output_json_schema: None, codex_linux_sandbox_exe: parent_turn_context.codex_linux_sandbox_exe.clone(), tool_call_gate: Arc::new(ReadinessFlag::new()), - truncation_policy: TruncationPolicy::new(&per_turn_config, model_family.truncation_policy), + truncation_policy: TruncationPolicy::new( + &per_turn_config, + model_info.truncation_policy.into(), + ), }; // Seed the child task with the review prompt as the initial user message. @@ -2335,11 +2335,8 @@ pub(crate) async fn run_task( return None; } - let auto_compact_limit = turn_context - .client - .get_model_family() - .auto_compact_token_limit() - .unwrap_or(i64::MAX); + let model_info = turn_context.client.get_model_info(); + let auto_compact_limit = model_info.auto_compact_token_limit().unwrap_or(i64::MAX); let total_usage_tokens = sess.get_total_token_usage().await; if total_usage_tokens >= auto_compact_limit { run_auto_compact(&sess, &turn_context).await; @@ -2517,7 +2514,7 @@ async fn run_turn( let model_supports_parallel = turn_context .client - .get_model_family() + .get_model_info() .supports_parallel_tool_calls; let prompt = Prompt { @@ -3459,13 +3456,13 @@ mod tests { fn otel_manager( conversation_id: ThreadId, config: &Config, - model_family: &ModelFamily, + model_info: &ModelInfo, session_source: SessionSource, ) -> OtelManager { OtelManager::new( conversation_id, ModelsManager::get_model_offline(config.model.as_deref()).as_str(), - model_family.slug.as_str(), + model_info.slug.as_str(), None, Some("test@test.com".to_string()), Some(AuthMode::ChatGPT), @@ -3504,14 +3501,14 @@ mod tests { session_source: SessionSource::Exec, }; let per_turn_config = Session::build_per_turn_config(&session_configuration); - let model_family = ModelsManager::construct_model_family_offline( + let model_info = ModelsManager::construct_model_info_offline( session_configuration.model.as_str(), &per_turn_config, ); let otel_manager = otel_manager( conversation_id, config.as_ref(), - &model_family, + &model_info, session_configuration.session_source.clone(), ); @@ -3541,7 +3538,7 @@ mod tests { session_configuration.provider.clone(), &session_configuration, per_turn_config, - model_family, + model_info, conversation_id, "turn_id".to_string(), ); @@ -3595,14 +3592,14 @@ mod tests { session_source: SessionSource::Exec, }; let per_turn_config = Session::build_per_turn_config(&session_configuration); - let model_family = ModelsManager::construct_model_family_offline( + let model_info = ModelsManager::construct_model_info_offline( session_configuration.model.as_str(), &per_turn_config, ); let otel_manager = otel_manager( conversation_id, config.as_ref(), - &model_family, + &model_info, session_configuration.session_source.clone(), ); @@ -3632,7 +3629,7 @@ mod tests { session_configuration.provider.clone(), &session_configuration, per_turn_config, - model_family, + model_info, conversation_id, "turn_id".to_string(), )); diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 035794f4aa..e2c3bb252d 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -268,7 +268,7 @@ pub struct Config { /// Additional filenames to try when looking for project-level docs. pub project_doc_fallback_filenames: Vec, - // todo(aibrahim): this should be used in the override model family + // todo(aibrahim): this should be used in the override model info /// Token budget applied when storing tool/function outputs in the context manager. pub tool_output_token_limit: Option, @@ -316,7 +316,7 @@ pub struct Config { /// Include the `apply_patch` tool for models that benefit from invoking /// file edits as a structured tool call. When unset, this falls back to the - /// model family's default preference. + /// model info's default preference. pub include_apply_patch_tool: bool, pub tools_web_search_request: bool, diff --git a/codex-rs/core/src/context_manager/history.rs b/codex-rs/core/src/context_manager/history.rs index 8dc55187a5..8f16f731f4 100644 --- a/codex-rs/core/src/context_manager/history.rs +++ b/codex-rs/core/src/context_manager/history.rs @@ -83,10 +83,9 @@ impl ContextManager { // Estimate token usage using byte-based heuristics from the truncation helpers. // This is a coarse lower bound, not a tokenizer-accurate count. pub(crate) fn estimate_token_count(&self, turn_context: &TurnContext) -> Option { - let model_family = turn_context.client.get_model_family(); - let base_tokens = - i64::try_from(approx_token_count(model_family.base_instructions.as_str())) - .unwrap_or(i64::MAX); + let model_info = turn_context.client.get_model_info(); + let base_instructions = model_info.base_instructions.as_str(); + let base_tokens = i64::try_from(approx_token_count(base_instructions)).unwrap_or(i64::MAX); let items_tokens = self.items.iter().fold(0i64, |acc, item| { acc + match item { diff --git a/codex-rs/core/src/models_manager/manager.rs b/codex-rs/core/src/models_manager/manager.rs index 29be88735b..c9185f0ff0 100644 --- a/codex-rs/core/src/models_manager/manager.rs +++ b/codex-rs/core/src/models_manager/manager.rs @@ -24,7 +24,7 @@ use crate::default_client::build_reqwest_client; use crate::error::Result as CoreResult; use crate::features::Feature; use crate::model_provider_info::ModelProviderInfo; -use crate::models_manager::model_family::ModelFamily; +use crate::models_manager::model_info; use crate::models_manager::model_presets::builtin_model_presets; const MODEL_CACHE_FILE: &str = "models_cache.json"; @@ -36,7 +36,6 @@ const CODEX_AUTO_BALANCED_MODEL: &str = "codex-auto-balanced"; /// Coordinates remote model discovery plus cached metadata on disk. #[derive(Debug)] pub struct ModelsManager { - // todo(aibrahim) merge available_models and model family creation into one struct local_models: Vec, remote_models: RwLock>, auth_manager: Arc, @@ -128,15 +127,19 @@ impl ModelsManager { Ok(self.build_available_models(remote_models)) } - fn find_family_for_model(slug: &str) -> ModelFamily { - super::model_family::find_family_for_model(slug) - } - - /// Look up the requested model family while applying remote metadata overrides. - pub async fn construct_model_family(&self, model: &str, config: &Config) -> ModelFamily { - Self::find_family_for_model(model) - .with_remote_overrides(self.remote_models(config).await) - .with_config_overrides(config) + /// Look up the requested model metadata while applying remote metadata overrides. + pub async fn construct_model_info(&self, model: &str, config: &Config) -> ModelInfo { + let remote = self + .remote_models(config) + .await + .into_iter() + .find(|m| m.slug == model); + let model = if let Some(remote) = remote { + remote + } else { + model_info::find_model_info_for_slug(model) + }; + model_info::with_config_overrides(model, config) } pub async fn get_model(&self, model: &Option, config: &Config) -> String { @@ -180,9 +183,9 @@ impl ModelsManager { } #[cfg(any(test, feature = "test-support"))] - /// Offline helper that builds a `ModelFamily` without consulting remote state. - pub fn construct_model_family_offline(model: &str, config: &Config) -> ModelFamily { - Self::find_family_for_model(model).with_config_overrides(config) + /// Offline helper that builds a `ModelInfo` without consulting remote state. + pub fn construct_model_info_offline(model: &str, config: &Config) -> ModelInfo { + model_info::with_config_overrides(model_info::find_model_info_for_slug(model), config) } async fn get_etag(&self) -> Option { @@ -363,14 +366,14 @@ mod tests { "supported_in_api": true, "priority": priority, "upgrade": null, - "base_instructions": null, + "base_instructions": "base instructions", "supports_reasoning_summaries": false, "support_verbosity": false, "default_verbosity": null, "apply_patch_tool_type": null, "truncation_policy": {"mode": "bytes", "limit": 10_000}, "supports_parallel_tool_calls": false, - "context_window": null, + "context_window": 272_000, "experimental_supported_tools": [], })) .expect("valid model") diff --git a/codex-rs/core/src/models_manager/mod.rs b/codex-rs/core/src/models_manager/mod.rs index 83ed30e872..d0e3c8214a 100644 --- a/codex-rs/core/src/models_manager/mod.rs +++ b/codex-rs/core/src/models_manager/mod.rs @@ -1,4 +1,4 @@ pub mod cache; pub mod manager; -pub mod model_family; +pub mod model_info; pub mod model_presets; diff --git a/codex-rs/core/src/models_manager/model_family.rs b/codex-rs/core/src/models_manager/model_family.rs deleted file mode 100644 index 9a6904cea1..0000000000 --- a/codex-rs/core/src/models_manager/model_family.rs +++ /dev/null @@ -1,557 +0,0 @@ -use codex_protocol::config_types::Verbosity; -use codex_protocol::openai_models::ApplyPatchToolType; -use codex_protocol::openai_models::ConfigShellToolType; -use codex_protocol::openai_models::ModelInfo; -use codex_protocol::openai_models::ReasoningEffort; - -use crate::config::Config; -use crate::truncate::TruncationPolicy; - -/// The `instructions` field in the payload sent to a model should always start -/// with this content. -const BASE_INSTRUCTIONS: &str = include_str!("../../prompt.md"); - -const GPT_5_CODEX_INSTRUCTIONS: &str = include_str!("../../gpt_5_codex_prompt.md"); -const GPT_5_1_INSTRUCTIONS: &str = include_str!("../../gpt_5_1_prompt.md"); -const GPT_5_2_INSTRUCTIONS: &str = include_str!("../../gpt_5_2_prompt.md"); -const GPT_5_1_CODEX_MAX_INSTRUCTIONS: &str = include_str!("../../gpt-5.1-codex-max_prompt.md"); -const GPT_5_2_CODEX_INSTRUCTIONS: &str = include_str!("../../gpt-5.2-codex_prompt.md"); -pub(crate) const CONTEXT_WINDOW_272K: i64 = 272_000; - -/// A model family is a group of models that share certain characteristics. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct ModelFamily { - /// The full model slug used to derive this model family, e.g. - /// "gpt-4.1-2025-04-14". - pub slug: String, - - /// The model family name, e.g. "gpt-4.1". This string is used when deriving - /// default metadata for the family, such as context windows. - pub family: String, - - /// True if the model needs additional instructions on how to use the - /// "virtual" `apply_patch` CLI. - pub needs_special_apply_patch_instructions: bool, - - /// Maximum supported context window, if known. - pub context_window: Option, - - /// Token threshold for automatic compaction if config does not override it. - auto_compact_token_limit: Option, - - // Whether the `reasoning` field can be set when making a request to this - // model family. Note it has `effort` and `summary` subfields (though - // `summary` is optional). - pub supports_reasoning_summaries: bool, - - // The reasoning effort to use for this model family when none is explicitly chosen. - pub default_reasoning_effort: Option, - - /// Whether this model supports parallel tool calls when using the - /// Responses API. - pub supports_parallel_tool_calls: bool, - - /// Present if the model performs better when `apply_patch` is provided as - /// a tool call instead of just a bash command - pub apply_patch_tool_type: Option, - - // Instructions to use for querying the model - pub base_instructions: String, - - /// Names of beta tools that should be exposed to this model family. - pub experimental_supported_tools: Vec, - - /// Percentage of the context window considered usable for inputs, after - /// reserving headroom for system prompts, tool overhead, and model output. - /// This is applied when computing the effective context window seen by - /// consumers. - pub effective_context_window_percent: i64, - - /// If the model family supports setting the verbosity level when using Responses API. - pub support_verbosity: bool, - - // The default verbosity level for this model family when using Responses API. - pub default_verbosity: Option, - - /// Preferred shell tool type for this model family when features do not override it. - pub shell_type: ConfigShellToolType, - - pub truncation_policy: TruncationPolicy, -} - -impl ModelFamily { - pub(super) fn with_config_overrides(mut self, config: &Config) -> Self { - if let Some(supports_reasoning_summaries) = config.model_supports_reasoning_summaries { - self.supports_reasoning_summaries = supports_reasoning_summaries; - } - if let Some(context_window) = config.model_context_window { - self.context_window = Some(context_window); - } - if let Some(auto_compact_token_limit) = config.model_auto_compact_token_limit { - self.auto_compact_token_limit = Some(auto_compact_token_limit); - } - self - } - pub(super) fn with_remote_overrides(mut self, remote_models: Vec) -> Self { - for model in remote_models { - if model.slug == self.slug { - self.apply_remote_overrides(model); - } - } - self - } - - fn apply_remote_overrides(&mut self, model: ModelInfo) { - let ModelInfo { - slug: _, - display_name: _, - description: _, - default_reasoning_level, - supported_reasoning_levels: _, - shell_type, - visibility: _, - supported_in_api: _, - priority: _, - upgrade: _, - base_instructions, - supports_reasoning_summaries, - support_verbosity, - default_verbosity, - apply_patch_tool_type, - truncation_policy, - supports_parallel_tool_calls, - context_window, - experimental_supported_tools, - } = model; - - self.default_reasoning_effort = Some(default_reasoning_level); - self.shell_type = shell_type; - if let Some(base) = base_instructions { - self.base_instructions = base; - } - self.supports_reasoning_summaries = supports_reasoning_summaries; - self.support_verbosity = support_verbosity; - self.default_verbosity = default_verbosity; - self.apply_patch_tool_type = apply_patch_tool_type; - self.truncation_policy = truncation_policy.into(); - self.supports_parallel_tool_calls = supports_parallel_tool_calls; - self.context_window = context_window; - self.experimental_supported_tools = experimental_supported_tools; - } - - pub fn auto_compact_token_limit(&self) -> Option { - self.auto_compact_token_limit - .or(self.context_window.map(Self::default_auto_compact_limit)) - } - - const fn default_auto_compact_limit(context_window: i64) -> i64 { - (context_window * 9) / 10 - } - - pub fn get_model_slug(&self) -> &str { - &self.slug - } -} - -macro_rules! model_family { - ( - $slug:expr, $family:expr $(, $key:ident : $value:expr )* $(,)? - ) => {{ - // defaults - #[allow(unused_mut)] - let mut mf = ModelFamily { - slug: $slug.to_string(), - family: $family.to_string(), - needs_special_apply_patch_instructions: false, - context_window: Some(CONTEXT_WINDOW_272K), - auto_compact_token_limit: None, - supports_reasoning_summaries: false, - supports_parallel_tool_calls: false, - apply_patch_tool_type: None, - base_instructions: BASE_INSTRUCTIONS.to_string(), - experimental_supported_tools: Vec::new(), - effective_context_window_percent: 95, - support_verbosity: false, - shell_type: ConfigShellToolType::Default, - default_verbosity: None, - default_reasoning_effort: None, - truncation_policy: TruncationPolicy::Bytes(10_000), - }; - - // apply overrides - $( - mf.$key = $value; - )* - mf - }}; -} - -/// Internal offline helper for `ModelsManager` that returns a `ModelFamily` for the given -/// model slug. -#[allow(clippy::if_same_then_else)] -pub(super) fn find_family_for_model(slug: &str) -> ModelFamily { - if slug.starts_with("o3") { - model_family!( - slug, "o3", - supports_reasoning_summaries: true, - needs_special_apply_patch_instructions: true, - context_window: Some(200_000), - ) - } else if slug.starts_with("o4-mini") { - model_family!( - slug, "o4-mini", - supports_reasoning_summaries: true, - needs_special_apply_patch_instructions: true, - context_window: Some(200_000), - ) - } else if slug.starts_with("codex-mini-latest") { - model_family!( - slug, "codex-mini-latest", - supports_reasoning_summaries: true, - needs_special_apply_patch_instructions: true, - shell_type: ConfigShellToolType::Local, - context_window: Some(200_000), - ) - } else if slug.starts_with("gpt-4.1") { - model_family!( - slug, "gpt-4.1", - needs_special_apply_patch_instructions: true, - context_window: Some(1_047_576), - ) - } else if slug.starts_with("gpt-oss") || slug.starts_with("openai/gpt-oss") { - model_family!( - slug, "gpt-oss", - apply_patch_tool_type: Some(ApplyPatchToolType::Function), - context_window: Some(96_000), - ) - } else if slug.starts_with("gpt-4o") { - model_family!( - slug, "gpt-4o", - needs_special_apply_patch_instructions: true, - context_window: Some(128_000), - ) - } else if slug.starts_with("gpt-3.5") { - model_family!( - slug, "gpt-3.5", - needs_special_apply_patch_instructions: true, - context_window: Some(16_385), - ) - } else if slug.starts_with("test-gpt-5") { - model_family!( - slug, slug, - supports_reasoning_summaries: true, - base_instructions: GPT_5_CODEX_INSTRUCTIONS.to_string(), - experimental_supported_tools: vec![ - "grep_files".to_string(), - "list_dir".to_string(), - "read_file".to_string(), - "test_sync_tool".to_string(), - ], - supports_parallel_tool_calls: true, - shell_type: ConfigShellToolType::ShellCommand, - support_verbosity: true, - truncation_policy: TruncationPolicy::Tokens(10_000), - ) - - // Experimental models. - } else if slug.starts_with("exp-codex") || slug.starts_with("codex-1p") { - // Same as gpt-5.1-codex-max. - model_family!( - slug, slug, - supports_reasoning_summaries: true, - base_instructions: GPT_5_2_CODEX_INSTRUCTIONS.to_string(), - apply_patch_tool_type: Some(ApplyPatchToolType::Freeform), - shell_type: ConfigShellToolType::ShellCommand, - supports_parallel_tool_calls: true, - support_verbosity: false, - truncation_policy: TruncationPolicy::Tokens(10_000), - context_window: Some(CONTEXT_WINDOW_272K), - ) - } else if slug.starts_with("exp-") { - model_family!( - slug, slug, - supports_reasoning_summaries: true, - apply_patch_tool_type: Some(ApplyPatchToolType::Freeform), - support_verbosity: true, - default_verbosity: Some(Verbosity::Low), - base_instructions: BASE_INSTRUCTIONS.to_string(), - default_reasoning_effort: Some(ReasoningEffort::Medium), - truncation_policy: TruncationPolicy::Bytes(10_000), - shell_type: ConfigShellToolType::UnifiedExec, - supports_parallel_tool_calls: true, - context_window: Some(CONTEXT_WINDOW_272K), - ) - - // Production models. - } else if slug.starts_with("gpt-5.2-codex") { - model_family!( - slug, slug, - supports_reasoning_summaries: true, - base_instructions: GPT_5_2_CODEX_INSTRUCTIONS.to_string(), - apply_patch_tool_type: Some(ApplyPatchToolType::Freeform), - shell_type: ConfigShellToolType::ShellCommand, - supports_parallel_tool_calls: true, - support_verbosity: false, - truncation_policy: TruncationPolicy::Tokens(10_000), - context_window: Some(CONTEXT_WINDOW_272K), - ) - } else if slug.starts_with("bengalfox") { - model_family!( - slug, slug, - supports_reasoning_summaries: true, - base_instructions: GPT_5_2_CODEX_INSTRUCTIONS.to_string(), - apply_patch_tool_type: Some(ApplyPatchToolType::Freeform), - shell_type: ConfigShellToolType::ShellCommand, - supports_parallel_tool_calls: true, - support_verbosity: false, - truncation_policy: TruncationPolicy::Tokens(10_000), - context_window: Some(CONTEXT_WINDOW_272K), - ) - } else if slug.starts_with("gpt-5.1-codex-max") { - model_family!( - slug, slug, - supports_reasoning_summaries: true, - base_instructions: GPT_5_1_CODEX_MAX_INSTRUCTIONS.to_string(), - apply_patch_tool_type: Some(ApplyPatchToolType::Freeform), - shell_type: ConfigShellToolType::ShellCommand, - supports_parallel_tool_calls: false, - support_verbosity: false, - truncation_policy: TruncationPolicy::Tokens(10_000), - context_window: Some(CONTEXT_WINDOW_272K), - ) - } else if slug.starts_with("gpt-5-codex") - || slug.starts_with("gpt-5.1-codex") - || slug.starts_with("codex-") - { - model_family!( - slug, slug, - supports_reasoning_summaries: true, - base_instructions: GPT_5_CODEX_INSTRUCTIONS.to_string(), - apply_patch_tool_type: Some(ApplyPatchToolType::Freeform), - shell_type: ConfigShellToolType::ShellCommand, - supports_parallel_tool_calls: false, - support_verbosity: false, - truncation_policy: TruncationPolicy::Tokens(10_000), - context_window: Some(CONTEXT_WINDOW_272K), - ) - } else if slug.starts_with("gpt-5.2") { - model_family!( - slug, slug, - supports_reasoning_summaries: true, - apply_patch_tool_type: Some(ApplyPatchToolType::Freeform), - support_verbosity: true, - default_verbosity: Some(Verbosity::Low), - base_instructions: GPT_5_2_INSTRUCTIONS.to_string(), - default_reasoning_effort: Some(ReasoningEffort::Medium), - truncation_policy: TruncationPolicy::Bytes(10_000), - shell_type: ConfigShellToolType::ShellCommand, - supports_parallel_tool_calls: true, - context_window: Some(CONTEXT_WINDOW_272K), - ) - } else if slug.starts_with("boomslang") { - model_family!( - slug, slug, - supports_reasoning_summaries: true, - apply_patch_tool_type: Some(ApplyPatchToolType::Freeform), - support_verbosity: true, - default_verbosity: Some(Verbosity::Low), - base_instructions: GPT_5_2_INSTRUCTIONS.to_string(), - default_reasoning_effort: Some(ReasoningEffort::Medium), - truncation_policy: TruncationPolicy::Bytes(10_000), - shell_type: ConfigShellToolType::ShellCommand, - supports_parallel_tool_calls: true, - context_window: Some(CONTEXT_WINDOW_272K), - ) - } else if slug.starts_with("gpt-5.1") { - model_family!( - slug, "gpt-5.1", - supports_reasoning_summaries: true, - apply_patch_tool_type: Some(ApplyPatchToolType::Freeform), - support_verbosity: true, - default_verbosity: Some(Verbosity::Low), - base_instructions: GPT_5_1_INSTRUCTIONS.to_string(), - default_reasoning_effort: Some(ReasoningEffort::Medium), - truncation_policy: TruncationPolicy::Bytes(10_000), - shell_type: ConfigShellToolType::ShellCommand, - supports_parallel_tool_calls: true, - context_window: Some(CONTEXT_WINDOW_272K), - ) - } else if slug.starts_with("gpt-5") { - model_family!( - slug, "gpt-5", - supports_reasoning_summaries: true, - needs_special_apply_patch_instructions: true, - shell_type: ConfigShellToolType::Default, - support_verbosity: true, - truncation_policy: TruncationPolicy::Bytes(10_000), - context_window: Some(CONTEXT_WINDOW_272K), - ) - } else { - derive_default_model_family(slug) - } -} - -fn derive_default_model_family(model: &str) -> ModelFamily { - tracing::warn!("Unknown model {model} is used. This will degrade the performance of Codex."); - ModelFamily { - slug: model.to_string(), - family: model.to_string(), - needs_special_apply_patch_instructions: false, - context_window: None, - auto_compact_token_limit: None, - supports_reasoning_summaries: false, - supports_parallel_tool_calls: false, - apply_patch_tool_type: None, - base_instructions: BASE_INSTRUCTIONS.to_string(), - experimental_supported_tools: Vec::new(), - effective_context_window_percent: 95, - support_verbosity: false, - shell_type: ConfigShellToolType::Default, - default_verbosity: None, - default_reasoning_effort: None, - truncation_policy: TruncationPolicy::Bytes(10_000), - } -} - -#[cfg(test)] -mod tests { - use super::*; - use codex_protocol::openai_models::ModelVisibility; - use codex_protocol::openai_models::ReasoningEffortPreset; - use codex_protocol::openai_models::TruncationPolicyConfig; - - fn remote(slug: &str, effort: ReasoningEffort, shell: ConfigShellToolType) -> ModelInfo { - ModelInfo { - slug: slug.to_string(), - display_name: slug.to_string(), - description: Some(format!("{slug} desc")), - default_reasoning_level: effort, - supported_reasoning_levels: vec![ReasoningEffortPreset { - effort, - description: effort.to_string(), - }], - shell_type: shell, - visibility: ModelVisibility::List, - supported_in_api: true, - priority: 1, - upgrade: None, - base_instructions: None, - supports_reasoning_summaries: false, - support_verbosity: false, - default_verbosity: None, - apply_patch_tool_type: None, - truncation_policy: TruncationPolicyConfig::bytes(10_000), - supports_parallel_tool_calls: false, - context_window: None, - experimental_supported_tools: Vec::new(), - } - } - - #[test] - fn remote_overrides_apply_when_slug_matches() { - let family = model_family!("gpt-4o-mini", "gpt-4o-mini"); - assert_ne!(family.default_reasoning_effort, Some(ReasoningEffort::High)); - - let updated = family.with_remote_overrides(vec![ - remote( - "gpt-4o-mini", - ReasoningEffort::High, - ConfigShellToolType::ShellCommand, - ), - remote( - "other-model", - ReasoningEffort::Low, - ConfigShellToolType::UnifiedExec, - ), - ]); - - assert_eq!( - updated.default_reasoning_effort, - Some(ReasoningEffort::High) - ); - assert_eq!(updated.shell_type, ConfigShellToolType::ShellCommand); - } - - #[test] - fn remote_overrides_skip_non_matching_models() { - let family = model_family!( - "codex-mini-latest", - "codex-mini-latest", - shell_type: ConfigShellToolType::Local - ); - - let updated = family.clone().with_remote_overrides(vec![remote( - "other", - ReasoningEffort::High, - ConfigShellToolType::ShellCommand, - )]); - - assert_eq!( - updated.default_reasoning_effort, - family.default_reasoning_effort - ); - assert_eq!(updated.shell_type, family.shell_type); - } - - #[test] - fn remote_overrides_apply_extended_metadata() { - let family = model_family!( - "gpt-5.1", - "gpt-5.1", - supports_reasoning_summaries: false, - support_verbosity: false, - default_verbosity: None, - apply_patch_tool_type: Some(ApplyPatchToolType::Function), - supports_parallel_tool_calls: false, - experimental_supported_tools: vec!["local".to_string()], - truncation_policy: TruncationPolicy::Bytes(10_000), - context_window: Some(100), - ); - - let updated = family.with_remote_overrides(vec![ModelInfo { - slug: "gpt-5.1".to_string(), - display_name: "gpt-5.1".to_string(), - description: Some("desc".to_string()), - default_reasoning_level: ReasoningEffort::High, - supported_reasoning_levels: vec![ReasoningEffortPreset { - effort: ReasoningEffort::High, - description: "High".to_string(), - }], - shell_type: ConfigShellToolType::ShellCommand, - visibility: ModelVisibility::List, - supported_in_api: true, - priority: 10, - upgrade: None, - base_instructions: Some("Remote instructions".to_string()), - supports_reasoning_summaries: true, - support_verbosity: true, - default_verbosity: Some(Verbosity::High), - apply_patch_tool_type: Some(ApplyPatchToolType::Freeform), - truncation_policy: TruncationPolicyConfig::tokens(2_000), - supports_parallel_tool_calls: true, - context_window: Some(400_000), - experimental_supported_tools: vec!["alpha".to_string(), "beta".to_string()], - }]); - - assert_eq!( - updated.default_reasoning_effort, - Some(ReasoningEffort::High) - ); - assert!(updated.supports_reasoning_summaries); - assert!(updated.support_verbosity); - assert_eq!(updated.default_verbosity, Some(Verbosity::High)); - assert_eq!(updated.shell_type, ConfigShellToolType::ShellCommand); - assert_eq!( - updated.apply_patch_tool_type, - Some(ApplyPatchToolType::Freeform) - ); - assert_eq!(updated.truncation_policy, TruncationPolicy::Tokens(2_000)); - assert!(updated.supports_parallel_tool_calls); - assert_eq!(updated.context_window, Some(400_000)); - assert_eq!( - updated.experimental_supported_tools, - vec!["alpha".to_string(), "beta".to_string()] - ); - assert_eq!(updated.base_instructions, "Remote instructions"); - } -} diff --git a/codex-rs/core/src/models_manager/model_info.rs b/codex-rs/core/src/models_manager/model_info.rs new file mode 100644 index 0000000000..f907237f19 --- /dev/null +++ b/codex-rs/core/src/models_manager/model_info.rs @@ -0,0 +1,348 @@ +use codex_protocol::config_types::Verbosity; +use codex_protocol::openai_models::ApplyPatchToolType; +use codex_protocol::openai_models::ConfigShellToolType; +use codex_protocol::openai_models::ModelInfo; +use codex_protocol::openai_models::ModelVisibility; +use codex_protocol::openai_models::ReasoningEffort; +use codex_protocol::openai_models::ReasoningEffortPreset; +use codex_protocol::openai_models::TruncationPolicyConfig; + +use crate::config::Config; +use tracing::warn; + +const BASE_INSTRUCTIONS: &str = include_str!("../../prompt.md"); +const BASE_INSTRUCTIONS_WITH_APPLY_PATCH: &str = + include_str!("../../prompt_with_apply_patch_instructions.md"); + +const GPT_5_CODEX_INSTRUCTIONS: &str = include_str!("../../gpt_5_codex_prompt.md"); +const GPT_5_1_INSTRUCTIONS: &str = include_str!("../../gpt_5_1_prompt.md"); +const GPT_5_2_INSTRUCTIONS: &str = include_str!("../../gpt_5_2_prompt.md"); +const GPT_5_1_CODEX_MAX_INSTRUCTIONS: &str = include_str!("../../gpt-5.1-codex-max_prompt.md"); +const GPT_5_2_CODEX_INSTRUCTIONS: &str = include_str!("../../gpt-5.2-codex_prompt.md"); + +pub(crate) const CONTEXT_WINDOW_272K: i64 = 272_000; + +macro_rules! model_info { + ( + $slug:expr $(, $key:ident : $value:expr )* $(,)? + ) => {{ + #[allow(unused_mut)] + let mut model = ModelInfo { + slug: $slug.to_string(), + display_name: $slug.to_string(), + description: None, + // This is primarily used when remote metadata is available. When running + // offline, core generally omits the effort field unless explicitly + // configured by the user. + default_reasoning_level: None, + supported_reasoning_levels: supported_reasoning_level_low_medium_high(), + shell_type: ConfigShellToolType::Default, + visibility: ModelVisibility::None, + supported_in_api: true, + priority: 99, + upgrade: None, + base_instructions: BASE_INSTRUCTIONS.to_string(), + supports_reasoning_summaries: false, + support_verbosity: false, + default_verbosity: None, + apply_patch_tool_type: None, + truncation_policy: TruncationPolicyConfig::bytes(10_000), + supports_parallel_tool_calls: false, + context_window: Some(CONTEXT_WINDOW_272K), + auto_compact_token_limit: None, + effective_context_window_percent: 95, + experimental_supported_tools: Vec::new(), + }; + + $( + model.$key = $value; + )* + model + }}; +} + +pub(crate) fn with_config_overrides(mut model: ModelInfo, config: &Config) -> ModelInfo { + if let Some(supports_reasoning_summaries) = config.model_supports_reasoning_summaries { + model.supports_reasoning_summaries = supports_reasoning_summaries; + } + if let Some(context_window) = config.model_context_window { + model.context_window = Some(context_window); + } + if let Some(auto_compact_token_limit) = config.model_auto_compact_token_limit { + model.auto_compact_token_limit = Some(auto_compact_token_limit); + } + model +} + +// todo(aibrahim): remove most of the entries here when enabling models.json +pub(crate) fn find_model_info_for_slug(slug: &str) -> ModelInfo { + if slug.starts_with("o3") || slug.starts_with("o4-mini") { + model_info!( + slug, + base_instructions: BASE_INSTRUCTIONS_WITH_APPLY_PATCH.to_string(), + supports_reasoning_summaries: true, + context_window: Some(200_000), + ) + } else if slug.starts_with("codex-mini-latest") { + model_info!( + slug, + base_instructions: BASE_INSTRUCTIONS_WITH_APPLY_PATCH.to_string(), + shell_type: ConfigShellToolType::Local, + supports_reasoning_summaries: true, + context_window: Some(200_000), + ) + } else if slug.starts_with("gpt-4.1") { + model_info!( + slug, + base_instructions: BASE_INSTRUCTIONS_WITH_APPLY_PATCH.to_string(), + supports_reasoning_summaries: false, + context_window: Some(1_047_576), + ) + } else if slug.starts_with("gpt-oss") || slug.starts_with("openai/gpt-oss") { + model_info!( + slug, + apply_patch_tool_type: Some(ApplyPatchToolType::Function), + context_window: Some(96_000), + ) + } else if slug.starts_with("gpt-4o") { + model_info!( + slug, + base_instructions: BASE_INSTRUCTIONS_WITH_APPLY_PATCH.to_string(), + supports_reasoning_summaries: false, + context_window: Some(128_000), + ) + } else if slug.starts_with("gpt-3.5") { + model_info!( + slug, + base_instructions: BASE_INSTRUCTIONS_WITH_APPLY_PATCH.to_string(), + supports_reasoning_summaries: false, + context_window: Some(16_385), + ) + } else if slug.starts_with("test-gpt-5") { + model_info!( + slug, + base_instructions: GPT_5_CODEX_INSTRUCTIONS.to_string(), + experimental_supported_tools: vec![ + "grep_files".to_string(), + "list_dir".to_string(), + "read_file".to_string(), + "test_sync_tool".to_string(), + ], + supports_parallel_tool_calls: true, + supports_reasoning_summaries: true, + shell_type: ConfigShellToolType::ShellCommand, + support_verbosity: true, + truncation_policy: TruncationPolicyConfig::tokens(10_000), + ) + } else if slug.starts_with("exp-codex") || slug.starts_with("codex-1p") { + model_info!( + slug, + base_instructions: GPT_5_2_CODEX_INSTRUCTIONS.to_string(), + apply_patch_tool_type: Some(ApplyPatchToolType::Freeform), + shell_type: ConfigShellToolType::ShellCommand, + supports_parallel_tool_calls: true, + supports_reasoning_summaries: true, + support_verbosity: false, + truncation_policy: TruncationPolicyConfig::tokens(10_000), + context_window: Some(CONTEXT_WINDOW_272K), + ) + } else if slug.starts_with("exp-") { + model_info!( + slug, + apply_patch_tool_type: Some(ApplyPatchToolType::Freeform), + supports_reasoning_summaries: true, + support_verbosity: true, + default_verbosity: Some(Verbosity::Low), + base_instructions: BASE_INSTRUCTIONS.to_string(), + default_reasoning_level: Some(ReasoningEffort::Medium), + truncation_policy: TruncationPolicyConfig::bytes(10_000), + shell_type: ConfigShellToolType::UnifiedExec, + supports_parallel_tool_calls: true, + context_window: Some(CONTEXT_WINDOW_272K), + ) + } else if slug.starts_with("gpt-5.2-codex") || slug.starts_with("bengalfox") { + model_info!( + slug, + base_instructions: GPT_5_2_CODEX_INSTRUCTIONS.to_string(), + apply_patch_tool_type: Some(ApplyPatchToolType::Freeform), + shell_type: ConfigShellToolType::ShellCommand, + supports_parallel_tool_calls: true, + supports_reasoning_summaries: true, + support_verbosity: false, + truncation_policy: TruncationPolicyConfig::tokens(10_000), + context_window: Some(CONTEXT_WINDOW_272K), + supported_reasoning_levels: supported_reasoning_level_low_medium_high_xhigh(), + ) + } else if slug.starts_with("gpt-5.1-codex-max") { + model_info!( + slug, + base_instructions: GPT_5_1_CODEX_MAX_INSTRUCTIONS.to_string(), + apply_patch_tool_type: Some(ApplyPatchToolType::Freeform), + shell_type: ConfigShellToolType::ShellCommand, + supports_parallel_tool_calls: false, + supports_reasoning_summaries: true, + support_verbosity: false, + truncation_policy: TruncationPolicyConfig::tokens(10_000), + context_window: Some(CONTEXT_WINDOW_272K), + supported_reasoning_levels: supported_reasoning_level_low_medium_high_xhigh(), + ) + } else if (slug.starts_with("gpt-5-codex") + || slug.starts_with("gpt-5.1-codex") + || slug.starts_with("codex-")) + && !slug.contains("-mini") + { + model_info!( + slug, + base_instructions: GPT_5_CODEX_INSTRUCTIONS.to_string(), + apply_patch_tool_type: Some(ApplyPatchToolType::Freeform), + shell_type: ConfigShellToolType::ShellCommand, + supports_parallel_tool_calls: false, + supports_reasoning_summaries: true, + support_verbosity: false, + truncation_policy: TruncationPolicyConfig::tokens(10_000), + context_window: Some(CONTEXT_WINDOW_272K), + supported_reasoning_levels: supported_reasoning_level_low_medium_high(), + ) + } else if slug.starts_with("gpt-5-codex") + || slug.starts_with("gpt-5.1-codex") + || slug.starts_with("codex-") + { + model_info!( + slug, + base_instructions: GPT_5_CODEX_INSTRUCTIONS.to_string(), + apply_patch_tool_type: Some(ApplyPatchToolType::Freeform), + shell_type: ConfigShellToolType::ShellCommand, + supports_parallel_tool_calls: false, + supports_reasoning_summaries: true, + support_verbosity: false, + truncation_policy: TruncationPolicyConfig::tokens(10_000), + context_window: Some(CONTEXT_WINDOW_272K), + ) + } else if (slug.starts_with("gpt-5.2") || slug.starts_with("boomslang")) + && !slug.contains("codex") + { + model_info!( + slug, + apply_patch_tool_type: Some(ApplyPatchToolType::Freeform), + supports_reasoning_summaries: true, + support_verbosity: true, + default_verbosity: Some(Verbosity::Low), + base_instructions: GPT_5_2_INSTRUCTIONS.to_string(), + default_reasoning_level: Some(ReasoningEffort::Medium), + truncation_policy: TruncationPolicyConfig::bytes(10_000), + shell_type: ConfigShellToolType::ShellCommand, + supports_parallel_tool_calls: true, + context_window: Some(CONTEXT_WINDOW_272K), + supported_reasoning_levels: supported_reasoning_level_low_medium_high_xhigh_non_codex(), + ) + } else if slug.starts_with("gpt-5.1") && !slug.contains("codex") { + model_info!( + slug, + apply_patch_tool_type: Some(ApplyPatchToolType::Freeform), + supports_reasoning_summaries: true, + support_verbosity: true, + default_verbosity: Some(Verbosity::Low), + base_instructions: GPT_5_1_INSTRUCTIONS.to_string(), + default_reasoning_level: Some(ReasoningEffort::Medium), + truncation_policy: TruncationPolicyConfig::bytes(10_000), + shell_type: ConfigShellToolType::ShellCommand, + supports_parallel_tool_calls: true, + context_window: Some(CONTEXT_WINDOW_272K), + supported_reasoning_levels: supported_reasoning_level_low_medium_high_non_codex(), + ) + } else if slug.starts_with("gpt-5") { + model_info!( + slug, + base_instructions: BASE_INSTRUCTIONS_WITH_APPLY_PATCH.to_string(), + shell_type: ConfigShellToolType::Default, + supports_reasoning_summaries: true, + support_verbosity: true, + truncation_policy: TruncationPolicyConfig::bytes(10_000), + context_window: Some(CONTEXT_WINDOW_272K), + ) + } else { + warn!("Unknown model {slug} is used. This will degrade the performance of Codex."); + model_info!( + slug, + context_window: None, + supported_reasoning_levels: Vec::new(), + default_reasoning_level: None + ) + } +} + +fn supported_reasoning_level_low_medium_high() -> Vec { + vec![ + ReasoningEffortPreset { + effort: ReasoningEffort::Low, + description: "Fast responses with lighter reasoning".to_string(), + }, + ReasoningEffortPreset { + effort: ReasoningEffort::Medium, + description: "Balances speed and reasoning depth for everyday tasks".to_string(), + }, + ReasoningEffortPreset { + effort: ReasoningEffort::High, + description: "Greater reasoning depth for complex problems".to_string(), + }, + ] +} + +fn supported_reasoning_level_low_medium_high_non_codex() -> Vec { + vec![ + ReasoningEffortPreset { + effort: ReasoningEffort::Low, + description: "Balances speed with some reasoning; useful for straightforward queries and short explanations".to_string(), + }, + ReasoningEffortPreset { + effort: ReasoningEffort::Medium, + description: "Provides a solid balance of reasoning depth and latency for general-purpose tasks".to_string(), + }, + ReasoningEffortPreset { + effort: ReasoningEffort::High, + description: "Maximizes reasoning depth for complex or ambiguous problems".to_string(), + }, + ] +} + +fn supported_reasoning_level_low_medium_high_xhigh() -> Vec { + vec![ + ReasoningEffortPreset { + effort: ReasoningEffort::Low, + description: "Fast responses with lighter reasoning".to_string(), + }, + ReasoningEffortPreset { + effort: ReasoningEffort::Medium, + description: "Balances speed and reasoning depth for everyday tasks".to_string(), + }, + ReasoningEffortPreset { + effort: ReasoningEffort::High, + description: "Greater reasoning depth for complex problems".to_string(), + }, + ReasoningEffortPreset { + effort: ReasoningEffort::XHigh, + description: "Extra high reasoning depth for complex problems".to_string(), + }, + ] +} + +fn supported_reasoning_level_low_medium_high_xhigh_non_codex() -> Vec { + vec![ + ReasoningEffortPreset { + effort: ReasoningEffort::Low, + description: "Balances speed with some reasoning; useful for straightforward queries and short explanations".to_string(), + }, + ReasoningEffortPreset { + effort: ReasoningEffort::Medium, + description: "Provides a solid balance of reasoning depth and latency for general-purpose tasks".to_string(), + }, + ReasoningEffortPreset { + effort: ReasoningEffort::High, + description: "Maximizes reasoning depth for complex or ambiguous problems".to_string(), + }, + ReasoningEffortPreset { + effort: ReasoningEffort::XHigh, + description: "Extra high reasoning for complex problems".to_string(), + }, + ] +} diff --git a/codex-rs/core/src/tools/spec.rs b/codex-rs/core/src/tools/spec.rs index d9e21bc24b..a92a624a7e 100644 --- a/codex-rs/core/src/tools/spec.rs +++ b/codex-rs/core/src/tools/spec.rs @@ -2,13 +2,13 @@ use crate::client_common::tools::ResponsesApiTool; use crate::client_common::tools::ToolSpec; use crate::features::Feature; use crate::features::Features; -use crate::models_manager::model_family::ModelFamily; use crate::tools::handlers::PLAN_TOOL; use crate::tools::handlers::apply_patch::create_apply_patch_freeform_tool; use crate::tools::handlers::apply_patch::create_apply_patch_json_tool; use crate::tools::registry::ToolRegistryBuilder; use codex_protocol::openai_models::ApplyPatchToolType; use codex_protocol::openai_models::ConfigShellToolType; +use codex_protocol::openai_models::ModelInfo; use serde::Deserialize; use serde::Serialize; use serde_json::Value as JsonValue; @@ -27,14 +27,14 @@ pub(crate) struct ToolsConfig { } pub(crate) struct ToolsConfigParams<'a> { - pub(crate) model_family: &'a ModelFamily, + pub(crate) model_info: &'a ModelInfo, pub(crate) features: &'a Features, } impl ToolsConfig { pub fn new(params: &ToolsConfigParams) -> Self { let ToolsConfigParams { - model_family, + model_info, features, } = params; let include_apply_patch_tool = features.enabled(Feature::ApplyPatchFreeform); @@ -52,10 +52,10 @@ impl ToolsConfig { ConfigShellToolType::ShellCommand } } else { - model_family.shell_type + model_info.shell_type }; - let apply_patch_tool_type = match model_family.apply_patch_tool_type { + let apply_patch_tool_type = match model_info.apply_patch_tool_type { Some(ApplyPatchToolType::Freeform) => Some(ApplyPatchToolType::Freeform), Some(ApplyPatchToolType::Function) => Some(ApplyPatchToolType::Function), None => { @@ -73,7 +73,7 @@ impl ToolsConfig { web_search_request: include_web_search_request, web_search_cached: include_web_search_cached, include_view_image_tool, - experimental_supported_tools: model_family.experimental_supported_tools.clone(), + experimental_supported_tools: model_info.experimental_supported_tools.clone(), } } } @@ -1232,13 +1232,13 @@ mod tests { #[test] fn test_full_toolset_specs_for_gpt5_codex_unified_exec_web_search() { let config = test_config(); - let model_family = ModelsManager::construct_model_family_offline("gpt-5-codex", &config); + let model_info = ModelsManager::construct_model_info_offline("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); features.enable(Feature::WebSearchRequest); features.enable(Feature::ViewImageTool); let config = ToolsConfig::new(&ToolsConfigParams { - model_family: &model_family, + model_info: &model_info, features: &features, }); let (tools, _) = build_specs(&config, None).build(); @@ -1294,9 +1294,9 @@ mod tests { fn assert_model_tools(model_slug: &str, features: &Features, expected_tools: &[&str]) { let config = test_config(); - let model_family = ModelsManager::construct_model_family_offline(model_slug, &config); + let model_info = ModelsManager::construct_model_info_offline(model_slug, &config); let tools_config = ToolsConfig::new(&ToolsConfigParams { - model_family: &model_family, + model_info: &model_info, features, }); let (tools, _) = build_specs(&tools_config, Some(HashMap::new())).build(); @@ -1307,12 +1307,12 @@ mod tests { #[test] fn web_search_cached_sets_external_web_access_false() { let config = test_config(); - let model_family = ModelsManager::construct_model_family_offline("gpt-5-codex", &config); + let model_info = ModelsManager::construct_model_info_offline("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.enable(Feature::WebSearchCached); let tools_config = ToolsConfig::new(&ToolsConfigParams { - model_family: &model_family, + model_info: &model_info, features: &features, }); let (tools, _) = build_specs(&tools_config, None).build(); @@ -1329,13 +1329,13 @@ mod tests { #[test] fn web_search_cached_takes_precedence_over_web_search_request() { let config = test_config(); - let model_family = ModelsManager::construct_model_family_offline("gpt-5-codex", &config); + let model_info = ModelsManager::construct_model_info_offline("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.enable(Feature::WebSearchCached); features.enable(Feature::WebSearchRequest); let tools_config = ToolsConfig::new(&ToolsConfigParams { - model_family: &model_family, + model_info: &model_info, features: &features, }); let (tools, _) = build_specs(&tools_config, None).build(); @@ -1532,12 +1532,12 @@ mod tests { #[test] fn test_build_specs_default_shell_present() { let config = test_config(); - let model_family = ModelsManager::construct_model_family_offline("o3", &config); + let model_info = ModelsManager::construct_model_info_offline("o3", &config); let mut features = Features::with_defaults(); features.enable(Feature::WebSearchRequest); features.enable(Feature::UnifiedExec); let tools_config = ToolsConfig::new(&ToolsConfigParams { - model_family: &model_family, + model_info: &model_info, features: &features, }); let (tools, _) = build_specs(&tools_config, Some(HashMap::new())).build(); @@ -1554,12 +1554,12 @@ mod tests { #[ignore] fn test_parallel_support_flags() { let config = test_config(); - let model_family = ModelsManager::construct_model_family_offline("gpt-5-codex", &config); + let model_info = ModelsManager::construct_model_info_offline("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.disable(Feature::ViewImageTool); features.enable(Feature::UnifiedExec); let tools_config = ToolsConfig::new(&ToolsConfigParams { - model_family: &model_family, + model_info: &model_info, features: &features, }); let (tools, _) = build_specs(&tools_config, None).build(); @@ -1572,14 +1572,13 @@ mod tests { } #[test] - fn test_test_model_family_includes_sync_tool() { + fn test_test_model_info_includes_sync_tool() { let config = test_config(); - let model_family = - ModelsManager::construct_model_family_offline("test-gpt-5-codex", &config); + let model_info = ModelsManager::construct_model_info_offline("test-gpt-5-codex", &config); let mut features = Features::with_defaults(); features.disable(Feature::ViewImageTool); let tools_config = ToolsConfig::new(&ToolsConfigParams { - model_family: &model_family, + model_info: &model_info, features: &features, }); let (tools, _) = build_specs(&tools_config, None).build(); @@ -1605,12 +1604,12 @@ mod tests { #[test] fn test_build_specs_mcp_tools_converted() { let config = test_config(); - let model_family = ModelsManager::construct_model_family_offline("o3", &config); + let model_info = ModelsManager::construct_model_info_offline("o3", &config); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); features.enable(Feature::WebSearchRequest); let tools_config = ToolsConfig::new(&ToolsConfigParams { - model_family: &model_family, + model_info: &model_info, features: &features, }); let (tools, _) = build_specs( @@ -1700,11 +1699,11 @@ mod tests { #[test] fn test_build_specs_mcp_tools_sorted_by_name() { let config = test_config(); - let model_family = ModelsManager::construct_model_family_offline("o3", &config); + let model_info = ModelsManager::construct_model_info_offline("o3", &config); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); let tools_config = ToolsConfig::new(&ToolsConfigParams { - model_family: &model_family, + model_info: &model_info, features: &features, }); @@ -1776,12 +1775,12 @@ mod tests { #[test] fn test_mcp_tool_property_missing_type_defaults_to_string() { let config = test_config(); - let model_family = ModelsManager::construct_model_family_offline("gpt-5-codex", &config); + let model_info = ModelsManager::construct_model_info_offline("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); features.enable(Feature::WebSearchRequest); let tools_config = ToolsConfig::new(&ToolsConfigParams { - model_family: &model_family, + model_info: &model_info, features: &features, }); @@ -1833,12 +1832,12 @@ mod tests { #[test] fn test_mcp_tool_integer_normalized_to_number() { let config = test_config(); - let model_family = ModelsManager::construct_model_family_offline("gpt-5-codex", &config); + let model_info = ModelsManager::construct_model_info_offline("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); features.enable(Feature::WebSearchRequest); let tools_config = ToolsConfig::new(&ToolsConfigParams { - model_family: &model_family, + model_info: &model_info, features: &features, }); @@ -1886,13 +1885,13 @@ mod tests { #[test] fn test_mcp_tool_array_without_items_gets_default_string_items() { let config = test_config(); - let model_family = ModelsManager::construct_model_family_offline("gpt-5-codex", &config); + let model_info = ModelsManager::construct_model_info_offline("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); features.enable(Feature::WebSearchRequest); features.enable(Feature::ApplyPatchFreeform); let tools_config = ToolsConfig::new(&ToolsConfigParams { - model_family: &model_family, + model_info: &model_info, features: &features, }); @@ -1943,12 +1942,12 @@ mod tests { #[test] fn test_mcp_tool_anyof_defaults_to_string() { let config = test_config(); - let model_family = ModelsManager::construct_model_family_offline("gpt-5-codex", &config); + let model_info = ModelsManager::construct_model_info_offline("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); features.enable(Feature::WebSearchRequest); let tools_config = ToolsConfig::new(&ToolsConfigParams { - model_family: &model_family, + model_info: &model_info, features: &features, }); @@ -2055,12 +2054,12 @@ Examples of valid command strings: #[test] fn test_get_openai_tools_mcp_tools_with_additional_properties_schema() { let config = test_config(); - let model_family = ModelsManager::construct_model_family_offline("gpt-5-codex", &config); + let model_info = ModelsManager::construct_model_info_offline("gpt-5-codex", &config); let mut features = Features::with_defaults(); features.enable(Feature::UnifiedExec); features.enable(Feature::WebSearchRequest); let tools_config = ToolsConfig::new(&ToolsConfigParams { - model_family: &model_family, + model_info: &model_info, features: &features, }); let (tools, _) = build_specs( diff --git a/codex-rs/core/tests/chat_completions_payload.rs b/codex-rs/core/tests/chat_completions_payload.rs index 536de44e83..b449f200d2 100644 --- a/codex-rs/core/tests/chat_completions_payload.rs +++ b/codex-rs/core/tests/chat_completions_payload.rs @@ -75,11 +75,11 @@ async fn run_request(input: Vec) -> Value { 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 model_info = ModelsManager::construct_model_info_offline(model.as_str(), &config); let otel_manager = OtelManager::new( conversation_id, model.as_str(), - model_family.slug.as_str(), + model_info.slug.as_str(), None, Some("test@test.com".to_string()), Some(AuthMode::ApiKey), @@ -91,7 +91,7 @@ async fn run_request(input: Vec) -> Value { let client = ModelClient::new( Arc::clone(&config), None, - model_family, + model_info, otel_manager, provider, effort, diff --git a/codex-rs/core/tests/chat_completions_sse.rs b/codex-rs/core/tests/chat_completions_sse.rs index 17d4a5a4ab..6c0e668e7b 100644 --- a/codex-rs/core/tests/chat_completions_sse.rs +++ b/codex-rs/core/tests/chat_completions_sse.rs @@ -76,11 +76,11 @@ async fn run_stream_with_bytes(sse_body: &[u8]) -> Vec { 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()); - let model_family = ModelsManager::construct_model_family_offline(model.as_str(), &config); + let model_info = ModelsManager::construct_model_info_offline(model.as_str(), &config); let otel_manager = OtelManager::new( conversation_id, model.as_str(), - model_family.slug.as_str(), + model_info.slug.as_str(), None, Some("test@test.com".to_string()), auth_mode, @@ -92,7 +92,7 @@ async fn run_stream_with_bytes(sse_body: &[u8]) -> Vec { let client = ModelClient::new( Arc::clone(&config), None, - model_family, + model_info, otel_manager, provider, effort, diff --git a/codex-rs/core/tests/responses_headers.rs b/codex-rs/core/tests/responses_headers.rs index 188397fd24..e1a2df1213 100644 --- a/codex-rs/core/tests/responses_headers.rs +++ b/codex-rs/core/tests/responses_headers.rs @@ -68,11 +68,11 @@ async fn responses_stream_includes_subagent_header_on_review() { 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); + let model_info = ModelsManager::construct_model_info_offline(model.as_str(), &config); let otel_manager = OtelManager::new( conversation_id, model.as_str(), - model_family.slug.as_str(), + model_info.slug.as_str(), None, Some("test@test.com".to_string()), Some(auth_mode), @@ -84,7 +84,7 @@ async fn responses_stream_includes_subagent_header_on_review() { let client = ModelClient::new( Arc::clone(&config), None, - model_family, + model_info, otel_manager, provider, effort, @@ -162,12 +162,12 @@ async fn responses_stream_includes_subagent_header_on_other() { 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); + let model_info = ModelsManager::construct_model_info_offline(model.as_str(), &config); let otel_manager = OtelManager::new( conversation_id, model.as_str(), - model_family.slug.as_str(), + model_info.slug.as_str(), None, Some("test@test.com".to_string()), Some(auth_mode), @@ -179,7 +179,7 @@ async fn responses_stream_includes_subagent_header_on_other() { let client = ModelClient::new( Arc::clone(&config), None, - model_family, + model_info, otel_manager, provider, effort, @@ -212,7 +212,7 @@ async fn responses_stream_includes_subagent_header_on_other() { } #[tokio::test] -async fn responses_respects_model_family_overrides_from_config() { +async fn responses_respects_model_info_overrides_from_config() { core_test_support::skip_if_no_network!(); let server = responses::start_mock_server().await; @@ -256,11 +256,11 @@ async fn responses_respects_model_family_overrides_from_config() { AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key")).get_auth_mode(); let session_source = SessionSource::SubAgent(SubAgentSource::Other("override-check".to_string())); - let model_family = ModelsManager::construct_model_family_offline(model.as_str(), &config); + let model_info = ModelsManager::construct_model_info_offline(model.as_str(), &config); let otel_manager = OtelManager::new( conversation_id, model.as_str(), - model_family.slug.as_str(), + model_info.slug.as_str(), None, Some("test@test.com".to_string()), auth_mode, @@ -272,7 +272,7 @@ async fn responses_respects_model_family_overrides_from_config() { let client = ModelClient::new( Arc::clone(&config), None, - model_family, + model_info, otel_manager, provider, effort, diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index 7d876ceb1f..7e0a6e3fa0 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -806,7 +806,7 @@ async fn includes_no_effort_in_request() -> anyhow::Result<()> { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn includes_default_reasoning_effort_in_request_when_defined_by_model_family() +async fn includes_default_reasoning_effort_in_request_when_defined_by_model_info() -> anyhow::Result<()> { skip_if_no_network!(Ok(())); let server = MockServer::start().await; @@ -1142,13 +1142,13 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() { let model = ModelsManager::get_model_offline(config.model.as_deref()); config.model = Some(model.clone()); let config = Arc::new(config); - let model_family = ModelsManager::construct_model_family_offline(model.as_str(), &config); + let model_info = ModelsManager::construct_model_info_offline(model.as_str(), &config); 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, model.as_str(), - model_family.slug.as_str(), + model_info.slug.as_str(), None, Some("test@test.com".to_string()), auth_manager.get_auth_mode(), @@ -1160,7 +1160,7 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() { let client = ModelClient::new( Arc::clone(&config), None, - model_family, + model_info, otel_manager, provider, effort, diff --git a/codex-rs/core/tests/suite/prompt_caching.rs b/codex-rs/core/tests/suite/prompt_caching.rs index 01c5908930..bde146ed47 100644 --- a/codex-rs/core/tests/suite/prompt_caching.rs +++ b/codex-rs/core/tests/suite/prompt_caching.rs @@ -86,7 +86,7 @@ async fn prompt_tools_are_consistent_across_requests() -> anyhow::Result<()> { .await?; let base_instructions = thread_manager .get_models_manager() - .construct_model_family( + .construct_model_info( config .model .as_deref() @@ -94,8 +94,7 @@ async fn prompt_tools_are_consistent_across_requests() -> anyhow::Result<()> { &config, ) .await - .base_instructions - .clone(); + .base_instructions; codex .submit(Op::UserInput { @@ -132,7 +131,7 @@ async fn prompt_tools_are_consistent_across_requests() -> anyhow::Result<()> { base_instructions } else { [ - base_instructions.clone(), + base_instructions, include_str!("../../../apply-patch/apply_patch_tool_instructions.md").to_string(), ] .join("\n") diff --git a/codex-rs/core/tests/suite/remote_models.rs b/codex-rs/core/tests/suite/remote_models.rs index 6968285319..d77ec7d383 100644 --- a/codex-rs/core/tests/suite/remote_models.rs +++ b/codex-rs/core/tests/suite/remote_models.rs @@ -64,7 +64,7 @@ async fn remote_models_remote_model_uses_unified_exec() -> Result<()> { slug: REMOTE_MODEL_SLUG.to_string(), display_name: "Remote Test".to_string(), description: Some("A remote model that requires the test shell".to_string()), - default_reasoning_level: ReasoningEffort::Medium, + default_reasoning_level: Some(ReasoningEffort::Medium), supported_reasoning_levels: vec![ReasoningEffortPreset { effort: ReasoningEffort::Medium, description: ReasoningEffort::Medium.to_string(), @@ -74,14 +74,16 @@ async fn remote_models_remote_model_uses_unified_exec() -> Result<()> { supported_in_api: true, priority: 1, upgrade: None, - base_instructions: None, + base_instructions: "base instructions".to_string(), supports_reasoning_summaries: false, support_verbosity: false, default_verbosity: None, apply_patch_tool_type: None, truncation_policy: TruncationPolicyConfig::bytes(10_000), supports_parallel_tool_calls: false, - context_window: None, + context_window: Some(272_000), + auto_compact_token_limit: None, + effective_context_window_percent: 95, experimental_supported_tools: Vec::new(), }; @@ -121,10 +123,10 @@ async fn remote_models_remote_model_uses_unified_exec() -> Result<()> { ); assert_eq!(requests[0].url.path(), "/v1/models"); - let family = models_manager - .construct_model_family(REMOTE_MODEL_SLUG, &config) + let model_info = models_manager + .construct_model_info(REMOTE_MODEL_SLUG, &config) .await; - assert_eq!(family.shell_type, ConfigShellToolType::UnifiedExec); + assert_eq!(model_info.shell_type, ConfigShellToolType::UnifiedExec); codex .submit(Op::OverrideTurnContext { @@ -201,7 +203,7 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> { slug: model.to_string(), display_name: "Parallel Remote".to_string(), description: Some("A remote model with custom instructions".to_string()), - default_reasoning_level: ReasoningEffort::Medium, + default_reasoning_level: Some(ReasoningEffort::Medium), supported_reasoning_levels: vec![ReasoningEffortPreset { effort: ReasoningEffort::Medium, description: ReasoningEffort::Medium.to_string(), @@ -211,14 +213,16 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> { supported_in_api: true, priority: 1, upgrade: None, - base_instructions: Some(remote_base.to_string()), + base_instructions: remote_base.to_string(), supports_reasoning_summaries: false, support_verbosity: false, default_verbosity: None, apply_patch_tool_type: None, truncation_policy: TruncationPolicyConfig::bytes(10_000), supports_parallel_tool_calls: false, - context_window: None, + context_window: Some(272_000), + auto_compact_token_limit: None, + effective_context_window_percent: 95, experimental_supported_tools: Vec::new(), }; mount_models_once( @@ -459,7 +463,7 @@ fn test_remote_model(slug: &str, visibility: ModelVisibility, priority: i32) -> slug: slug.to_string(), display_name: format!("{slug} display"), description: Some(format!("{slug} description")), - default_reasoning_level: ReasoningEffort::Medium, + default_reasoning_level: Some(ReasoningEffort::Medium), supported_reasoning_levels: vec![ReasoningEffortPreset { effort: ReasoningEffort::Medium, description: ReasoningEffort::Medium.to_string(), @@ -469,14 +473,16 @@ fn test_remote_model(slug: &str, visibility: ModelVisibility, priority: i32) -> supported_in_api: true, priority, upgrade: None, - base_instructions: None, + base_instructions: "base instructions".to_string(), supports_reasoning_summaries: false, support_verbosity: false, default_verbosity: None, apply_patch_tool_type: None, truncation_policy: TruncationPolicyConfig::bytes(10_000), supports_parallel_tool_calls: false, - context_window: None, + context_window: Some(272_000), + auto_compact_token_limit: None, + effective_context_window_percent: 95, experimental_supported_tools: Vec::new(), } } diff --git a/codex-rs/mcp-types/schema/2025-03-26/schema.json b/codex-rs/mcp-types/schema/2025-03-26/schema.json index a1e3f26799..328ff95f4b 100644 --- a/codex-rs/mcp-types/schema/2025-03-26/schema.json +++ b/codex-rs/mcp-types/schema/2025-03-26/schema.json @@ -1167,7 +1167,7 @@ "description": "Hints to use for model selection.\n\nKeys not declared here are currently left unspecified by the spec and are up\nto the client to interpret.", "properties": { "name": { - "description": "A hint for a model name.\n\nThe client SHOULD treat this as a substring of a model name; for example:\n - `claude-3-5-sonnet` should match `claude-3-5-sonnet-20241022`\n - `sonnet` should match `claude-3-5-sonnet-20241022`, `claude-3-sonnet-20240229`, etc.\n - `claude` should match any Claude model\n\nThe client MAY also map the string to a different provider's model name or a different model family, as long as it fills a similar niche; for example:\n - `gemini-1.5-flash` could match `claude-3-haiku-20240307`", + "description": "A hint for a model name.\n\nThe client SHOULD treat this as a substring of a model name; for example:\n - `claude-3-5-sonnet` should match `claude-3-5-sonnet-20241022`\n - `sonnet` should match `claude-3-5-sonnet-20241022`, `claude-3-sonnet-20240229`, etc.\n - `claude` should match any Claude model\n\nThe client MAY also map the string to a different provider's model name or a different model info, as long as it fills a similar niche; for example:\n - `gemini-1.5-flash` could match `claude-3-haiku-20240307`", "type": "string" } }, @@ -2136,4 +2136,3 @@ } } } - diff --git a/codex-rs/mcp-types/schema/2025-06-18/schema.json b/codex-rs/mcp-types/schema/2025-06-18/schema.json index 24ba4f6309..d5faee82cd 100644 --- a/codex-rs/mcp-types/schema/2025-06-18/schema.json +++ b/codex-rs/mcp-types/schema/2025-06-18/schema.json @@ -1334,7 +1334,7 @@ "description": "Hints to use for model selection.\n\nKeys not declared here are currently left unspecified by the spec and are up\nto the client to interpret.", "properties": { "name": { - "description": "A hint for a model name.\n\nThe client SHOULD treat this as a substring of a model name; for example:\n - `claude-3-5-sonnet` should match `claude-3-5-sonnet-20241022`\n - `sonnet` should match `claude-3-5-sonnet-20241022`, `claude-3-sonnet-20240229`, etc.\n - `claude` should match any Claude model\n\nThe client MAY also map the string to a different provider's model name or a different model family, as long as it fills a similar niche; for example:\n - `gemini-1.5-flash` could match `claude-3-haiku-20240307`", + "description": "A hint for a model name.\n\nThe client SHOULD treat this as a substring of a model name; for example:\n - `claude-3-5-sonnet` should match `claude-3-5-sonnet-20241022`\n - `sonnet` should match `claude-3-5-sonnet-20241022`, `claude-3-sonnet-20240229`, etc.\n - `claude` should match any Claude model\n\nThe client MAY also map the string to a different provider's model name or a different model info, as long as it fills a similar niche; for example:\n - `gemini-1.5-flash` could match `claude-3-haiku-20240307`", "type": "string" } }, @@ -2514,4 +2514,3 @@ } } } - diff --git a/codex-rs/protocol/src/openai_models.rs b/codex-rs/protocol/src/openai_models.rs index ae426e6296..60c7cc74b0 100644 --- a/codex-rs/protocol/src/openai_models.rs +++ b/codex-rs/protocol/src/openai_models.rs @@ -159,30 +159,53 @@ impl TruncationPolicyConfig { #[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, TS, JsonSchema)] pub struct ClientVersion(pub i32, pub i32, pub i32); +const fn default_effective_context_window_percent() -> i64 { + 95 +} + /// Model metadata returned by the Codex backend `/models` endpoint. #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema)] pub struct ModelInfo { pub slug: String, pub display_name: String, pub description: Option, - pub default_reasoning_level: ReasoningEffort, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub default_reasoning_level: Option, pub supported_reasoning_levels: Vec, pub shell_type: ConfigShellToolType, pub visibility: ModelVisibility, pub supported_in_api: bool, pub priority: i32, pub upgrade: Option, - pub base_instructions: Option, + pub base_instructions: String, pub supports_reasoning_summaries: bool, pub support_verbosity: bool, pub default_verbosity: Option, pub apply_patch_tool_type: Option, pub truncation_policy: TruncationPolicyConfig, pub supports_parallel_tool_calls: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] pub context_window: Option, + /// Token threshold for automatic compaction. When omitted, core derives it + /// from `context_window` (90%). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub auto_compact_token_limit: Option, + /// Percentage of the context window considered usable for inputs, after + /// reserving headroom for system prompts, tool overhead, and model output. + #[serde(default = "default_effective_context_window_percent")] + pub effective_context_window_percent: i64, pub experimental_supported_tools: Vec, } +impl ModelInfo { + pub fn auto_compact_token_limit(&self) -> Option { + self.auto_compact_token_limit.or_else(|| { + self.context_window + .map(|context_window| (context_window * 9) / 10) + }) + } +} + /// Response wrapper for `/models`. #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, TS, JsonSchema, Default)] pub struct ModelsResponse { @@ -197,7 +220,9 @@ impl From for ModelPreset { model: info.slug.clone(), display_name: info.display_name, description: info.description.unwrap_or_default(), - default_reasoning_effort: info.default_reasoning_level, + default_reasoning_effort: info + .default_reasoning_level + .unwrap_or(ReasoningEffort::None), supported_reasoning_efforts: info.supported_reasoning_levels.clone(), is_default: false, // default is the highest priority available model upgrade: info.upgrade.as_ref().map(|upgrade_slug| ModelUpgrade { diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index f5351576f9..e3748bafc6 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -127,7 +127,7 @@ pub enum Op { #[serde(skip_serializing_if = "Option::is_none")] sandbox_policy: Option, - /// Updated model slug. When set, the model family is derived + /// Updated model slug. When set, the model info is derived /// automatically. #[serde(skip_serializing_if = "Option::is_none")] model: Option, @@ -874,6 +874,7 @@ pub struct TaskCompleteEvent { #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)] pub struct TaskStartedEvent { + // TODO(aibrahim): make this not optional pub model_context_window: Option, } @@ -895,6 +896,7 @@ pub struct TokenUsage { pub struct TokenUsageInfo { pub total_token_usage: TokenUsage, pub last_token_usage: TokenUsage, + // TODO(aibrahim): make this not optional #[ts(type = "number | null")] pub model_context_window: Option, } diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 370d3d3f6a..cfb50a996d 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -540,10 +540,10 @@ impl App { } async fn handle_event(&mut self, tui: &mut tui::Tui, event: AppEvent) -> Result { - let model_family = self + let model_info = self .server .get_models_manager() - .construct_model_family(self.current_model.as_str(), &self.config) + .construct_model_info(self.current_model.as_str(), &self.config) .await; match event { AppEvent::NewSession => { @@ -564,7 +564,7 @@ impl App { model: self.current_model.clone(), }; self.chat_widget = ChatWidget::new(init, self.server.clone()); - self.current_model = model_family.get_model_slug().to_string(); + self.current_model = model_info.slug.clone(); if let Some(summary) = summary { let mut lines: Vec> = vec![summary.usage_line.clone().into()]; if let Some(command) = summary.resume_command { @@ -618,7 +618,7 @@ impl App { resumed.thread, resumed.session_configured, ); - self.current_model = model_family.get_model_slug().to_string(); + self.current_model = model_info.slug.clone(); if let Some(summary) = summary { let mut lines: Vec> = vec![summary.usage_line.clone().into()]; diff --git a/codex-rs/tui/src/status/tests.rs b/codex-rs/tui/src/status/tests.rs index c6f6c73599..bb16f13328 100644 --- a/codex-rs/tui/src/status/tests.rs +++ b/codex-rs/tui/src/status/tests.rs @@ -38,9 +38,8 @@ fn test_auth_manager(config: &Config) -> AuthManager { } fn token_info_for(model_slug: &str, config: &Config, usage: &TokenUsage) -> TokenUsageInfo { - let context_window = ModelsManager::construct_model_family_offline(model_slug, config) - .context_window - .or(config.model_context_window); + let context_window = + ModelsManager::construct_model_info_offline(model_slug, config).context_window; TokenUsageInfo { total_token_usage: usage.clone(), last_token_usage: usage.clone(), diff --git a/codex-rs/tui2/src/status/tests.rs b/codex-rs/tui2/src/status/tests.rs index 7eb18dd48b..b16940a4de 100644 --- a/codex-rs/tui2/src/status/tests.rs +++ b/codex-rs/tui2/src/status/tests.rs @@ -38,9 +38,8 @@ fn test_auth_manager(config: &Config) -> AuthManager { } fn token_info_for(model_slug: &str, config: &Config, usage: &TokenUsage) -> TokenUsageInfo { - let context_window = ModelsManager::construct_model_family_offline(model_slug, config) - .context_window - .or(config.model_context_window); + let context_window = + ModelsManager::construct_model_info_offline(model_slug, config).context_window; TokenUsageInfo { total_token_usage: usage.clone(), last_token_usage: usage.clone(), From c31960b13afe061aad4ba9d4f3d87440edc41cbe Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Wed, 7 Jan 2026 10:43:10 -0800 Subject: [PATCH 6/8] remove unnecessary todos (#8842) > // todo(aibrahim): why are we passing model here while it can change? we update it on each turn with `.with_model` > //TODO(aibrahim): run CI in release mode. although it's good to have, release builds take double the time tests take. > // todo(aibrahim): make this async function we figured out another way of doing this sync --- codex-rs/core/src/codex.rs | 1 - .../core/src/context_manager/history_tests.rs | 1 - codex-rs/tui/src/chatwidget.rs | 23 ++++++++----------- codex-rs/tui2/src/chatwidget.rs | 23 ++++++++----------- 4 files changed, 20 insertions(+), 28 deletions(-) diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index a496d246eb..ac879d3e63 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -643,7 +643,6 @@ impl Session { } maybe_push_chat_wire_api_deprecation(&config, &mut post_session_configured_events); - // todo(aibrahim): why are we passing model here while it can change? let otel_manager = OtelManager::new( conversation_id, session_configuration.model.as_str(), diff --git a/codex-rs/core/src/context_manager/history_tests.rs b/codex-rs/core/src/context_manager/history_tests.rs index 1fb47e1e66..a89020f7e3 100644 --- a/codex-rs/core/src/context_manager/history_tests.rs +++ b/codex-rs/core/src/context_manager/history_tests.rs @@ -593,7 +593,6 @@ fn format_exec_output_prefers_line_marker_when_both_limits_exceeded() { assert_truncated_message_matches(&truncated, "line-0-", 17_423); } -//TODO(aibrahim): run CI in release mode. #[cfg(not(debug_assertions))] #[test] fn normalize_adds_missing_output_for_function_call() { diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index b4ae8b76a5..868319893c 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -2440,19 +2440,16 @@ impl ChatWidget { /// Open a popup to choose a quick auto model. Selecting "All models" /// opens the full picker with every available preset. pub(crate) fn open_model_popup(&mut self) { - let presets: Vec = - // todo(aibrahim): make this async function - match self.models_manager.try_list_models(&self.config) { - Ok(models) => models, - Err(_) => { - self.add_info_message( - "Models are being updated; please try /model again in a moment." - .to_string(), - None, - ); - return; - } - }; + let presets: Vec = match self.models_manager.try_list_models(&self.config) { + Ok(models) => models, + Err(_) => { + self.add_info_message( + "Models are being updated; please try /model again in a moment.".to_string(), + None, + ); + return; + } + }; self.open_model_popup_with_presets(presets); } diff --git a/codex-rs/tui2/src/chatwidget.rs b/codex-rs/tui2/src/chatwidget.rs index 05b66d3aea..bd65066300 100644 --- a/codex-rs/tui2/src/chatwidget.rs +++ b/codex-rs/tui2/src/chatwidget.rs @@ -2236,19 +2236,16 @@ impl ChatWidget { /// Open a popup to choose a quick auto model. Selecting "All models" /// opens the full picker with every available preset. pub(crate) fn open_model_popup(&mut self) { - let presets: Vec = - // todo(aibrahim): make this async function - match self.models_manager.try_list_models(&self.config) { - Ok(models) => models, - Err(_) => { - self.add_info_message( - "Models are being updated; please try /model again in a moment." - .to_string(), - None, - ); - return; - } - }; + let presets: Vec = match self.models_manager.try_list_models(&self.config) { + Ok(models) => models, + Err(_) => { + self.add_info_message( + "Models are being updated; please try /model again in a moment.".to_string(), + None, + ); + return; + } + }; self.open_model_popup_with_presets(presets); } From 018de994b011f0b3a4f2e5b02db21999b608a1d9 Mon Sep 17 00:00:00 2001 From: pakrym-oai Date: Wed, 7 Jan 2026 10:56:20 -0800 Subject: [PATCH 7/8] Stop using AuthManager as the source of codex_home (#8846) --- codex-rs/app-server/src/message_processor.rs | 1 + codex-rs/core/src/auth.rs | 21 ++-------------- codex-rs/core/src/codex.rs | 10 ++++++-- codex-rs/core/src/models_manager/manager.rs | 26 +++++++++++++------- codex-rs/core/src/thread_manager.rs | 20 +++++++++------ codex-rs/core/tests/suite/client.rs | 6 ++++- codex-rs/core/tests/suite/remote_models.rs | 2 ++ codex-rs/exec/src/lib.rs | 6 ++++- codex-rs/mcp-server/src/message_processor.rs | 6 ++++- codex-rs/tui/src/app.rs | 6 ++++- codex-rs/tui/src/chatwidget/tests.rs | 8 ++++-- codex-rs/tui2/src/app.rs | 6 ++++- codex-rs/tui2/src/chatwidget/tests.rs | 8 ++++-- 13 files changed, 79 insertions(+), 47 deletions(-) diff --git a/codex-rs/app-server/src/message_processor.rs b/codex-rs/app-server/src/message_processor.rs index 6b2ea8d0ab..731342e3c5 100644 --- a/codex-rs/app-server/src/message_processor.rs +++ b/codex-rs/app-server/src/message_processor.rs @@ -52,6 +52,7 @@ impl MessageProcessor { config.cli_auth_credentials_store_mode, ); let thread_manager = Arc::new(ThreadManager::new( + config.codex_home.clone(), auth_manager.clone(), SessionSource::VSCode, )); diff --git a/codex-rs/core/src/auth.rs b/codex-rs/core/src/auth.rs index 7d7e7b0bc2..71a542912e 100644 --- a/codex-rs/core/src/auth.rs +++ b/codex-rs/core/src/auth.rs @@ -32,11 +32,7 @@ use crate::token_data::parse_id_token; use crate::util::try_parse_error_message; use codex_client::CodexHttpClient; use codex_protocol::account::PlanType as AccountPlanType; -#[cfg(any(test, feature = "test-support"))] -use once_cell::sync::Lazy; use serde_json::Value; -#[cfg(any(test, feature = "test-support"))] -use tempfile::TempDir; use thiserror::Error; #[derive(Debug, Clone)] @@ -66,9 +62,6 @@ const REFRESH_TOKEN_UNKNOWN_MESSAGE: &str = const REFRESH_TOKEN_URL: &str = "https://auth.openai.com/oauth/token"; pub const REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR: &str = "CODEX_REFRESH_TOKEN_URL_OVERRIDE"; -#[cfg(any(test, feature = "test-support"))] -static TEST_AUTH_TEMP_DIRS: Lazy>> = Lazy::new(|| Mutex::new(Vec::new())); - #[derive(Debug, Error)] pub enum RefreshTokenError { #[error("{0}")] @@ -672,18 +665,12 @@ impl AuthManager { } #[cfg(any(test, feature = "test-support"))] - #[expect(clippy::expect_used)] /// Create an AuthManager with a specific CodexAuth, for testing only. pub fn from_auth_for_testing(auth: CodexAuth) -> Arc { let cached = CachedAuth { auth: Some(auth) }; - let temp_dir = tempfile::tempdir().expect("temp codex home"); - let codex_home = temp_dir.path().to_path_buf(); - TEST_AUTH_TEMP_DIRS - .lock() - .expect("lock test codex homes") - .push(temp_dir); + Arc::new(Self { - codex_home, + codex_home: PathBuf::from("non-existent"), inner: RwLock::new(cached), enable_codex_api_key_env: false, auth_credentials_store_mode: AuthCredentialsStoreMode::File, @@ -707,10 +694,6 @@ impl AuthManager { self.inner.read().ok().and_then(|c| c.auth.clone()) } - pub fn codex_home(&self) -> &Path { - &self.codex_home - } - /// Force a reload of the auth information from auth.json. Returns /// whether the auth value changed. pub fn reload(&self) -> bool { diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index ac879d3e63..98b34d51a2 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -3479,7 +3479,10 @@ mod tests { 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())); + let models_manager = Arc::new(ModelsManager::new( + config.codex_home.clone(), + auth_manager.clone(), + )); let agent_control = AgentControl::default(); let exec_policy = ExecPolicyManager::default(); let agent_status = Arc::new(RwLock::new(AgentStatus::PendingInit)); @@ -3570,7 +3573,10 @@ mod tests { 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())); + let models_manager = Arc::new(ModelsManager::new( + config.codex_home.clone(), + auth_manager.clone(), + )); let agent_control = AgentControl::default(); let exec_policy = ExecPolicyManager::default(); let agent_status = Arc::new(RwLock::new(AgentStatus::PendingInit)); diff --git a/codex-rs/core/src/models_manager/manager.rs b/codex-rs/core/src/models_manager/manager.rs index c9185f0ff0..87ff1b76d7 100644 --- a/codex-rs/core/src/models_manager/manager.rs +++ b/codex-rs/core/src/models_manager/manager.rs @@ -47,8 +47,7 @@ pub struct ModelsManager { impl ModelsManager { /// Construct a manager scoped to the provided `AuthManager`. - pub fn new(auth_manager: Arc) -> Self { - let codex_home = auth_manager.codex_home().to_path_buf(); + pub fn new(codex_home: PathBuf, auth_manager: Arc) -> Self { Self { local_models: builtin_model_presets(auth_manager.get_auth_mode()), remote_models: RwLock::new(Self::load_remote_models_from_file().unwrap_or_default()), @@ -62,8 +61,11 @@ impl ModelsManager { #[cfg(any(test, feature = "test-support"))] /// Construct a manager scoped to the provided `AuthManager` with a specific provider. Used for integration tests. - pub fn with_provider(auth_manager: Arc, provider: ModelProviderInfo) -> Self { - let codex_home = auth_manager.codex_home().to_path_buf(); + pub fn with_provider( + codex_home: PathBuf, + auth_manager: Arc, + provider: ModelProviderInfo, + ) -> Self { Self { local_models: builtin_model_presets(auth_manager.get_auth_mode()), remote_models: RwLock::new(Self::load_remote_models_from_file().unwrap_or_default()), @@ -422,7 +424,8 @@ mod tests { let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing()); let provider = provider_for(server.uri()); - let manager = ModelsManager::with_provider(auth_manager, provider); + let manager = + ModelsManager::with_provider(codex_home.path().to_path_buf(), auth_manager, provider); manager .refresh_available_models_with_cache(&config) @@ -481,7 +484,8 @@ mod tests { AuthCredentialsStoreMode::File, )); let provider = provider_for(server.uri()); - let manager = ModelsManager::with_provider(auth_manager, provider); + let manager = + ModelsManager::with_provider(codex_home.path().to_path_buf(), auth_manager, provider); manager .refresh_available_models_with_cache(&config) @@ -535,7 +539,8 @@ mod tests { AuthCredentialsStoreMode::File, )); let provider = provider_for(server.uri()); - let manager = ModelsManager::with_provider(auth_manager, provider); + let manager = + ModelsManager::with_provider(codex_home.path().to_path_buf(), auth_manager, provider); manager .refresh_available_models_with_cache(&config) @@ -605,7 +610,8 @@ mod tests { let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing()); let provider = provider_for(server.uri()); - let mut manager = ModelsManager::with_provider(auth_manager, provider); + let mut manager = + ModelsManager::with_provider(codex_home.path().to_path_buf(), auth_manager, provider); manager.cache_ttl = Duration::ZERO; manager @@ -653,10 +659,12 @@ mod tests { #[test] fn build_available_models_picks_default_after_hiding_hidden_models() { + let codex_home = tempdir().expect("temp dir"); let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("Test API Key")); let provider = provider_for("http://example.test".to_string()); - let mut manager = ModelsManager::with_provider(auth_manager, provider); + let mut manager = + ModelsManager::with_provider(codex_home.path().to_path_buf(), auth_manager, provider); manager.local_models = Vec::new(); let hidden_model = remote_model_with_visibility("hidden", "Hidden", 0, "hide"); diff --git a/codex-rs/core/src/thread_manager.rs b/codex-rs/core/src/thread_manager.rs index 08f432a596..742de1c530 100644 --- a/codex-rs/core/src/thread_manager.rs +++ b/codex-rs/core/src/thread_manager.rs @@ -59,14 +59,19 @@ pub(crate) struct ThreadManagerState { } impl ThreadManager { - pub fn new(auth_manager: Arc, session_source: SessionSource) -> Self { + pub fn new( + codex_home: PathBuf, + auth_manager: Arc, + session_source: SessionSource, + ) -> Self { Self { 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(), + models_manager: Arc::new(ModelsManager::new( + codex_home.clone(), + auth_manager.clone(), )), + skills_manager: Arc::new(SkillsManager::new(codex_home)), auth_manager, session_source, }), @@ -94,17 +99,16 @@ impl ThreadManager { provider: ModelProviderInfo, codex_home: PathBuf, ) -> Self { - let auth_manager = AuthManager::from_auth_for_testing_with_home(auth, codex_home); + let auth_manager = AuthManager::from_auth_for_testing(auth); Self { state: Arc::new(ThreadManagerState { threads: Arc::new(RwLock::new(HashMap::new())), models_manager: Arc::new(ModelsManager::with_provider( + codex_home.clone(), auth_manager.clone(), provider, )), - skills_manager: Arc::new(SkillsManager::new( - auth_manager.codex_home().to_path_buf(), - )), + skills_manager: Arc::new(SkillsManager::new(codex_home)), auth_manager, session_source: SessionSource::Exec, }), diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index 7e0a6e3fa0..0e5fe0d9bd 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -574,7 +574,11 @@ 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 thread_manager = ThreadManager::new(auth_manager, SessionSource::Exec); + let thread_manager = ThreadManager::new( + codex_home.path().to_path_buf(), + auth_manager, + SessionSource::Exec, + ); let NewThread { thread: codex, .. } = thread_manager .start_thread(config) .await diff --git a/codex-rs/core/tests/suite/remote_models.rs b/codex-rs/core/tests/suite/remote_models.rs index d77ec7d383..507af97f25 100644 --- a/codex-rs/core/tests/suite/remote_models.rs +++ b/codex-rs/core/tests/suite/remote_models.rs @@ -320,6 +320,7 @@ async fn remote_models_preserve_builtin_presets() -> Result<()> { ..built_in_model_providers()["openai"].clone() }; let manager = ModelsManager::with_provider( + codex_home.path().to_path_buf(), codex_core::auth::AuthManager::from_auth_for_testing(auth), provider, ); @@ -377,6 +378,7 @@ async fn remote_models_hide_picker_only_models() -> Result<()> { ..built_in_model_providers()["openai"].clone() }; let manager = ModelsManager::with_provider( + codex_home.path().to_path_buf(), codex_core::auth::AuthManager::from_auth_for_testing(auth), provider, ); diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index b82e371d23..89be1ac6c6 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -286,7 +286,11 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any true, config.cli_auth_credentials_store_mode, ); - let thread_manager = ThreadManager::new(auth_manager.clone(), SessionSource::Exec); + let thread_manager = ThreadManager::new( + config.codex_home.clone(), + auth_manager.clone(), + SessionSource::Exec, + ); let default_model = thread_manager .get_models_manager() .get_model(&config.model, &config) diff --git a/codex-rs/mcp-server/src/message_processor.rs b/codex-rs/mcp-server/src/message_processor.rs index 955dc06030..dcf5411a09 100644 --- a/codex-rs/mcp-server/src/message_processor.rs +++ b/codex-rs/mcp-server/src/message_processor.rs @@ -58,7 +58,11 @@ impl MessageProcessor { false, config.cli_auth_credentials_store_mode, ); - let thread_manager = Arc::new(ThreadManager::new(auth_manager, SessionSource::Mcp)); + let thread_manager = Arc::new(ThreadManager::new( + config.codex_home.clone(), + auth_manager, + SessionSource::Mcp, + )); Self { outgoing, initialized: false, diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index cfb50a996d..32223f18ec 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -344,7 +344,11 @@ impl App { let (app_event_tx, mut app_event_rx) = unbounded_channel(); let app_event_tx = AppEventSender::new(app_event_tx); - let thread_manager = Arc::new(ThreadManager::new(auth_manager.clone(), SessionSource::Cli)); + let thread_manager = Arc::new(ThreadManager::new( + config.codex_home.clone(), + auth_manager.clone(), + SessionSource::Cli, + )); let mut model = thread_manager .get_models_manager() .get_model(&config.model, &config) diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index 03f11c050d..cf53b7fac9 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -366,6 +366,7 @@ async fn make_chatwidget_manual( skills: None, }); let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("test")); + let codex_home = cfg.codex_home.clone(); let widget = ChatWidget { app_event_tx, codex_op_tx: op_tx, @@ -374,7 +375,7 @@ async fn make_chatwidget_manual( config: cfg, model: resolved_model.clone(), auth_manager: auth_manager.clone(), - models_manager: Arc::new(ModelsManager::new(auth_manager)), + models_manager: Arc::new(ModelsManager::new(codex_home, auth_manager)), session_header: SessionHeader::new(resolved_model), initial_user_message: None, token_info: None, @@ -415,7 +416,10 @@ async fn make_chatwidget_manual( fn set_chatgpt_auth(chat: &mut ChatWidget) { chat.auth_manager = AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing()); - chat.models_manager = Arc::new(ModelsManager::new(chat.auth_manager.clone())); + chat.models_manager = Arc::new(ModelsManager::new( + chat.config.codex_home.clone(), + chat.auth_manager.clone(), + )); } pub(crate) async fn make_chatwidget_manual_with_sender() -> ( diff --git a/codex-rs/tui2/src/app.rs b/codex-rs/tui2/src/app.rs index 5bea13a973..ead4135a4d 100644 --- a/codex-rs/tui2/src/app.rs +++ b/codex-rs/tui2/src/app.rs @@ -407,7 +407,11 @@ impl App { let (app_event_tx, mut app_event_rx) = unbounded_channel(); let app_event_tx = AppEventSender::new(app_event_tx); - let thread_manager = Arc::new(ThreadManager::new(auth_manager.clone(), SessionSource::Cli)); + let thread_manager = Arc::new(ThreadManager::new( + config.codex_home.clone(), + auth_manager.clone(), + SessionSource::Cli, + )); let mut model = thread_manager .get_models_manager() .get_model(&config.model, &config) diff --git a/codex-rs/tui2/src/chatwidget/tests.rs b/codex-rs/tui2/src/chatwidget/tests.rs index 01b6e0ed56..09f5073e75 100644 --- a/codex-rs/tui2/src/chatwidget/tests.rs +++ b/codex-rs/tui2/src/chatwidget/tests.rs @@ -363,6 +363,7 @@ async fn make_chatwidget_manual( skills: None, }); let auth_manager = AuthManager::from_auth_for_testing(CodexAuth::from_api_key("test")); + let codex_home = cfg.codex_home.clone(); let widget = ChatWidget { app_event_tx, codex_op_tx: op_tx, @@ -371,7 +372,7 @@ async fn make_chatwidget_manual( config: cfg, model: resolved_model.clone(), auth_manager: auth_manager.clone(), - models_manager: Arc::new(ModelsManager::new(auth_manager)), + models_manager: Arc::new(ModelsManager::new(codex_home, auth_manager)), session_header: SessionHeader::new(resolved_model), initial_user_message: None, token_info: None, @@ -410,7 +411,10 @@ async fn make_chatwidget_manual( fn set_chatgpt_auth(chat: &mut ChatWidget) { chat.auth_manager = AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing()); - chat.models_manager = Arc::new(ModelsManager::new(chat.auth_manager.clone())); + chat.models_manager = Arc::new(ModelsManager::new( + chat.config.codex_home.clone(), + chat.auth_manager.clone(), + )); } pub(crate) async fn make_chatwidget_manual_with_sender() -> ( From a0d1a1f8e30688f04c297dc2aeb05dc4bf2acddd Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Wed, 7 Jan 2026 13:39:05 -0800 Subject: [PATCH 8/8] fix: update resource path resolution logic so it works with Bazel --- codex-rs/exec-server/tests/common/lib.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/codex-rs/exec-server/tests/common/lib.rs b/codex-rs/exec-server/tests/common/lib.rs index a5a6a3cac5..d5e3af208a 100644 --- a/codex-rs/exec-server/tests/common/lib.rs +++ b/codex-rs/exec-server/tests/common/lib.rs @@ -34,10 +34,11 @@ where { let mcp_executable = codex_utils_cargo_bin::cargo_bin("codex-exec-mcp-server")?; let execve_wrapper = codex_utils_cargo_bin::cargo_bin("codex-execve-wrapper")?; - // `bash` requires a special lookup when running under Buck because it is a + + // `bash` requires a special lookup when running under Bazel because it is a // _resource_ rather than a binary target. - let bash = if let Some(root) = codex_utils_cargo_bin::buck_project_root()? { - root.join("codex-rs/exec-server/tests/suite/bash") + let bash = if let Some(bazel_runfiles_dir) = std::env::var_os("RUNFILES_DIR") { + PathBuf::from(bazel_runfiles_dir).join("_main/codex-rs/exec-server/tests/suite/bash") } else { Path::new(env!("CARGO_MANIFEST_DIR")) .join("..")