separate thread names and make metadata repair atomic

This commit is contained in:
anais
2026-06-22 15:36:02 -07:00
parent 372b5ac7c2
commit 591b7a47e7
16 changed files with 343 additions and 79 deletions
+7 -7
View File
@@ -176,13 +176,13 @@ pub(super) fn permission_profile_to_metadata_value(
}
}
pub(super) fn distinct_thread_metadata_title(metadata: &ThreadMetadata) -> Option<String> {
let title = metadata.title.trim();
if title.is_empty() || metadata.first_user_message.as_deref().map(str::trim) == Some(title) {
None
} else {
Some(title.to_string())
}
pub(super) fn thread_metadata_name(metadata: &ThreadMetadata) -> Option<String> {
metadata
.name
.as_deref()
.map(str::trim)
.filter(|name| !name.is_empty())
.map(str::to_string)
}
pub(super) fn set_thread_name_from_title(thread: &mut StoredThread, title: String) {
@@ -8,9 +8,9 @@ use codex_rollout::find_thread_names_by_ids;
use codex_rollout::parse_cursor;
use super::LocalThreadStore;
use super::helpers::distinct_thread_metadata_title;
use super::helpers::set_thread_name_from_title;
use super::helpers::stored_thread_from_rollout_item;
use super::helpers::thread_metadata_name;
use crate::ListThreadsParams;
use crate::SortDirection;
use crate::ThreadPage;
@@ -86,7 +86,7 @@ pub(super) async fn list_threads(
let Ok(Some(metadata)) = state_db_ctx.get_thread(thread_id).await else {
continue;
};
if let Some(title) = distinct_thread_metadata_title(&metadata) {
if let Some(title) = thread_metadata_name(&metadata) {
names.insert(thread_id, title);
}
}
@@ -13,12 +13,12 @@ use codex_rollout::read_thread_item_from_rollout;
use codex_state::ThreadMetadata;
use super::LocalThreadStore;
use super::helpers::distinct_thread_metadata_title;
use super::helpers::git_info_from_parts;
use super::helpers::permission_profile_from_metadata_value;
use super::helpers::rollout_path_is_archived;
use super::helpers::set_thread_name_from_title;
use super::helpers::stored_thread_from_rollout_item;
use super::helpers::thread_metadata_name;
use super::live_writer;
use crate::ReadThreadParams;
use crate::StoredThread;
@@ -117,6 +117,9 @@ 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);
}
let existing_git_info = thread.git_info.take();
let (fallback_sha, fallback_branch, fallback_origin_url) = match existing_git_info {
Some(info) => (
@@ -303,7 +306,7 @@ async fn stored_thread_from_sqlite_metadata(
store: &LocalThreadStore,
metadata: ThreadMetadata,
) -> StoredThread {
let name = match distinct_thread_metadata_title(&metadata) {
let name = match thread_metadata_name(&metadata) {
Some(title) => Some(title),
None => find_thread_name_by_id(store.config.codex_home.as_path(), &metadata.id)
.await
@@ -701,7 +704,7 @@ mod tests {
builder.cwd = home.path().to_path_buf();
builder.cli_version = Some("test_version".to_string());
let mut metadata = builder.build(config.default_model_provider_id.as_str());
metadata.title = "Saved title".to_string();
metadata.name = Some("Saved title".to_string());
metadata.first_user_message = Some("Hello from user".to_string());
runtime
.upsert_thread(&metadata)
@@ -852,7 +855,7 @@ mod tests {
builder.model_provider = Some(config.default_model_provider_id.clone());
builder.cwd = home.path().join("sqlite-workspace");
let mut metadata = builder.build(config.default_model_provider_id.as_str());
metadata.title = "Saved title".to_string();
metadata.name = Some("Saved title".to_string());
metadata.first_user_message = Some("Hello from sqlite".to_string());
metadata.sandbox_policy = "workspace-write".to_string();
runtime
@@ -956,7 +959,7 @@ mod tests {
builder.cwd = home.path().join("workspace");
builder.cli_version = Some("sqlite-cli".to_string());
let mut metadata = builder.build(config.default_model_provider_id.as_str());
metadata.title = "Command-only thread".to_string();
metadata.name = Some("Command-only thread".to_string());
runtime
.upsert_thread(&metadata)
.await
@@ -10,9 +10,9 @@ use codex_rollout::parse_cursor;
use codex_rollout::search_rollout_matches;
use super::LocalThreadStore;
use super::helpers::distinct_thread_metadata_title;
use super::helpers::set_thread_name_from_title;
use super::helpers::stored_thread_from_rollout_item;
use super::helpers::thread_metadata_name;
use super::list_threads::list_rollout_threads;
use crate::ListThreadsParams;
use crate::SearchThreadsParams;
@@ -211,7 +211,7 @@ async fn set_thread_search_result_names(
let Ok(Some(metadata)) = state_db_ctx.get_thread(thread_id).await else {
continue;
};
if let Some(title) = distinct_thread_metadata_title(&metadata) {
if let Some(title) = thread_metadata_name(&metadata) {
names.insert(thread_id, title);
}
}
@@ -92,7 +92,7 @@ pub(super) async fn update_thread_metadata(
.await;
if let Some(name) = name {
apply_thread_name(store, thread_id, name.unwrap_or_default()).await?;
apply_thread_name(store, thread_id, name).await?;
}
let resolved_git_info = match git_info {
@@ -240,9 +240,6 @@ async fn apply_metadata_update(
if let Some(preview) = patch.preview {
metadata.preview = Some(preview);
}
if let Some(name) = patch.name {
metadata.title = name.unwrap_or_default();
}
if let Some(title) = patch.title {
metadata.title = title;
}
@@ -515,11 +512,11 @@ async fn apply_thread_git_info_to_rollout(
async fn apply_thread_name(
store: &LocalThreadStore,
thread_id: ThreadId,
name: String,
name: Option<String>,
) -> ThreadStoreResult<()> {
if let Some(state_db) = store.state_db().await {
let updated = state_db
.update_thread_title(thread_id, &name)
.update_thread_name(thread_id, name.as_deref())
.await
.map_err(|err| ThreadStoreError::Internal {
message: format!("failed to set thread name: {err}"),
@@ -531,11 +528,15 @@ async fn apply_thread_name(
}
}
append_thread_name(store.config.codex_home.as_path(), thread_id, &name)
.await
.map_err(|err| ThreadStoreError::Internal {
message: format!("failed to index thread name: {err}"),
})
append_thread_name(
store.config.codex_home.as_path(),
thread_id,
name.as_deref().unwrap_or_default(),
)
.await
.map_err(|err| ThreadStoreError::Internal {
message: format!("failed to index thread name: {err}"),
})
}
async fn apply_thread_memory_mode(
@@ -1266,7 +1267,7 @@ mod tests {
}
#[tokio::test]
async fn metadata_patch_applies_title_over_existing_name() {
async fn metadata_patch_preserves_explicit_name_over_derived_title() {
let home = TempDir::new().expect("temp dir");
let config = test_config(home.path());
let runtime = codex_state::StateRuntime::init(
@@ -1305,7 +1306,7 @@ mod tests {
.await
.expect("apply observed metadata");
assert_eq!(thread.name.as_deref(), Some("Derived first message"));
assert_eq!(thread.name.as_deref(), Some("User chosen name"));
}
#[tokio::test]