setting thread_name in db

This commit is contained in:
pap
2026-01-30 18:20:18 +00:00
parent 09d25e91e9
commit dba5a93873
8 changed files with 151 additions and 25 deletions

View File

@@ -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;

View File

@@ -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(())
}

View File

@@ -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)

View File

@@ -0,0 +1 @@
ALTER TABLE threads RENAME COLUMN title TO name;

View File

@@ -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"]);
}
}

View File

@@ -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<ThreadRow> for ThreadMetadata {
source,
model_provider,
cwd,
title,
name,
sandbox_policy,
approval_mode,
tokens_used,
@@ -309,7 +309,7 @@ impl TryFrom<ThreadRow> for ThreadMetadata {
source,
model_provider,
cwd: PathBuf::from(cwd),
title,
name,
sandbox_policy,
approval_mode,
tokens_used,

View File

@@ -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<bool>,
) -> anyhow::Result<Option<PathBuf>> {
let mut builder =
QueryBuilder::<Sqlite>::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::<String, _>("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,

View File

@@ -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<bool>,
) -> std::io::Result<Option<PathBuf>> {
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),