Preserve runtime workspace roots across thread resume (#43848)

## Why

Resuming a thread should retain its selected workspace folders, including additional roots and explicit empty selections. Resume overrides also need to survive a subsequent resume when no turn has run.

## What changed

- Persist `runtime_workspace_roots` in startup metadata and thread settings snapshots, separately from explicit environment selections and permission-profile roots.
- Restore roots from the latest snapshot owned by the resumed thread, falling back to owned startup metadata only when no snapshot exists. Honor explicit `runtimeWorkspaceRoots` overrides, retarget the old `cwd` root when `cwd` changes, deduplicate roots, and validate restored paths for the current host.
- Checkpoint effective settings on resume and restored settings after revert. Reload resume configuration if saved workspace roots change during loading.
- Normalize Windows rollout path spellings when matching thread search results, preserving selection of the correct rollout after revert, including compressed rollouts.

## Testing

Add regression coverage for workspace restoration, empty and explicit overrides, foreign paths, compaction and revert, resume checkpoints without recency changes, concurrent settings persistence, and rollout search path matching.

GitOrigin-RevId: d98d9d34dd63934d441120916c61c12b69e7f062
This commit is contained in:
rhan-oai
2026-09-08 16:24:38 +00:00
committed by copyberry
parent dd9512c000
commit 6515a72db7
46 changed files with 984 additions and 135 deletions
+20 -1
View File
@@ -1,4 +1,4 @@
//! Path normalization, symlink resolution, and atomic writes shared across Codex crates.
//! Path normalization, replacement, symlink resolution, and atomic writes.
pub(crate) mod env;
pub use env::is_wsl;
@@ -8,6 +8,7 @@ pub use system_commands::system_path;
use codex_utils_absolute_path::AbsolutePathBuf;
use std::collections::HashSet;
use std::hash::Hash;
use std::io;
use std::io::Write;
use std::path::Path;
@@ -32,6 +33,24 @@ pub fn paths_match_after_normalization(left: impl AsRef<Path>, right: impl AsRef
left.as_ref() == right.as_ref()
}
/// Replace paths equal to `old_path` and remove duplicates, preserving their order.
///
/// Other paths, including descendants of `old_path`, remain unchanged. This uses
/// path equality without filesystem access or normalization; callers validate paths.
pub fn replace_path_and_deduplicate<P>(mut paths: Vec<P>, old_path: &Path, new_path: P) -> Vec<P>
where
P: AsRef<Path> + Clone + Eq + Hash,
{
for path in &mut paths {
if path.as_ref() == old_path {
*path = new_path.clone();
}
}
let mut seen = HashSet::new();
paths.retain(|path| seen.insert(path.clone()));
paths
}
pub fn normalize_for_native_workdir(path: impl AsRef<Path>) -> PathBuf {
normalize_for_native_workdir_with_flag(path.as_ref().to_path_buf(), cfg!(windows))
}
@@ -1,3 +1,24 @@
use super::replace_path_and_deduplicate;
use pretty_assertions::assert_eq;
use std::path::Path;
#[test]
fn replace_path_and_deduplicate_preserves_other_paths_and_order() {
let old_path = Path::new("old");
let new_path = Path::new("new");
let extra = Path::new("extra");
let descendant = Path::new("old/child");
assert_eq!(
replace_path_and_deduplicate(
vec![old_path, extra, descendant, new_path, extra],
old_path,
new_path,
),
vec![new_path, extra, descendant]
);
}
#[cfg(unix)]
mod symlinks {
use super::super::resolve_symlink_write_paths;