close_agent can wait indefinitely before session teardown, leaking MCP subprocesses, pipes, and pidfds
What version of the Codex App are you using (From “About Codex” dialog)?
0.145.0
What subscription do you have?
Pro
What platform is your computer?
AppServer on Ubuntu 2404
What issue are you seeing?
This is a teardown for #29937
close_agent can block forever while waiting for a child agent’s session loop to terminate. When the child is stuck during task-abort or shutdown lifecycle handling, normal session cleanup is never reached. This leaves the child agent’s session and MCP connection sets alive. Stdio MCP subprocesses, their stdin/stdout/stderr pipes, and associated pidfds consequently remain owned by the long-running app-server process. A timeout that only releases the close_agent caller is insufficient: teardown must either complete or transfer the still-live task, session, transports, and subprocesses to a reliable background reaper.
A several hour long Codex 0.145.0 app-server session executing a detailed plan via /goal reached approximately:
930 open file descriptors against a soft limit of 1024
619 pipe descriptors
207 pidfds
204 MCP-related child processes
Further inspection found:
All 619 pipe descriptors mapped to MCP child stdin, stdout, or stderr.
Most corresponding agent threads had already logically completed or been aborted.
The resources remained held by the long-lived app-server.
Individual close_agent operations had remained pending for hours.
What steps can reproduce the bug?
The problem can be reproduced without relying on a real model request:
- Start a child agent with a real stdio MCP test server.
- Have the server write its PID to a temporary file.
- Install a SessionTask whose run future remains pending.
- Make its task-abort callback remain pending after signalling that it was entered.
- Call close_agent.
- Wait longer than the intended shutdown bound.
- Inspect the child PID and thread-manager state.
Against the affected upstream code:
- close_agent does not return.
- The task-abort callback has been entered.
- The agent thread remains registered.
- The MCP subprocess remains alive.
- Its stdio descriptors remain held by the app-server.
A second variant can block a turn-lifecycle abort hook instead of SessionTask::abort; it produces the same outcome.
A useful stronger regression also retains an older MCP binding, refreshes the runtime to create a replacement connection, and then closes the agent. Both the current and superseded MCP subprocesses must eventually exit.
What is the expected behavior?
When close_agent is requested:
- The caller returns within a finite, documented bound.
- Graceful shutdown is attempted.
- If graceful shutdown stalls, the child session is forcibly quiesced or transferred to a reliable reaper.
- Active child tasks are cancelled and joined.
- MCP refresh/startup workers cannot publish new transports during teardown.
- All current and superseded live MCP connection sets are shut down.
- Stdio MCP subprocess termination is initiated under a cancellation-safe owner.
- The thread is removed and agent capacity released only after teardown completes or reliable reaper ownership has been established.
- The operation must not report clean success while the agent remains live and unmanaged.
Additional information
The relevant control flow is effectively:
close_agent
-> send Op::Shutdown
-> wait_until_terminated().await
-> remove thread
-> release residency/capacity
If the session loop does not terminate, wait_until_terminated() never completes. Everything after it is skipped.
During normal shutdown, the session first aborts active tasks. Task-specific SessionTask::abort and lifecycle callbacks are asynchronous and are not intrinsically bounded. A callback that never resolves prevents the session loop from reaching the later MCP and process cleanup stages.
The resulting failure chain is:
close_agent sends Op::Shutdown
-> session begins aborting its active task
-> task abort callback or lifecycle hook hangs
-> session loop never reaches terminated state
-> close_agent waits indefinitely
-> thread removal and capacity release never run
-> session and MCP runtime remain strongly referenced
-> MCP transports and subprocess handles remain alive
-> child pipes and pidfds accumulate in app-server
Related reports
#17574 : broader per-agent MCP process/resource accumulation
#19092: close could appear successful while MCP processes remained alive
#11324: /exit retained MCP PIDs while teardown through /archive removed them
1 Comment
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action