Active goals can auto-continue concurrently across app-server processes
Codex version
Reproduced with codex-cli 0.144.3. The same process-local guards are present on main at 8b2c84ddccafe40dc0dc09f9f52bcbdc9dc45d66.
What happened?
Two app-server processes sharing one CODEX_HOME can independently resume the same thread while its persisted goal is active. Each process sees its own in-memory thread as idle and starts an automatic goal-continuation turn. The turns can overlap and execute tools concurrently in the same workspace.
I observed an existing CLI goal turn remain in flight while two later remote thread/resume requests each launched another real goal-continuation turn.
Reproduction outline
- Start app-server A with the goals feature enabled.
- Materialize a persistent thread, set an active goal, and keep its automatic continuation in flight.
- Start app-server B with the same
CODEX_HOME. - Send
thread/resumefor the same thread to app-server B. - Observe another automatic continuation and a second
turn/started/ Responses request while A is still running.
Using a single managed daemon and attaching the TUI with codex --remote unix:// resume avoids the race, but independent app-server processes remain possible (for example a foreground Remote Control host or another Codex surface).
Expected behavior
A second process may resume/read the thread and receive its goal snapshot, but it must not start automatic goal work while another live process owns that thread's goal continuation. If the owner exits or crashes, a later process should be able to continue it.
Source trace
- A cold
thread/resumesends the response and then callsemit_resume_goal_snapshot_and_continue:
- That path emits the thread-idle lifecycle whenever goals are enabled:
GoalRuntimeHandle::continue_if_idleuses a per-runtime semaphore and calls the local thread'stry_start_turn_if_idle:
- The thread map, active-turn check, mailbox, and goal semaphore are process-local. The shared
thread_goalsSQLite row has no owner or lease, so both processes can pass the idle gate.
A goal-specific cross-process advisory lock or ownership lease, released automatically on process death and held for the continuation lifetime, seems like the narrowest fix. This is related to multi-client attachment (#32445, #14722), but it is independently an execution-safety issue.
2 Comments
Additional IDE-specific case that appears to hit the same execution-safety gap:
CODEX_HOME.The dangerous part is that the two clients may present the same thread id/history while silently running two independent tool-owning continuations. Depending on the workspace roots, they may either race on the same checkout or execute the same goal against different workspace contexts.
Expected behavior: opening an active-goal thread from another VS Code workspace should attach as an observer/client of the existing continuation, or explicitly require the user to stop, transfer ownership, or fork. It should never silently start a second goal executor.
A related user-facing report is #34034, where two parallel executions retained the same thread id across clients and duplicated/interfered with work:
https://github.com/openai/codex/issues/34034
Confirmed on
codex-cli 0.146.0that the active-goal path makes the same-thread split brain self-sustaining across turn boundaries.Sanitized topology: one managed Remote Control app-server plus one independent local
codex resume <redacted-session>process, sharing oneCODEX_HOME. Both held the same rollout withO_RDWR|O_APPENDand each had a nonterminal turn.Observed lifecycle:
task_complete, then emitted a newtask_started6 ms later.task_complete, then emitted a newtask_started8 ms later.This supplements the process/file-descriptor reproduction posted at #34767.
codex doctor --all --jsonremained healthy with zero duplicate thread IDs/paths/stale rows during the overlap.The practical mitigation described in this issue is supported mechanically in 0.146.0: retain one managed app-server and attach the local TUI with
codex --remote unix:// resumeinstead of starting an independentcodex resumeroot. Remote Control remains experimental, so this should be treated as an operational workaround rather than a cross-process safety guarantee.