diff --git a/rust/src/codex_workspaces/indexer.rs b/rust/src/codex_workspaces/indexer.rs index eff8d42aa0..cba4c9476b 100644 --- a/rust/src/codex_workspaces/indexer.rs +++ b/rust/src/codex_workspaces/indexer.rs @@ -144,13 +144,15 @@ impl CodexWorkspacesIndex { let sidecar = self.sidecar()?; let source_status = read_catalog_status(&scope.state_database); - if !force_refresh - && let Ok(Some(cached)) = - sidecar.load_latest_snapshot(scope.scope_signature(), self.history_days) - { - let mut snap = cached; - snap.source_status = source_status; - return Ok(snap); + if !force_refresh { + match sidecar.load_latest_snapshot(scope.scope_signature(), self.history_days) { + Ok(Some(mut cached)) => { + cached.source_status = source_status; + return Ok(cached); + } + Ok(None) => {} + Err(error) => return Err(error.into()), + } } progress(Progress::phase(ProgressPhase::ScanningLogs)); @@ -833,6 +835,46 @@ mod tests { assert_eq!(cached.indexed_file_count, 2); } + #[test] + fn cached_snapshot_is_not_reused_for_another_codex_home() { + let tmp = TempDir::new().unwrap(); + let first_home = tmp.path().join("first-codex"); + let first_sessions = first_home.join("sessions"); + fs::create_dir_all(&first_sessions).unwrap(); + let day = Local::now().date_naive().format("%Y-%m-%d").to_string(); + write_session( + &first_sessions, + &day, + "first-session.jsonl", + &tmp.path().join("first-project").to_string_lossy(), + "gpt-5", + 100, + 20, + ); + + let sidecar = tmp.path().join("sidecar.sqlite"); + let first = CodexWorkspacesIndex::new(30) + .with_codex_home(&first_home) + .with_sidecar_path(&sidecar); + let first_snapshot = first.load_snapshot(true, |_| {}).unwrap(); + assert_eq!(first_snapshot.indexed_file_count, 1); + + let second_home = tmp.path().join("second-codex"); + fs::create_dir_all(second_home.join("sessions")).unwrap(); + let second = CodexWorkspacesIndex::new(30) + .with_codex_home(&second_home) + .with_sidecar_path(&sidecar); + + assert!(second.load_cached_snapshot().unwrap().is_none()); + let second_snapshot = second.load_snapshot(false, |_| {}).unwrap(); + assert_eq!(second_snapshot.indexed_file_count, 0); + assert!(second_snapshot.projects.is_empty()); + assert_ne!( + first_snapshot.scope_signature, + second_snapshot.scope_signature + ); + } + #[test] fn foreign_user_version_is_rejected() { let tmp = TempDir::new().unwrap(); diff --git a/rust/src/codex_workspaces/sidecar.rs b/rust/src/codex_workspaces/sidecar.rs index 732e7e435a..a87aa84f10 100644 --- a/rust/src/codex_workspaces/sidecar.rs +++ b/rust/src/codex_workspaces/sidecar.rs @@ -23,6 +23,15 @@ pub enum SidecarError { "workspaces sidecar schema incompatible (user_version={found}, expected {SCHEMA_VERSION})" )] Incompatible { found: i32 }, + #[error( + "workspaces sidecar cache scope mismatch (expected scope={expected}, history_days={expected_history_days}; found scope={found}, history_days={found_history_days})" + )] + ScopeMismatch { + expected: String, + expected_history_days: u32, + found: String, + found_history_days: u32, + }, #[error("workspaces sidecar error: {0}")] Sqlite(#[from] rusqlite::Error), #[error("workspaces sidecar encode/decode failed: {0}")] @@ -83,7 +92,15 @@ impl WorkspaceUsageSidecar { if format_version != PAYLOAD_FORMAT_VERSION { return Ok(None); } - let snapshot = serde_json::from_slice(&payload)?; + let snapshot: CodexLocalProjectUsageSnapshot = serde_json::from_slice(&payload)?; + if snapshot.scope_signature != scope_signature || snapshot.history_days != history_days { + return Err(SidecarError::ScopeMismatch { + expected: scope_signature.to_string(), + expected_history_days: history_days, + found: snapshot.scope_signature, + found_history_days: snapshot.history_days, + }); + } Ok(Some(snapshot)) } @@ -188,3 +205,23 @@ impl WorkspaceUsageSidecar { Ok(()) } } + +#[cfg(test)] +mod tests { + use super::SidecarError; + + #[test] + fn scope_mismatch_error_includes_both_history_windows() { + let error = SidecarError::ScopeMismatch { + expected: "codex-workspaces:expected".to_string(), + expected_history_days: 30, + found: "codex-workspaces:expected".to_string(), + found_history_days: 7, + }; + + assert_eq!( + error.to_string(), + "workspaces sidecar cache scope mismatch (expected scope=codex-workspaces:expected, history_days=30; found scope=codex-workspaces:expected, history_days=7)" + ); + } +}