mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
a8488fec5e
## 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`.
201 lines
7.1 KiB
Rust
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);
|
|
}
|
|
}
|