diff --git a/codex-rs/state/src/runtime/threads.rs b/codex-rs/state/src/runtime/threads.rs index 4556be32e2..8e69cda5ce 100644 --- a/codex-rs/state/src/runtime/threads.rs +++ b/codex-rs/state/src/runtime/threads.rs @@ -377,7 +377,7 @@ ON CONFLICT(child_thread_id) DO NOTHING search_term: None, }, ); - builder.push(" AND COALESCE(NULLIF(threads.name, ''), threads.title) = "); + builder.push(" AND COALESCE(threads.name, threads.title) = "); builder.push_bind(title); if let Some(cwd) = cwd { builder.push(" AND threads.cwd = "); @@ -658,11 +658,19 @@ ON CONFLICT(id) DO NOTHING thread_id: ThreadId, name: Option<&str>, ) -> anyhow::Result { - let result = sqlx::query("UPDATE threads SET name = ? WHERE id = ?") - .bind(name) + let result = match name { + Some(name) => sqlx::query("UPDATE threads SET name = ? WHERE id = ?") + .bind(name) + .bind(thread_id.to_string()) + .execute(self.pool.as_ref()) + .await?, + None => sqlx::query( + "UPDATE threads SET title = CASE WHEN name IS NOT NULL AND title = name THEN '' ELSE title END, name = '' WHERE id = ?", + ) .bind(thread_id.to_string()) .execute(self.pool.as_ref()) - .await?; + .await?, + }; Ok(result.rows_affected() > 0) } diff --git a/codex-rs/thread-store/src/local/helpers.rs b/codex-rs/thread-store/src/local/helpers.rs index 870bdb3250..1fd5e94842 100644 --- a/codex-rs/thread-store/src/local/helpers.rs +++ b/codex-rs/thread-store/src/local/helpers.rs @@ -176,15 +176,21 @@ pub(super) fn permission_profile_to_metadata_value( } } -pub(super) fn thread_metadata_name(metadata: &ThreadMetadata) -> Option { - let explicit_name = metadata - .name - .as_deref() - .map(str::trim) - .filter(|name| !name.is_empty()) - .map(str::to_string); - if explicit_name.is_some() { - return explicit_name; +#[derive(Clone)] +pub(super) enum ThreadMetadataName { + Explicit(String), + Legacy(String), + Cleared, +} + +pub(super) fn thread_metadata_name(metadata: &ThreadMetadata) -> Option { + if let Some(name) = metadata.name.as_deref() { + let name = name.trim(); + return Some(if name.is_empty() { + ThreadMetadataName::Cleared + } else { + ThreadMetadataName::Explicit(name.to_string()) + }); } // Preserve display names from rows written before explicit names had their own column. @@ -194,7 +200,15 @@ pub(super) fn thread_metadata_name(metadata: &ThreadMetadata) -> Option { None } else { - Some(legacy_title.to_string()) + Some(ThreadMetadataName::Legacy(legacy_title.to_string())) + } +} + +pub(super) fn apply_thread_metadata_name(thread: &mut StoredThread, name: ThreadMetadataName) { + match name { + ThreadMetadataName::Explicit(name) => thread.name = Some(name), + ThreadMetadataName::Legacy(name) => set_thread_name_from_title(thread, name), + ThreadMetadataName::Cleared => thread.name = None, } } diff --git a/codex-rs/thread-store/src/local/list_threads.rs b/codex-rs/thread-store/src/local/list_threads.rs index 9c9b7d94ed..3850fc1fa7 100644 --- a/codex-rs/thread-store/src/local/list_threads.rs +++ b/codex-rs/thread-store/src/local/list_threads.rs @@ -8,7 +8,8 @@ use codex_rollout::find_thread_names_by_ids; use codex_rollout::parse_cursor; use super::LocalThreadStore; -use super::helpers::set_thread_name_from_title; +use super::helpers::ThreadMetadataName; +use super::helpers::apply_thread_metadata_name; use super::helpers::stored_thread_from_rollout_item; use super::helpers::thread_metadata_name; use crate::ListThreadsParams; @@ -80,14 +81,14 @@ pub(super) async fn list_threads( .iter() .map(|thread| thread.thread_id) .collect::>(); - let mut names = HashMap::::with_capacity(thread_ids.len()); + let mut names = HashMap::::with_capacity(thread_ids.len()); if let Some(state_db_ctx) = store.state_db().await { for &thread_id in &thread_ids { let Ok(Some(metadata)) = state_db_ctx.get_thread(thread_id).await else { continue; }; - if let Some(title) = thread_metadata_name(&metadata) { - names.insert(thread_id, title); + if let Some(name) = thread_metadata_name(&metadata) { + names.insert(thread_id, name); } } } @@ -96,12 +97,14 @@ pub(super) async fn list_threads( find_thread_names_by_ids(store.config.codex_home.as_path(), &thread_ids).await { for (thread_id, title) in legacy_names { - names.entry(thread_id).or_insert(title); + names + .entry(thread_id) + .or_insert(ThreadMetadataName::Legacy(title)); } } for thread in &mut items { - if let Some(title) = names.get(&thread.thread_id).cloned() { - set_thread_name_from_title(thread, title); + if let Some(name) = names.get(&thread.thread_id).cloned() { + apply_thread_metadata_name(thread, name); } } diff --git a/codex-rs/thread-store/src/local/read_thread.rs b/codex-rs/thread-store/src/local/read_thread.rs index d5e50fa965..016b8bca61 100644 --- a/codex-rs/thread-store/src/local/read_thread.rs +++ b/codex-rs/thread-store/src/local/read_thread.rs @@ -13,6 +13,8 @@ use codex_rollout::read_thread_item_from_rollout; use codex_state::ThreadMetadata; use super::LocalThreadStore; +use super::helpers::ThreadMetadataName; +use super::helpers::apply_thread_metadata_name; use super::helpers::git_info_from_parts; use super::helpers::permission_profile_from_metadata_value; use super::helpers::rollout_path_is_archived; @@ -118,7 +120,7 @@ pub(super) async fn read_thread_by_rollout_path( if let Some(metadata) = read_sqlite_metadata(store, thread.thread_id).await { thread.recency_at = metadata.recency_at; if let Some(name) = thread_metadata_name(&metadata) { - set_thread_name_from_title(&mut thread, name); + apply_thread_metadata_name(&mut thread, name); } let existing_git_info = thread.git_info.take(); let (fallback_sha, fallback_branch, fallback_origin_url) = match existing_git_info { @@ -307,7 +309,8 @@ async fn stored_thread_from_sqlite_metadata( metadata: ThreadMetadata, ) -> StoredThread { let name = match thread_metadata_name(&metadata) { - Some(title) => Some(title), + Some(ThreadMetadataName::Explicit(name) | ThreadMetadataName::Legacy(name)) => Some(name), + Some(ThreadMetadataName::Cleared) => None, None => find_thread_name_by_id(store.config.codex_home.as_path(), &metadata.id) .await .ok() diff --git a/codex-rs/thread-store/src/local/search_threads.rs b/codex-rs/thread-store/src/local/search_threads.rs index d5acc9ad51..131f13ca62 100644 --- a/codex-rs/thread-store/src/local/search_threads.rs +++ b/codex-rs/thread-store/src/local/search_threads.rs @@ -10,7 +10,8 @@ use codex_rollout::parse_cursor; use codex_rollout::search_rollout_matches; use super::LocalThreadStore; -use super::helpers::set_thread_name_from_title; +use super::helpers::ThreadMetadataName; +use super::helpers::apply_thread_metadata_name; use super::helpers::stored_thread_from_rollout_item; use super::helpers::thread_metadata_name; use super::list_threads::list_rollout_threads; @@ -205,14 +206,14 @@ async fn set_thread_search_result_names( .iter() .map(|item| item.thread.thread_id) .collect::>(); - let mut names = HashMap::::with_capacity(thread_ids.len()); + let mut names = HashMap::::with_capacity(thread_ids.len()); if let Some(state_db_ctx) = store.state_db().await { for &thread_id in &thread_ids { let Ok(Some(metadata)) = state_db_ctx.get_thread(thread_id).await else { continue; }; - if let Some(title) = thread_metadata_name(&metadata) { - names.insert(thread_id, title); + if let Some(name) = thread_metadata_name(&metadata) { + names.insert(thread_id, name); } } } @@ -221,12 +222,14 @@ async fn set_thread_search_result_names( find_thread_names_by_ids(store.config.codex_home.as_path(), &thread_ids).await { for (thread_id, title) in legacy_names { - names.entry(thread_id).or_insert(title); + names + .entry(thread_id) + .or_insert(ThreadMetadataName::Legacy(title)); } } for item in items { - if let Some(title) = names.get(&item.thread.thread_id).cloned() { - set_thread_name_from_title(&mut item.thread, title); + if let Some(name) = names.get(&item.thread.thread_id).cloned() { + apply_thread_metadata_name(&mut item.thread, name); } } }