mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Migrate archive/unarchive to local ThreadStore (#17892)
# Summary - implement local ThreadStore archive/unarchive operations - implement local ThreadStore read_thread operation - break up the various ThreadStore local method implementations into separate files - migrate app-server archive/unarchive and core archive fixture to use ThreadStore (but not all read operations yet!) - use the ThreadStore's read operation as a proxy check for thread persistence/existence in the app server code - move all other filesystem operations related to archive (path validation etc) into the local thread store. # Tests - add dedicated local store archive/unarchive tests
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
use codex_rollout::find_archived_thread_path_by_id_str;
|
||||
use codex_rollout::read_thread_item_from_rollout;
|
||||
use codex_rollout::rollout_date_parts;
|
||||
|
||||
use super::LocalThreadStore;
|
||||
use super::helpers::matching_rollout_file_name;
|
||||
use super::helpers::scoped_rollout_path;
|
||||
use super::helpers::stored_thread_from_rollout_item;
|
||||
use super::helpers::touch_modified_time;
|
||||
use crate::ArchiveThreadParams;
|
||||
use crate::StoredThread;
|
||||
use crate::ThreadStoreError;
|
||||
use crate::ThreadStoreResult;
|
||||
|
||||
pub(super) async fn unarchive_thread(
|
||||
store: &LocalThreadStore,
|
||||
params: ArchiveThreadParams,
|
||||
) -> ThreadStoreResult<StoredThread> {
|
||||
let thread_id = params.thread_id;
|
||||
let archived_path = find_archived_thread_path_by_id_str(
|
||||
store.config.codex_home.as_path(),
|
||||
&thread_id.to_string(),
|
||||
)
|
||||
.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) = codex_rollout::state_db::get_state_db(&store.config).await {
|
||||
let _ = ctx
|
||||
.mark_unarchived(thread_id, restored_path.as_path())
|
||||
.await;
|
||||
}
|
||||
|
||||
let item = read_thread_item_from_rollout(restored_path.clone())
|
||||
.await
|
||||
.ok_or_else(|| ThreadStoreError::Internal {
|
||||
message: format!(
|
||||
"failed to read unarchived thread {}",
|
||||
restored_path.display()
|
||||
),
|
||||
})?;
|
||||
stored_thread_from_rollout_item(
|
||||
item,
|
||||
/*archived*/ false,
|
||||
store.config.model_provider_id.as_str(),
|
||||
)
|
||||
.ok_or_else(|| ThreadStoreError::Internal {
|
||||
message: format!(
|
||||
"failed to read unarchived thread id from {}",
|
||||
restored_path.display()
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use chrono::Utc;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
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()));
|
||||
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 store = LocalThreadStore::new(config.clone());
|
||||
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(
|
||||
home.path().to_path_buf(),
|
||||
config.model_provider_id.clone(),
|
||||
)
|
||||
.await
|
||||
.expect("state db should initialize");
|
||||
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.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.model_provider_id.as_str());
|
||||
metadata.archived_at = Some(metadata.updated_at);
|
||||
runtime
|
||||
.upsert_thread(&metadata)
|
||||
.await
|
||||
.expect("state db upsert should succeed");
|
||||
|
||||
store
|
||||
.unarchive_thread(ArchiveThreadParams { thread_id })
|
||||
.await
|
||||
.expect("unarchive thread");
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user