App-server daemon restart stalls on an unreaped zombie after update

Open 💬 3 comments Opened Jul 10, 2026 by dyxushuai

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:

  1. Start a PID-managed app-server as a child of the long-lived updater.
  2. Let the app-server exit while the updater remains alive without reaping it.
  3. Confirm that the child is in state Z and that its PID record still contains the matching start time.
  4. Ask the PID backend to stop or restart the app-server.
  5. 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:

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: passed
  • just 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.

View original on GitHub ↗

3 Comments

StephanDollberg · 1 month ago

Had the same happen to me today. Going from 0.144 to 0.144.1

midhunmonachan · 1 month ago

Independent reproduction during 0.144.1 -> 0.144.2 update on Linux ARM64

I 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.1 transition reported above.

The app-server had one active assistant turn when the updater requested shutdown:

2026-07-13T05:21:19.738766945Z INFO codex_app_server received shutdown signal; entering graceful restart drain (connections=1, runningAssistantTurns=1, requests still accepted until no assistant turns are running)
2026-07-13T05:21:19.738776105Z INFO codex_app_server shutdown signal restart: waiting for 1 running assistant turn(s) to finish

About 70 seconds later the control socket refused connections, and the first failed restart reported managed version 0.144.2. The old 0.144.1 updater remained alive with the exited app-server as its zombie child:

    PID    PPID S COMMAND
2373216       1 S <redacted for home directory>/.codex/packages/standalone/releases/0.144.1-aarch64-unknown-linux-musl/bin/codex app-server daemon pid-update-loop
2400015 2373216 Z [codex] <defunct>

{"pid":2400015,"processStartTime":"Thu Jul  9 23:14:08 2026"}
state=Z ppid=2373216 raw_exit_code=9

The state persisted for more than eight hours. Normal remote-control start and daemon bootstrap attempts failed, and the managed installation advanced to 0.144.3 while the old 0.144.1 updater and matching zombie still blocked recovery.

After I verified both process identities and sent SIGTERM to updater PID 2373216, the updater exited cleanly and PID 2400015 was immediately reaped. No SIGKILL fallback was needed. codex remote-control start then succeeded with both the CLI and app-server on 0.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.

rrajpuro · 6 days ago

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:

  • A long-lived codex app-server daemon pid-update-loop remained the parent of an exited app-server child.
  • ps showed the child in zombie state (Zs, [codex] <defunct>).
  • codex remote-control stop repeatedly failed with Error: timed out waiting for pid-managed app server <pid> to stop.
  • After verifying the parent command and sending SIGTERM to the updater parent, both the updater and zombie disappeared. A subsequent codex remote-control --json stop returned status: notRunning.

This occurrence was very likely associated with a managed update: the 0.149.0 release 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.