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.
This commit is contained in:
Eric Traut
2026-04-10 23:08:02 -07:00
committed by GitHub
Unverified
parent a9796e39c4
commit e9e7ef3d36
9 changed files with 58 additions and 47 deletions
@@ -222,6 +222,7 @@ use codex_core::find_thread_name_by_id;
use codex_core::find_thread_names_by_ids;
use codex_core::find_thread_path_by_id_str;
use codex_core::parse_cursor;
use codex_core::path_utils;
use codex_core::plugins::MarketplaceError;
use codex_core::plugins::MarketplacePluginSource;
use codex_core::plugins::OPENAI_CURATED_MARKETPLACE_NAME;
@@ -4768,9 +4769,9 @@ impl CodexMessageProcessor {
if source_kind_filter
.as_ref()
.is_none_or(|filter| source_kind_matches(&summary.source, filter))
&& cwd
.as_ref()
.is_none_or(|expected_cwd| &summary.cwd == expected_cwd)
&& cwd.as_ref().is_none_or(|expected_cwd| {
path_utils::paths_match_after_normalization(&summary.cwd, expected_cwd)
})
{
filtered.push(summary);
if filtered.len() >= remaining {
+1 -8
View File
@@ -629,14 +629,7 @@ fn validate_config(value: &TomlValue) -> Result<(), toml::de::Error> {
}
fn paths_match(expected: impl AsRef<Path>, provided: impl AsRef<Path>) -> bool {
if let (Ok(expanded_expected), Ok(expanded_provided)) = (
path_utils::normalize_for_path_comparison(&expected),
path_utils::normalize_for_path_comparison(&provided),
) {
expanded_expected == expanded_provided
} else {
expected.as_ref() == provided.as_ref()
}
path_utils::paths_match_after_normalization(expected, provided)
}
fn value_at_path<'a>(root: &'a TomlValue, segments: &[String]) -> Option<&'a TomlValue> {
+1 -8
View File
@@ -76,14 +76,7 @@ pub(crate) fn maybe_wrap_shell_lc_with_snapshot(
return command.to_vec();
}
if if let (Ok(snapshot_cwd), Ok(command_cwd)) = (
path_utils::normalize_for_path_comparison(snapshot.cwd.as_path()),
path_utils::normalize_for_path_comparison(cwd),
) {
snapshot_cwd != command_cwd
} else {
snapshot.cwd != cwd
} {
if !path_utils::paths_match_after_normalization(snapshot.cwd.as_path(), cwd) {
return command.to_vec();
}
+1 -7
View File
@@ -1205,13 +1205,7 @@ async fn parse_latest_turn_context_cwd(path: &Path) -> Option<PathBuf> {
}
fn cwds_match(current_cwd: &Path, session_cwd: &Path) -> bool {
match (
path_utils::normalize_for_path_comparison(current_cwd),
path_utils::normalize_for_path_comparison(session_cwd),
) {
(Ok(current), Ok(session)) => current == session,
_ => current_cwd == session_cwd,
}
path_utils::paths_match_after_normalization(current_cwd, session_cwd)
}
async fn resolve_resume_thread_id(
+1 -7
View File
@@ -1342,13 +1342,7 @@ async fn select_resume_path_from_db_page(
}
fn cwd_matches(session_cwd: &Path, cwd: &Path) -> bool {
if let (Ok(ca), Ok(cb)) = (
path_utils::normalize_for_path_comparison(session_cwd),
path_utils::normalize_for_path_comparison(cwd),
) {
return ca == cb;
}
session_cwd == cwd
path_utils::paths_match_after_normalization(session_cwd, cwd)
}
#[cfg(test)]
+1 -7
View File
@@ -1524,13 +1524,7 @@ async fn read_latest_turn_context(path: &Path) -> Option<TurnContextItem> {
}
pub(crate) fn cwds_differ(current_cwd: &Path, session_cwd: &Path) -> bool {
match (
path_utils::normalize_for_path_comparison(current_cwd),
path_utils::normalize_for_path_comparison(session_cwd),
) {
(Ok(current), Ok(session)) => current != session,
_ => current_cwd != session_cwd,
}
!path_utils::paths_match_after_normalization(current_cwd, session_cwd)
}
pub(crate) enum ResolveCwdOutcome {
+1 -7
View File
@@ -1148,13 +1148,7 @@ fn thread_list_params(
}
fn paths_match(a: &Path, b: &Path) -> bool {
if let (Ok(ca), Ok(cb)) = (
path_utils::normalize_for_path_comparison(a),
path_utils::normalize_for_path_comparison(b),
) {
return ca == cb;
}
a == b
path_utils::paths_match_after_normalization(a, b)
}
#[cfg_attr(not(test), allow(dead_code))]
+13
View File
@@ -16,6 +16,19 @@ pub fn normalize_for_path_comparison(path: impl AsRef<Path>) -> std::io::Result<
Ok(normalize_for_wsl(canonical))
}
/// Compare paths after applying Codex's filesystem normalization.
///
/// If either path cannot be normalized, this falls back to direct path equality.
pub fn paths_match_after_normalization(left: impl AsRef<Path>, right: impl AsRef<Path>) -> bool {
if let (Ok(left), Ok(right)) = (
normalize_for_path_comparison(left.as_ref()),
normalize_for_path_comparison(right.as_ref()),
) {
return left == right;
}
left.as_ref() == right.as_ref()
}
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))
}
@@ -78,3 +78,38 @@ mod native_workdir {
assert_eq!(normalized, path);
}
}
mod path_comparison {
use super::super::paths_match_after_normalization;
use std::path::PathBuf;
#[test]
fn matches_identical_existing_paths() -> std::io::Result<()> {
let dir = tempfile::tempdir()?;
assert!(paths_match_after_normalization(dir.path(), dir.path()));
Ok(())
}
#[test]
fn falls_back_to_raw_equality_when_paths_cannot_be_normalized() {
assert!(paths_match_after_normalization(
PathBuf::from("missing"),
PathBuf::from("missing"),
));
assert!(!paths_match_after_normalization(
PathBuf::from("missing-a"),
PathBuf::from("missing-b"),
));
}
#[cfg(windows)]
#[test]
fn matches_windows_verbatim_paths() -> std::io::Result<()> {
let dir = tempfile::tempdir()?;
let verbatim_dir = PathBuf::from(format!(r"\\?\{}", dir.path().display()));
assert!(paths_match_after_normalization(verbatim_dir, dir.path()));
Ok(())
}
}