mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
## Why Codex and SQLite data can use separate home directories, but state consumers could reconstruct database paths from the Codex home instead of consistently using the resolved SQLite configuration. ## What changed - Pass `SqliteConfig` through the core, rollout, state runtime, and thread store instead of passing a directory and rebuilding the configuration downstream. - Use that shared configuration for state, logs, memories, goals, and paginated thread-history database access, including integrity checks and cleanup. - Reject state database handles whose SQLite configuration does not match the requesting store. ## Testing Add coverage with separate Codex and SQLite homes that verifies startup backfill, thread listing, and paginated history all use the configured SQLite directory. GitOrigin-RevId: 1de1cdd1d6ff1d70bbb6c360c8352e6543fb8ebf
194 lines
6.9 KiB
Rust
194 lines
6.9 KiB
Rust
use super::LocalThreadStore;
|
|
use super::helpers::matching_rollout_file_name;
|
|
use super::helpers::scoped_rollout_path;
|
|
use super::helpers::touch_modified_time;
|
|
use crate::ArchiveThreadParams;
|
|
use crate::ReadThreadParams;
|
|
use crate::StoredThread;
|
|
use crate::ThreadStoreError;
|
|
use crate::ThreadStoreResult;
|
|
use codex_rollout::find_archived_thread_path_by_id_str;
|
|
use codex_rollout::rollout_date_parts;
|
|
|
|
pub(super) async fn unarchive_thread(
|
|
store: &LocalThreadStore,
|
|
params: ArchiveThreadParams,
|
|
) -> ThreadStoreResult<StoredThread> {
|
|
let thread_id = params.thread_id;
|
|
let state_db_ctx = store.state_db().await;
|
|
let archived_path = find_archived_thread_path_by_id_str(
|
|
store.config.codex_home.as_path(),
|
|
&thread_id.to_string(),
|
|
state_db_ctx.as_deref(),
|
|
)
|
|
.await
|
|
.map_err(|err| ThreadStoreError::InvalidRequest {
|
|
message: format!("failed to locate archived thread id {thread_id}: {err}"),
|
|
})?
|
|
.ok_or_else(|| ThreadStoreError::InvalidRequest {
|
|
message: format!("no archived rollout found for thread id {thread_id}"),
|
|
})?;
|
|
|
|
let canonical_archived_path = scoped_rollout_path(
|
|
store
|
|
.config
|
|
.codex_home
|
|
.join(codex_rollout::ARCHIVED_SESSIONS_SUBDIR),
|
|
archived_path.as_path(),
|
|
"archived",
|
|
)?;
|
|
let file_name = matching_rollout_file_name(
|
|
canonical_archived_path.as_path(),
|
|
thread_id,
|
|
archived_path.as_path(),
|
|
)?;
|
|
let Some((year, month, day)) = rollout_date_parts(&file_name) else {
|
|
return Err(ThreadStoreError::InvalidRequest {
|
|
message: format!(
|
|
"rollout path `{}` missing filename timestamp",
|
|
archived_path.display()
|
|
),
|
|
});
|
|
};
|
|
|
|
let dest_dir = store
|
|
.config
|
|
.codex_home
|
|
.join(codex_rollout::SESSIONS_SUBDIR)
|
|
.join(year)
|
|
.join(month)
|
|
.join(day);
|
|
std::fs::create_dir_all(&dest_dir).map_err(|err| ThreadStoreError::Internal {
|
|
message: format!("failed to unarchive thread: {err}"),
|
|
})?;
|
|
let restored_path = dest_dir.join(&file_name);
|
|
std::fs::rename(&canonical_archived_path, &restored_path).map_err(|err| {
|
|
ThreadStoreError::Internal {
|
|
message: format!("failed to unarchive thread: {err}"),
|
|
}
|
|
})?;
|
|
touch_modified_time(restored_path.as_path()).map_err(|err| ThreadStoreError::Internal {
|
|
message: format!("failed to update unarchived thread timestamp: {err}"),
|
|
})?;
|
|
|
|
if let Some(ctx) = state_db_ctx {
|
|
let _ = ctx
|
|
.mark_unarchived(thread_id, restored_path.as_path())
|
|
.await;
|
|
}
|
|
|
|
super::read_thread::read_thread(
|
|
store,
|
|
ReadThreadParams {
|
|
thread_id,
|
|
include_archived: false,
|
|
include_history: false,
|
|
},
|
|
)
|
|
.await
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use chrono::Utc;
|
|
use codex_protocol::ThreadId;
|
|
use codex_protocol::protocol::SessionSource;
|
|
use codex_utils_absolute_path::test_support::PathExt;
|
|
use pretty_assertions::assert_eq;
|
|
use tempfile::TempDir;
|
|
use uuid::Uuid;
|
|
|
|
use super::*;
|
|
use crate::ThreadStore;
|
|
use crate::local::LocalThreadStore;
|
|
use crate::local::test_support::test_config;
|
|
use crate::local::test_support::write_archived_session_file;
|
|
|
|
#[tokio::test]
|
|
async fn unarchive_thread_restores_rollout_and_returns_updated_thread() {
|
|
let home = TempDir::new().expect("temp dir");
|
|
let store = LocalThreadStore::new(test_config(home.path()), /*state_db*/ None);
|
|
let uuid = Uuid::from_u128(203);
|
|
let thread_id = ThreadId::from_string(&uuid.to_string()).expect("valid thread id");
|
|
let archived_path = write_archived_session_file(home.path(), "2025-01-03T13-00-00", uuid)
|
|
.expect("archived session file");
|
|
|
|
let thread = store
|
|
.unarchive_thread(ArchiveThreadParams { thread_id })
|
|
.await
|
|
.expect("unarchive thread");
|
|
|
|
assert!(!archived_path.exists());
|
|
let restored_path = home
|
|
.path()
|
|
.join("sessions/2025/01/03")
|
|
.join(archived_path.file_name().expect("file name"));
|
|
assert!(restored_path.exists());
|
|
assert_eq!(thread.thread_id, thread_id);
|
|
assert_eq!(thread.rollout_path, Some(restored_path));
|
|
assert_eq!(thread.archived_at, None);
|
|
assert_eq!(thread.preview, "Archived user message");
|
|
assert_eq!(
|
|
thread.first_user_message.as_deref(),
|
|
Some("Archived user message")
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn unarchive_thread_updates_sqlite_metadata_when_present() {
|
|
let home = TempDir::new().expect("temp dir");
|
|
let config = test_config(home.path());
|
|
let uuid = Uuid::from_u128(204);
|
|
let thread_id = ThreadId::from_string(&uuid.to_string()).expect("valid thread id");
|
|
let archived_path = write_archived_session_file(home.path(), "2025-01-03T13-00-00", uuid)
|
|
.expect("archived session file");
|
|
let runtime = codex_state::StateRuntime::init(
|
|
codex_state::SqliteConfig::new_for_testing(home.path().abs()),
|
|
config.default_model_provider_id.clone(),
|
|
)
|
|
.await
|
|
.expect("state db should initialize");
|
|
let store = LocalThreadStore::new(config.clone(), Some(runtime.clone()));
|
|
runtime
|
|
.mark_backfill_complete(/*last_watermark*/ None)
|
|
.await
|
|
.expect("backfill should be complete");
|
|
let mut builder = codex_state::ThreadMetadataBuilder::new(
|
|
thread_id,
|
|
archived_path.clone(),
|
|
Utc::now(),
|
|
SessionSource::Cli,
|
|
);
|
|
builder.model_provider = Some(config.default_model_provider_id.clone());
|
|
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.archived_at = Some(metadata.updated_at);
|
|
metadata.is_pinned = true;
|
|
runtime
|
|
.upsert_thread(&metadata)
|
|
.await
|
|
.expect("state db upsert should succeed");
|
|
|
|
let unarchived = store
|
|
.unarchive_thread(ArchiveThreadParams { thread_id })
|
|
.await
|
|
.expect("unarchive thread");
|
|
assert!(unarchived.is_pinned);
|
|
|
|
let restored_path = home
|
|
.path()
|
|
.join("sessions/2025/01/03")
|
|
.join(archived_path.file_name().expect("file name"));
|
|
let updated = runtime
|
|
.get_thread(thread_id)
|
|
.await
|
|
.expect("state db read should succeed")
|
|
.expect("thread metadata should exist");
|
|
assert_eq!(updated.rollout_path, restored_path);
|
|
assert_eq!(updated.archived_at, None);
|
|
assert_eq!(updated.recency_at, metadata.recency_at);
|
|
assert!(updated.is_pinned);
|
|
}
|
|
}
|