Bound v1 close_agent shutdown waits for unresponsive child agents
Problem
multi_agent_v1.close_agent can block the parent turn indefinitely when a child agent accepts Op::Shutdown but its session loop never reaches termination.
This makes cleanup unsafe to put on the critical path: a parent that is otherwise done can become stuck for hours just trying to close an unresponsive child.
Related existing issues:
- #25870
- #24389
Fresh reproduction evidence
Observed in a local native multi-agent Codex session on 2026-06-24.
Environment:
- parent session:
019ef523-5927-74e2-b687-369f17ae6bbc - child session:
019ef7e2-c130-72d1-9b3e-280ff9c36e90 - child role/nickname: native
explorer,Hilbert - platform: macOS arm64
- parent CLI around run:
0.138.0, child CLI around run:0.141.0, current local CLI after update:0.142.0
Timeline from local rollout JSONL:
2026-06-24T04:28:15Z spawn_agent -> Hilbert 019ef7e2-c130-72d1-9b3e-280ff9c36e90
2026-06-24T04:48:42Z wait_agent timeout_ms=1000 -> {"status":{},"timed_out":true}
2026-06-24T04:50:29Z wait_agent timeout_ms=10000 -> {"status":{},"timed_out":true}
2026-06-24T04:50:46Z wait_agent timeout_ms=60000 -> {"status":{},"timed_out":true}
2026-06-24T04:51:56Z close_agent Hilbert, plus cleanup closes for two sibling agents
2026-06-24T11:56:27Z close_agent returned only after user abort
The tool outputs reported approximately:
aborted by user after 24930.6s
aborted by user after 24930.5s
So the close path occupied about 7h04m wall-clock and only ended because the user aborted it.
The child transcript contained session metadata, task start/abort lifecycle, and no useful assistant/tool result.
Current code path
Current main still appears to have this shape:
state.send_op(agent_id, Op::Shutdown {}).await
thread.wait_until_terminated().await
The wait on thread.wait_until_terminated() is unbounded. If shutdown/task abort/lifecycle cleanup never completes, close_agent blocks the parent tool call indefinitely.
Desired behavior
close_agent should not wait forever for a child runtime to finish shutdown.
Suggested behavior:
- submit shutdown as today
- bound the wait for session-loop termination
- if the timeout fires, return a structured timeout/error to the parent instead of blocking indefinitely
- do not remove/release/forget the child if termination was not observed, so the still-live thread remains inspectable/retryable and its slot is not falsely freed
- normal completed shutdown should keep existing cleanup behavior
Candidate patch
Because external code contributions are by invitation only, I am filing this as an issue/proposal rather than expecting an unsolicited PR to be accepted.
I pushed a minimal candidate implementation in a public fork for reference:
- branch: https://github.com/alygg77/codex/tree/codex/bound-v1-close-agent-shutdown
- commit:
ca2890159e3e264e2f52599ac5d122ee4f66717f - compare: https://github.com/openai/codex/compare/main...alygg77:codex/bound-v1-close-agent-shutdown?expand=1
The candidate patch:
- adds a bounded v1 close-agent termination wait
- returns the existing request-timeout error on termination timeout
- leaves the live thread tracked on timeout
- adds a deterministic regression test with a child task whose abort hook blocks
Validation on candidate patch
Commands run locally:
cargo fmt --check
cargo test -p codex-core close_agent_times_out_when_child_session_does_not_terminate
cargo test -p codex-core close_agent_submits_shutdown_and_returns_previous_status
RUST_MIN_STACK=8388608 cargo test -p codex-core shutdown_agent_tree_closes
RUST_MIN_STACK=8388608 cargo test -p codex-core tool_handlers_cascade_close_and_resume_and_keep_explicitly_closed_subtrees_closed
RUST_MIN_STACK=8388608 cargo test -p codex-core spawn_agent_releases_slot_after_shutdown
Notes:
cargo fmt --checkpassed, with stable rustfmt warnings about the repo config keyimports_granularitybeing unstable.- On this local macOS/Rust 1.95 setup, some existing agent-control tests stack-overflow without
RUST_MIN_STACK; I verified upstreammainshows the same stack overflow forspawn_agent_releases_slot_after_shutdown, so that appears pre-existing in this environment rather than introduced by the candidate patch.
Why this matters
Cleanup should not be able to consume hours of wall-clock time after useful work is done. Once wait_agent has already timed out repeatedly, calling close_agent should be a bounded cleanup operation, not a potentially indefinite parent-turn blocker.
4 Comments
Seeing this every time, but the larger issue remains - agents get 'stuck' randomly and often: https://github.com/openai/codex/issues/30400
Same issue
What issue are you seeing?
A Codex Desktop subagent can stall during startup/tool discovery, then later be marked closed in the spawn-edge table while its child thread remains visible/unarchived and incomplete. After that,
close_agenthangs instead of treating the agent as already closed or cleaning up the orphaned child thread.This creates a confusing state for the user: another agent cannot close the subagent, the child appears live/stuck, but the evidence shows it never actually ran user work.
Environment
0.142.4gpt-5.5019f195b-3609-76a0-bc62-a65e208feee0019f1684-c843-7ad1-94f0-bf77bd5acbb2SingerexplorerSteps to reproduce
closed, but the child thread row remains unarchived and visible.close_agentcall against the same child id hangs.Actual behavior
The child thread rollout contains only four records:
Adding #31036 as additional corroborating Desktop/app-server evidence for this same failure mode.
I rechecked current
main, and the relevant v1 close path still appears unbounded:multi_agent_v1.close_agentawaitssession.services.agent_control.close_agent(agent_id).AgentControl::shutdown_live_agentincodex-rs/core/src/agent/control/legacy.rssendsOp::Shutdownand then awaitsthread.wait_until_terminated()without a timeout.So #31036 looks like another instance of the same root problem: once an interrupted/nonresponsive subagent never reaches termination,
close_agentcan block the parent turn indefinitely.I agree with the bounded shutdown-wait direction. The main semantic question seems to be timeout behavior: should
close_agentreturn a timeout while leaving the live thread tracked for retry/inspection, or should it continue through cleanup/removal after the bounded wait? Either would be much better than an unbounded parent-turn block; keeping the thread tracked seems safer if termination was not actually observed.This might be the root cause of an FD exhaustion on linux bug. Subagents aren't being cleaned up and holding their FD's open.
It’s the Codex app-server, PID 2877. Not the repo, and not global Linux exhaustion.
Live evidence:
Soft limit: 1,024 FDs
Observed: 928–930 open
619 pipes — every one maps to child-process stdin/stdout/stderr
207 pidfd handles
204 duplicate MCP-server processes:
[snip MCP list]
Dozens of old session JSONL files also remain open.
Ed:
OK, Codex kept investigating on its own (so grain of salt etc):
Because Codex’s cleanup is tied to thread shutdown, not turn/subagent completion—and that shutdown transition is not happening.
The chain is:
task_complete / turn_aborted→ thread remains loaded and subscribed → per-threadMcpConnectionManagerremains alive → Codex retains the three stdio pipe ends and PID handle → MCP process never receives EOF → nothing exits.Live evidence from this app-server:
thread/unsubscriberequest was receivedCodex 0.145.0 automatically subscribes clients when starting/resuming threads. Its documented 30-minute unload timer only begins after the last subscriber unsubscribes. Turn completion does not count. [Exact 0.145.0 lifecycle documentation](https://github.com/openai/codex/blob/rust-v0.145.0/codex-rs/app-server/README.md)
The MCP shutdown implementation itself was fixed to terminate children when session shutdown actually runs. [Merged shutdown fix](https://github.com/openai/codex/pull/19753) But completed subagents are not reliably reaching that shutdown path—an acknowledged lifecycle bug that remains open upstream. [Subagent MCP leak](https://github.com/openai/codex/issues/17574)
So the short answer is: Codex marks the work finished but keeps the owning session object alive. The MCP processes and pipes therefore still have a legitimate live owner and cannot clean themselves up.