Codex Handbook
thread-store/src/local/archive_thread.rs 178 lines
use chrono::Utc;use codex_rollout::find_thread_path_by_id_str;use super::LocalThreadStore;use super::helpers::matching_rollout_file_name;use super::helpers::scoped_rollout_path;use crate::ArchiveThreadParams;use crate::ThreadStoreError;use crate::ThreadStoreResult;pub(super) async fn archive_thread(    store: &LocalThreadStore,    params: ArchiveThreadParams,) -> ThreadStoreResult<()> {    let thread_id = params.thread_id;    let state_db_ctx = store.state_db().await;    let rollout_path = find_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 thread id {thread_id}: {err}"),    })?    .ok_or_else(|| ThreadStoreError::InvalidRequest {        message: format!("no rollout found for thread id {thread_id}"),    })?;    let canonical_rollout_path = scoped_rollout_path(        store.config.codex_home.join(codex_rollout::SESSIONS_SUBDIR),        rollout_path.as_path(),        "sessions",    )?;    let file_name = matching_rollout_file_name(        canonical_rollout_path.as_path(),        thread_id,        rollout_path.as_path(),    )?;    let archive_folder = store        .config        .codex_home        .join(codex_rollout::ARCHIVED_SESSIONS_SUBDIR);    std::fs::create_dir_all(&archive_folder).map_err(|err| ThreadStoreError::Internal {        message: format!("failed to archive thread: {err}"),    })?;    let archived_path = archive_folder.join(&file_name);    std::fs::rename(&canonical_rollout_path, &archived_path).map_err(|err| {        ThreadStoreError::Internal {            message: format!("failed to archive thread: {err}"),        }    })?;    if let Some(ctx) = state_db_ctx {        let _ = ctx            .mark_archived(thread_id, archived_path.as_path(), Utc::now())            .await;    }    Ok(())}#[cfg(test)]mod tests {    use chrono::Utc;    use codex_protocol::ThreadId;    use codex_protocol::protocol::SessionSource;    use codex_rollout::ARCHIVED_SESSIONS_SUBDIR;    use pretty_assertions::assert_eq;    use tempfile::TempDir;    use uuid::Uuid;    use super::*;    use crate::ListThreadsParams;    use crate::ThreadSortKey;    use crate::ThreadStore;    use crate::local::LocalThreadStore;    use crate::local::test_support::test_config;    use crate::local::test_support::write_session_file;    #[tokio::test]    async fn archive_thread_moves_rollout_to_archived_collection() {        let home = TempDir::new().expect("temp dir");        let store = LocalThreadStore::new(test_config(home.path()), /*state_db*/ None);        let uuid = Uuid::from_u128(201);        let thread_id = ThreadId::from_string(&uuid.to_string()).expect("valid thread id");        let active_path =            write_session_file(home.path(), "2025-01-03T12-00-00", uuid).expect("session file");        store            .archive_thread(ArchiveThreadParams { thread_id })            .await            .expect("archive thread");        assert!(!active_path.exists());        let archived_path = home            .path()            .join(ARCHIVED_SESSIONS_SUBDIR)            .join(active_path.file_name().expect("file name"));        assert!(archived_path.exists());        let archived = store            .list_threads(ListThreadsParams {                page_size: 10,                cursor: None,                sort_key: ThreadSortKey::CreatedAt,                sort_direction: crate::SortDirection::Desc,                allowed_sources: Vec::new(),                model_providers: None,                cwd_filters: None,                archived: true,                search_term: None,                parent_thread_id: None,                use_state_db_only: false,            })            .await            .expect("archived listing");        assert_eq!(archived.items.len(), 1);        assert_eq!(archived.items[0].thread_id, thread_id);        assert_eq!(archived.items[0].rollout_path, Some(archived_path));        assert_eq!(            archived.items[0].archived_at,            Some(archived.items[0].updated_at)        );    }    #[tokio::test]    async fn archive_thread_updates_sqlite_metadata_when_present() {        let home = TempDir::new().expect("temp dir");        let config = test_config(home.path());        let uuid = Uuid::from_u128(202);        let thread_id = ThreadId::from_string(&uuid.to_string()).expect("valid thread id");        let active_path =            write_session_file(home.path(), "2025-01-03T12-00-00", uuid).expect("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.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,            active_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 metadata = builder.build(config.default_model_provider_id.as_str());        runtime            .upsert_thread(&metadata)            .await            .expect("state db upsert should succeed");        store            .archive_thread(ArchiveThreadParams { thread_id })            .await            .expect("archive thread");        let archived_path = home            .path()            .join(ARCHIVED_SESSIONS_SUBDIR)            .join(active_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, archived_path);        assert!(updated.archived_at.is_some());    }}