From ad355d4c9641953bb85c242c92f72f924edd4687 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Tue, 2 Jun 2026 12:42:42 -0700 Subject: [PATCH] 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` --- .../request_processors/thread_processor.rs | 2 +- .../tests/suite/v2/thread_resume.rs | 30 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/codex-rs/app-server/src/request_processors/thread_processor.rs b/codex-rs/app-server/src/request_processors/thread_processor.rs index 32dfb1eeb..b26c2e4df 100644 --- a/codex-rs/app-server/src/request_processors/thread_processor.rs +++ b/codex-rs/app-server/src/request_processors/thread_processor.rs @@ -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 `{}`", diff --git a/codex-rs/app-server/tests/suite/v2/thread_resume.rs b/codex-rs/app-server/tests/suite/v2/thread_resume.rs index c016b3882..3073b382a 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_resume.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_resume.rs @@ -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::(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"))?;