chore: unify conversation with thread name (#8830)

Done and verified by Codex + refactor feature of RustRover
This commit is contained in:
jif-oai authored and GitHub committed 2026-01-07 17:04:53 +00:00
1 parent 0d788e6263
commit 116059c3a0
83 files changed
+1094 -1203

No files matched your search

+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,
},
@@ -74,7 +74,7 @@ enum RolloutCmd {
impl RolloutRecorderParams {
pub fn new(
conversation_id: ConversationId,
conversation_id: ThreadId,
instructions: Option<String>,
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<ConversationsPage> {
get_conversations(
) -> std::io::Result<ThreadsPage> {
get_threads(
codex_home,
page_size,
cursor,
@@ -215,7 +215,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;
@@ -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<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 {
@@ -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<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()),
@@ -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<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()),
@@ -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(())