Bug Description
When closing a chat tab in Codex Desktop, the codex app-server --listen stdio:// and its parent node_repl processes are not terminated, accumulating zombie processes over time.
Process Tree
Codex Desktop (Electron)
└── node_repl
└── codex app-server --listen stdio://
Each new chat tab spawns a fresh set of these processes. After closing tabs, they remain running indefinitely with 0% CPU and 0 children.
Steps to Reproduce
- Open Codex Desktop
- Open multiple chat tabs (e.g., 10)
- Close most of them, keeping only 1-2
- Check processes:
ps aux | grep 'codex app-server'
- Observe that nearly all processes from closed tabs remain alive
Observed Behavior
$ ps aux | grep 'codex app-server' | grep -v grep | wc -l
9
Only 1 of the 9 processes had active children (26 MCP sub-processes). The other 8 were idle zombies consuming memory.
Root Cause Analysis
The app-server in stdio mode exits only on stdin EOF (codex-rs/app-server-transport/src/transport/stdio.rs:68):
// codex-rs/app-server-transport/src/transport/stdio.rs:43-80
// stdin reader loop — blocks on lines.next_line().await
// EOF triggers ConnectionClosed event
And shutdown_when_no_connections is set based on stdio mode (codex-rs/app-server/src/lib.rs:662-663):
let single_client_mode = matches!(&transport, AppServerTransport::Stdio);
let shutdown_when_no_connections = single_client_mode;
The leak occurs because:
- When Desktop closes a tab,
node_repl may be killed (SIGKILL) without gracefully closing the stdin pipe to codex app-server
- Or
node_repl exits but the stdin fd is orphaned — the kernel doesn't always reclaim it immediately
codex app-server blocks forever on lines.next_line().await, never receiving EOF
- There is no heartbeat/timeout mechanism to detect this zombie state
- Daemon uses
setsid() (app-server-daemon/src/backend/pid.rs:171-176) creating independent process groups, so SIGTERM doesn't cascade to children
Suggested Fixes
Fix 1: Add heartbeat/timeout in stdio transport
Add a periodic check in stdio.rs — if no data received on stdin for N minutes, treat as EOF and shut down.
Fix 2: PID file tracking for stdio mode
Daemon mode already uses PID files (pid.rs) for process lifecycle management. Stdio mode should similarly track child PIDs so orphaned processes can be discovered and cleaned up.
Fix 3: Desktop-side fix (Electron)
Ensure that when a tab is closed, node_repl gracefully closes the stdin pipe to codex app-server before terminating, giving it a chance to shut down cleanly.
Environment
- macOS Darwin 25.5.0
- Codex Desktop 26.602.40724
- Codex app-server started via
--listen stdio://
Workaround
Manually kill zombie processes:
# Find idle app-server processes (0 children, 0% CPU)
ps aux | grep 'codex app-server' | grep -v grep
# Kill specific PIDs
kill <pid>
Bug Description
When closing a chat tab in Codex Desktop, the
codex app-server --listen stdio://and its parentnode_replprocesses are not terminated, accumulating zombie processes over time.Process Tree
Each new chat tab spawns a fresh set of these processes. After closing tabs, they remain running indefinitely with 0% CPU and 0 children.
Steps to Reproduce
ps aux | grep 'codex app-server'Observed Behavior
Only 1 of the 9 processes had active children (26 MCP sub-processes). The other 8 were idle zombies consuming memory.
Root Cause Analysis
The app-server in stdio mode exits only on stdin EOF (
codex-rs/app-server-transport/src/transport/stdio.rs:68):And
shutdown_when_no_connectionsis set based on stdio mode (codex-rs/app-server/src/lib.rs:662-663):The leak occurs because:
node_replmay be killed (SIGKILL) without gracefully closing the stdin pipe tocodex app-servernode_replexits but the stdin fd is orphaned — the kernel doesn't always reclaim it immediatelycodex app-serverblocks forever onlines.next_line().await, never receiving EOFsetsid()(app-server-daemon/src/backend/pid.rs:171-176) creating independent process groups, so SIGTERM doesn't cascade to childrenSuggested Fixes
Fix 1: Add heartbeat/timeout in stdio transport
Add a periodic check in
stdio.rs— if no data received on stdin for N minutes, treat as EOF and shut down.Fix 2: PID file tracking for stdio mode
Daemon mode already uses PID files (
pid.rs) for process lifecycle management. Stdio mode should similarly track child PIDs so orphaned processes can be discovered and cleaned up.Fix 3: Desktop-side fix (Electron)
Ensure that when a tab is closed,
node_replgracefully closes the stdin pipe tocodex app-serverbefore terminating, giving it a chance to shut down cleanly.Environment
--listen stdio://Workaround
Manually kill zombie processes: