mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
7dc7096ae1
## Summary - Revert #28655, restoring the thread `recencyAt` behavior introduced by #27910. - Move `threads_recency_at` to migration 0039 so it no longer collides with `external_agent_config_imports` at version 0038. - Repair databases that already applied the recency migration as version 38 by moving the matching migration-history row to version 39 before SQLx validation. The current version-38 migration can then apply normally. ## Validation - `just test -p codex-state migrations::tests::repairs_recency_migration_that_was_applied_as_version_38` - `just test -p codex-state -p codex-rollout -p codex-thread-store -p codex-app-server-protocol -p codex-tui`: 3,439 passed; six TUI tests could not open the machine's existing read-only incident database at `~/.codex/sqlite/state_5.sqlite`. - `just fix -p codex-state` - `just fmt` - Verified that state migration versions are unique.
202 lines
7.1 KiB
Rust
202 lines
7.1 KiB
Rust
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 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;
|
|
}
|
|
|
|
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.default_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()), /*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(
|
|
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,
|
|
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);
|
|
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);
|
|
assert_eq!(updated.recency_at, metadata.recency_at);
|
|
}
|
|
}
|