Bound v1 close_agent shutdown waits for unresponsive child agents

Open 💬 3 comments Opened Jun 25, 2026 by alygg77

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:

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 --check passed, with stable rustfmt warnings about the repo config key imports_granularity being unstable.
  • On this local macOS/Rust 1.95 setup, some existing agent-control tests stack-overflow without RUST_MIN_STACK; I verified upstream main shows the same stack overflow for spawn_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.

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗