Skip to content

Commit e9e7ef3

Browse files
Fix thread/list cwd filtering for Windows verbatim paths (#17414)
Addresses #17302 Problem: `thread/list` compared cwd filters with raw path equality, so `resume --last` could miss Windows sessions when the saved cwd used a verbatim path form and the current cwd did not. Solution: Normalize cwd comparisons through the existing path comparison utilities before falling back to direct equality, and add Windows regression coverage for verbatim paths. I made this a general utility function and replaced all of the duplicated instance of it across the code base.
1 parent a9796e3 commit e9e7ef3

9 files changed

Lines changed: 58 additions & 47 deletions

File tree

codex-rs/app-server/src/codex_message_processor.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ use codex_core::find_thread_name_by_id;
222222
use codex_core::find_thread_names_by_ids;
223223
use codex_core::find_thread_path_by_id_str;
224224
use codex_core::parse_cursor;
225+
use codex_core::path_utils;
225226
use codex_core::plugins::MarketplaceError;
226227
use codex_core::plugins::MarketplacePluginSource;
227228
use codex_core::plugins::OPENAI_CURATED_MARKETPLACE_NAME;
@@ -4768,9 +4769,9 @@ impl CodexMessageProcessor {
47684769
if source_kind_filter
47694770
.as_ref()
47704771
.is_none_or(|filter| source_kind_matches(&summary.source, filter))
4771-
&& cwd
4772-
.as_ref()
4773-
.is_none_or(|expected_cwd| &summary.cwd == expected_cwd)
4772+
&& cwd.as_ref().is_none_or(|expected_cwd| {
4773+
path_utils::paths_match_after_normalization(&summary.cwd, expected_cwd)
4774+
})
47744775
{
47754776
filtered.push(summary);
47764777
if filtered.len() >= remaining {

codex-rs/core/src/config/service.rs

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -629,14 +629,7 @@ fn validate_config(value: &TomlValue) -> Result<(), toml::de::Error> {
629629
}
630630

631631
fn paths_match(expected: impl AsRef<Path>, provided: impl AsRef<Path>) -> bool {
632-
if let (Ok(expanded_expected), Ok(expanded_provided)) = (
633-
path_utils::normalize_for_path_comparison(&expected),
634-
path_utils::normalize_for_path_comparison(&provided),
635-
) {
636-
expanded_expected == expanded_provided
637-
} else {
638-
expected.as_ref() == provided.as_ref()
639-
}
632+
path_utils::paths_match_after_normalization(expected, provided)
640633
}
641634

642635
fn value_at_path<'a>(root: &'a TomlValue, segments: &[String]) -> Option<&'a TomlValue> {

codex-rs/core/src/tools/runtimes/mod.rs

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -76,14 +76,7 @@ pub(crate) fn maybe_wrap_shell_lc_with_snapshot(
7676
return command.to_vec();
7777
}
7878

79-
if if let (Ok(snapshot_cwd), Ok(command_cwd)) = (
80-
path_utils::normalize_for_path_comparison(snapshot.cwd.as_path()),
81-
path_utils::normalize_for_path_comparison(cwd),
82-
) {
83-
snapshot_cwd != command_cwd
84-
} else {
85-
snapshot.cwd != cwd
86-
} {
79+
if !path_utils::paths_match_after_normalization(snapshot.cwd.as_path(), cwd) {
8780
return command.to_vec();
8881
}
8982

codex-rs/exec/src/lib.rs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1205,13 +1205,7 @@ async fn parse_latest_turn_context_cwd(path: &Path) -> Option<PathBuf> {
12051205
}
12061206

12071207
fn cwds_match(current_cwd: &Path, session_cwd: &Path) -> bool {
1208-
match (
1209-
path_utils::normalize_for_path_comparison(current_cwd),
1210-
path_utils::normalize_for_path_comparison(session_cwd),
1211-
) {
1212-
(Ok(current), Ok(session)) => current == session,
1213-
_ => current_cwd == session_cwd,
1214-
}
1208+
path_utils::paths_match_after_normalization(current_cwd, session_cwd)
12151209
}
12161210

12171211
async fn resolve_resume_thread_id(

codex-rs/rollout/src/recorder.rs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1342,13 +1342,7 @@ async fn select_resume_path_from_db_page(
13421342
}
13431343

13441344
fn cwd_matches(session_cwd: &Path, cwd: &Path) -> bool {
1345-
if let (Ok(ca), Ok(cb)) = (
1346-
path_utils::normalize_for_path_comparison(session_cwd),
1347-
path_utils::normalize_for_path_comparison(cwd),
1348-
) {
1349-
return ca == cb;
1350-
}
1351-
session_cwd == cwd
1345+
path_utils::paths_match_after_normalization(session_cwd, cwd)
13521346
}
13531347

13541348
#[cfg(test)]

codex-rs/tui/src/lib.rs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1524,13 +1524,7 @@ async fn read_latest_turn_context(path: &Path) -> Option<TurnContextItem> {
15241524
}
15251525

15261526
pub(crate) fn cwds_differ(current_cwd: &Path, session_cwd: &Path) -> bool {
1527-
match (
1528-
path_utils::normalize_for_path_comparison(current_cwd),
1529-
path_utils::normalize_for_path_comparison(session_cwd),
1530-
) {
1531-
(Ok(current), Ok(session)) => current != session,
1532-
_ => current_cwd != session_cwd,
1533-
}
1527+
!path_utils::paths_match_after_normalization(current_cwd, session_cwd)
15341528
}
15351529

15361530
pub(crate) enum ResolveCwdOutcome {

codex-rs/tui/src/resume_picker.rs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1148,13 +1148,7 @@ fn thread_list_params(
11481148
}
11491149

11501150
fn paths_match(a: &Path, b: &Path) -> bool {
1151-
if let (Ok(ca), Ok(cb)) = (
1152-
path_utils::normalize_for_path_comparison(a),
1153-
path_utils::normalize_for_path_comparison(b),
1154-
) {
1155-
return ca == cb;
1156-
}
1157-
a == b
1151+
path_utils::paths_match_after_normalization(a, b)
11581152
}
11591153

11601154
#[cfg_attr(not(test), allow(dead_code))]

codex-rs/utils/path-utils/src/lib.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,19 @@ pub fn normalize_for_path_comparison(path: impl AsRef<Path>) -> std::io::Result<
1616
Ok(normalize_for_wsl(canonical))
1717
}
1818

19+
/// Compare paths after applying Codex's filesystem normalization.
20+
///
21+
/// If either path cannot be normalized, this falls back to direct path equality.
22+
pub fn paths_match_after_normalization(left: impl AsRef<Path>, right: impl AsRef<Path>) -> bool {
23+
if let (Ok(left), Ok(right)) = (
24+
normalize_for_path_comparison(left.as_ref()),
25+
normalize_for_path_comparison(right.as_ref()),
26+
) {
27+
return left == right;
28+
}
29+
left.as_ref() == right.as_ref()
30+
}
31+
1932
pub fn normalize_for_native_workdir(path: impl AsRef<Path>) -> PathBuf {
2033
normalize_for_native_workdir_with_flag(path.as_ref().to_path_buf(), cfg!(windows))
2134
}

codex-rs/utils/path-utils/src/path_utils_tests.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,38 @@ mod native_workdir {
7878
assert_eq!(normalized, path);
7979
}
8080
}
81+
82+
mod path_comparison {
83+
use super::super::paths_match_after_normalization;
84+
use std::path::PathBuf;
85+
86+
#[test]
87+
fn matches_identical_existing_paths() -> std::io::Result<()> {
88+
let dir = tempfile::tempdir()?;
89+
90+
assert!(paths_match_after_normalization(dir.path(), dir.path()));
91+
Ok(())
92+
}
93+
94+
#[test]
95+
fn falls_back_to_raw_equality_when_paths_cannot_be_normalized() {
96+
assert!(paths_match_after_normalization(
97+
PathBuf::from("missing"),
98+
PathBuf::from("missing"),
99+
));
100+
assert!(!paths_match_after_normalization(
101+
PathBuf::from("missing-a"),
102+
PathBuf::from("missing-b"),
103+
));
104+
}
105+
106+
#[cfg(windows)]
107+
#[test]
108+
fn matches_windows_verbatim_paths() -> std::io::Result<()> {
109+
let dir = tempfile::tempdir()?;
110+
let verbatim_dir = PathBuf::from(format!(r"\\?\{}", dir.path().display()));
111+
112+
assert!(paths_match_after_normalization(verbatim_dir, dir.path()));
113+
Ok(())
114+
}
115+
}

0 commit comments

Comments
 (0)