From dba5a93873dd45979eb232a0cdbed7ef30c1dc45 Mon Sep 17 00:00:00 2001 From: pap Date: Fri, 30 Jan 2026 18:20:18 +0000 Subject: [PATCH] setting thread_name in db --- codex-rs/core/src/codex.rs | 7 +++ codex-rs/core/tests/suite/sqlite_state.rs | 50 +++++++++++++++++++ codex-rs/exec/src/lib.rs | 17 ++++++- .../state/migrations/0004_threads_name.sql | 1 + codex-rs/state/src/extract.rs | 14 +++--- codex-rs/state/src/model/thread_metadata.rs | 18 +++---- codex-rs/state/src/runtime.rs | 45 +++++++++++++++-- codex-rs/tui/src/lib.rs | 24 ++++++++- 8 files changed, 151 insertions(+), 25 deletions(-) create mode 100644 codex-rs/state/migrations/0004_threads_name.sql diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 7eefd04ec4..fd6536c056 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -3033,6 +3033,13 @@ mod handlers { sess.send_event_raw(event).await; return; } + if let Some(state_db) = sess.services.state_db.as_ref() + && let Err(err) = state_db + .update_thread_name(sess.conversation_id, name.as_str()) + .await + { + warn!("Failed to update thread name in state db: {err}"); + } { let mut state = sess.state.lock().await; diff --git a/codex-rs/core/tests/suite/sqlite_state.rs b/codex-rs/core/tests/suite/sqlite_state.rs index 2582f90cbe..19c06b6b58 100644 --- a/codex-rs/core/tests/suite/sqlite_state.rs +++ b/codex-rs/core/tests/suite/sqlite_state.rs @@ -2,6 +2,7 @@ use anyhow::Result; use codex_core::features::Feature; use codex_protocol::ThreadId; use codex_protocol::protocol::EventMsg; +use codex_protocol::protocol::Op; use codex_protocol::protocol::RolloutItem; use codex_protocol::protocol::RolloutLine; use codex_protocol::protocol::SessionMeta; @@ -17,6 +18,7 @@ use core_test_support::responses::ev_response_created; use core_test_support::responses::mount_sse_sequence; use core_test_support::responses::start_mock_server; use core_test_support::test_codex::test_codex; +use core_test_support::wait_for_event; use pretty_assertions::assert_eq; use serde_json::json; use std::fs; @@ -278,3 +280,51 @@ async fn tool_call_logs_include_thread_id() -> Result<()> { Ok(()) } + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn thread_rename_updates_state_db_name() -> Result<()> { + let server = start_mock_server().await; + let mut builder = test_codex().with_config(|config| { + config.features.enable(Feature::Sqlite); + }); + let test = builder.build(&server).await?; + + let db_path = test.config.codex_home.join(STATE_DB_FILENAME); + for _ in 0..100 { + if tokio::fs::try_exists(&db_path).await.unwrap_or(false) { + break; + } + tokio::time::sleep(Duration::from_millis(25)).await; + } + + let db = test.codex.state_db().expect("state db enabled"); + let thread_id = test.session_configured.session_id; + let new_name = "renamed thread"; + + test.codex + .submit(Op::SetThreadName { + name: new_name.to_string(), + }) + .await?; + wait_for_event(&test.codex, |event| { + matches!(event, EventMsg::ThreadNameUpdated(_)) + }) + .await; + + let mut metadata = None; + for _ in 0..100 { + metadata = db.get_thread(thread_id).await?; + if metadata + .as_ref() + .is_some_and(|entry| entry.name == new_name) + { + break; + } + tokio::time::sleep(Duration::from_millis(25)).await; + } + + let metadata = metadata.expect("thread should exist in state db"); + assert_eq!(metadata.name, new_name); + + Ok(()) +} diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index f7176c13b0..9df4c36f6f 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -67,6 +67,7 @@ use crate::event_processor::EventProcessor; use codex_core::default_client::set_default_originator; use codex_core::find_thread_path_by_id_str; use codex_core::find_thread_path_by_name_str; +use codex_core::state_db; enum InitialOperation { UserTurn { @@ -625,8 +626,20 @@ async fn resolve_resume_path( let path = find_thread_path_by_id_str(&config.codex_home, id_str).await?; Ok(path) } else { - let path = find_thread_path_by_name_str(&config.codex_home, id_str).await?; - Ok(path) + let db_path = if let Some(db) = state_db::get_state_db(config, None).await { + db.find_rollout_path_by_name(id_str, Some(false)) + .await + .ok() + .flatten() + } else { + None + }; + match db_path { + Some(path) => Ok(Some(path)), + None => find_thread_path_by_name_str(&config.codex_home, id_str) + .await + .map_err(Into::into), + } } } else { Ok(None) diff --git a/codex-rs/state/migrations/0004_threads_name.sql b/codex-rs/state/migrations/0004_threads_name.sql new file mode 100644 index 0000000000..633d4938d7 --- /dev/null +++ b/codex-rs/state/migrations/0004_threads_name.sql @@ -0,0 +1 @@ +ALTER TABLE threads RENAME COLUMN title TO name; diff --git a/codex-rs/state/src/extract.rs b/codex-rs/state/src/extract.rs index 9c8ce21556..9a008d8e01 100644 --- a/codex-rs/state/src/extract.rs +++ b/codex-rs/state/src/extract.rs @@ -60,8 +60,8 @@ fn apply_event_msg(metadata: &mut ThreadMetadata, event: &EventMsg) { } EventMsg::UserMessage(user) => { metadata.has_user_event = true; - if metadata.title.is_empty() { - metadata.title = strip_user_message_prefix(user.message.as_str()).to_string(); + if metadata.name.is_empty() { + metadata.name = strip_user_message_prefix(user.message.as_str()).to_string(); } } _ => {} @@ -71,8 +71,8 @@ fn apply_event_msg(metadata: &mut ThreadMetadata, event: &EventMsg) { fn apply_response_item(metadata: &mut ThreadMetadata, item: &ResponseItem) { if let Some(text) = extract_user_message_text(item) { metadata.has_user_event = true; - if metadata.title.is_empty() { - metadata.title = text; + if metadata.name.is_empty() { + metadata.name = text; } } } @@ -163,7 +163,7 @@ mod tests { source: "cli".to_string(), model_provider: "openai".to_string(), cwd: PathBuf::from("/tmp"), - title: "hello".to_string(), + name: "hello".to_string(), sandbox_policy: "read-only".to_string(), approval_mode: "on-request".to_string(), tokens_used: 1, @@ -175,8 +175,8 @@ mod tests { }; let mut other = base.clone(); other.tokens_used = 2; - other.title = "world".to_string(); + other.name = "world".to_string(); let diffs = base.diff_fields(&other); - assert_eq!(diffs, vec!["title", "tokens_used"]); + assert_eq!(diffs, vec!["name", "tokens_used"]); } } diff --git a/codex-rs/state/src/model/thread_metadata.rs b/codex-rs/state/src/model/thread_metadata.rs index 7d475efffc..5947a5a300 100644 --- a/codex-rs/state/src/model/thread_metadata.rs +++ b/codex-rs/state/src/model/thread_metadata.rs @@ -66,8 +66,8 @@ pub struct ThreadMetadata { pub model_provider: String, /// The working directory for the thread. pub cwd: PathBuf, - /// A best-effort thread title. - pub title: String, + /// Thread name. + pub name: String, /// The sandbox policy (stringified enum). pub sandbox_policy: String, /// The approval mode (stringified enum). @@ -163,7 +163,7 @@ impl ThreadMetadataBuilder { .clone() .unwrap_or_else(|| default_provider.to_string()), cwd: self.cwd.clone(), - title: String::new(), + name: String::new(), sandbox_policy, approval_mode, tokens_used: 0, @@ -201,8 +201,8 @@ impl ThreadMetadata { if self.cwd != other.cwd { diffs.push("cwd"); } - if self.title != other.title { - diffs.push("title"); + if self.name != other.name { + diffs.push("name"); } if self.sandbox_policy != other.sandbox_policy { diffs.push("sandbox_policy"); @@ -245,7 +245,7 @@ pub(crate) struct ThreadRow { source: String, model_provider: String, cwd: String, - title: String, + name: String, sandbox_policy: String, approval_mode: String, tokens_used: i64, @@ -266,7 +266,7 @@ impl ThreadRow { source: row.try_get("source")?, model_provider: row.try_get("model_provider")?, cwd: row.try_get("cwd")?, - title: row.try_get("title")?, + name: row.try_get("name")?, sandbox_policy: row.try_get("sandbox_policy")?, approval_mode: row.try_get("approval_mode")?, tokens_used: row.try_get("tokens_used")?, @@ -291,7 +291,7 @@ impl TryFrom for ThreadMetadata { source, model_provider, cwd, - title, + name, sandbox_policy, approval_mode, tokens_used, @@ -309,7 +309,7 @@ impl TryFrom for ThreadMetadata { source, model_provider, cwd: PathBuf::from(cwd), - title, + name, sandbox_policy, approval_mode, tokens_used, diff --git a/codex-rs/state/src/runtime.rs b/codex-rs/state/src/runtime.rs index e0805e5b65..fad7311ae6 100644 --- a/codex-rs/state/src/runtime.rs +++ b/codex-rs/state/src/runtime.rs @@ -95,7 +95,7 @@ SELECT source, model_provider, cwd, - title, + name, sandbox_policy, approval_mode, tokens_used, @@ -139,6 +139,31 @@ WHERE id = ? .map(PathBuf::from)) } + /// Find a rollout path by thread name using the underlying database. + pub async fn find_rollout_path_by_name( + &self, + name: &str, + archived_only: Option, + ) -> anyhow::Result> { + let mut builder = + QueryBuilder::::new("SELECT rollout_path FROM threads WHERE name = "); + builder.push_bind(name); + match archived_only { + Some(true) => { + builder.push(" AND archived = 1"); + } + Some(false) => { + builder.push(" AND archived = 0"); + } + None => {} + } + builder.push(" ORDER BY updated_at DESC, id DESC LIMIT 1"); + let row = builder.build().fetch_optional(self.pool.as_ref()).await?; + Ok(row + .and_then(|r| r.try_get::("rollout_path").ok()) + .map(PathBuf::from)) + } + /// List threads using the underlying database. pub async fn list_threads( &self, @@ -161,7 +186,7 @@ SELECT source, model_provider, cwd, - title, + name, sandbox_policy, approval_mode, tokens_used, @@ -315,7 +340,7 @@ INSERT INTO threads ( source, model_provider, cwd, - title, + name, sandbox_policy, approval_mode, tokens_used, @@ -333,7 +358,7 @@ ON CONFLICT(id) DO UPDATE SET source = excluded.source, model_provider = excluded.model_provider, cwd = excluded.cwd, - title = excluded.title, + name = excluded.name, sandbox_policy = excluded.sandbox_policy, approval_mode = excluded.approval_mode, tokens_used = excluded.tokens_used, @@ -352,7 +377,7 @@ ON CONFLICT(id) DO UPDATE SET .bind(metadata.source.as_str()) .bind(metadata.model_provider.as_str()) .bind(metadata.cwd.display().to_string()) - .bind(metadata.title.as_str()) + .bind(metadata.name.as_str()) .bind(metadata.sandbox_policy.as_str()) .bind(metadata.approval_mode.as_str()) .bind(metadata.tokens_used) @@ -367,6 +392,16 @@ ON CONFLICT(id) DO UPDATE SET Ok(()) } + /// Update the thread name for an existing thread. + pub async fn update_thread_name(&self, thread_id: ThreadId, name: &str) -> anyhow::Result<()> { + sqlx::query("UPDATE threads SET name = ? WHERE id = ?") + .bind(name) + .bind(thread_id.to_string()) + .execute(self.pool.as_ref()) + .await?; + Ok(()) + } + /// Apply rollout items incrementally using the underlying database. pub async fn apply_rollout_items( &self, diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index a8c6e2b88b..9306713c93 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -32,6 +32,7 @@ use codex_core::find_thread_path_by_name_str; use codex_core::path_utils; use codex_core::protocol::AskForApproval; use codex_core::read_session_meta_line; +use codex_core::state_db; use codex_core::terminal::Multiplexer; use codex_core::windows_sandbox::WindowsSandboxLevelExt; use codex_protocol::config_types::AltScreenMode; @@ -400,6 +401,25 @@ pub async fn run_main( .map_err(|err| std::io::Error::other(err.to_string())) } +async fn find_rollout_path_by_name( + config: &Config, + name: &str, + archived_only: Option, +) -> std::io::Result> { + if let Some(db) = state_db::get_state_db(config, None).await { + if let Some(path) = db + .find_rollout_path_by_name(name, archived_only) + .await + .ok() + .flatten() + { + return Ok(Some(path)); + } + } + + find_thread_path_by_name_str(&config.codex_home, name).await +} + async fn run_ratatui_app( cli: Cli, initial_config: Config, @@ -534,7 +554,7 @@ async fn run_ratatui_app( let path = if is_uuid { find_thread_path_by_id_str(&config.codex_home, id_str).await? } else { - find_thread_path_by_name_str(&config.codex_home, id_str).await? + find_rollout_path_by_name(&config, id_str, Some(false)).await? }; match path { Some(path) => resume_picker::SessionSelection::Fork(path), @@ -590,7 +610,7 @@ async fn run_ratatui_app( let path = if is_uuid { find_thread_path_by_id_str(&config.codex_home, id_str).await? } else { - find_thread_path_by_name_str(&config.codex_home, id_str).await? + find_rollout_path_by_name(&config, id_str, Some(false)).await? }; match path { Some(path) => resume_picker::SessionSelection::Resume(path),