Codex Desktop: thread/resume can return a thread while Shutdown is pending, leaving a dead agent loop
Summary
Codex Desktop can acknowledge thread/resume for an existing thread while a Shutdown submission for that same thread is still pending. The agent loop then exits, but the UI keeps routing turn/start to the stale runtime, causing repeated message submission failures.
Environment
- Surface: Codex Desktop (Windows MSIX)
- Codex App version:
26.715.3651.0 - Platform: Windows 24H2, build
26100.7840, x64
User-visible error
Error creating task
failed to start turn: internal error; agent loop died unexpectedly
Subsequent attempts show:
Error submitting message
Sanitized lifecycle evidence
Local app-server logs show this ordering for the affected persisted thread:
11:43:25 Submission ... op: Shutdown
11:43:35 composing running thread resume response ... active_turn_present=false
11:43:52 Shutting down Codex instance
11:43:52 Agent loop exited
11:45:38 turn/start -> -32603: failed to start turn: internal error; agent loop died unexpectedly
11:45:40 turn/start -> same error
11:45:47 turn/start -> same error
The conversation remained present in the Desktop UI and on disk. There was no matching panic, worker-process exit, or OOM event around this sequence. Restarting only the Desktop codex.exe child backend restored operation.
This looks like a lifecycle race/state desynchronization: the resume path returns a handle to a runtime that is already committed to shutting down.
Steps to reproduce
The trigger is intermittent, but the observed flow is:
- Open an existing Desktop thread and complete a turn.
- A thread lifecycle transition submits
Shutdownfor the loaded runtime. - Resume/open the same persisted thread while that shutdown is still pending.
- After the loop exits, submit another message.
- Observe repeated
agent loop died unexpectedly/Error submitting message.
Expected behavior
One of the following should happen:
thread/resumewaits for the in-flight shutdown and creates a fresh runtime;- the shutdown is cancelled before resume succeeds; or
turn/startdetects the absent loop and transparently rehydrates the persisted thread.
A resume response should not expose a runtime that can exit immediately afterward while remaining registered as usable.
Actual behavior
The resume path reports the thread as running, the old agent loop exits, and later turn/start requests are routed to the dead instance until the backend is restarted.
Related reports
- #33206 reports the same user-visible error on Windows but does not include the shutdown/resume ordering.
- #23971 reports the same error after a different trigger involving subagent cleanup.
Privacy
Thread IDs, request IDs, local usernames, workspace paths, task titles, prompts, and unrelated log content have been omitted. Additional sanitized excerpts can be provided if useful.
7 Comments
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
I did a static lifecycle trace on current public
main(56395bddaf26eb2829387ca6a417bf9128e5b239). One cross-layer path can produce exactly the reported ordering, and it is not covered by App Server's existingpending_thread_unloadsguard.For a MultiAgentV2 resident subagent, core eviction currently does this:
try_unload_one_resident()obtains the candidate fromThreadManager.candidate_thread.shutdown_and_wait()while the thread is still present in the manager.During step 2, App Server can still resolve that same
Arc<CodexThread>as running:resume_running_thread()usesget_thread(), then later enqueues a running-thread resume response on its listener (lines 3486-3506). That leaves a window whereShutdownis committed butthread/resumecan subscribe and acknowledge the old runtime before its loop exits.App Server does have two good closing checks—at
thread_resume_inner()and immediately before subscription in the listener (lines 598-623)—but both consult the App-Server-localpending_thread_unloadsset. Core residency eviction does not populate that set, so the guard cannot see this shutdown source.This is a hypothesis to validate against the affected thread's source, not a claim that every occurrence has this cause. If the logged thread was a V2 subagent (or another core-owned residency candidate), the ordering is a close match. Checking its persisted/session source would distinguish this path from App Server's own idle-unload path.
The durable fix likely needs a lifecycle primitive shared by core and App Server, rather than another App-Server-local flag:
RunningtoClosingbefore submittingOp::Shutdown;get_thread_for_resume/resume leasing fail or wait when the phase isClosing;turn/startreject or rehydrate aClosing/Closedruntime so a stale handle cannot remain indefinitely usable in the registry.A useful deterministic regression would put a barrier between
Op::Shutdownsubmission and session-loop termination, concurrently invokethread/resume, and assert that no response exposes the old runtime. I would run that against both shutdown owners: App Server idle unload and core V2 residency eviction. Releasing the barrier should allow a cold resume, after which the firstturn/startmust succeed. Repeated concurrent resume attempts should converge on one live runtime.For frontier multi-agent systems, resumability is part of the control plane: a thread must be either leasable or closing, never both. Making that state explicit prevents stale agent handles, duplicate runtimes, and recovery loops across every client—not just the Desktop symptom here.
Disclosure: this analysis was produced with significant assistance from OpenAI Codex. It uses only the public
openai/codexrepository and synthetic concurrency reasoning; it contains no private code, data, prompts, roadmaps, or product-specific examples.Confirmed the same lifecycle sequence independently on ChatGPT desktop app
26.715.2305.0, app-server0.145.0-alpha.18, and Windows10.0.26200.0 x64.My logs show
Shutdownsubmitted,thread/resumesucceeding about 10 seconds later, the agent loop exiting about 5 seconds after that, and subsequentturn/startrequests repeatedly failing with-32603: agent loop died unexpectedly.Restarting the desktop app restored the affected task. I initially filed #34131 before seeing this report; it contains the screenshot, timestamps, and affected session ID.
I now have an isolated public-source implementation for the completed-thread idle-unload race described here, rebased onto
openai/codexmain at2deed3fb9c00c74dac3d177ea700d6fb7a94539d:https://github.com/openai/codex/compare/main...luziyi123448-gif:codex/loop-death-fix?expand=1
The published head is fixed at
c6abb714d5351e744669b928aa57521b9540b1db(two commits, 34 files). The implementation covers the full acknowledgement boundary rather than only adding another resume-side check:SessionIotransitions from accepting to closing onceShutdownis successfully enqueued, so later user input is rejected withThreadClosingbefore loop termination;THREAD_DRAININGerrors from resume/listener/turn-start paths until actual termination;turn/startis acknowledged only after the durableTurnStartedevent is persisted and observed;runtimeInstanceIdplus cancellation-safe(threadId, clientUserMessageId)deduplication gives retries an exactly-once result within one runtime;Post-rebase validation on Windows:
LEAKbecause its test process exited late);codex-rmcp-client: 109 passed, 5 skipped;codex-app-server-protocol,codex-protocol, andcodex-analytics: 603 passed;just fmt-check, andgit diff --checkpassed.I have not opened an unsolicited PR because the repository contribution policy is invitation-only. If this lifecycle contract matches the intended direction, would a maintainer be willing to invite a PR from the published branch (or indicate any scope changes needed before invitation)?
Confirming the same failure on a newer Windows build, with an additional recovery path that may help isolate the stale-runtime boundary.
Environment
26.730.8199.0(x64)10.0.26200.0Sanitized lifecycle ordering
The affected thread's app-server logs showed:
The logged resume response was therefore composed after the loop-exit event in this occurrence. This still looks consistent with the stale runtime registration described here, and suggests the acknowledgement path can complete after termination rather than only while shutdown is pending.
User-visible behavior
The composer contained a normal text prompt plus 25 in-app Browser annotation attachments. The content is omitted here.
Every manual retry produced:
Reopening the task did not recover it. A task-native background send attempt also returned the same error before a user-input submission was created for the dead thread. The UI continued retaining the complete composer payload.
Recovery evidence
A supported Codex task handoff from the local checkout to a fresh Codex worktree succeeded:
This recovered the work without restarting the whole Desktop app. It also indicates that the persisted history and composer payload were healthy; the unusable state was attached to the old runtime/task registration.
A useful product recovery would be to make
turn/startperform this cold rehydration automatically when the registered loop is absent, without requiring a worktree handoff or replacement task.Privacy: thread/request IDs, usernames, local paths, workspace/repository names, task title, account details, prompt text, annotation contents, and unrelated logs have been removed.
Follow-up after rechecking the affected task's archived rollout, collaboration events, and persisted thread state.
My earlier comment said the failing resend did not involve closing subagents. That remains true, but it omitted relevant multi-agent history:
Shutdown; it was not immediately adjacent to the failure.openfor that parent task when inspected after the failure. This live-roster/persisted-edge mismatch may be relevant to residency or lifecycle bookkeeping.So the precise close-window sequence was not “child replies, parent replies, immediate crash.” It was: extensive earlier two-way subagent history; apparently root-only live roster but many persisted
openchild edges; then a one-way cross-task send; later the shutdown/resume race and permanent dead-loop behavior.This does not establish that inter-agent messaging caused the failure. It does add a concrete state condition worth testing alongside the MultiAgentV2 residency hypothesis already discussed here: a completed top-level task whose live agent roster and persisted spawn-edge state disagree, followed by cross-task coordination and idle/resume lifecycle activity.
Useful regression variants may be:
open;Privacy: child/task IDs, agent names, prompts, message contents, repository/workspace names, local paths, and unrelated logs are omitted.
Hi @luziyi123448-gif, this subagent persistence/history issue aligns with some boundary anomalies observed in multi-agent rollouts. Codex Rescue Alpha5 provides read-only lifecycle and subagent boundary diagnostics, cleanly separating historical start markers from current live execution state without altering the source rollout.
If you have access to the local session, you can run a non-destructive check:
No raw session data is required, and please redact private paths if you share any output.