Fix empty rollout path app-server handling (#23400)

## Summary
- Coerce `path: ""` to `None` at the v2 protocol params deserialization
boundary for `thread/resume` and `thread/fork`.
- Restore the pre-ThreadStore running-thread resume behavior: if
`threadId` is already running, rejoin it by id and treat a non-empty
`path` only as a consistency check; otherwise cold resume keeps `history
> path > threadId` precedence.
- Add protocol, resume, and fork regression coverage for empty path
payloads; refresh app-server schema fixtures for the clarified params
docs.

## Tests
- `just fmt`
- `just write-app-server-schema`
- `cargo test -p codex-app-server-protocol
thread_path_params_deserialize_empty_path_as_none`
- `cargo test -p codex-app-server-protocol --test schema_fixtures`
- `cargo test -p codex-app-server empty_path`
- `RUST_MIN_STACK=8388608 cargo test -p codex-app-server --test all
thread_resume_rejects_mismatched_path_for_running_thread_id`
- `RUST_MIN_STACK=8388608 cargo test -p codex-app-server --test all
thread_resume_uses_path_over_non_running_thread_id`
This commit is contained in:
Tom authored and GitHub committed 2026-05-19 21:19:38 +00:00
1 parent 40be41763c
commit 954a9c8579
13 files changed
+203 -101

No files matched your search

@@ -2639,31 +2639,6 @@ impl ThreadRequestProcessor {
)));
}
None
} else if params.path.is_some() {
let source_thread = self
.read_stored_thread_for_resume(
&params.thread_id,
params.path.as_ref(),
/*include_history*/ true,
)
.await?;
let existing_thread_id = source_thread.thread_id;
if let Ok(existing_thread) = self.thread_manager.get_thread(existing_thread_id).await {
if let (Some(requested_path), Some(active_path)) = (
params.path.as_ref(),
existing_thread.rollout_path().as_ref(),
) && requested_path != active_path
{
return Err(invalid_request(format!(
"cannot resume running thread {existing_thread_id} with stale path: requested `{}`, active `{}`",
requested_path.display(),
active_path.display()
)));
}
Some((existing_thread_id, existing_thread, source_thread))
} else {
None
}
} else if let Ok(existing_thread_id) = ThreadId::from_string(&params.thread_id)
&& let Ok(existing_thread) = self.thread_manager.get_thread(existing_thread_id).await
{
@@ -2686,6 +2661,19 @@ impl ThreadRequestProcessor {
};
if let Some((existing_thread_id, existing_thread, source_thread)) = running_thread {
let existing_thread_rollout_path = existing_thread.rollout_path();
let active_path = existing_thread_rollout_path
.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
{
return Err(invalid_request(format!(
"cannot resume running thread {existing_thread_id} with stale path: requested `{}`, active `{}`",
requested_path.display(),
active_path.display()
)));
}
let redact_resume_payloads =
should_redact_thread_resume_payloads(app_server_client_name.as_deref());
let history_items = source_thread