From 6341d4b989fbb3cd31412ccf9b149526845e367f Mon Sep 17 00:00:00 2001 From: Tom Wiltzius Date: Tue, 12 May 2026 13:29:28 -0700 Subject: [PATCH] Cover local metadata compatibility regressions --- codex-rs/Cargo.lock | 1 + codex-rs/thread-store/Cargo.toml | 1 + .../src/local/update_thread_metadata.rs | 132 ++++++++++++++++-- 3 files changed, 126 insertions(+), 8 deletions(-) diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index d15a1062d9..824a40528f 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -3670,6 +3670,7 @@ dependencies = [ "codex-protocol", "codex-rollout", "codex-state", + "codex-utils-path", "pretty_assertions", "serde", "serde_json", diff --git a/codex-rs/thread-store/Cargo.toml b/codex-rs/thread-store/Cargo.toml index 0f8e83fe60..c9f7713b4f 100644 --- a/codex-rs/thread-store/Cargo.toml +++ b/codex-rs/thread-store/Cargo.toml @@ -19,6 +19,7 @@ codex-git-utils = { workspace = true } codex-protocol = { workspace = true } codex-rollout = { workspace = true } codex-state = { workspace = true } +codex-utils-path = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } thiserror = { workspace = true } diff --git a/codex-rs/thread-store/src/local/update_thread_metadata.rs b/codex-rs/thread-store/src/local/update_thread_metadata.rs index 70efc78a31..c09ca80f97 100644 --- a/codex-rs/thread-store/src/local/update_thread_metadata.rs +++ b/codex-rs/thread-store/src/local/update_thread_metadata.rs @@ -172,6 +172,9 @@ async fn apply_metadata_update( ) -> ThreadStoreResult { let live_rollout_path = live_writer::rollout_path(store, thread_id).await.ok(); let mut rollout_path = patch.rollout_path.clone().or(live_rollout_path); + let mut rollout_path_archived = rollout_path + .as_deref() + .is_some_and(|path| rollout_path_is_archived(store, path)); let state_db = store.state_db().await; let sqlite_write_result: ThreadStoreResult<()> = if let Some(state_db) = state_db.as_ref() { let patch = patch.clone(); @@ -184,11 +187,9 @@ async fn apply_metadata_update( message: format!("failed to read thread metadata for {thread_id}: {err}"), })?; if existing.is_none() && rollout_path.is_none() { - rollout_path = Some( - resolve_rollout_path(store, thread_id, include_archived) - .await? - .path, - ); + let resolved = resolve_rollout_path(store, thread_id, include_archived).await?; + rollout_path_archived = resolved.archived; + rollout_path = Some(resolved.path); } let mut metadata = existing.clone().unwrap_or_else(|| { let created_at = patch @@ -206,9 +207,13 @@ async fn apply_metadata_update( builder.agent_nickname = patch.agent_nickname.clone().flatten(); builder.agent_role = patch.agent_role.clone().flatten(); builder.agent_path = patch.agent_path.clone().flatten(); - builder.cwd = patch.cwd.clone().unwrap_or_default(); + builder.cwd = patch.cwd.clone().map(normalize_cwd).unwrap_or_default(); builder.cli_version = patch.cli_version.clone(); - builder.build(store.config.default_model_provider_id.as_str()) + let mut metadata = builder.build(store.config.default_model_provider_id.as_str()); + if rollout_path_archived { + metadata.archived_at = Some(metadata.updated_at); + } + metadata }); if let Some(rollout_path) = rollout_path { metadata.rollout_path = rollout_path; @@ -253,7 +258,7 @@ async fn apply_metadata_update( metadata.agent_path = agent_path; } if let Some(cwd) = patch.cwd { - metadata.cwd = cwd; + metadata.cwd = normalize_cwd(cwd); } if let Some(cli_version) = patch.cli_version { metadata.cli_version = cli_version; @@ -367,6 +372,10 @@ fn enum_to_string(value: &T) -> String { } } +fn normalize_cwd(cwd: PathBuf) -> PathBuf { + codex_utils_path::normalize_for_path_comparison(cwd.as_path()).unwrap_or(cwd) +} + async fn apply_thread_git_info( store: &LocalThreadStore, thread_id: ThreadId, @@ -580,10 +589,13 @@ mod tests { use super::*; use crate::GitInfoPatch; + use crate::ListThreadsParams; use crate::ResumeThreadParams; + use crate::SortDirection; use crate::ThreadEventPersistenceMode; use crate::ThreadMetadataPatch; use crate::ThreadPersistenceMetadata; + use crate::ThreadSortKey; use crate::ThreadStore; use crate::local::LocalThreadStore; use crate::local::test_support::test_config; @@ -1263,6 +1275,110 @@ mod tests { assert!(metadata.is_none()); } + #[tokio::test] + async fn update_thread_metadata_recreates_missing_archived_sqlite_row_as_archived() { + let home = TempDir::new().expect("temp dir"); + let config = test_config(home.path()); + let uuid = Uuid::from_u128(315); + let thread_id = ThreadId::from_string(&uuid.to_string()).expect("valid thread id"); + write_archived_session_file(home.path(), "2025-01-03T19-30-00", uuid) + .expect("archived session file"); + let runtime = codex_state::StateRuntime::init( + home.path().to_path_buf(), + config.default_model_provider_id.clone(), + ) + .await + .expect("state db should initialize"); + let store = LocalThreadStore::new(config, Some(runtime.clone())); + + let thread = store + .update_thread_metadata(UpdateThreadMetadataParams { + thread_id, + patch: ThreadMetadataPatch { + preview: Some("Archived missing sqlite row".to_string()), + ..Default::default() + }, + include_archived: true, + }) + .await + .expect("update archived thread without sqlite row"); + + assert!(thread.archived_at.is_some()); + assert!( + runtime + .get_thread(thread_id) + .await + .expect("get metadata") + .expect("metadata") + .archived_at + .is_some() + ); + } + + #[tokio::test] + async fn observed_metadata_normalizes_cwd_for_list_filters() { + let home = TempDir::new().expect("temp dir"); + let config = test_config(home.path()); + let runtime = codex_state::StateRuntime::init( + home.path().to_path_buf(), + config.default_model_provider_id.clone(), + ) + .await + .expect("state db should initialize"); + let store = LocalThreadStore::new(config, Some(runtime.clone())); + let uuid = Uuid::from_u128(316); + let thread_id = ThreadId::from_string(&uuid.to_string()).expect("valid thread id"); + write_session_file(home.path(), "2025-01-03T20-00-00", uuid).expect("session file"); + let workspace = home.path().join("workspace"); + let child = workspace.join("child"); + std::fs::create_dir_all(child.as_path()).expect("create workspace"); + let unnormalized_cwd = child.join(".."); + let normalized_cwd = codex_utils_path::normalize_for_path_comparison(workspace.as_path()) + .expect("normalize cwd"); + + store + .update_thread_metadata(UpdateThreadMetadataParams { + thread_id, + patch: ThreadMetadataPatch { + cwd: Some(unnormalized_cwd), + preview: Some("cwd preview".to_string()), + ..Default::default() + }, + include_archived: false, + }) + .await + .expect("update observed cwd"); + + let metadata = runtime + .get_thread(thread_id) + .await + .expect("get metadata") + .expect("metadata"); + assert_eq!(metadata.cwd, normalized_cwd); + let page = store + .list_threads(ListThreadsParams { + page_size: 10, + cursor: None, + sort_key: ThreadSortKey::UpdatedAt, + sort_direction: SortDirection::Desc, + allowed_sources: Vec::new(), + model_providers: Some(Vec::new()), + cwd_filters: Some(vec![workspace]), + archived: false, + search_term: None, + use_state_db_only: true, + }) + .await + .expect("list threads by cwd"); + assert_eq!( + page.items + .iter() + .map(|thread| thread.thread_id) + .collect::>(), + vec![thread_id] + ); + } + #[tokio::test] async fn update_thread_metadata_keeps_archived_thread_archived_in_sqlite() { let home = TempDir::new().expect("temp dir");