From d3abd8774ed2caa13db253ee232791e5a2d2d8c5 Mon Sep 17 00:00:00 2001 From: jif Date: Wed, 10 Jun 2026 18:23:42 +0100 Subject: [PATCH] Fix compressed rollout search path matching (#27407) ## Why `thread/search` found content inside compressed rollouts but could drop the result when joining it with SQLite-backed thread metadata. Search returned the physical `.jsonl.zst` path while SQLite retained the logical `.jsonl` path, so exact path matching failed. ## What changed - Key rollout search matches by their canonical logical `.jsonl` path, independent of the on-disk representation. - Canonicalize thread-list paths before joining them with content-search matches. - Update compressed-rollout coverage to assert the logical-path contract. ## Validation - Ran `just fmt`. - Ran `git diff --check`. - Tests and Clippy were intentionally left to CI. --- codex-rs/rollout/src/compression_tests.rs | 5 ++--- codex-rs/rollout/src/search.rs | 11 +++++++++-- codex-rs/thread-store/src/local/search_threads.rs | 3 ++- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/codex-rs/rollout/src/compression_tests.rs b/codex-rs/rollout/src/compression_tests.rs index eb8111296..682361ea9 100644 --- a/codex-rs/rollout/src/compression_tests.rs +++ b/codex-rs/rollout/src/compression_tests.rs @@ -106,14 +106,13 @@ async fn append_rollout_item_materializes_compressed_rollout() -> anyhow::Result } #[tokio::test] -async fn search_rollout_matches_returns_compressed_snippet() -> anyhow::Result<()> { +async fn search_rollout_matches_uses_logical_path_for_compressed_rollout() -> anyhow::Result<()> { let home = TempDir::new()?; let uuid = Uuid::from_u128(15); let thread_id = ThreadId::from_string(&uuid.to_string())?; let rollout_path = rollout_path(home.path(), "2025-01-03T12-00-00", uuid); write_rollout(&rollout_path, thread_id, "targeted search term")?; compress_now(&rollout_path)?; - let compressed_path = compressed_rollout_path(&rollout_path); let matches = search_rollout_matches( std::path::Path::new("missing-rg-for-test"), @@ -124,7 +123,7 @@ async fn search_rollout_matches_returns_compressed_snippet() -> anyhow::Result<( .await?; assert_eq!( - matches.get(compressed_path.as_path()), + matches.get(rollout_path.as_path()), Some(&Some("targeted search term".to_string())) ); Ok(()) diff --git a/codex-rs/rollout/src/search.rs b/codex-rs/rollout/src/search.rs index 705d24e7b..622fac8b0 100644 --- a/codex-rs/rollout/src/search.rs +++ b/codex-rs/rollout/src/search.rs @@ -21,6 +21,7 @@ use super::compression; const MATCH_CONTEXT_BEFORE_CHARS: usize = 48; const MATCH_CONTEXT_AFTER_CHARS: usize = 96; +/// Search matches keyed by the canonical `.jsonl` path for each rollout. pub type RolloutSearchMatches = HashMap>; pub async fn search_rollout_paths( @@ -145,7 +146,10 @@ async fn scan_rollout_matches( if let Some(snippet) = first_rollout_content_match_snippet(rollout_file.path(), search_term).await? { - matches.insert(rollout_file.into_path(), Some(snippet)); + matches.insert( + compression::plain_rollout_path(rollout_file.path()), + Some(snippet), + ); } continue; } @@ -217,7 +221,10 @@ async fn scan_compressed_rollout_matches( if let Some(snippet) = first_rollout_content_match_snippet(rollout_file.path(), search_term).await? { - matches.insert(rollout_file.into_path(), Some(snippet)); + matches.insert( + compression::plain_rollout_path(rollout_file.path()), + Some(snippet), + ); } } } diff --git a/codex-rs/thread-store/src/local/search_threads.rs b/codex-rs/thread-store/src/local/search_threads.rs index 20f5da824..c9394c396 100644 --- a/codex-rs/thread-store/src/local/search_threads.rs +++ b/codex-rs/thread-store/src/local/search_threads.rs @@ -109,7 +109,8 @@ pub(super) async fn search_threads( ) .await?; for item in page.items { - let Some(snippet) = (match remaining_rollouts.remove(item.path.as_path()) { + let logical_path = codex_rollout::plain_rollout_path(item.path.as_path()); + let Some(snippet) = (match remaining_rollouts.remove(logical_path.as_path()) { Some(Some(snippet)) => Some(snippet), Some(None) => first_rollout_content_match_snippet(item.path.as_path(), search_term) .await