diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index d51e84d054..6b0947b8b1 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -3859,6 +3859,7 @@ dependencies = [ "codex-otel", "codex-protocol", "codex-state", + "codex-utils-absolute-path", "codex-utils-path", "pretty_assertions", "regex", @@ -4087,6 +4088,7 @@ dependencies = [ "codex-protocol", "codex-rollout", "codex-state", + "codex-utils-absolute-path", "codex-utils-path", "futures", "pretty_assertions", diff --git a/codex-rs/app-server/src/lib.rs b/codex-rs/app-server/src/lib.rs index 585b93a7f9..169a6ca73e 100644 --- a/codex-rs/app-server/src/lib.rs +++ b/codex-rs/app-server/src/lib.rs @@ -1196,8 +1196,16 @@ async fn init_sqlite_state_db_with_fresh_start_on_corruption( } Err(err) => err, }; - let database_path = codex_state::runtime_db_path_for_corruption_error(&err) - .unwrap_or_else(|| codex_state::state_db_path(config.sqlite_home.as_path())); + let database_path = + codex_state::runtime_db_path_for_corruption_error(&err).unwrap_or_else(|| { + codex_state::SqliteConfig::from_sqlite_home( + codex_utils_absolute_path::AbsolutePathBuf::resolve_path_against_base( + config.sqlite_home.clone(), + &config.codex_home, + ), + ) + .state_db_path() + }); if !codex_state::is_sqlite_corruption_error(&err) && !sqlite_home_is_blocking_file(database_path.as_path()) { diff --git a/codex-rs/app-server/tests/suite/v2/remote_thread_store.rs b/codex-rs/app-server/tests/suite/v2/remote_thread_store.rs index 22da2b3d25..27c99fa2ad 100644 --- a/codex-rs/app-server/tests/suite/v2/remote_thread_store.rs +++ b/codex-rs/app-server/tests/suite/v2/remote_thread_store.rs @@ -58,6 +58,7 @@ use codex_thread_store::CreateThreadParams as StoreCreateThreadParams; use codex_thread_store::InMemoryThreadStore; use codex_thread_store::ThreadPersistenceMetadata; use codex_thread_store::ThreadStore; +use codex_utils_absolute_path::test_support::PathExt; use pretty_assertions::assert_eq; use tempfile::TempDir; use tokio::time::timeout; @@ -404,7 +405,9 @@ fn assert_no_local_persistence_artifacts(codex_home: &Path) -> Result<()> { "non-local thread persistence should not create archived rollout sessions" ); assert!( - !codex_state::state_db_path(codex_home).exists(), + !codex_state::SqliteConfig::new_for_testing(codex_home.abs()) + .state_db_path() + .exists(), "non-local thread persistence should not create local thread sqlite" ); diff --git a/codex-rs/app-server/tests/suite/v2/thread_read.rs b/codex-rs/app-server/tests/suite/v2/thread_read.rs index 9bd4d1be4b..a15d9d94ff 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_read.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_read.rs @@ -80,6 +80,7 @@ use codex_thread_store::ThreadMetadataPatch; use codex_thread_store::ThreadPersistenceMetadata; use codex_thread_store::ThreadStore; use codex_thread_store::UpdateThreadMetadataParams; +use codex_utils_absolute_path::test_support::PathExt; use core_test_support::responses; use pretty_assertions::assert_eq; use serde_json::Value; @@ -467,15 +468,14 @@ async fn thread_search_occurrences_reads_paginated_projection() -> Result<()> { let codex_home = TempDir::new()?; MockResponsesConfig::new(&server.uri()).write(codex_home.path())?; let thread_id = codex_protocol::ThreadId::default(); - let state_db = codex_state::StateRuntime::init( - codex_home.path().to_path_buf(), - "mock_provider".to_string(), - ) - .await?; + let sqlite = codex_state::SqliteConfig::new_for_testing(codex_home.path().abs()); + let state_db = + codex_state::StateRuntime::init(sqlite.home().to_path_buf(), "mock_provider".to_string()) + .await?; let store = LocalThreadStore::new( LocalThreadStoreConfig { codex_home: codex_home.path().to_path_buf(), - sqlite_home: codex_home.path().to_path_buf(), + sqlite, default_model_provider_id: "mock_provider".to_string(), }, Some(state_db), @@ -1445,15 +1445,14 @@ async fn paginated_history_lists_use_projected_turns_and_items() -> Result<()> { let codex_home = TempDir::new()?; MockResponsesConfig::new(&server.uri()).write(codex_home.path())?; let thread_id = codex_protocol::ThreadId::default(); - let state_db = codex_state::StateRuntime::init( - codex_home.path().to_path_buf(), - "mock_provider".to_string(), - ) - .await?; + let sqlite = codex_state::SqliteConfig::new_for_testing(codex_home.path().abs()); + let state_db = + codex_state::StateRuntime::init(sqlite.home().to_path_buf(), "mock_provider".to_string()) + .await?; let store = LocalThreadStore::new( LocalThreadStoreConfig { codex_home: codex_home.path().to_path_buf(), - sqlite_home: codex_home.path().to_path_buf(), + sqlite, default_model_provider_id: "mock_provider".to_string(), }, Some(state_db), diff --git a/codex-rs/cli/src/doctor.rs b/codex-rs/cli/src/doctor.rs index ed93f43ed5..db9d21dad4 100644 --- a/codex-rs/cli/src/doctor.rs +++ b/codex-rs/cli/src/doctor.rs @@ -2165,7 +2165,13 @@ async fn state_check(config: &Config) -> DoctorCheck { path_readiness(&mut details, "log dir", &config.log_dir); path_readiness(&mut details, "sqlite home", &config.sqlite_home); let mut integrity_failures = Vec::new(); - for db in codex_state::runtime_db_paths(&config.sqlite_home) { + let sqlite = codex_state::SqliteConfig::from_sqlite_home( + codex_utils_absolute_path::AbsolutePathBuf::resolve_path_against_base( + &config.sqlite_home, + &config.codex_home, + ), + ); + for db in sqlite.runtime_db_paths() { path_readiness(&mut details, db.label, &db.path); sqlite_integrity_detail(&mut details, &mut integrity_failures, db.label, &db.path).await; } diff --git a/codex-rs/cli/src/doctor/thread_inventory.rs b/codex-rs/cli/src/doctor/thread_inventory.rs index 52fbd8ba7b..61fb6b74b6 100644 --- a/codex-rs/cli/src/doctor/thread_inventory.rs +++ b/codex-rs/cli/src/doctor/thread_inventory.rs @@ -91,9 +91,15 @@ impl RolloutScan { } pub(super) async fn thread_inventory_check(config: &Config) -> DoctorCheck { + let sqlite = codex_state::SqliteConfig::from_sqlite_home( + codex_utils_absolute_path::AbsolutePathBuf::resolve_path_against_base( + &config.sqlite_home, + &config.codex_home, + ), + ); thread_inventory_check_for_roots( config.codex_home.as_path(), - config.sqlite_home.as_path(), + &sqlite, config.model_provider_id.as_str(), ) .await @@ -101,11 +107,11 @@ pub(super) async fn thread_inventory_check(config: &Config) -> DoctorCheck { async fn thread_inventory_check_for_roots( codex_home: &Path, - sqlite_home: &Path, + sqlite: &codex_state::SqliteConfig, default_provider: &str, ) -> DoctorCheck { let scan = scan_rollout_files(codex_home).await; - let state_db_path = codex_state::state_db_path(sqlite_home); + let state_db_path = sqlite.state_db_path(); let mut details = vec![ format!("default model provider: {default_provider}"), @@ -779,7 +785,7 @@ mod tests { let check = thread_inventory_check_for_roots( fixture.codex_home.path(), - fixture.sqlite_home.path(), + &fixture.sqlite(), "test-provider", ) .await; @@ -826,7 +832,7 @@ mod tests { let check = thread_inventory_check_for_roots( fixture.codex_home.path(), - fixture.sqlite_home.path(), + &fixture.sqlite(), "test-provider", ) .await; @@ -876,7 +882,7 @@ mod tests { let check = thread_inventory_check_for_roots( fixture.codex_home.path(), - fixture.sqlite_home.path(), + &fixture.sqlite(), "test-provider", ) .await; @@ -902,7 +908,7 @@ mod tests { let check = thread_inventory_check_for_roots( fixture.codex_home.path(), - fixture.sqlite_home.path(), + &fixture.sqlite(), "test-provider", ) .await; @@ -932,7 +938,7 @@ mod tests { let check = thread_inventory_check_for_roots( fixture.codex_home.path(), - fixture.sqlite_home.path(), + &fixture.sqlite(), "test-provider", ) .await; @@ -959,7 +965,7 @@ mod tests { let check = thread_inventory_check_for_roots( fixture.codex_home.path(), - fixture.sqlite_home.path(), + &fixture.sqlite(), "test-provider", ) .await; @@ -1013,7 +1019,7 @@ mod tests { let check = thread_inventory_check_for_roots( fixture.codex_home.path(), - fixture.sqlite_home.path(), + &fixture.sqlite(), "test-provider", ) .await; @@ -1045,7 +1051,7 @@ mod tests { let check = thread_inventory_check_for_roots( fixture.codex_home.path(), - fixture.sqlite_home.path(), + &fixture.sqlite(), "test-provider", ) .await; @@ -1067,7 +1073,7 @@ mod tests { let check = thread_inventory_check_for_roots( fixture.codex_home.path(), - fixture.sqlite_home.path(), + &fixture.sqlite(), "test-provider", ) .await; @@ -1315,6 +1321,10 @@ mod tests { } } + fn sqlite(&self) -> codex_state::SqliteConfig { + codex_state::SqliteConfig::new_for_testing(self.sqlite_home.path().abs()) + } + fn write_rollout(&self, archived: bool, timestamp: &str, thread_id: &str) -> PathBuf { let root = if archived { self.codex_home.path().join("archived_sessions") @@ -1348,8 +1358,9 @@ mod tests { } async fn insert_thread_row(&self, id: &str, rollout_path: &Path, archived: bool) { - let state_db_path = codex_state::state_db_path(self.sqlite_home.path()); - let pool = codex_state::SqliteConfig::new_for_testing(self.sqlite_home.path().abs()) + let sqlite = self.sqlite(); + let state_db_path = sqlite.state_db_path(); + let pool = sqlite .open_read_write_pool(&state_db_path) .await .expect("sqlite pool"); diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index 297c3297d6..8dd1e118e2 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -26,8 +26,8 @@ use codex_execpolicy::ExecPolicyCheckCommand; use codex_responses_api_proxy::Args as ResponsesApiProxyArgs; use codex_rollout_trace::REDUCED_STATE_FILE_NAME; use codex_rollout_trace::replay_bundle; +use codex_state::SqliteConfig; use codex_state::StateRuntime; -use codex_state::memories_db_path; use codex_tui::AppExitInfo; use codex_tui::Cli as TuiCli; use codex_tui::ExitReason; @@ -2052,7 +2052,9 @@ async fn run_debug_clear_memories_command( .build() .await?; - let memories_path = memories_db_path(config.sqlite_home.as_path()); + let sqlite_home = + AbsolutePathBuf::resolve_path_against_base(&config.sqlite_home, &config.codex_home); + let memories_path = SqliteConfig::from_sqlite_home(sqlite_home).memories_db_path(); let cleared_memories_db = StateRuntime::clear_memory_data_in_sqlite_home(config.sqlite_home.as_path()).await?; diff --git a/codex-rs/cli/src/state_db_recovery.rs b/codex-rs/cli/src/state_db_recovery.rs index 89bb16643c..34acbb5104 100644 --- a/codex-rs/cli/src/state_db_recovery.rs +++ b/codex-rs/cli/src/state_db_recovery.rs @@ -96,6 +96,7 @@ fn backup_folder(backups: &[RuntimeDbBackup]) -> Option<&Path> { #[cfg(test)] mod tests { use super::*; + use codex_utils_absolute_path::test_support::PathExt; use pretty_assertions::assert_eq; use std::path::PathBuf; use tempfile::TempDir; @@ -103,8 +104,9 @@ mod tests { #[tokio::test] async fn backup_backs_up_only_failed_database_file() -> std::io::Result<()> { let temp_dir = TempDir::new()?; - let state_path = codex_state::state_db_path(temp_dir.path()); - let failed_db_path = codex_state::logs_db_path(temp_dir.path()); + let sqlite = codex_state::SqliteConfig::new_for_testing(temp_dir.path().abs()); + let state_path = sqlite.state_db_path(); + let failed_db_path = sqlite.logs_db_path(); tokio::fs::write(state_path.as_path(), b"state").await?; tokio::fs::write(failed_db_path.as_path(), b"logs").await?; @@ -130,10 +132,9 @@ mod tests { let temp_dir = TempDir::new()?; let sqlite_home = temp_dir.path().join("sqlite-home"); tokio::fs::write(sqlite_home.as_path(), b"not-a-directory").await?; - let startup_error = LocalStateDbStartupError::new( - codex_state::state_db_path(sqlite_home.as_path()), - "File exists".to_string(), - ); + let sqlite = codex_state::SqliteConfig::new_for_testing(sqlite_home.as_path().abs()); + let startup_error = + LocalStateDbStartupError::new(sqlite.state_db_path(), "File exists".to_string()); assert!(is_auto_backup_recoverable(&startup_error)); let backups = backup_files_for_fresh_start(&startup_error).await?; diff --git a/codex-rs/cli/tests/debug_clear_memories.rs b/codex-rs/cli/tests/debug_clear_memories.rs index b43a1d936d..1266ddb60c 100644 --- a/codex-rs/cli/tests/debug_clear_memories.rs +++ b/codex-rs/cli/tests/debug_clear_memories.rs @@ -2,8 +2,6 @@ use std::path::Path; use anyhow::Result; use codex_state::StateRuntime; -use codex_state::memories_db_path; -use codex_state::state_db_path; use codex_utils_absolute_path::test_support::PathExt; use predicates::str::contains; use tempfile::TempDir; @@ -23,9 +21,9 @@ async fn debug_clear_memories_resets_state_and_removes_memory_dir() -> Result<() drop(runtime); let thread_id = "00000000-0000-0000-0000-000000000123"; - let db_path = state_db_path(codex_home.path()); + let db_path = sqlite.state_db_path(); let pool = sqlite.open_read_write_pool(&db_path).await?; - let memories_db_path = memories_db_path(codex_home.path()); + let memories_db_path = sqlite.memories_db_path(); let memories_pool = sqlite.open_read_write_pool(&memories_db_path).await?; sqlx::query( @@ -145,8 +143,8 @@ async fn debug_clear_memories_resets_memories_db_without_state_db() -> Result<() StateRuntime::init(codex_home.path().to_path_buf(), "test-provider".to_string()).await?; runtime.close().await; - let db_path = state_db_path(codex_home.path()); - let memories_db_path = memories_db_path(codex_home.path()); + let db_path = sqlite.state_db_path(); + let memories_db_path = sqlite.memories_db_path(); let memories_pool = sqlite.open_read_write_pool(&memories_db_path).await?; sqlx::query( diff --git a/codex-rs/core/src/thread_manager.rs b/codex-rs/core/src/thread_manager.rs index 4bfd964cb2..451a6d6d56 100644 --- a/codex-rs/core/src/thread_manager.rs +++ b/codex-rs/core/src/thread_manager.rs @@ -437,7 +437,7 @@ impl ThreadManager { set_thread_manager_test_mode_for_tests(/*enabled*/ true); let auth_manager = AuthManager::from_auth_for_testing(auth); let installation_id = uuid::Uuid::new_v4().to_string(); - let skills_codex_home = match AbsolutePathBuf::from_absolute_path_checked(&codex_home) { + let absolute_codex_home = match AbsolutePathBuf::from_absolute_path_checked(&codex_home) { Ok(codex_home) => codex_home, Err(err) => panic!("test codex_home should be absolute: {err}"), }; @@ -450,7 +450,7 @@ impl ThreadManager { )); let mcp_manager = Arc::new(McpManager::new(Arc::clone(&plugins_manager))); let skills_service = Arc::new(SkillsService::new_with_restriction_product( - skills_codex_home, + absolute_codex_home.clone(), /*bundled_skills_enabled*/ true, restriction_product, )); @@ -459,7 +459,7 @@ impl ThreadManager { let thread_store: Arc = Arc::new(LocalThreadStore::new( LocalThreadStoreConfig { codex_home: codex_home.clone(), - sqlite_home: codex_home.clone(), + sqlite: codex_state::SqliteConfig::new_for_testing(absolute_codex_home), default_model_provider_id: OPENAI_PROVIDER_ID.to_string(), }, state_db.clone(), diff --git a/codex-rs/core/tests/suite/sqlite_state.rs b/codex-rs/core/tests/suite/sqlite_state.rs index 0ea4a9db9a..e1c055d321 100644 --- a/codex-rs/core/tests/suite/sqlite_state.rs +++ b/codex-rs/core/tests/suite/sqlite_state.rs @@ -23,6 +23,7 @@ use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::UserMessageEvent; use codex_protocol::user_input::UserInput; use codex_web_search_extension::install as install_web_search_extension; +use core_test_support::PathExt; use core_test_support::responses; use core_test_support::responses::ev_completed; use core_test_support::responses::ev_function_call; @@ -65,7 +66,9 @@ async fn new_thread_is_recorded_in_state_db() -> Result<()> { let thread_id = test.session_configured.thread_id; let rollout_path = test.codex.rollout_path().expect("rollout path"); - let db_path = codex_state::state_db_path(test.config.sqlite_home.as_path()); + let db_path = + codex_state::SqliteConfig::new_for_testing(test.config.sqlite_home.as_path().abs()) + .state_db_path(); for _ in 0..100 { if tokio::fs::try_exists(&db_path).await.unwrap_or(false) { @@ -419,7 +422,9 @@ async fn backfill_scans_existing_rollouts() -> Result<()> { let test = builder.build(&server).await?; - let db_path = codex_state::state_db_path(test.config.sqlite_home.as_path()); + let db_path = + codex_state::SqliteConfig::new_for_testing(test.config.sqlite_home.as_path().abs()) + .state_db_path(); let rollout_path = test.config.codex_home.join(&rollout_rel_path); let default_provider = test.config.model_provider_id.clone(); @@ -470,7 +475,9 @@ async fn user_messages_persist_in_state_db() -> Result<()> { }); let test = builder.build(&server).await?; - let db_path = codex_state::state_db_path(test.config.sqlite_home.as_path()); + let db_path = + codex_state::SqliteConfig::new_for_testing(test.config.sqlite_home.as_path().abs()) + .state_db_path(); for _ in 0..100 { if tokio::fs::try_exists(&db_path).await.unwrap_or(false) { break; diff --git a/codex-rs/rollout/Cargo.toml b/codex-rs/rollout/Cargo.toml index 0949202dfc..da257fbcec 100644 --- a/codex-rs/rollout/Cargo.toml +++ b/codex-rs/rollout/Cargo.toml @@ -21,6 +21,7 @@ codex-git-utils = { workspace = true } codex-otel = { workspace = true } codex-protocol = { workspace = true } codex-state = { workspace = true } +codex-utils-absolute-path = { workspace = true } codex-utils-path = { workspace = true } regex = { workspace = true } serde = { workspace = true, features = ["derive"] } diff --git a/codex-rs/rollout/src/state_db.rs b/codex-rs/rollout/src/state_db.rs index 213fb65d0f..1b4db3f73e 100644 --- a/codex-rs/rollout/src/state_db.rs +++ b/codex-rs/rollout/src/state_db.rs @@ -14,6 +14,7 @@ use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::ThreadHistoryMode; pub use codex_state::LogEntry; use codex_state::ThreadMetadataBuilder; +use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_path::normalize_for_path_comparison; use serde_json::Value; use std::path::Path; @@ -216,7 +217,8 @@ fn emit_startup_warning(message: &str) { /// Unlike [`init`], this helper does not run rollout backfill. It is for /// optional local reads from non-owning contexts such as remote app-server mode. pub async fn get_state_db(config: &impl RolloutConfigView) -> Option { - let state_path = codex_state::state_db_path(config.sqlite_home()); + let sqlite_home = AbsolutePathBuf::try_from(config.sqlite_home()).ok()?; + let state_path = codex_state::SqliteConfig::from_sqlite_home(sqlite_home).state_db_path(); if !tokio::fs::try_exists(&state_path).await.unwrap_or(false) { codex_state::record_fallback( "get_state_db", diff --git a/codex-rs/state/src/bin/logs_client.rs b/codex-rs/state/src/bin/logs_client.rs index bc45c24eaa..8002226da1 100644 --- a/codex-rs/state/src/bin/logs_client.rs +++ b/codex-rs/state/src/bin/logs_client.rs @@ -7,7 +7,9 @@ use clap::Parser; use clap::ValueEnum; use codex_state::LogQuery; use codex_state::LogRow; +use codex_state::SqliteConfig; use codex_state::StateRuntime; +use codex_utils_absolute_path::AbsolutePathBuf; use dirs::home_dir; use owo_colors::OwoColorize; @@ -136,7 +138,8 @@ fn resolve_db_path(args: &Args) -> anyhow::Result { } let codex_home = args.codex_home.clone().unwrap_or_else(default_codex_home); - Ok(codex_state::logs_db_path(codex_home.as_path())) + let sqlite_home = AbsolutePathBuf::relative_to_current_dir(codex_home)?; + Ok(SqliteConfig::from_sqlite_home(sqlite_home).logs_db_path()) } fn default_codex_home() -> PathBuf { diff --git a/codex-rs/state/src/lib.rs b/codex-rs/state/src/lib.rs index 5df56e96ef..0c79b7492d 100644 --- a/codex-rs/state/src/lib.rs +++ b/codex-rs/state/src/lib.rs @@ -63,26 +63,15 @@ pub use runtime::GoalUpdate; pub use runtime::MemoryStore; pub use runtime::RemoteControlEnrollmentRecord; pub use runtime::RuntimeDbBackup; -pub use runtime::RuntimeDbPath; pub use runtime::ThreadFilterOptions; pub use runtime::backup_runtime_db_for_fresh_start; -pub use runtime::goals_db_filename; -pub use runtime::goals_db_path; pub use runtime::is_sqlite_corruption_error; -pub use runtime::logs_db_filename; -pub use runtime::logs_db_path; -pub use runtime::memories_db_filename; -pub use runtime::memories_db_path; pub use runtime::open_thread_history_db; pub use runtime::runtime_db_path_for_corruption_error; -pub use runtime::runtime_db_paths; pub use runtime::sqlite_error_detail_is_corruption; pub use runtime::sqlite_error_detail_is_lock; pub use runtime::sqlite_integrity_check; -pub use runtime::state_db_filename; -pub use runtime::state_db_path; -pub use runtime::thread_history_db_filename; -pub use runtime::thread_history_db_path; +pub use sqlite::RuntimeDbPath; pub use telemetry::DbTelemetry; pub use telemetry::DbTelemetryHandle; pub use telemetry::install_process_db_telemetry; @@ -92,12 +81,6 @@ pub use telemetry::record_fallback; /// Environment variable for overriding the SQLite state database home directory. pub const SQLITE_HOME_ENV: &str = "CODEX_SQLITE_HOME"; -pub const LOGS_DB_FILENAME: &str = "logs_2.sqlite"; -pub const GOALS_DB_FILENAME: &str = "goals_1.sqlite"; -pub const MEMORIES_DB_FILENAME: &str = "memories_1.sqlite"; -pub const STATE_DB_FILENAME: &str = "state_5.sqlite"; -pub const THREAD_HISTORY_DB_FILENAME: &str = "thread_history_1.sqlite"; - /// Errors encountered during DB operations. Tags: [stage] pub const DB_ERROR_METRIC: &str = "codex.db.error"; /// Metrics on backfill process. Tags: [status] diff --git a/codex-rs/state/src/migrations_tests.rs b/codex-rs/state/src/migrations_tests.rs index 13da97f134..38878f4dcd 100644 --- a/codex-rs/state/src/migrations_tests.rs +++ b/codex-rs/state/src/migrations_tests.rs @@ -7,7 +7,6 @@ use std::borrow::Cow; use super::STATE_MIGRATOR; use super::repair_legacy_recency_migration_version; -use crate::state_db_path; fn migrator_through(version: i64) -> Migrator { Migrator { @@ -37,8 +36,9 @@ async fn agent_job_tables_are_dropped_when_upgrading() { let _ = std::fs::remove_dir_all(sqlite_home); }); let sqlite = crate::SqliteConfig::new_for_testing(sqlite_home.as_path().abs()); + let state_path = sqlite.state_db_path(); let pool = sqlite - .open_read_write_pool(&state_db_path(&sqlite_home)) + .open_read_write_pool(&state_path) .await .expect("sqlite database should open"); migrator_through(/*version*/ 15) @@ -130,8 +130,9 @@ async fn recency_migration_backfills_and_seeds_old_binary_inserts() { let _ = std::fs::remove_dir_all(sqlite_home); }); let sqlite = crate::SqliteConfig::new_for_testing(sqlite_home.as_path().abs()); + let state_path = sqlite.state_db_path(); let pool = sqlite - .open_read_write_pool(&state_db_path(&sqlite_home)) + .open_read_write_pool(&state_path) .await .expect("sqlite database should open"); migrator_through(/*version*/ 37) @@ -243,8 +244,9 @@ async fn repairs_recency_migration_that_was_applied_as_version_38() { let _ = std::fs::remove_dir_all(sqlite_home); }); let sqlite = crate::SqliteConfig::new_for_testing(sqlite_home.as_path().abs()); + let state_path = sqlite.state_db_path(); let pool = sqlite - .open_read_write_pool(&state_db_path(&sqlite_home)) + .open_read_write_pool(&state_path) .await .expect("sqlite database should open"); migrator_through(/*version*/ 37) @@ -319,7 +321,7 @@ async fn repair_recency_migration_succeeds_while_another_connection_holds_writer let _ = std::fs::remove_dir_all(sqlite_home); }); let sqlite = crate::SqliteConfig::new_for_testing(sqlite_home.as_path().abs()); - let state_path = state_db_path(&sqlite_home); + let state_path = sqlite.state_db_path(); let pool = sqlite .open_read_write_pool(&state_path) .await diff --git a/codex-rs/state/src/runtime.rs b/codex-rs/state/src/runtime.rs index aa6d72a6d7..c247b93be1 100644 --- a/codex-rs/state/src/runtime.rs +++ b/codex-rs/state/src/runtime.rs @@ -1,18 +1,12 @@ -use crate::GOALS_DB_FILENAME; -use crate::LOGS_DB_FILENAME; use crate::LogEntry; use crate::LogQuery; use crate::LogRow; -use crate::MEMORIES_DB_FILENAME; -use crate::STATE_DB_FILENAME; use crate::SortKey; use crate::SqliteConfig; -use crate::THREAD_HISTORY_DB_FILENAME; use crate::ThreadMetadata; use crate::ThreadMetadataBuilder; use crate::ThreadsPage; use crate::apply_rollout_item; -use crate::migrations::repair_legacy_recency_migration_version; use crate::migrations::runtime_goals_migrator; use crate::migrations::runtime_logs_migrator; use crate::migrations::runtime_memories_migrator; @@ -37,7 +31,6 @@ use sqlx::Row; use sqlx::Sqlite; use sqlx::SqliteConnection; use sqlx::SqlitePool; -use sqlx::migrate::Migrator; use std::collections::BTreeSet; use std::path::Path; use std::path::PathBuf; @@ -67,6 +60,7 @@ pub use goals::GoalStore; pub use goals::GoalUpdate; pub use memories::MemoryStore; pub use recovery::RuntimeDbBackup; +pub(super) use recovery::RuntimeDbInitError; pub use recovery::backup_runtime_db_for_fresh_start; pub use recovery::is_sqlite_corruption_error; pub use recovery::runtime_db_path_for_corruption_error; @@ -84,70 +78,6 @@ pub use threads::ThreadFilterOptions; const LOG_PARTITION_SIZE_LIMIT_BYTES: i64 = 10 * 1024 * 1024; const LOG_PARTITION_ROW_LIMIT: i64 = 1_000; -#[derive(Clone, Copy)] -struct RuntimeDbSpec { - label: &'static str, - filename: &'static str, - kind: DbKind, - open_phase: &'static str, - migrate_phase: &'static str, -} - -impl RuntimeDbSpec { - fn path(self, codex_home: &Path) -> PathBuf { - codex_home.join(self.filename) - } -} - -const STATE_DB: RuntimeDbSpec = RuntimeDbSpec { - label: "state DB", - filename: STATE_DB_FILENAME, - kind: DbKind::State, - open_phase: "open_state", - migrate_phase: "migrate_state", -}; - -const LOGS_DB: RuntimeDbSpec = RuntimeDbSpec { - label: "log DB", - filename: LOGS_DB_FILENAME, - kind: DbKind::Logs, - open_phase: "open_logs", - migrate_phase: "migrate_logs", -}; - -const GOALS_DB: RuntimeDbSpec = RuntimeDbSpec { - label: "goals DB", - filename: GOALS_DB_FILENAME, - kind: DbKind::Goals, - open_phase: "open_goals", - migrate_phase: "migrate_goals", -}; - -const MEMORIES_DB: RuntimeDbSpec = RuntimeDbSpec { - label: "memories DB", - filename: MEMORIES_DB_FILENAME, - kind: DbKind::Memories, - open_phase: "open_memories", - migrate_phase: "migrate_memories", -}; - -const THREAD_HISTORY_DB: RuntimeDbSpec = RuntimeDbSpec { - label: "thread history DB", - filename: THREAD_HISTORY_DB_FILENAME, - kind: DbKind::ThreadHistory, - open_phase: "open_thread_history", - migrate_phase: "migrate_thread_history", -}; - -const RUNTIME_DBS: [RuntimeDbSpec; 5] = - [STATE_DB, LOGS_DB, GOALS_DB, MEMORIES_DB, THREAD_HISTORY_DB]; - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct RuntimeDbPath { - pub label: &'static str, - pub path: PathBuf, -} - #[derive(Clone)] pub struct StateRuntime { codex_home: PathBuf, @@ -195,17 +125,13 @@ impl StateRuntime { let logs_migrator = runtime_logs_migrator(); let goals_migrator = runtime_goals_migrator(); let memories_migrator = runtime_memories_migrator(); - let state_path = STATE_DB.path(codex_home.as_path()); - let logs_path = LOGS_DB.path(codex_home.as_path()); - let goals_path = GOALS_DB.path(codex_home.as_path()); - let memories_path = MEMORIES_DB.path(codex_home.as_path()); - let pool = match open_state_sqlite( - &sqlite, - &state_path, - &state_migrator, - telemetry_override, - ) - .await + let state_path = sqlite.state_db_path(); + let logs_path = sqlite.logs_db_path(); + let goals_path = sqlite.goals_db_path(); + let memories_path = sqlite.memories_db_path(); + let pool = match sqlite + .open_state_db(&state_migrator, telemetry_override) + .await { Ok(db) => Arc::new(db), Err(err) => { @@ -213,22 +139,20 @@ impl StateRuntime { return Err(err); } }; - let logs_pool = - match open_logs_sqlite(&sqlite, &logs_path, &logs_migrator, telemetry_override).await { - Ok(db) => Arc::new(db), - Err(err) => { - warn!("failed to open logs db at {}: {err}", logs_path.display()); - close_sqlite_pools(&[pool.as_ref()]).await; - return Err(err); - } - }; - let goals_pool = match open_goals_sqlite( - &sqlite, - &goals_path, - &goals_migrator, - telemetry_override, - ) - .await + let logs_pool = match sqlite + .open_logs_db(&logs_migrator, telemetry_override) + .await + { + Ok(db) => Arc::new(db), + Err(err) => { + warn!("failed to open logs db at {}: {err}", logs_path.display()); + close_sqlite_pools(&[pool.as_ref()]).await; + return Err(err); + } + }; + let goals_pool = match sqlite + .open_goals_db(&goals_migrator, telemetry_override) + .await { Ok(db) => Arc::new(db), Err(err) => { @@ -237,13 +161,9 @@ impl StateRuntime { return Err(err); } }; - let memories_pool = match open_memories_sqlite( - &sqlite, - &memories_path, - &memories_migrator, - telemetry_override, - ) - .await + let memories_pool = match sqlite + .open_memories_db(&memories_migrator, telemetry_override) + .await { Ok(db) => Arc::new(db), Err(err) => { @@ -347,19 +267,15 @@ impl StateRuntime { pub async fn clear_memory_data_in_sqlite_home(sqlite_home: &Path) -> anyhow::Result { let sqlite = SqliteConfig::from_sqlite_home(AbsolutePathBuf::try_from(sqlite_home)?); - let memories_path = MEMORIES_DB.path(sqlite_home); + let memories_path = sqlite.memories_db_path(); if !tokio::fs::try_exists(&memories_path).await? { return Ok(false); } let memories_migrator = runtime_memories_migrator(); - let pool = open_memories_sqlite( - &sqlite, - &memories_path, - &memories_migrator, - /*telemetry_override*/ None, - ) - .await?; + let pool = sqlite + .open_memories_db(&memories_migrator, /*telemetry_override*/ None) + .await?; memories::clear_memory_data_in_pool(&pool).await?; pool.close().await; Ok(true) @@ -372,100 +288,13 @@ async fn close_sqlite_pools(pools: &[&SqlitePool]) { } } -async fn open_state_sqlite( - sqlite: &SqliteConfig, - path: &Path, - migrator: &Migrator, - telemetry_override: Option<&dyn DbTelemetry>, -) -> anyhow::Result { - // New state DBs should use incremental auto-vacuum, but retrofitting an - // existing DB requires a full VACUUM. Do not attempt that during process - // startup: it is maintenance work that can contend with foreground writers. - open_sqlite(sqlite, path, migrator, STATE_DB, telemetry_override).await -} - -async fn open_logs_sqlite( - sqlite: &SqliteConfig, - path: &Path, - migrator: &Migrator, - telemetry_override: Option<&dyn DbTelemetry>, -) -> anyhow::Result { - open_sqlite(sqlite, path, migrator, LOGS_DB, telemetry_override).await -} - -async fn open_goals_sqlite( - sqlite: &SqliteConfig, - path: &Path, - migrator: &Migrator, - telemetry_override: Option<&dyn DbTelemetry>, -) -> anyhow::Result { - open_sqlite(sqlite, path, migrator, GOALS_DB, telemetry_override).await -} - -async fn open_memories_sqlite( - sqlite: &SqliteConfig, - path: &Path, - migrator: &Migrator, - telemetry_override: Option<&dyn DbTelemetry>, -) -> anyhow::Result { - open_sqlite(sqlite, path, migrator, MEMORIES_DB, telemetry_override).await -} - /// Open and migrate the rebuildable paginated thread-history database. pub async fn open_thread_history_db(sqlite_home: &Path) -> anyhow::Result { let sqlite = SqliteConfig::from_sqlite_home(AbsolutePathBuf::try_from(sqlite_home)?); let migrator = runtime_thread_history_migrator(); - open_sqlite( - &sqlite, - thread_history_db_path(sqlite_home).as_path(), - &migrator, - THREAD_HISTORY_DB, - /*telemetry_override*/ None, - ) - .await -} - -async fn open_sqlite( - sqlite: &SqliteConfig, - path: &Path, - migrator: &Migrator, - spec: RuntimeDbSpec, - telemetry_override: Option<&dyn DbTelemetry>, -) -> anyhow::Result { - let started = Instant::now(); - let pool_result = sqlite - .open_read_write_pool(path) + sqlite + .open_thread_history_db(&migrator, /*telemetry_override*/ None) .await - .map_err(anyhow::Error::from); - crate::telemetry::record_init_result( - telemetry_override, - spec.kind, - spec.open_phase, - started.elapsed(), - &pool_result, - ); - let pool = pool_result - .map_err(|source| recovery::RuntimeDbInitError::new(spec.label, "open", path, source))?; - let started = Instant::now(); - let migrate_result = async { - if matches!(spec.kind, DbKind::State) { - repair_legacy_recency_migration_version(&pool, migrator).await?; - } - migrator.run(&pool).await.map_err(anyhow::Error::from) - } - .await; - crate::telemetry::record_init_result( - telemetry_override, - spec.kind, - spec.migrate_phase, - started.elapsed(), - &migrate_result, - ); - if let Err(source) = migrate_result { - pool.close().await; - return Err(recovery::RuntimeDbInitError::new(spec.label, "migrate", path, source).into()); - } - Ok(pool) } pub(super) async fn ensure_backfill_state_row_in_pool( @@ -496,56 +325,6 @@ ON CONFLICT(id) DO NOTHING Ok(()) } -pub fn state_db_filename() -> String { - STATE_DB.filename.to_string() -} - -pub fn state_db_path(codex_home: &Path) -> PathBuf { - STATE_DB.path(codex_home) -} - -pub fn logs_db_filename() -> String { - LOGS_DB.filename.to_string() -} - -pub fn logs_db_path(codex_home: &Path) -> PathBuf { - LOGS_DB.path(codex_home) -} - -pub fn goals_db_filename() -> String { - GOALS_DB.filename.to_string() -} - -pub fn goals_db_path(codex_home: &Path) -> PathBuf { - GOALS_DB.path(codex_home) -} - -pub fn memories_db_filename() -> String { - MEMORIES_DB.filename.to_string() -} - -pub fn memories_db_path(codex_home: &Path) -> PathBuf { - MEMORIES_DB.path(codex_home) -} - -pub fn thread_history_db_filename() -> String { - THREAD_HISTORY_DB.filename.to_string() -} - -pub fn thread_history_db_path(codex_home: &Path) -> PathBuf { - THREAD_HISTORY_DB.path(codex_home) -} - -pub fn runtime_db_paths(codex_home: &Path) -> Vec { - RUNTIME_DBS - .iter() - .map(|spec| RuntimeDbPath { - label: spec.label, - path: spec.path(codex_home), - }) - .collect() -} - /// Run SQLite's built-in integrity check against an existing database file. pub async fn sqlite_integrity_check(path: &Path) -> anyhow::Result> { let sqlite = @@ -561,10 +340,8 @@ pub async fn sqlite_integrity_check(path: &Path) -> anyhow::Result> #[cfg(test)] mod tests { use super::StateRuntime; - use super::open_state_sqlite; use super::runtime_state_migrator; use super::sqlite_integrity_check; - use super::state_db_path; use super::test_support::unique_temp_dir; use crate::DB_INIT_METRIC; use crate::DbTelemetry; @@ -642,8 +419,9 @@ mod tests { tokio::fs::create_dir_all(&codex_home) .await .expect("create codex home"); - let path = state_db_path(codex_home.as_path()); - let pool = crate::SqliteConfig::new_for_testing(codex_home.as_path().abs()) + let sqlite = crate::SqliteConfig::new_for_testing(codex_home.as_path().abs()); + let path = sqlite.state_db_path(); + let pool = sqlite .open_read_write_pool(&path) .await .expect("open sqlite db"); @@ -667,8 +445,9 @@ mod tests { tokio::fs::create_dir_all(&codex_home) .await .expect("create codex home"); - let state_path = state_db_path(codex_home.as_path()); - let pool = crate::SqliteConfig::new_for_testing(codex_home.as_path().abs()) + let sqlite = crate::SqliteConfig::new_for_testing(codex_home.as_path().abs()); + let state_path = sqlite.state_db_path(); + let pool = sqlite .open_read_write_pool(&state_path) .await .expect("open state db"); @@ -698,14 +477,10 @@ mod tests { strict_pool.close().await; let tolerant_migrator = runtime_state_migrator(); - let tolerant_pool = open_state_sqlite( - &crate::SqliteConfig::new_for_testing(codex_home.as_path().abs()), - state_path.as_path(), - &tolerant_migrator, - /*telemetry_override*/ None, - ) - .await - .expect("runtime migrator should tolerate newer applied migrations"); + let tolerant_pool = crate::SqliteConfig::new_for_testing(codex_home.as_path().abs()) + .open_state_db(&tolerant_migrator, /*telemetry_override*/ None) + .await + .expect("runtime migrator should tolerate newer applied migrations"); tolerant_pool.close().await; let _ = tokio::fs::remove_dir_all(codex_home).await; diff --git a/codex-rs/state/src/runtime/backfill.rs b/codex-rs/state/src/runtime/backfill.rs index 027a9a6f63..1a0074e52b 100644 --- a/codex-rs/state/src/runtime/backfill.rs +++ b/codex-rs/state/src/runtime/backfill.rs @@ -175,8 +175,9 @@ mod tests { let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string()) .await .expect("initialize runtime"); - let write_pool = crate::SqliteConfig::new_for_testing(codex_home.as_path().abs()) - .open_read_write_pool(&crate::state_db_path(codex_home.as_path())) + let sqlite = crate::SqliteConfig::new_for_testing(codex_home.as_path().abs()); + let write_pool = sqlite + .open_read_write_pool(&sqlite.state_db_path()) .await .expect("open write pool"); let mut write_connection = write_pool.acquire().await.expect("open write connection"); diff --git a/codex-rs/state/src/runtime/logs.rs b/codex-rs/state/src/runtime/logs.rs index 7ff179ca78..29a4efe7a0 100644 --- a/codex-rs/state/src/runtime/logs.rs +++ b/codex-rs/state/src/runtime/logs.rs @@ -545,7 +545,6 @@ mod tests { use super::test_support::unique_temp_dir; use crate::LogEntry; use crate::LogQuery; - use crate::logs_db_path; use crate::migrations::LOGS_MIGRATOR; use chrono::Utc; use codex_utils_absolute_path::test_support::PathExt; @@ -596,7 +595,9 @@ mod tests { .await .expect("insert test logs"); - let logs_count = log_row_count(logs_db_path(codex_home.as_path()).as_path()).await; + let logs_path = + crate::SqliteConfig::new_for_testing(codex_home.as_path().abs()).logs_db_path(); + let logs_count = log_row_count(logs_path.as_path()).await; assert_eq!(logs_count, 1); @@ -609,7 +610,8 @@ mod tests { tokio::fs::create_dir_all(&codex_home) .await .expect("create codex home"); - let logs_path = logs_db_path(codex_home.as_path()); + let logs_path = + crate::SqliteConfig::new_for_testing(codex_home.as_path().abs()).logs_db_path(); let old_logs_migrator = Migrator { migrations: Cow::Owned(vec![LOGS_MIGRATOR.migrations[0].clone()]), ignore_missing: false, @@ -706,7 +708,9 @@ mod tests { .await .expect("initialize runtime"); - let pool = open_db_pool(logs_db_path(codex_home.as_path()).as_path()).await; + let logs_path = + crate::SqliteConfig::new_for_testing(codex_home.as_path().abs()).logs_db_path(); + let pool = open_db_pool(logs_path.as_path()).await; let auto_vacuum = sqlx::query_scalar::<_, i64>("PRAGMA auto_vacuum") .fetch_one(&pool) .await diff --git a/codex-rs/state/src/runtime/recovery_tests.rs b/codex-rs/state/src/runtime/recovery_tests.rs index 7424dbcfe1..bb14710804 100644 --- a/codex-rs/state/src/runtime/recovery_tests.rs +++ b/codex-rs/state/src/runtime/recovery_tests.rs @@ -1,12 +1,14 @@ use super::*; use crate::runtime::test_support::unique_temp_dir; +use codex_utils_absolute_path::test_support::PathExt; use pretty_assertions::assert_eq; #[tokio::test] async fn backup_moves_only_requested_runtime_db_files_to_backup_folder() -> std::io::Result<()> { let sqlite_home = unique_temp_dir(); tokio::fs::create_dir_all(sqlite_home.as_path()).await?; - let runtime_paths = super::super::runtime_db_paths(sqlite_home.as_path()); + let sqlite = crate::SqliteConfig::new_for_testing(sqlite_home.as_path().abs()); + let runtime_paths = sqlite.runtime_db_paths(); let mut expected_paths = Vec::new(); for db_path in runtime_paths.iter().map(|db| db.path.as_path()) { for path in sqlite_paths(db_path) { @@ -14,7 +16,7 @@ async fn backup_moves_only_requested_runtime_db_files_to_backup_folder() -> std: expected_paths.push(path); } } - let failed_db_path = super::super::logs_db_path(sqlite_home.as_path()); + let failed_db_path = sqlite.logs_db_path(); let failed_paths = sqlite_paths(failed_db_path.as_path()); let backups = backup_runtime_db_for_fresh_start(failed_db_path.as_path()).await?; @@ -48,7 +50,9 @@ async fn backup_replaces_blocking_sqlite_home_file() -> std::io::Result<()> { tokio::fs::write(sqlite_home.as_path(), b"not-a-directory").await?; let backups = backup_runtime_db_for_fresh_start( - super::super::state_db_path(sqlite_home.as_path()).as_path(), + crate::SqliteConfig::new_for_testing(sqlite_home.as_path().abs()) + .state_db_path() + .as_path(), ) .await?; @@ -79,7 +83,7 @@ async fn runtime_db_path_for_corruption_error_returns_failed_database_path() -> { let sqlite_home = unique_temp_dir(); tokio::fs::create_dir_all(sqlite_home.as_path()).await?; - let path = super::super::state_db_path(sqlite_home.as_path()); + let path = crate::SqliteConfig::new_for_testing(sqlite_home.as_path().abs()).state_db_path(); tokio::fs::write(path.as_path(), b"not sqlite").await?; let err = match super::super::StateRuntime::init(sqlite_home, "openai".to_string()).await { diff --git a/codex-rs/state/src/runtime/remote_control.rs b/codex-rs/state/src/runtime/remote_control.rs index eda2adc3b3..4831fdef65 100644 --- a/codex-rs/state/src/runtime/remote_control.rs +++ b/codex-rs/state/src/runtime/remote_control.rs @@ -157,7 +157,6 @@ mod tests { use super::StateRuntime; use super::test_support::unique_temp_dir; use crate::migrations::STATE_MIGRATOR; - use crate::state_db_path; use codex_utils_absolute_path::test_support::PathExt; use pretty_assertions::assert_eq; use sqlx::migrate::Migrator; @@ -344,8 +343,9 @@ mod tests { table_name: STATE_MIGRATOR.table_name.clone(), create_schemas: STATE_MIGRATOR.create_schemas.clone(), }; - let pool = crate::SqliteConfig::new_for_testing(codex_home.as_path().abs()) - .open_read_write_pool(&state_db_path(codex_home.as_path())) + let sqlite = crate::SqliteConfig::new_for_testing(codex_home.as_path().abs()); + let pool = sqlite + .open_read_write_pool(&sqlite.state_db_path()) .await .expect("open old state db"); old_state_migrator diff --git a/codex-rs/state/src/sqlite.rs b/codex-rs/state/src/sqlite.rs index 4d92c98830..a32d0989d8 100644 --- a/codex-rs/state/src/sqlite.rs +++ b/codex-rs/state/src/sqlite.rs @@ -1,17 +1,95 @@ //! Shared SQLite connection configuration. +use crate::DbTelemetry; +use crate::migrations::repair_legacy_recency_migration_version; +use crate::runtime::RuntimeDbInitError; +use crate::telemetry; +use crate::telemetry::DbKind; use codex_utils_absolute_path::AbsolutePathBuf; use log::LevelFilter; use sqlx::ConnectOptions; use sqlx::Error; use sqlx::SqlitePool; +use sqlx::migrate::Migrator; use sqlx::sqlite::SqliteAutoVacuum; use sqlx::sqlite::SqliteConnectOptions; use sqlx::sqlite::SqliteJournalMode; use sqlx::sqlite::SqlitePoolOptions; use sqlx::sqlite::SqliteSynchronous; use std::path::Path; +use std::path::PathBuf; use std::time::Duration; +use std::time::Instant; + +const LOGS_DB_FILENAME: &str = "logs_2.sqlite"; +const GOALS_DB_FILENAME: &str = "goals_1.sqlite"; +const MEMORIES_DB_FILENAME: &str = "memories_1.sqlite"; +const STATE_DB_FILENAME: &str = "state_5.sqlite"; +const THREAD_HISTORY_DB_FILENAME: &str = "thread_history_1.sqlite"; + +#[derive(Clone, Copy)] +struct RuntimeDbSpec { + label: &'static str, + filename: &'static str, + kind: DbKind, + open_phase: &'static str, + migrate_phase: &'static str, +} + +impl RuntimeDbSpec { + fn path(self, codex_home: &Path) -> PathBuf { + codex_home.join(self.filename) + } +} + +const STATE_DB: RuntimeDbSpec = RuntimeDbSpec { + label: "state DB", + filename: STATE_DB_FILENAME, + kind: DbKind::State, + open_phase: "open_state", + migrate_phase: "migrate_state", +}; + +const LOGS_DB: RuntimeDbSpec = RuntimeDbSpec { + label: "log DB", + filename: LOGS_DB_FILENAME, + kind: DbKind::Logs, + open_phase: "open_logs", + migrate_phase: "migrate_logs", +}; + +const GOALS_DB: RuntimeDbSpec = RuntimeDbSpec { + label: "goals DB", + filename: GOALS_DB_FILENAME, + kind: DbKind::Goals, + open_phase: "open_goals", + migrate_phase: "migrate_goals", +}; + +const MEMORIES_DB: RuntimeDbSpec = RuntimeDbSpec { + label: "memories DB", + filename: MEMORIES_DB_FILENAME, + kind: DbKind::Memories, + open_phase: "open_memories", + migrate_phase: "migrate_memories", +}; + +const THREAD_HISTORY_DB: RuntimeDbSpec = RuntimeDbSpec { + label: "thread history DB", + filename: THREAD_HISTORY_DB_FILENAME, + kind: DbKind::ThreadHistory, + open_phase: "open_thread_history", + migrate_phase: "migrate_thread_history", +}; + +const RUNTIME_DBS: [RuntimeDbSpec; 5] = + [STATE_DB, LOGS_DB, GOALS_DB, MEMORIES_DB, THREAD_HISTORY_DB]; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RuntimeDbPath { + pub label: &'static str, + pub path: PathBuf, +} /// Resolved configuration shared by all Codex SQLite connections. #[derive(Clone, Debug, Eq, PartialEq)] @@ -32,6 +110,136 @@ impl SqliteConfig { self.sqlite_home.as_path() } + /// Return the path to the primary state database. + pub fn state_db_path(&self) -> PathBuf { + STATE_DB.path(self.home()) + } + + /// Return the path to the logs database. + pub fn logs_db_path(&self) -> PathBuf { + LOGS_DB.path(self.home()) + } + + /// Return the path to the goals database. + pub fn goals_db_path(&self) -> PathBuf { + GOALS_DB.path(self.home()) + } + + /// Return the path to the memories database. + pub fn memories_db_path(&self) -> PathBuf { + MEMORIES_DB.path(self.home()) + } + + /// Return the path to the paginated thread-history database. + pub fn thread_history_db_path(&self) -> PathBuf { + THREAD_HISTORY_DB.path(self.home()) + } + + /// Return the paths to every database managed by the state runtime. + pub fn runtime_db_paths(&self) -> Vec { + RUNTIME_DBS + .iter() + .map(|spec| RuntimeDbPath { + label: spec.label, + path: spec.path(self.home()), + }) + .collect() + } + + pub(super) async fn open_state_db( + &self, + migrator: &Migrator, + telemetry_override: Option<&dyn DbTelemetry>, + ) -> anyhow::Result { + // New state DBs should use incremental auto-vacuum, but retrofitting an + // existing DB requires a full VACUUM. Do not attempt that during process + // startup: it is maintenance work that can contend with foreground writers. + self.open_runtime_db(STATE_DB, migrator, telemetry_override) + .await + } + + pub(super) async fn open_logs_db( + &self, + migrator: &Migrator, + telemetry_override: Option<&dyn DbTelemetry>, + ) -> anyhow::Result { + self.open_runtime_db(LOGS_DB, migrator, telemetry_override) + .await + } + + pub(super) async fn open_goals_db( + &self, + migrator: &Migrator, + telemetry_override: Option<&dyn DbTelemetry>, + ) -> anyhow::Result { + self.open_runtime_db(GOALS_DB, migrator, telemetry_override) + .await + } + + pub(super) async fn open_memories_db( + &self, + migrator: &Migrator, + telemetry_override: Option<&dyn DbTelemetry>, + ) -> anyhow::Result { + self.open_runtime_db(MEMORIES_DB, migrator, telemetry_override) + .await + } + + pub(super) async fn open_thread_history_db( + &self, + migrator: &Migrator, + telemetry_override: Option<&dyn DbTelemetry>, + ) -> anyhow::Result { + self.open_runtime_db(THREAD_HISTORY_DB, migrator, telemetry_override) + .await + } + + async fn open_runtime_db( + &self, + spec: RuntimeDbSpec, + migrator: &Migrator, + telemetry_override: Option<&dyn DbTelemetry>, + ) -> anyhow::Result { + let path = spec.path(self.home()); + let started = Instant::now(); + let pool_result = self + .open_read_write_pool(&path) + .await + .map_err(anyhow::Error::from); + telemetry::record_init_result( + telemetry_override, + spec.kind, + spec.open_phase, + started.elapsed(), + &pool_result, + ); + let pool = pool_result.map_err(|source| { + RuntimeDbInitError::new(spec.label, "open", path.as_path(), source) + })?; + let started = Instant::now(); + let migrate_result = async { + if matches!(spec.kind, DbKind::State) { + repair_legacy_recency_migration_version(&pool, migrator).await?; + } + migrator.run(&pool).await.map_err(anyhow::Error::from) + } + .await; + telemetry::record_init_result( + telemetry_override, + spec.kind, + spec.migrate_phase, + started.elapsed(), + &migrate_result, + ); + if let Err(source) = migrate_result { + pool.close().await; + return Err( + RuntimeDbInitError::new(spec.label, "migrate", path.as_path(), source).into(), + ); + } + Ok(pool) + } + /// Open a writable Codex SQLite database, creating it if necessary. pub async fn open_read_write_pool(&self, path: &Path) -> Result { let options = SqliteConnectOptions::new() diff --git a/codex-rs/thread-store/Cargo.toml b/codex-rs/thread-store/Cargo.toml index 844d1769f8..42a8ece463 100644 --- a/codex-rs/thread-store/Cargo.toml +++ b/codex-rs/thread-store/Cargo.toml @@ -21,6 +21,7 @@ codex-otel = { workspace = true } codex-protocol = { workspace = true } codex-rollout = { workspace = true } codex-state = { workspace = true } +codex-utils-absolute-path = { workspace = true } codex-utils-path = { workspace = true } futures = { workspace = true } pulldown-cmark = { workspace = true } diff --git a/codex-rs/thread-store/src/local/create_thread.rs b/codex-rs/thread-store/src/local/create_thread.rs index 0c4af760e7..457f715dec 100644 --- a/codex-rs/thread-store/src/local/create_thread.rs +++ b/codex-rs/thread-store/src/local/create_thread.rs @@ -20,7 +20,7 @@ pub(super) async fn create_thread( })?; let config = RolloutConfig { codex_home: store.config.codex_home.clone(), - sqlite_home: store.config.sqlite_home.clone(), + sqlite_home: store.config.sqlite.home().to_path_buf(), cwd, model_provider_id: params.metadata.model_provider.clone(), generate_memories: matches!(params.metadata.memory_mode, ThreadMemoryMode::Enabled), diff --git a/codex-rs/thread-store/src/local/list_threads.rs b/codex-rs/thread-store/src/local/list_threads.rs index e63d3e28bc..8d15de4e31 100644 --- a/codex-rs/thread-store/src/local/list_threads.rs +++ b/codex-rs/thread-store/src/local/list_threads.rs @@ -41,7 +41,7 @@ pub(super) async fn list_threads( let state_db = store.state_db().await; let rollout_config = RolloutConfig { codex_home: store.config.codex_home.clone(), - sqlite_home: store.config.sqlite_home.clone(), + sqlite_home: store.config.sqlite.home().to_path_buf(), cwd: store.config.codex_home.clone(), model_provider_id: store.config.default_model_provider_id.clone(), generate_memories: false, diff --git a/codex-rs/thread-store/src/local/live_writer.rs b/codex-rs/thread-store/src/local/live_writer.rs index 66e01243ce..e4d49f33d3 100644 --- a/codex-rs/thread-store/src/local/live_writer.rs +++ b/codex-rs/thread-store/src/local/live_writer.rs @@ -93,7 +93,7 @@ pub(super) async fn resume_thread( })?; let config = RolloutConfig { codex_home: store.config.codex_home.clone(), - sqlite_home: store.config.sqlite_home.clone(), + sqlite_home: store.config.sqlite.home().to_path_buf(), cwd, model_provider_id: params.metadata.model_provider.clone(), generate_memories: matches!(params.metadata.memory_mode, ThreadMemoryMode::Enabled), diff --git a/codex-rs/thread-store/src/local/mod.rs b/codex-rs/thread-store/src/local/mod.rs index 1b5d6c8ede..3ebc837829 100644 --- a/codex-rs/thread-store/src/local/mod.rs +++ b/codex-rs/thread-store/src/local/mod.rs @@ -22,6 +22,8 @@ use codex_protocol::ThreadId; use codex_protocol::protocol::ThreadHistoryMode; use codex_rollout::RolloutRecorder; use codex_rollout::StateDbHandle; +use codex_state::SqliteConfig; +use codex_utils_absolute_path::AbsolutePathBuf; use std::collections::HashMap; use std::collections::hash_map::Entry; use std::path::PathBuf; @@ -115,16 +117,23 @@ impl LiveWriterLocks { #[derive(Clone, Debug, PartialEq, Eq)] pub struct LocalThreadStoreConfig { pub codex_home: PathBuf, - pub sqlite_home: PathBuf, + pub sqlite: SqliteConfig, /// Provider used only when older local metadata does not contain one. pub default_model_provider_id: String, } impl LocalThreadStoreConfig { + #[expect( + clippy::expect_used, + reason = "resolved Codex configuration paths must be absolute" + )] pub fn from_config(config: &impl codex_rollout::RolloutConfigView) -> Self { Self { codex_home: config.codex_home().to_path_buf(), - sqlite_home: config.sqlite_home().to_path_buf(), + sqlite: SqliteConfig::from_sqlite_home( + AbsolutePathBuf::from_absolute_path_checked(config.sqlite_home()) + .expect("sqlite home from resolved config should be absolute"), + ), default_model_provider_id: config.model_provider_id().to_string(), } } @@ -158,7 +167,7 @@ impl LocalThreadStore { async fn thread_history_db(&self) -> ThreadStoreResult<&sqlx::SqlitePool> { self.thread_history_db .get_or_try_init(|| async { - codex_state::open_thread_history_db(self.config.sqlite_home.as_path()).await + codex_state::open_thread_history_db(self.config.sqlite.home()).await }) .await .map_err(|err| ThreadStoreError::Internal { @@ -499,7 +508,7 @@ mod tests { let home = TempDir::new().expect("temp dir"); let config = test_config(home.path()); let runtime = codex_state::StateRuntime::init( - config.sqlite_home.clone(), + config.sqlite.home().to_path_buf(), config.default_model_provider_id.clone(), ) .await @@ -534,7 +543,7 @@ mod tests { let home = TempDir::new().expect("temp dir"); let config = test_config(home.path()); let runtime = codex_state::StateRuntime::init( - config.sqlite_home.clone(), + config.sqlite.home().to_path_buf(), config.default_model_provider_id.clone(), ) .await @@ -569,7 +578,7 @@ mod tests { let home = TempDir::new().expect("temp dir"); let config = test_config(home.path()); let runtime = codex_state::StateRuntime::init( - config.sqlite_home.clone(), + config.sqlite.home().to_path_buf(), config.default_model_provider_id.clone(), ) .await @@ -637,7 +646,7 @@ mod tests { let home = TempDir::new().expect("temp dir"); let config = test_config(home.path()); let runtime = codex_state::StateRuntime::init( - config.sqlite_home.clone(), + config.sqlite.home().to_path_buf(), config.default_model_provider_id.clone(), ) .await @@ -726,7 +735,7 @@ mod tests { let home = TempDir::new().expect("temp dir"); let config = test_config(home.path()); let runtime = codex_state::StateRuntime::init( - config.sqlite_home.clone(), + config.sqlite.home().to_path_buf(), config.default_model_provider_id.clone(), ) .await @@ -762,7 +771,7 @@ mod tests { let home = TempDir::new().expect("temp dir"); let config = test_config(home.path()); let runtime = codex_state::StateRuntime::init( - config.sqlite_home.clone(), + config.sqlite.home().to_path_buf(), config.default_model_provider_id.clone(), ) .await @@ -802,7 +811,7 @@ mod tests { let home = TempDir::new().expect("temp dir"); let config = test_config(home.path()); let runtime = codex_state::StateRuntime::init( - config.sqlite_home.clone(), + config.sqlite.home().to_path_buf(), config.default_model_provider_id.clone(), ) .await @@ -846,7 +855,7 @@ mod tests { let home = TempDir::new().expect("temp dir"); let config = test_config(home.path()); let runtime = codex_state::StateRuntime::init( - config.sqlite_home.clone(), + config.sqlite.home().to_path_buf(), config.default_model_provider_id.clone(), ) .await @@ -901,7 +910,7 @@ mod tests { let external_home = TempDir::new().expect("external temp dir"); let config = test_config(home.path()); let runtime = codex_state::StateRuntime::init( - config.sqlite_home.clone(), + config.sqlite.home().to_path_buf(), config.default_model_provider_id.clone(), ) .await diff --git a/codex-rs/thread-store/src/local/read_thread.rs b/codex-rs/thread-store/src/local/read_thread.rs index cbf29df686..6a6ed7dd6d 100644 --- a/codex-rs/thread-store/src/local/read_thread.rs +++ b/codex-rs/thread-store/src/local/read_thread.rs @@ -608,7 +608,7 @@ mod tests { let active_path = write_session_file(home.path(), "2025-01-03T12-00-00", uuid).expect("session file"); let runtime = codex_state::StateRuntime::init( - config.sqlite_home.clone(), + config.sqlite.home().to_path_buf(), config.default_model_provider_id.clone(), ) .await @@ -767,7 +767,7 @@ mod tests { let rollout_path = write_session_file(home.path(), "2025-01-03T12-00-00", uuid).expect("session file"); let runtime = codex_state::StateRuntime::init( - config.sqlite_home.clone(), + config.sqlite.home().to_path_buf(), config.default_model_provider_id.clone(), ) .await @@ -812,7 +812,7 @@ mod tests { ) .expect("session file"); let runtime = codex_state::StateRuntime::init( - config.sqlite_home.clone(), + config.sqlite.home().to_path_buf(), config.default_model_provider_id.clone(), ) .await @@ -855,7 +855,7 @@ mod tests { let rollout_path = write_session_file(home.path(), "2025-01-03T12-00-00", uuid).expect("session file"); let runtime = codex_state::StateRuntime::init( - config.sqlite_home.clone(), + config.sqlite.home().to_path_buf(), config.default_model_provider_id.clone(), ) .await @@ -896,7 +896,7 @@ mod tests { let rollout_path = write_session_file(home.path(), "2025-01-03T12-00-00", uuid).expect("session file"); let runtime = codex_state::StateRuntime::init( - config.sqlite_home.clone(), + config.sqlite.home().to_path_buf(), config.default_model_provider_id.clone(), ) .await @@ -930,7 +930,7 @@ mod tests { let home = TempDir::new().expect("temp dir"); let config = test_config(home.path()); let runtime = codex_state::StateRuntime::init( - config.sqlite_home.clone(), + config.sqlite.home().to_path_buf(), config.default_model_provider_id.clone(), ) .await @@ -1066,7 +1066,7 @@ mod tests { writeln!(file, "{meta}").expect("write session meta"); let runtime = codex_state::StateRuntime::init( - config.sqlite_home.clone(), + config.sqlite.home().to_path_buf(), config.default_model_provider_id.clone(), ) .await @@ -1120,7 +1120,7 @@ mod tests { write_session_file(home.path(), "2025-01-03T12-00-00", uuid).expect("session file"); let stale_path = external.path().join("missing-rollout.jsonl"); let runtime = codex_state::StateRuntime::init( - config.sqlite_home.clone(), + config.sqlite.home().to_path_buf(), config.default_model_provider_id.clone(), ) .await @@ -1171,7 +1171,7 @@ mod tests { let stale_path = write_session_file(external.path(), "2025-01-04T12-00-00", other_uuid) .expect("other session file"); let runtime = codex_state::StateRuntime::init( - config.sqlite_home.clone(), + config.sqlite.home().to_path_buf(), config.default_model_provider_id.clone(), ) .await @@ -1270,7 +1270,7 @@ mod tests { .path() .join(format!("rollout-2025-01-03T12-00-00-{uuid}.jsonl")); let runtime = codex_state::StateRuntime::init( - config.sqlite_home.clone(), + config.sqlite.home().to_path_buf(), config.default_model_provider_id.clone(), ) .await @@ -1332,7 +1332,7 @@ mod tests { .path() .join(format!("rollout-2025-01-03T12-00-00-{uuid}.jsonl")); let runtime = codex_state::StateRuntime::init( - config.sqlite_home.clone(), + config.sqlite.home().to_path_buf(), config.default_model_provider_id.clone(), ) .await @@ -1387,7 +1387,7 @@ mod tests { let archived_path = write_archived_session_file(home.path(), "2025-01-03T12-00-00", uuid) .expect("archived session file"); let runtime = codex_state::StateRuntime::init( - config.sqlite_home.clone(), + config.sqlite.home().to_path_buf(), config.default_model_provider_id.clone(), ) .await diff --git a/codex-rs/thread-store/src/local/search_threads.rs b/codex-rs/thread-store/src/local/search_threads.rs index 84ecfea963..f6f2d77db8 100644 --- a/codex-rs/thread-store/src/local/search_threads.rs +++ b/codex-rs/thread-store/src/local/search_threads.rs @@ -60,7 +60,7 @@ pub(super) async fn search_threads( let state_db = store.state_db().await; let rollout_config = RolloutConfig { codex_home: store.config.codex_home.clone(), - sqlite_home: store.config.sqlite_home.clone(), + sqlite_home: store.config.sqlite.home().to_path_buf(), cwd: store.config.codex_home.clone(), model_provider_id: store.config.default_model_provider_id.clone(), generate_memories: false, diff --git a/codex-rs/thread-store/src/local/test_support.rs b/codex-rs/thread-store/src/local/test_support.rs index 24f1e41769..e7b014b43b 100644 --- a/codex-rs/thread-store/src/local/test_support.rs +++ b/codex-rs/thread-store/src/local/test_support.rs @@ -5,6 +5,7 @@ use std::path::PathBuf; use codex_protocol::protocol::ThreadHistoryMode; use codex_rollout::ARCHIVED_SESSIONS_SUBDIR; +use codex_utils_absolute_path::test_support::PathExt; use uuid::Uuid; use super::LocalThreadStoreConfig; @@ -12,7 +13,7 @@ use super::LocalThreadStoreConfig; pub(super) fn test_config(codex_home: &Path) -> LocalThreadStoreConfig { LocalThreadStoreConfig { codex_home: codex_home.to_path_buf(), - sqlite_home: codex_home.to_path_buf(), + sqlite: codex_state::SqliteConfig::new_for_testing(codex_home.abs()), default_model_provider_id: "test-provider".to_string(), } } diff --git a/codex-rs/thread-store/src/local/thread_history.rs b/codex-rs/thread-store/src/local/thread_history.rs index 12a07c679b..616c627969 100644 --- a/codex-rs/thread-store/src/local/thread_history.rs +++ b/codex-rs/thread-store/src/local/thread_history.rs @@ -32,7 +32,7 @@ pub(super) async fn next_rollout_byte_offset( store: &LocalThreadStore, thread_id: ThreadId, ) -> ThreadStoreResult { - let db_path = codex_state::thread_history_db_path(store.config.sqlite_home.as_path()); + let db_path = store.config.sqlite.thread_history_db_path(); if !tokio::fs::try_exists(db_path.as_path()) .await .map_err(thread_history_error)? @@ -140,7 +140,7 @@ pub(super) async fn delete_thread( store: &LocalThreadStore, thread_id: ThreadId, ) -> ThreadStoreResult<()> { - let db_path = codex_state::thread_history_db_path(store.config.sqlite_home.as_path()); + let db_path = store.config.sqlite.thread_history_db_path(); if !tokio::fs::try_exists(db_path.as_path()) .await .map_err(thread_history_delete_error)? diff --git a/codex-rs/thread-store/src/local/thread_history/read_tests.rs b/codex-rs/thread-store/src/local/thread_history/read_tests.rs index abb702b537..f6152bbdc5 100644 --- a/codex-rs/thread-store/src/local/thread_history/read_tests.rs +++ b/codex-rs/thread-store/src/local/thread_history/read_tests.rs @@ -507,7 +507,7 @@ async fn store_with_mode(history_mode: ThreadHistoryMode) -> (TempDir, LocalThre ); } let runtime = codex_state::StateRuntime::init( - config.sqlite_home.clone(), + config.sqlite.home().to_path_buf(), config.default_model_provider_id.clone(), ) .await diff --git a/codex-rs/thread-store/src/local/thread_history_materialization_tests.rs b/codex-rs/thread-store/src/local/thread_history_materialization_tests.rs index abeb1149a4..5cdc5e31e7 100644 --- a/codex-rs/thread-store/src/local/thread_history_materialization_tests.rs +++ b/codex-rs/thread-store/src/local/thread_history_materialization_tests.rs @@ -21,6 +21,7 @@ use codex_protocol::protocol::ThreadMemoryMode; use codex_protocol::protocol::TurnCompleteEvent; use codex_protocol::protocol::TurnStartedEvent; use codex_rollout::RolloutRecorder; +use codex_utils_absolute_path::test_support::PathExt; use pretty_assertions::assert_eq; use tempfile::TempDir; @@ -456,7 +457,7 @@ async fn summary_items_use_final_answers_and_ignore_commentary() { let config = test_config(home.path()); let thread_id = ThreadId::default(); let runtime = codex_state::StateRuntime::init( - config.sqlite_home.clone(), + config.sqlite.home().to_path_buf(), config.default_model_provider_id.clone(), ) .await @@ -832,7 +833,11 @@ async fn jsonl_failure_does_not_create_projection_database() { .await .expect_err("JSONL append should fail"); - assert!(!codex_state::thread_history_db_path(home.path()).exists()); + assert!( + !codex_state::SqliteConfig::new_for_testing(home.path().abs()) + .thread_history_db_path() + .exists() + ); } #[tokio::test] @@ -866,7 +871,7 @@ async fn sqlite_failure_does_not_fail_durable_jsonl_write() { let sqlite_home = home.path().join("not-a-directory"); fs::write(sqlite_home.as_path(), "not a directory").expect("block sqlite home"); let mut config = test_config(home.path()); - config.sqlite_home = sqlite_home; + config.sqlite = codex_state::SqliteConfig::new_for_testing(sqlite_home.as_path().abs()); let store = LocalThreadStore::new(config, /*state_db*/ None); let thread_id = ThreadId::default(); create_paginated_thread(&store, thread_id).await; 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 533e07aff6..ccad7983ce 100644 --- a/codex-rs/thread-store/src/local/update_thread_metadata.rs +++ b/codex-rs/thread-store/src/local/update_thread_metadata.rs @@ -1095,7 +1095,7 @@ mod tests { let path = write_session_file(home.path(), "2025-01-03T18-30-00", uuid).expect("session file"); let runtime = codex_state::StateRuntime::init( - config.sqlite_home.clone(), + config.sqlite.home().to_path_buf(), config.default_model_provider_id.clone(), ) .await @@ -1200,7 +1200,7 @@ mod tests { let home = TempDir::new().expect("temp dir"); let config = test_config(home.path()); let runtime = codex_state::StateRuntime::init( - config.sqlite_home.clone(), + config.sqlite.home().to_path_buf(), config.default_model_provider_id.clone(), ) .await @@ -1243,7 +1243,7 @@ mod tests { let home = TempDir::new().expect("temp dir"); let config = test_config(home.path()); let runtime = codex_state::StateRuntime::init( - config.sqlite_home.clone(), + config.sqlite.home().to_path_buf(), config.default_model_provider_id.clone(), ) .await @@ -1298,7 +1298,7 @@ mod tests { let home = TempDir::new().expect("temp dir"); let config = test_config(home.path()); let runtime = codex_state::StateRuntime::init( - config.sqlite_home.clone(), + config.sqlite.home().to_path_buf(), config.default_model_provider_id.clone(), ) .await @@ -1356,7 +1356,7 @@ mod tests { let home = TempDir::new().expect("temp dir"); let config = test_config(home.path()); let runtime = codex_state::StateRuntime::init( - config.sqlite_home.clone(), + config.sqlite.home().to_path_buf(), config.default_model_provider_id.clone(), ) .await diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index c22a38fc9a..6329073004 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -290,7 +290,15 @@ async fn init_state_db_for_app_server_target( match app_server_target { AppServerTarget::Embedded => state_db::try_init(config).await.map(Some).map_err(|err| { let database_path = codex_state::runtime_db_path_for_corruption_error(&err) - .unwrap_or_else(|| codex_state::state_db_path(config.sqlite_home.as_path())); + .unwrap_or_else(|| { + codex_state::SqliteConfig::from_sqlite_home( + AbsolutePathBuf::resolve_path_against_base( + &config.sqlite_home, + &config.codex_home, + ), + ) + .state_db_path() + }); std::io::Error::other(LocalStateDbStartupError::new( database_path, format!("{err:#}"), @@ -2027,6 +2035,7 @@ mod tests { use codex_app_server_protocol::ThreadStartParams; use codex_app_server_protocol::ThreadStartResponse; use codex_config::config_toml::ProjectConfig; + use codex_utils_absolute_path::test_support::PathExt; use pretty_assertions::assert_eq; use serial_test::serial; use tempfile::TempDir; @@ -3114,7 +3123,9 @@ mod tests { assert_eq!( startup_error.state_db_path(), - codex_state::state_db_path(occupied_sqlite_home.as_path()).as_path() + codex_state::SqliteConfig::new_for_testing(config.sqlite_home.as_path().abs()) + .state_db_path() + .as_path() ); assert!( startup_error @@ -3132,7 +3143,8 @@ mod tests { let mut config = build_config(&temp_dir).await?; let sqlite_home = temp_dir.path().join("sqlite-home"); std::fs::create_dir_all(&sqlite_home)?; - let logs_db_path = codex_state::logs_db_path(&sqlite_home); + let logs_db_path = + codex_state::SqliteConfig::new_for_testing(sqlite_home.as_path().abs()).logs_db_path(); std::fs::write(&logs_db_path, "not a sqlite database")?; config.sqlite_home = sqlite_home;