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.
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗