Files
codex/codex-rs/thread-store/src/local/unarchive_thread.rs
T
pakrym-oai a8488fec5e Revert state DB injection and agent graph store (#21481)
## Why

Reverts #20689 to restore the previous optional state DB plumbing. The
conflict resolution keeps the newer installation ID and session/thread
identity changes that landed after #20689, while removing the mandatory
state DB and agent graph store dependency from ThreadManager
construction.

## What changed

- Restored `Option<StateDbHandle>` through app-server, MCP server,
prompt debug, and test entry points.
- Removed the `codex-core` dependency on `codex-agent-graph-store` and
reverted descendant lookup back to the existing state DB path when
available.
- Kept newer `installation_id` forwarding by passing it beside the
optional DB handle.
- Kept local thread-name updates working when the optional state DB
handle is absent.

## Validation

- `git diff --check`
- `cargo test -p codex-thread-store`
- `cargo test -p codex-state -p codex-rollout -p
codex-app-server-protocol`
- Attempted `env CARGO_INCREMENTAL=0 cargo test -p codex-core -p
codex-app-server -p codex-app-server-client -p codex-mcp-server -p
codex-thread-manager-sample -p codex-tui`; blocked locally by a rustc
ICE while compiling `v8 v146.4.0` with `rustc 1.93.0 (254b59607
2026-01-19)` on `aarch64-apple-darwin`.
2026-05-06 22:48:29 -07:00

201 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);
}
}