App-server daemon restart stalls on an unreaped zombie after update
What version of Codex CLI is running?
The old updater was codex-cli 0.143.0. The managed update installed 0.144.1, whose restart then waited for the old updater to release the daemon lifecycle lock.
What subscription do you have?
Not relevant to this local daemon lifecycle failure.
Which model were you using?
Not applicable.
What platform is your computer?
A remote Linux server reached from the ChatGPT desktop app over SSH. The exact uname -mprs output was not captured during the incident.
What terminal emulator and version are you using (if applicable)?
The failure affected a ChatGPT desktop remote connection. Recovery was performed from a separate SSH shell.
Codex doctor report
Not captured for this incident.
What issue are you seeing?
After the managed Codex CLI updated, the remote app-server did not come back and the ChatGPT desktop app could no longer reconnect to the host. The normal restart path appeared stuck, so the only practical recovery was to SSH into the server and inspect the daemon processes manually.
The process tree showed a specific lifecycle failure:
old updater: PID 1419016, Codex 0.143.0
app-server child: PID 3294956, defunct/zombie
new restart: Codex 0.144.1, waiting for daemon ownership
The old updater was still the parent of the exited app-server. Because the child had not been reaped, it remained in the process table as a zombie. Sending a normal SIGTERM to the old updater allowed the new version to take over and restored the daemon.
This is more than a stale PID file. On Unix, kill(pid, 0) succeeds for a zombie. The PID backend therefore considers the exited app-server active when its recorded start time still matches. The old updater remains inside the stop/restart path while holding the operation lock, and the newer restart cannot take ownership.
What steps can reproduce the bug?
The full timing depends on a managed update, but the process-level failure is deterministic:
- Start a PID-managed app-server as a child of the long-lived updater.
- Let the app-server exit while the updater remains alive without reaping it.
- Confirm that the child is in state
Zand that its PID record still contains the matching start time. - Ask the PID backend to stop or restart the app-server.
- Observe that the backend still reports the zombie as active because
kill(pid, 0)succeeds, so restart cannot complete normally.
In the real incident, this happened during the 0.143.0 to 0.144.1 managed update.
What is the expected behavior?
An exited app-server child should be reaped and treated as inactive. A managed update should release daemon ownership and start the replacement app-server without requiring a separate SSH session or a manual signal to the old updater.
Additional information
The relevant implementation first checks process existence with kill(pid, 0) and then validates the recorded start time. That protects against PID reuse, but it does not distinguish a running process from a zombie:
process_existsandprocess_matches_recordin 0.144.1- the stop loop that waits for the record to become inactive
A focused local regression test creates an exited child with a matching PID record and confirms that the current implementation still classifies it as active. A minimal fix can validate the recorded start time and then call waitpid(pid, WNOHANG): a successfully reaped child is inactive, while ECHILD preserves the existing behavior for processes not parented by the current daemon.
Local validation of that approach:
just test -p codex-app-server-daemon: 32/32 passed- zombie regression stress run: 50/50 passed
just fix -p codex-app-server-daemon: passedjust fmt: passed
The safe recovery used for this incident was to verify the process tree, send SIGTERM to the old updater, and let the new updater take over. Users should not need this process-level workaround.
Related: #31835 describes a stale managed daemon surviving a desktop upgrade. #32037 describes a separate automatic-restart lifecycle failure during an active turn. This report is narrower: an exited app-server remains a zombie, is misclassified as active, and prevents the next updater from taking ownership.
I can provide the focused regression test and minimal patch if a maintainer confirms the intended behavior and invites a pull request.
3 Comments
Had the same happen to me today. Going from 0.144 to 0.144.1
Independent reproduction during
0.144.1->0.144.2update on Linux ARM64I reproduced the same unreaped-zombie/PID-liveness failure during a later managed update. This adds evidence that the bug persisted beyond the
0.143.0->0.144.1transition reported above.The app-server had one active assistant turn when the updater requested shutdown:
About 70 seconds later the control socket refused connections, and the first failed restart reported managed version
0.144.2. The old0.144.1updater remained alive with the exited app-server as its zombie child:The state persisted for more than eight hours. Normal
remote-control startand daemon bootstrap attempts failed, and the managed installation advanced to0.144.3while the old0.144.1updater and matching zombie still blocked recovery.After I verified both process identities and sent
SIGTERMto updater PID2373216, the updater exited cleanly and PID2400015was immediately reaped. NoSIGKILLfallback was needed.codex remote-control startthen succeeded with both the CLI and app-server on0.144.3.This occurrence also shows one trigger for the zombie state: the automatic update began during an active turn, then the child exited during the daemon's forced-stop window. That connects the trigger to #32037 while preserving the distinct failure described here.
I reproduced this on Linux with
codex-cli 0.149.0, despite the zombie-reaping change from #32838 being present in that release.Observed behavior:
codex app-server daemon pid-update-loopremained the parent of an exited app-server child.psshowed the child in zombie state (Zs,[codex] <defunct>).codex remote-control stoprepeatedly failed withError: timed out waiting for pid-managed app server <pid> to stop.SIGTERMto the updater parent, both the updater and zombie disappeared. A subsequentcodex remote-control --json stopreturnedstatus: notRunning.This occurrence was very likely associated with a managed update: the
0.149.0release installation timestamp and the app-server child start time matched to the same second, while the updater predated both. I cannot prove the update transition conclusively because the relevant updater log was not retained.