Fix Windows running thread resume path normalization (#25509)

## Why

Fixes #24944.

On Windows, app-server resume could reject an active running thread when
the requested session path used normal `C:\...` form and the
already-running path used verbatim `\\?\C:\...` form. The paths point at
the same JSONL file, but the resume stale-path guard compared raw
`PathBuf`s, so desktop resume and heartbeat flows could fail with a
mismatched-path error.

## What Changed

- Compare requested and active rollout paths with
`path_utils::paths_match_after_normalization`.
- Extend the existing running-thread mismatched-path test with a
Windows-only same-file resume case before the stale-path rejection.

## Verification

- `just test -p codex-app-server
thread_resume_rejects_mismatched_path_for_running_thread_id`
This commit is contained in:
Eric Traut
2026-06-02 12:42:42 -07:00
committed by GitHub
Unverified
parent af18e92140
commit ad355d4c96
2 changed files with 31 additions and 1 deletions
@@ -2766,7 +2766,7 @@ impl ThreadRequestProcessor {
.as_ref()
.or(source_thread.rollout_path.as_ref());
if let (Some(requested_path), Some(active_path)) = (params.path.as_ref(), active_path)
&& requested_path != active_path
&& !path_utils::paths_match_after_normalization(requested_path, active_path)
{
return Err(invalid_request(format!(
"cannot resume running thread {existing_thread_id} with stale path: requested `{}`, active `{}`",
@@ -2410,6 +2410,36 @@ async fn thread_resume_rejects_mismatched_path_for_running_thread_id() -> Result
)
.await??;
#[cfg(windows)]
{
let active_path = thread.path.as_ref().expect("thread should have path");
let active_path_display = active_path.as_os_str().to_string_lossy();
let equivalent_path = if let Some(path) = active_path_display.strip_prefix(r"\\?\UNC\") {
PathBuf::from(format!(r"\\{path}"))
} else if let Some(path) = active_path_display.strip_prefix(r"\\?\") {
PathBuf::from(path)
} else if let Some(path) = active_path_display.strip_prefix(r"\\") {
PathBuf::from(format!(r"\\?\UNC\{path}"))
} else {
PathBuf::from(format!(r"\\?\{active_path_display}"))
};
let normalized_resume_id = primary
.send_thread_resume_request(ThreadResumeParams {
thread_id: thread_id.clone(),
path: Some(equivalent_path),
..Default::default()
})
.await?;
let normalized_resume_resp: JSONRPCResponse = timeout(
DEFAULT_READ_TIMEOUT,
primary.read_stream_until_response_message(RequestId::Integer(normalized_resume_id)),
)
.await??;
let ThreadResumeResponse { thread, .. } =
to_response::<ThreadResumeResponse>(normalized_resume_resp)?;
assert_eq!(thread.id, thread_id);
}
let stale_thread_id = Uuid::new_v4().to_string();
let stale_path = rollout_path(codex_home.path(), "2025-01-01T00-00-00", &stale_thread_id);
std::fs::create_dir_all(stale_path.parent().expect("stale path parent"))?;