Process leak: codex app-server --listen stdio:// not cleaned up when chat tab is closed

Open 💬 1 comment Opened Jun 6, 2026 by hrygo

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

  1. Open Codex Desktop
  2. Open multiple chat tabs (e.g., 10)
  3. Close most of them, keeping only 1-2
  4. Check processes: ps aux | grep 'codex app-server'
  5. 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:

  1. When Desktop closes a tab, node_repl may be killed (SIGKILL) without gracefully closing the stdin pipe to codex app-server
  2. Or node_repl exits but the stdin fd is orphaned — the kernel doesn't always reclaim it immediately
  3. codex app-server blocks forever on lines.next_line().await, never receiving EOF
  4. There is no heartbeat/timeout mechanism to detect this zombie state
  5. 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>

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗