multi_agent_v1.close_agent can hang for hours when closing an unresponsive subagent
Summary
A parent Codex Desktop thread appeared stuck for more than 8 hours after attempting to close an unresponsive subagent. The blocking point appears to be multi_agent_v1.close_agent.
This was observed while working in a local project. The project task itself had already completed; the stuck task was a follow-up code-review subagent.
Environment
- macOS 26.3.1 arm64
- Codex Desktop
- Parent session
cli_version:0.131.0-alpha.9 - Subagent session
cli_version:0.133.0-alpha.1 - Commit involved in reviewed workspace:
61a66df7e829de9fab98f783c2f6d7bb56c6d92f
What Happened
A parent thread spawned a worker subagent for a code review task:
- Agent id:
019e5a75-f6ef-7c23-9f26-418094bd282b - Thread name:
Review Task 5 iOS charts
The parent then called wait_agent twice with timeout_ms=600000. Both calls returned:
{"status":{},"timed_out":true}
The parent then called:
{"target":"019e5a75-f6ef-7c23-9f26-418094bd282b"}
via multi_agent_v1.close_agent.
Actual Result
close_agent blocked for 29235.5s and only returned after the user interrupted the turn:
aborted by user after 29235.5s
This made the parent Codex thread look dead/frozen for roughly 8h 7m. The user eventually created a new worktree/session to continue.
Expected Result
close_agent should return promptly, or at least have a bounded timeout/failure result, especially when the subagent has already produced no status and has been interrupted/aborted. It should not block the parent turn for hours.
Evidence Available Locally
Parent session log:
~/.codex/sessions/2026/05/18/rollout-2026-05-18T20-38-23-019e3b18-36e0-7a02-adb4-1362ca3d426d.jsonl
Relevant lines:
8797: firstwait_agent8798: timed out with empty status8807: secondwait_agent8808: timed out with empty status8813:close_agentcall8815:close_agentreturned after29235.5s
Subagent session log:
~/.codex/sessions/2026/05/24/rollout-2026-05-24T22-49-00-019e5a75-f6ef-7c23-9f26-418094bd282b.jsonl
The subagent log had only 4 lines: session metadata, task_started, user interruption, and turn_aborted. There was no assistant output or tool call output from the subagent before it was aborted.
Session index entry:
~/.codex/session_index.jsonl:56
{"id":"019e5a75-f6ef-7c23-9f26-418094bd282b","thread_name":"Review Task 5 iOS charts","updated_at":"2026-05-24T14:49:20.571331Z"}
Additional Observation
A process listing after the incident showed multiple long-lived Codex/MCP child processes, including node_repl, xcodebuildmcp, and mcp-server-mobile, some running for 9+ hours. This supports, but does not by itself prove, a possible cleanup/resource leak around subagent/MCP lifecycle.
Suggested Fix
- Add a hard timeout to
close_agent. - Make
close_agentidempotent for already-aborted or unresponsive agents. - Ensure MCP/node_repl child processes are detached or cleaned up without blocking the parent thread indefinitely.
- Consider returning a structured failure status instead of blocking the parent turn while waiting for shutdown.
14 Comments
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
Thanks. Ill review #23700 and #23971 and close this if its a duplicate.
Fix: multi_agent_v1.close_agent Hangs for Hours When Closing an Unresponsive Subagent
Issue: openai/codex #24389
Severity: High — blocks parent agent indefinitely
Component:
codex-rs/core/src/agent/control.rs—AgentControl::shutdown_live_agentLabels: multi-agent, hang, timeout, shutdown
---
1. Root Cause Analysis
1.1 The Problem
When
close_agentis called on an unresponsive subagent, the call can hang for hours (or indefinitely). The root cause is an unbounded await in the shutdown path:1.2 Why It Hangs
The call chain is:
close_agent(agent_id)→shutdown_agent_tree(agent_id)→shutdown_live_agent(agent_id)shutdown_live_agentsendsOp::Shutdownto the subagent's submission channelthread.wait_until_terminated().await— which awaitssession_loop_terminationsession_loop_terminationis aShared<BoxFuture<'static, ()>>wrapping aJoinHandle<()>JoinHandleonly completes when thesubmission_looptask exitsThe
submission_loop(incodex-rs/core/src/session/handlers.rs) processes operations sequentially:If the session loop is currently:
Then
Op::Shutdownsits in the channel queue behind the current operation, andwait_until_terminated()blocks forever.1.3 Impact Scope
| Component | Affected | Description |
|-----------|----------|-------------|
|
close_agenttool (v1) | Yes | Callsshutdown_live_agent||
close_agenttool (v2) | Yes | Same code path ||
shutdown_agent_tree| Yes | Cascades to all descendants ||
shutdown_live_agent| Yes | Direct culprit ||
CodexThread::shutdown_and_wait| Yes | Same unbounded await pattern ||
Session::shutdown_and_wait| Yes | Same unbounded await pattern |1.4 Why Tests Do Not Catch This
The test infrastructure uses
ThreadManager::with_models_provider_for_tests()which operates in a test mode where:captured_ops()records ops without actually running a real session loop---
2. Step-by-Step Fix Approach
2.1 Design Goals
shutdown_live_agentso it never blocks indefinitely2.2 Implementation Steps
Step 1: Add
shutdown_timeout_mstoMultiAgentV2ConfigFile:
codex-rs/core/src/config/mod.rsStep 2: Store the
JoinHandleinCodexfor forceful terminationFile:
codex-rs/core/src/session/mod.rsUpdate
Codex::spawnto store the handle:Step 3: Add
wait_until_terminated_with_timeouttoCodexThreadFile:
codex-rs/core/src/codex_thread.rsStep 4: Update
shutdown_live_agentwith timeout and fallbackFile:
codex-rs/core/src/agent/control.rsStep 5: Add config parsing for
shutdown_timeout_msFile:
codex-rs/core/src/config/mod.rsAdd to
MultiAgentV2ConfigTomland the parsing function:Add validation:
2.3 Alternative Approach: Abort Without Config
If adding a config option is too invasive, the simpler approach uses a hardcoded default:
---
3. Code Snippets
3.1 Full
shutdown_live_agentwith Fix3.2
CodexThreadExtensions3.3 Config Example
---
4. Testing Strategy
4.1 Unit Tests
Test 1: Graceful Shutdown Within Timeout
Test 2: Timeout and Force Abort
Test 3: Cascade Shutdown with Mixed Responsiveness
Test 4: Config Timeout Respected
4.2 Integration Tests
Test 5: Real Unresponsive Agent (API Hang Simulation)
Test 6: Multiple Concurrent Close Operations
4.3 Regression Tests
Test 7: Existing Tests Still Pass
All existing tests in
multi_agents_tests.rsandcontrol_tests.rsshould continue to pass:4.4 Performance Tests
Test 8: Timeout Does Not Affect Normal Shutdown
---
5. Impact Assessment
5.1 Positive Impacts
| Area | Impact |
|------|--------|
| Reliability | Parent agents no longer hang indefinitely when closing unresponsive subagents |
| Observability | Warning logs provide visibility into shutdown issues |
| Resource Management | Resources are eventually cleaned up even for stuck agents |
| User Experience | Users see timely feedback instead of silent hangs |
5.2 Potential Risks
| Risk | Mitigation |
|------|------------|
| Force abort may leave resources inconsistent | The abort only cancels the session loop task; cleanup (thread removal, spawn slot release) still proceeds. MCP connections are handled by
shutdown_session_runtimewhich runs before the timeout. || Rollout data loss |
ensure_rollout_materialized()andflush_rollout()are called before the timeout, so rollout data is persisted before any force abort || Timeout too short for slow agents | Configurable timeout allows users to adjust. Default of 30s is generous for normal operations. |
| Breaking change for long-running tools | Agents with intentionally long-running operations (>30s) may be force-aborted. Users can increase the timeout. |
5.3 Backward Compatibility
shutdown_timeout_msfield is optional with a sensible default (30s)close_agentstill returnsCloseAgentResult5.4 Files Modified
| File | Changes |
|------|---------|
|
codex-rs/core/src/agent/control.rs| Add timeout toshutdown_live_agent, addget_shutdown_timeouthelper ||
codex-rs/core/src/codex_thread.rs| Addwait_until_terminated_with_timeoutandabort_session_loop||
codex-rs/core/src/session/mod.rs| StoreJoinHandleinCodexstruct ||
codex-rs/core/src/config/mod.rs| Addshutdown_timeout_mstoMultiAgentV2Config||
codex-rs/core/src/agent/control_tests.rs| Add tests for timeout and force abort ||
codex-rs/core/src/tools/handlers/multi_agents_tests.rs| Add integration tests for unresponsive agents |5.5 Performance Impact
Option<JoinHandle>perCodexinstance5.6 Security Considerations
tokio::task::JoinHandle::abort()which is a standard Rust pattern---
6. Rollout Plan
Phase 1: Core Fix (PR #1)
shutdown_timeout_msto config with default of 30sshutdown_live_agentabort_session_loopcapabilityPhase 2: Observability (PR #2)
Phase 3: Tuning (PR #3)
---
7. References
codex-rs/core/src/agent/control.rs—AgentControl::shutdown_live_agentcodex-rs/core/src/codex_thread.rs—CodexThread::wait_until_terminatedcodex-rs/core/src/session/mod.rs—Codexstruct,SessionLoopTerminationcodex-rs/core/src/session/handlers.rs—submission_loopcodex-rs/core/src/config/mod.rs—MultiAgentV2Configapp-server/src/in_process.rs—SHUTDOWN_TIMEOUT = Duration::from_secs(5)app-server-client/src/remote.rs—SHUTDOWN_TIMEOUTfor remote connectionsI hit a related long-running subagent lifecycle failure and inspected the current public source. The
close_agenthang described here looks consistent with an unbounded shutdown await in the shared close path.Both
multi_agents/close_agent.rsandmulti_agents_v2/close_agent.rscallAgentControl::close_agent(...), which reachesshutdown_agent_tree(...)and thenshutdown_live_agent(...).In
codex-rs/core/src/agent/control.rs,shutdown_live_agentsendsOp::Shutdownand then awaits:without an outer timeout or fallback. If the child session loop is stuck, blocked in a tool call, transport-stalled, or otherwise unable to process/complete shutdown, the parent close_agent tool can remain blocked indefinitely.
I hit this issue in Codex Desktop as well and prepared a minimal branch with a bounded shutdown wait:
https://github.com/tianpeng-dev/codex/tree/fix/bound-agent-shutdown-wait
Commit:
ae4888b fix(core): bound live agent shutdown waitLocal evidence points to the same lifecycle path described here:
shutdown_live_agentsubmitsOp::Shutdownand then waits onthread.wait_until_terminated()without a timeout. If the child session loop is stalled,close_agentcan remain blocked and the UI stays inClosing ....The branch keeps the
close_agentresult schema unchanged and adds a bounded wait around live agent termination. If the shutdown request was submitted but the agent does not terminate, it returnsCodexErr::RequestTimeoutand continues the existing cleanup path.Validated locally with:
cargo fmt --check -p codex-corecargo test -p codex-core agent::control::legacy::tests::wait_for_live_agent_termination_times_out_when_agent_never_exitscargo test -p codex-core close_agent_submits_shutdown_and_returns_previous_statusRUST_MIN_STACK=8388608 cargo test -p codex-core shutdown_agent_tree_closes_live_descendantsNote:
shutdown_agent_tree_closes_live_descendantsoverflowed the default test stack locally, but passed withRUST_MIN_STACK=8388608.Would the Codex team be open to this minimal approach, and if so, could you invite a PR for this branch?
Fresh reproduction from June 17, 2026, on a local Codex Desktop session. This is the same failure mode:
multi_agent_v1.close_agentwaited until the user aborted the parent turn, and subsequent subagent work hit the thread cap.The affected target in both long closes was
019ed2f7-e5e2-7320-94cb-c90c6e56c428.I traced the current
mainpath tocodex-rs/core/src/agent/control/legacy.rs:shutdown_live_agentsubmitsOp::Shutdownand then awaitsthread.wait_until_terminated()without bounding the wait. A focused patch is forthcoming that interrupts an active agent task before queuing shutdown and wraps the termination wait in a timeout so the parent close tool cannot hang indefinitely.A focused patch is available here:
45b61dc0a5 fix: bound multi-agent close shutdownI attempted to open the PR directly against
openai/codex, but this account is blocked from creating upstream pull requests:The REST endpoint was also unavailable from this token:
Net change in the branch:
Op::Shutdown.shutdown_live_agentwhile the agent has an active turn task.Local verification on the branch:
Fresh Codex Desktop datapoint from 2026-06-20 that looks like the same close-hang family, with an extra UI/registry mismatch signal.
Environment:
26.616.32156(4157)codex-cli 0.142.0-alpha.1codex-cli 0.141.015.7.7/ Darwin24.6.0arm64Observed sequence:
Zeno.Subagentslist no longer containedZeno; it only showed other subagents such asHooke,Halley,Kepler, andTuring.Zeno, and there was no visible subagent card/row to inspect for it.Why this seems useful for this issue:
Expected behavior:
already_closed,already_shutdown, ornot_found_after_spawn, not block the parent turn indefinitely.I am not attaching the raw screenshot/logs here because they include workspace/task details, but the key visible contradiction was:
This supports the same fix direction discussed above: make
close_agentbounded and idempotent, and make the close path treat missing/terminal/already-detached child state as a terminal cleanup result rather than waiting forever on a termination future or stale handle.I can reproduce a closely matching case on Codex Desktop where a normal delegated message to an old parent thread completes quickly, but asking that same parent thread to close an already-unresponsive subagent leaves the parent turn in progress.
Environment / context:
019ee0a0-c3b5-7843-951c-0b6d93258937019ef395-2511-7d03-9cef-4aef2fa407fdMendel/Users/fujiwarakasei/Documents/Project/LAUNOVA~/.codex/sessions/2026/06/23/rollout-2026-06-23T17-24-58-019ef395-2511-7d03-9cef-4aef2fa407fd.jsonlObserved sequence:
wait_agentcalls for that subagent timed out with empty status, thensend_input(... interrupt=true ...)also failed to produce a usable result.multi_agent_v1.close_agentfor that subagent, and the user interrupted the turn while it was still stuck around the close operation.019ef395-2511-7d03-9cef-4aef2fa407fd. The parent emitted “开始关闭 sub-agent。” and then stayedinProgress; after more than two minutes there was still noclose_agentreturn or final response.This makes the failure mode look specific to
multi_agent_v1.close_agenton an aborted/unresponsive subagent, rather than the parent thread being generally unable to receive or answer new messages.The local evidence strongly matches this issue’s report:
wait_agentgives no useful status for the subagent, andclose_agentcan block the parent turn instead of returning promptly or reporting a bounded failure.This issue maps closely to LS work on bounded lifecycle transitions, execution leases, and durable termination receipts.
The central distinction is:
termination requested
≠ termination completed
≠ cleanup completed
≠ parent must remain blocked
A control-plane operation such as "close_agent" should never depend indefinitely on the responsiveness of the child it is trying to terminate.
A possible termination request:
{
"schema_version": "agent-termination-request/v0.1",
"request_id": "terminate-184",
"dispatch_id": "dispatch-review-05",
"parent_agent_id": "agent-root",
"child_agent_id": "agent-reviewer",
"runtime_instance_id": "runtime-7712",
"mode": "GRACEFUL",
"requested_at": "2026-06-24T20:10:00Z",
"grace_timeout_ms": 10000,
"hard_timeout_ms": 30000
}
A bounded shutdown path could be:
request graceful cancellation
→ wait within grace timeout
→ escalate to forced termination
→ persist terminal or unresolved state
→ return control to parent
→ continue asynchronous cleanup if necessary
The key invariant would be:
«Failure to confirm child termination must not block the parent agent indefinitely.»
A second invariant:
«"close_agent" should be idempotent and return the current lifecycle state when the child is already aborted, terminated, missing, or under cleanup.»
Possible results:
TERMINATED
ALREADY_TERMINATED
TERMINATION_PENDING
FORCE_TERMINATED
NOT_FOUND
TERMINATION_FAILED
CLEANUP_INCOMPLETE
A structured result could look like:
{
"request_id": "terminate-184",
"child_agent_id": "agent-reviewer",
"status": "CLEANUP_INCOMPLETE",
"runtime_terminated": true,
"remaining_resources": [
"mcp-process:xcodebuildmcp",
"node-repl:process-889"
],
"parent_blocked": false,
"follow_up": "BACKGROUND_RECONCILIATION"
}
This separates three concerns that should not be collapsed:
child task status
child runtime status
descendant-resource cleanup status
For example, the child turn may already be "ABORTED", while an MCP descendant remains alive. That should not require keeping the parent turn blocked for hours.
The parent should receive a bounded receipt:
{
"event_type": "AGENT_TERMINATION_RECEIPT",
"child_agent_id": "agent-reviewer",
"terminal_status": "ABORTED",
"termination_confirmed": true,
"cleanup_confirmed": false,
"elapsed_ms": 30000,
"decision": "RETURN_TO_PARENT"
}
A reconciliation worker could then asynchronously inspect:
agent registry
↔ operating-system process tree
↔ MCP child-process registry
↔ launch-slot accounting
and emit a later cleanup receipt.
A deterministic conformance fixture could test:
Related LS work:
https://github.com/safal207/LS/issues/594
https://github.com/safal207/LS/issues/595
https://github.com/safal207/LS/issues/597
https://github.com/safal207/LS/pull/651
Would a small vendor-neutral "AgentTerminationRequest", "TerminationReceipt", and asynchronous cleanup fixture help make shutdown latency, escalation, idempotency, and parent non-blocking behavior machine-testable?
Adding a fresh matching datapoint from 2026-06-24; I posted the detailed timeline on #25870 because that issue focuses on stale/unresponsive child cleanup, but this is the same user-visible failure class.
Short version: a native explorer subagent timed out under
wait_agentthree times, thenmulti_agent_v1.close_agentwas called at2026-06-24T04:51:56Zand did not return until the user aborted at2026-06-24T11:56:27Z. The outputs wereaborted by user after 24930.6s/24930.5s. The child transcript had only session metadata,task_started, andturn_aborted; no assistant/tool output.This reinforces that close_agent needs a bounded shutdown/cleanup path and should not keep the parent turn blocked while waiting on an unresponsive child runtime.
Follow-up: I pushed a minimal candidate fix from this reproduction to a public fork branch:
The patch bounds the v1 live-agent termination wait and returns the existing request-timeout error if the child accepts shutdown but does not terminate, while leaving the still-live thread tracked instead of removing/releasing it. I attempted to open a draft PR, but GitHub rejected both CreatePullRequest and REST pull creation for this token/repo permission set.
Adding a fresh sanitized datapoint from Codex Desktop on 2026-06-27. This looks like the same multi-agent lifecycle failure family described here, with one nuance: the user-visible stuck point was a
close_agentattempt on an old timed-out review subagent, while the persisted parent transcript later shows the parent turn still in progress around multi-agent wait/cleanup orchestration.Environment:
codex-cli 0.142.3Codex Desktopvscode019f083a-d4d6-7b12-a4be-254b1393943fObserved sequence, sanitized:
At this point the parent transcript stopped with the
wait_agentcall and no corresponding output in the local JSONL transcript. The Codex app still reports that parent turn asinProgresswithcompletedAt: null.The child transcript for
019f09d7-08ee-7bb3-b742-684405012763shows it continued running and applying changes after the parent wait began. It progressed at least until about2026-06-27T16:13:07Z, including successful local command outputs, but the child transcript also has no final completed/subagent notification record. So the user-visible effect was that the parent orchestration was stuck even though child work had continued.Additional UI-side observation from the same parent thread summary:
close_agentitself got stuck, and it would stop touching that old agent.inProgressaround subsequent subagent lifecycle management.Local evidence checked:
~/.codex/sessions/2026/06/27/rollout-2026-06-27T16-38-21-019f083a-d4d6-7b12-a4be-254b1393943f.jsonl~/.codex/sessions/2026/06/28/for the child ids aboveread_threadfor the parent still reports statusinProgress,completedAt: nullI am not attaching raw logs because they include workspace and task details. The sanitized takeaway is:
close_agentcan be the user-visible stuck point for an old timed-out review child.close_agentcalls return promptly, the parent/child lifecycle can still fail to reconcile: a child continues doing work, but the parentwait_agentnever receives a terminal result and the parent turn remainsinProgress.Expected behavior remains the same as discussed above: child close/wait lifecycle operations should be bounded and idempotent, and parent control should return with a structured timeout/partial-cleanup result instead of leaving the parent turn open indefinitely.
Adding another fresh Codex Desktop datapoint. This looks like the same close-agent lifecycle bug, but with one detail that may be useful: after a rollback/resume, the parent tried to close the same stale child again and hit the same hang a second time.
Environment:
26.623.42026(4514)0.142.3Codex DesktopvscodeDarwin 25.4.0 arm64 arm019f0a4c-dddb-7561-951f-31015132d59f019f09ce-7776-75f0-9dca-491a96971b27explorer/FermatObserved sequence:
So this was not just a slow child task. The child had already reached an interrupted/aborted state, but
close_agentstill put the parent turn into a blocking close path. After the user interrupted and the parent thread rolled back, the same cleanup action was attempted again and wedged the parent again.Why this matters:
thread_rolled_back.This matches the current source shape discussed above. In current
main, the v1 close handler still awaitssession.services.agent_control.close_agent(agent_id), and the legacy shutdown path sendsOp::Shutdownand then awaitsthread.wait_until_terminated()without a bounded close-specific timeout.Interruptedis also not treated as final by the public status helper, which seems relevant for stale/interrupted children that are terminal enough for cleanup but not final enough for the lifecycle machinery.Expected behavior:
close_agentshould be idempotent across rollback/resume/retry.shutdown_timed_out,already_interrupted,cleanup_pending, ornot_found, rather than holding the parent turn open.This feels adjacent to #25870 and #25426, but the extra signal here is the repeated close after rollback/resume: the same stale interrupted child can keep re-poisoning the parent turn until the user stops touching it.