app-server thread/resume rejects equivalent symlinked rollout paths as stale

Open 💬 2 comments Opened May 9, 2026 by SDGLBL
💡 Likely answer: A maintainer (github-actions[bot], contributor) responded on this thread — see the highlighted reply below.

What issue are you seeing?

When using the experimental app-server API as a third-party client, thread/resume can reject a running thread if the requested rollout path and the active rollout path refer to the same file through different symlink spellings.

The failure happens in the running-thread resume path when params.path is provided. The current check compares the two PathBuf values literally:

requested_path != active_path

On machines where the home directory is a symlink, one path can use the symlinked home while the other uses the resolved real path. These paths point to the same rollout JSONL, but app-server treats them as different and returns:

cannot resume running thread <thread-id> with stale path: requested `<symlink-home>/.../rollout-<id>.jsonl`, active `<real-home>/.../rollout-<id>.jsonl`

This blocks third-party clients that rely on experimental thread/resume with path.

What steps can reproduce the bug?

  1. Run Codex app-server with experimental API enabled.
  2. Use an environment where the user home directory is a symlink, for example:
  • /home/user resolves to /mnt/users/user
  • or /Users/user resolves through another mounted path
  1. Start or resume a thread so app-server has a running thread with an active rollout path.
  2. Call thread/resume for that same running thread with params.path set to the rollout JSONL path using the symlinked home spelling.
  3. Observe that app-server compares the requested path and active rollout path literally and rejects the request as stale path, even though both paths resolve to the same file.

A downstream workaround was to temporarily disable this stale-path check, but the proper upstream fix should preserve the stale-path protection while comparing normalized/canonicalized paths, or otherwise using a path identity check that treats equivalent symlinked paths as the same file.

What is the expected behavior?

thread/resume should allow resuming a running thread when params.path and the active rollout path resolve to the same file.

It should still reject genuinely stale paths that point to a different rollout/thread.

Additional information

This appears related to the broader class of path identity issues reported in #18483, #20317, and #20882, but the trigger here is narrower: the app-server v2 experimental thread/resume running-thread path check rejects equivalent symlinked paths before the client can resume.

Environment observed downstream:

  • Codex CLI/app-server version: 0.130.0
  • Platform: Darwin 24.6.0 arm64 arm
  • Scenario: third-party app-server client, experimental API enabled
  • Home directory is symlinked on internal remote machines

View original on GitHub ↗

2 Comments

github-actions[bot] contributor · 3 months ago

Potential duplicates detected. Please review them and close your issue if it is a duplicate.

  • #20517
  • #20317

Powered by Codex Action

bdteo · 3 months ago

Adding another reproduction from Codex Desktop on macOS, because this is also breaking first-party heartbeat automations, not only third-party app-server clients.

Environment:

  • Codex Desktop / CLI: 0.131.0-alpha.9
  • Platform: macOS / Darwin, Apple Silicon
  • ~/.codex is a symlink:
  • /Users/boris/.codex -> /Users/boris/DevEnvs/Boris/MacShellUtils/Users/Boris/codex

Failure observed from a heartbeat automation targeting the current local thread:

cannot resume running thread 019e40d5-806b-7d91-930a-6c0b59e796ed with stale path: requested `/Users/boris/.codex/sessions/2026/05/19/rollout-2026-05-19T18-23-14-019e40d5-806b-7d91-930a-6c0b59e796ed.jsonl`, active `/Users/boris/DevEnvs/Boris/MacShellUtils/Users/Boris/codex/sessions/2026/05/19/rollout-2026-05-19T18-23-14-019e40d5-806b-7d91-930a-6c0b59e796ed.jsonl`

Both paths are the same rollout JSONL via symlink vs canonical path. The heartbeat automation TOML itself was normal and only stored the target_thread_id; the Desktop automation scheduler read the thread, called resumeThread(id, { cwd, path }), and app-server rejected it on raw path inequality.

Impact:

  • Heartbeat automations become unusable for symlinked / repo-backed Codex homes.
  • The automation can be created and updated successfully, so the failure only appears later when the scheduler tries to resume the target thread.
  • A local launchd monitor was needed as a workaround for a production runbook because the native heartbeat could not start.

Expected behavior is exactly as described above: keep the stale-path protection, but compare canonicalized paths or filesystem identity so symlink/canonical forms of the same JSONL are accepted.