chore: unify conversation with thread name (#8830)

Done and verified by Codex + refactor feature of RustRover
This commit is contained in:
jif-oai
2026-01-09 11:37:07 +00:00
committed by pap
parent 137b26bd46
commit ef39b121f2
83 changed files with 1094 additions and 1203 deletions
+29 -30
View File
@@ -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<ConversationManagerState>,
/// `ThreadManagerState -> CodexThread -> Session -> SessionServices -> ThreadManagerState`.
manager: Weak<ThreadManagerState>,
}
impl AgentControl {
/// Construct a new `AgentControl` that can spawn/message agents via the given manager state.
pub(crate) fn new(manager: Weak<ConversationManagerState>) -> Self {
pub(crate) fn new(manager: Weak<ThreadManagerState>) -> 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<ConversationId> {
) -> CodexResult<ThreadId> {
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<String> {
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<Arc<ConversationManagerState>> {
self.manager.upgrade().ok_or_else(|| {
CodexErr::UnsupportedOperation("conversation manager dropped".to_string())
})
fn upgrade(&self) -> CodexResult<Arc<ThreadManagerState>> {
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<CodexConversation>) {
fn spawn_headless_drain(thread: Arc<CodexThread>) {
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);
}
+3 -3
View File
@@ -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<ReasoningEffortConfig>,
summary: ReasoningSummaryConfig,
session_source: SessionSource,
@@ -76,7 +76,7 @@ impl ModelClient {
provider: ModelProviderInfo,
effort: Option<ReasoningEffortConfig>,
summary: ReasoningSummaryConfig,
conversation_id: ConversationId,
conversation_id: ThreadId,
session_source: SessionSource,
) -> Self {
Self {
+17 -13
View File
@@ -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<Event>,
agent_status: Arc<RwLock<AgentStatus>>,
state: Mutex<SessionState>,
@@ -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(
@@ -2958,7 +2962,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"),
}))
@@ -3035,7 +3039,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"),
}))
@@ -3471,7 +3475,7 @@ mod tests {
}
fn otel_manager(
conversation_id: ConversationId,
conversation_id: ThreadId,
config: &Config,
model_family: &ModelFamily,
session_source: SessionSource,
@@ -3494,7 +3498,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()));
@@ -3585,7 +3589,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()));
+4 -4
View File
@@ -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<AuthManager>,
models_manager: Arc<ModelsManager>,
@@ -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<AuthManager>,
models_manager: Arc<ModelsManager>,
@@ -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,
@@ -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,
+2 -2
View File
@@ -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;
}
+1 -1
View File
@@ -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.
+5 -5
View File
@@ -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<Duration>),
#[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,
+15 -7
View File
@@ -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;
+7 -5
View File
@@ -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<u64> {
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);
+1 -1
View File
@@ -33,7 +33,7 @@ fn map_rollout_io_error(io_err: &std::io::Error, codex_home: &Path) -> Option<Co
sessions_dir.display()
),
ErrorKind::InvalidData | ErrorKind::InvalidInput => 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!(
+25 -18
View File
@@ -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<ConversationItem>,
pub struct ThreadsPage {
/// Thread summaries ordered newest first.
pub items: Vec<ThreadItem>,
/// Opaque pagination token to resume after the last item, or `None` if end.
pub next_cursor: Option<Cursor>,
/// 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<String>,
}
#[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<serde_json::Value>,
@@ -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<ConversationsPage> {
) -> io::Result<ThreadsPage> {
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-<uuid>.jsonl`
/// Returned newest (latest) first.
@@ -148,8 +155,8 @@ async fn traverse_directories_for_paths(
anchor: Option<Cursor>,
allowed_sources: &[SessionSource],
provider_matcher: Option<&ProviderMatcher<'_>>,
) -> io::Result<ConversationsPage> {
let mut items: Vec<ConversationItem> = Vec::with_capacity(page_size);
) -> io::Result<ThreadsPage> {
let mut items: Vec<ThreadItem> = 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<Cursor> {
Some(Cursor::new(ts, uuid))
}
fn build_next_cursor(items: &[ConversationItem]) -> Option<Cursor> {
fn build_next_cursor(items: &[ThreadItem]) -> Option<Cursor> {
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<Option<String>> {
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<Option<PathBuf>> {
+3 -1
View File
@@ -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;
+19 -22
View File
@@ -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<String>,
source: SessionSource,
},
@@ -79,7 +79,7 @@ enum RolloutCmd {
impl RolloutRecorderParams {
pub fn new(
conversation_id: ConversationId,
conversation_id: ThreadId,
instructions: Option<String>,
source: SessionSource,
) -> Self {
@@ -96,16 +96,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<ConversationsPage> {
get_conversations(
) -> std::io::Result<ThreadsPage> {
get_threads(
codex_home,
page_size,
cursor,
@@ -231,7 +231,7 @@ impl RolloutRecorder {
}
let mut items: Vec<RolloutItem> = Vec::new();
let mut conversation_id: Option<ConversationId> = None;
let mut thread_id: Option<ThreadId> = None;
for line in text.lines() {
if line.trim().is_empty() {
continue;
@@ -249,9 +249,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));
}
@@ -275,12 +275,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);
@@ -318,16 +318,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<LogFileInfo> {
fn create_log_file(config: &Config, conversation_id: ThreadId) -> std::io::Result<LogFileInfo> {
// 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}")))?;
+39 -39
View File
@@ -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<Option<String>> =
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<Option<String>> =
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 {
@@ -625,7 +625,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,
@@ -664,7 +664,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,
@@ -702,15 +702,15 @@ async fn test_stable_ordering_same_second_pagination() {
let updated_page1: Vec<Option<String>> =
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()),
@@ -723,7 +723,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(),
@@ -741,8 +741,8 @@ async fn test_stable_ordering_same_second_pagination() {
.join(format!("rollout-2025-07-01T00-00-00-{u1}.jsonl"));
let updated_page2: Vec<Option<String>> =
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()),
@@ -781,7 +781,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,
@@ -802,7 +802,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
@@ -856,7 +856,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,
@@ -881,7 +881,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,
@@ -901,7 +901,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,
@@ -912,7 +912,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(())
+2 -2
View File
@@ -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(),
@@ -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<CodexConversation>,
pub struct NewThread {
pub thread_id: ThreadId,
pub thread: Arc<CodexThread>,
pub session_configured: SessionConfiguredEvent,
}
/// [`ConversationManager`] is responsible for creating conversations and
/// maintaining them in memory.
pub struct ConversationManager {
state: Arc<ConversationManagerState>,
/// [`ThreadManager`] is responsible for creating threads and maintaining
/// them in memory.
pub struct ThreadManager {
state: Arc<ThreadManagerState>,
#[cfg(any(test, feature = "test-support"))]
_test_codex_home_guard: Option<TempDir>,
}
/// 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<RwLock<HashMap<ConversationId, Arc<CodexConversation>>>>,
pub(crate) struct ThreadManagerState {
threads: Arc<RwLock<HashMap<ThreadId, Arc<CodexThread>>>>,
auth_manager: Arc<AuthManager>,
models_manager: Arc<ModelsManager>,
skills_manager: Arc<SkillsManager>,
session_source: SessionSource,
}
impl ConversationManager {
impl ThreadManager {
pub fn new(auth_manager: Arc<AuthManager>, 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<Arc<CodexConversation>> {
self.state.get_conversation(conversation_id).await
pub async fn get_thread(&self, thread_id: ThreadId) -> CodexResult<Arc<CodexThread>> {
self.state.get_thread(thread_id).await
}
pub async fn new_conversation(&self, config: Config) -> CodexResult<NewConversation> {
pub async fn start_thread(&self, config: Config) -> CodexResult<NewThread> {
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<AuthManager>,
) -> CodexResult<NewThread> {
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<AuthManager>,
) -> CodexResult<NewThread> {
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<Arc<CodexThread>> {
self.get_thread(thread_id).await
}
#[deprecated(note = "use start_thread")]
pub async fn new_conversation(&self, config: Config) -> CodexResult<NewThread> {
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<AuthManager>,
) -> CodexResult<NewConversation> {
let initial_history = RolloutRecorder::get_rollout_history(&rollout_path).await?;
self.resume_conversation_with_history(config, initial_history, auth_manager)
) -> CodexResult<NewThread> {
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<AuthManager>,
) -> CodexResult<NewConversation> {
self.state
.spawn_conversation(config, initial_history, auth_manager, self.agent_control())
) -> CodexResult<NewThread> {
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<CodexConversation>`, 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<Arc<CodexConversation>> {
self.state
.conversations
.write()
.await
.remove(conversation_id)
#[deprecated(note = "use remove_thread")]
pub async fn remove_conversation(&self, thread_id: &ThreadId) -> Option<Arc<CodexThread>> {
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<NewConversation> {
) -> CodexResult<NewThread> {
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<CodexThread>`, 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<Arc<CodexThread>> {
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<NewThread> {
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<Arc<CodexConversation>> {
let conversations = self.conversations.read().await;
conversations
.get(&conversation_id)
impl ThreadManagerState {
pub(crate) async fn get_thread(&self, thread_id: ThreadId) -> CodexResult<Arc<CodexThread>> {
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<String> {
self.get_conversation(conversation_id)
.await?
.submit(op)
.await
pub(crate) async fn send_op(&self, thread_id: ThreadId, op: Op) -> CodexResult<String> {
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<NewConversation> {
self.spawn_conversation(
) -> CodexResult<NewThread> {
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<AuthManager>,
agent_control: AgentControl,
) -> CodexResult<NewConversation> {
) -> CodexResult<NewThread> {
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<NewConversation> {
thread_id: ThreadId,
) -> CodexResult<NewThread> {
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,
})
}
+1 -1
View File
@@ -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 {
@@ -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<ResponseItem>) -> 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(
+2 -2
View File
@@ -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<ResponseEvent> {
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());
+4 -7
View File
@@ -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<std::path::Path>, id: &str) ->
.collect()
}
pub async fn wait_for_event<F>(
codex: &CodexConversation,
predicate: F,
) -> codex_core::protocol::EventMsg
pub async fn wait_for_event<F>(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<T, F>(codex: &CodexConversation, matcher: F) -> T
pub async fn wait_for_event_match<T, F>(codex: &CodexThread, matcher: F) -> T
where
F: Fn(&codex_core::protocol::EventMsg) -> Option<T>,
{
@@ -190,7 +187,7 @@ where
}
pub async fn wait_for_event_with_timeout<F>(
codex: &CodexConversation,
codex: &CodexThread,
mut predicate: F,
wait_time: tokio::time::Duration,
) -> codex_core::protocol::EventMsg
+11 -15
View File
@@ -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<PathBuf>,
) -> anyhow::Result<TestCodex> {
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<TempDir>,
pub cwd: Arc<TempDir>,
pub codex: Arc<CodexConversation>,
pub codex: Arc<CodexThread>,
pub session_configured: SessionConfiguredEvent,
pub config: Config,
pub conversation_manager: Arc<ConversationManager>,
pub thread_manager: Arc<ThreadManager>,
}
impl TestCodex {
+4 -4
View File
@@ -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 =
+1 -1
View File
@@ -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,
+57 -61
View File
@@ -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");
+31 -42
View File
@@ -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 {
@@ -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<CodexConversation>) {
) -> (TempDir, Config, ThreadManager, Arc<CodexThread>) {
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<CodexConversation>, text: &str) {
async fn user_turn(conversation: &Arc<CodexThread>, text: &str) {
conversation
.submit(Op::UserInput {
items: vec![UserInput::Text { text: text.into() }],
@@ -891,7 +891,7 @@ async fn user_turn(conversation: &Arc<CodexConversation>, text: &str) {
wait_for_event(conversation, |ev| matches!(ev, EventMsg::TaskComplete(_))).await;
}
async fn compact_conversation(conversation: &Arc<CodexConversation>) {
async fn compact_conversation(conversation: &Arc<CodexThread>) {
conversation
.submit(Op::Compact)
.await
@@ -904,34 +904,34 @@ async fn compact_conversation(conversation: &Arc<CodexConversation>) {
wait_for_event(conversation, |ev| matches!(ev, EventMsg::TaskComplete(_))).await;
}
async fn fetch_conversation_path(conversation: &Arc<CodexConversation>) -> std::path::PathBuf {
async fn fetch_conversation_path(conversation: &Arc<CodexThread>) -> std::path::PathBuf {
conversation.rollout_path()
}
async fn resume_conversation(
manager: &ConversationManager,
manager: &ThreadManager,
config: &Config,
path: std::path::PathBuf,
) -> Arc<CodexConversation> {
) -> Arc<CodexThread> {
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<CodexConversation> {
let NewConversation { conversation, .. } = manager
.fork_conversation(nth_user_message, config.clone(), path)
) -> Arc<CodexThread> {
let NewThread { thread, .. } = manager
.fork_thread(nth_user_message, config.clone(), path)
.await
.expect("fork conversation");
conversation
thread
}
@@ -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");
+3 -3
View File
@@ -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(),
);
+1 -1
View File
@@ -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;
+9 -9
View File
@@ -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 {
+2 -2
View File
@@ -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
+13 -15
View File
@@ -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<CodexConversation>,
codex: Arc<CodexThread>,
cwd: Arc<TempDir>,
config: Config,
conversation_manager: Arc<ConversationManager>,
thread_manager: Arc<ThreadManager>,
}
// 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,
})
}
+10 -7
View File
@@ -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");
+12 -12
View File
@@ -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<F>(
server: &MockServer,
codex_home: &TempDir,
mutator: F,
) -> Arc<CodexConversation>
) -> Arc<CodexThread>
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<F>(
codex_home: &TempDir,
resume_path: std::path::PathBuf,
mutator: F,
) -> Arc<CodexConversation>
) -> Arc<CodexThread>
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
}
@@ -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();
+4 -4
View File
@@ -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<CodexConversation>) -> Result<UndoCompletedEvent> {
async fn invoke_undo(codex: &Arc<CodexThread>) -> Result<UndoCompletedEvent> {
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<CodexConversation>) -> Result<UndoCompletedEven
Ok(event)
}
async fn expect_successful_undo(codex: &Arc<CodexConversation>) -> Result<UndoCompletedEvent> {
async fn expect_successful_undo(codex: &Arc<CodexThread>) -> Result<UndoCompletedEvent> {
let event = invoke_undo(codex).await?;
assert!(
event.success,
@@ -128,7 +128,7 @@ async fn expect_successful_undo(codex: &Arc<CodexConversation>) -> Result<UndoCo
Ok(event)
}
async fn expect_failed_undo(codex: &Arc<CodexConversation>) -> Result<UndoCompletedEvent> {
async fn expect_failed_undo(codex: &Arc<CodexThread>) -> Result<UndoCompletedEvent> {
let event = invoke_undo(codex).await?;
assert!(
!event.success,
+8 -14
View File
@@ -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");