Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 27 additions & 26 deletions codex-rs/core/src/rollout/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1074,6 +1074,29 @@ async fn find_thread_path_by_id_str_in_subdir(
return Ok(None);
}

// Prefer DB lookup, then fall back to rollout file search.
// TODO(jif): sqlite migration phase 1
let archived_only = match subdir {
SESSIONS_SUBDIR => Some(false),
ARCHIVED_SESSIONS_SUBDIR => Some(true),
_ => None,
};
let state_db_ctx = state_db::open_if_present(codex_home, "").await;
if let Some(state_db_ctx) = state_db_ctx.as_deref()
&& let Ok(thread_id) = ThreadId::from_string(id_str)
{
let db_path = state_db::find_rollout_path_by_id(
Some(state_db_ctx),
thread_id,
archived_only,
"find_path_query",
)
.await;
if db_path.is_some() {
return Ok(db_path);
}
}

let mut root = codex_home.to_path_buf();
root.push(subdir);
if !root.exists() {
Expand All @@ -1093,33 +1116,11 @@ async fn find_thread_path_by_id_str_in_subdir(
.map_err(|e| io::Error::other(format!("file search failed: {e}")))?;

let found = results.matches.into_iter().next().map(|m| m.full_path());

// Checking if DB is at parity.
// TODO(jif): sqlite migration phase 1
let archived_only = match subdir {
SESSIONS_SUBDIR => Some(false),
ARCHIVED_SESSIONS_SUBDIR => Some(true),
_ => None,
};
let state_db_ctx = state_db::open_if_present(codex_home, "").await;
if let Some(state_db_ctx) = state_db_ctx.as_deref()
&& let Ok(thread_id) = ThreadId::from_string(id_str)
{
let db_path = state_db::find_rollout_path_by_id(
Some(state_db_ctx),
thread_id,
archived_only,
"find_path_query",
)
.await;
let canonical_path = found.as_deref();
if db_path.as_deref() != canonical_path {
tracing::warn!(
"state db path mismatch for thread {thread_id:?}: canonical={canonical_path:?} db={db_path:?}"
);
state_db::record_discrepancy("find_thread_path_by_id_str_in_subdir", "path_mismatch");
}
if found.is_some() {
tracing::error!("state db missing rollout path for thread {id_str}");
state_db::record_discrepancy("find_thread_path_by_id_str_in_subdir", "path_mismatch");
}

Ok(found)
}

Expand Down
55 changes: 55 additions & 0 deletions codex-rs/core/tests/suite/rollout_list_find.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::io::Write;
use std::path::Path;
use std::path::PathBuf;

use chrono::Utc;
use codex_core::RolloutRecorder;
use codex_core::RolloutRecorderParams;
use codex_core::config::ConfigBuilder;
Expand All @@ -12,6 +13,8 @@ use codex_core::find_thread_path_by_name_str;
use codex_core::protocol::SessionSource;
use codex_protocol::ThreadId;
use codex_protocol::models::BaseInstructions;
use codex_state::StateRuntime;
use codex_state::ThreadMetadataBuilder;
use pretty_assertions::assert_eq;
use tempfile::TempDir;
use uuid::Uuid;
Expand Down Expand Up @@ -52,6 +55,21 @@ fn write_minimal_rollout_with_id(codex_home: &Path, id: Uuid) -> PathBuf {
write_minimal_rollout_with_id_in_subdir(codex_home, "sessions", id)
}

async fn upsert_thread_metadata(codex_home: &Path, thread_id: ThreadId, rollout_path: PathBuf) {
let runtime = StateRuntime::init(codex_home.to_path_buf(), "test-provider".to_string(), None)
.await
.unwrap();
let mut builder = ThreadMetadataBuilder::new(
thread_id,
rollout_path,
Utc::now(),
SessionSource::default(),
);
builder.cwd = codex_home.to_path_buf();
let metadata = builder.build("test-provider");
runtime.upsert_thread(&metadata).await.unwrap();
}

#[tokio::test]
async fn find_locates_rollout_file_by_id() {
let home = TempDir::new().unwrap();
Expand Down Expand Up @@ -81,6 +99,43 @@ async fn find_handles_gitignore_covering_codex_home_directory() {
assert_eq!(found, Some(expected));
}

#[tokio::test]
async fn find_prefers_sqlite_path_by_id() {
let home = TempDir::new().unwrap();
let id = Uuid::new_v4();
let thread_id = ThreadId::from_string(&id.to_string()).unwrap();
let db_path = home
.path()
.join("sessions/2030/12/30/rollout-2030-12-30T00-00-00-db.jsonl");
write_minimal_rollout_with_id(home.path(), id);
upsert_thread_metadata(home.path(), thread_id, db_path.clone()).await;

let found = find_thread_path_by_id_str(home.path(), &id.to_string())
.await
.unwrap();

assert_eq!(found, Some(db_path));
}

#[tokio::test]
async fn find_falls_back_to_filesystem_when_sqlite_has_no_match() {
let home = TempDir::new().unwrap();
let id = Uuid::new_v4();
let expected = write_minimal_rollout_with_id(home.path(), id);
let unrelated_id = Uuid::new_v4();
let unrelated_thread_id = ThreadId::from_string(&unrelated_id.to_string()).unwrap();
let unrelated_path = home
.path()
.join("sessions/2030/12/30/rollout-2030-12-30T00-00-00-unrelated.jsonl");
upsert_thread_metadata(home.path(), unrelated_thread_id, unrelated_path).await;

let found = find_thread_path_by_id_str(home.path(), &id.to_string())
.await
.unwrap();

assert_eq!(found, Some(expected));
}

#[tokio::test]
async fn find_ignores_granular_gitignore_rules() {
let home = TempDir::new().unwrap();
Expand Down
Loading