Persistent orphaned subagents, missing lifecycle controls, and eventual session freezes
What version of Codex CLI is running?
0.123.0
What subscription do you have?
Pro+ ($200)
Which model were you using?
gpt-5.4 high
What platform is your computer?
Ubuntu 24.04.4 LTS (GNU/Linux 6.8.0-90-generic x86_64)
What terminal emulator and version are you using (if applicable)?
tmux
What issue are you seeing?
Summary
Codex leaks subagents and does not manage their lifecycle reliably. After enough subagents accumulate, Codex itself hits the too many subagents launched limit and attempts cleanup, but usually terminates only 1–3 subagents. The remaining stuck/orphaned subagents are no longer visible to it, continue to count against the limit, and block further launches. If I ask the main thread to clean up inactive subagents, the result is the same: only partial cleanup, with the remaining subagents still stuck and still counted. Over time, the session can hard-freeze mid-task and must be killed manually.
This does not look like simple memory exhaustion: free RAM is still available and swap is large when the freeze happens.
This is not an MCP/apps issue.
I had already disabled apps explicitly in codex-config.toml because MCP/apps initialization was previously getting stuck indefinitely:
[features]
multi_agent = true
apps = false
Actual behavior
- Subagents can remain stuck after their work is finished.
- Codex eventually reaches its own subagent limit.
- When this happens, Codex may try to clean up automatically, but usually terminates only 1–3 subagents.
- The remaining stuck/orphaned subagents are not properly visible to Codex anymore.
- Those invisible subagents still count against the launch limit.
- Asking the main thread to clean up inactive subagents produces the same partial cleanup.
- Entering a subagent through
/agentsdoes not provide a way to terminate it. - There are no usable user-facing lifecycle controls for subagents.
- In long-running degraded sessions, Codex can hard-freeze and must be killed manually.
- This can happen even with plenty of free RAM and large swap available.
<img width="510" height="243" alt="Image" src="https://github.com/user-attachments/assets/b6e9e8ce-8c0d-475d-90c5-9ec1c11d054c" />
What steps can reproduce the bug?
- Use a session heavily with subagents.
- Let Codex spawn multiple subagents across tasks.
- After some time, some subagents remain alive after their useful work is already finished.
- Codex reaches the subagent limit and starts failing with
too many subagents launched. - Codex attempts cleanup on its own, but usually removes only 1–3 subagents.
- The remaining stuck/orphaned subagents are not cleaned up and appear to be no longer visible to Codex.
- Asking the main thread to clean up inactive subagents leads to the same partial result.
- New subagents still cannot be launched reliably.
- If the session keeps running in this degraded state, Codex may eventually freeze and require a manual kill.
What is the expected behavior?
- Codex should track all active subagents accurately.
- Finished, inactive, or orphaned subagents should remain visible until fully cleaned up.
- Automatic cleanup should remove all inactive/orphaned subagents, not just 1–3.
- Manual cleanup from the main thread should also remove all inactive/orphaned subagents reliably.
- Stuck subagents should not continue counting against the limit after cleanup is requested.
/agentsshould allow explicit termination of the current subagent.- Users should have direct lifecycle controls for listing, terminating, force-killing, and resyncing subagents.
- Leaked subagents should not be able to degrade the session into a blocked or frozen state.
Impact
This is a serious blocker for subagent-heavy workflows. Instead of enabling parallel decomposition, the current behavior causes degraded sessions, blocked launches, repeated manual recovery attempts, and eventual session loss.
Related issue
There is also a separate delegation quality problem: even when the main session runs on GPT-5.4, Codex often delegates unsuitable tasks to Codex 5.2, including research or spec writing, and often adds unnecessary restrictive instructions. This is separate, but it makes the subagent failure mode worse.
12 Comments
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
I reviewed the suggested duplicates.
This is not an MCP/apps issue on my side.
I had already disabled apps explicitly in
codex-config.tomlbecause MCP/apps initialization was previously getting stuck indefinitely:Adding a narrower abort-path reproduction here instead of opening another issue, because this appears to fit the same orphaned-subagent lifecycle family. I searched existing issues first for
close_agent,wait_agent,subagent, andturn_aborted.My read of the related issues: #18335 is the closest earlier technical report but is closed and framed around spawn-slot leaks in persistent sessions; #17574/#19092 are mostly MCP helper process leaks; #19344 is broader app freeze/session loss; and #20908 is recovery messaging. This issue looks like the best active place for evidence about a subagent that has a
turn_abortedterminal event but still cannot be waited/closed correctly.What version of the Codex App are you using?
I did not capture the About Codex dialog version during the repro. The affected session metadata reports:
What subscription do you have?
ChatGPT Pro.
What platform is your computer?
What issue are you seeing?
A Codex Desktop subagent that was interrupted/aborted became a stale lifecycle handle:
wait_agentreturned completed statuses for other subagents, but no terminal status for the interrupted one.close_agentagainst the interrupted/orphaned subagent blocked indefinitely until I manually interrupted the parent turn.This is a narrower form of the orphaned-subagent behavior described in this issue: the trigger was an explicit interrupted/aborted subagent turn.
What steps can reproduce the bug?
explorersubagents from one parent thread.wait_agentfor all subagent ids.close_agentfor the interrupted agent id.close_agenthangs until the parent turn is manually interrupted.Affected identifiers from my repro:
Local session index entry for the affected subagent:
Session file tail for that subagent:
What is the expected behavior?
wait_agentshould surface a terminal state for interrupted/aborted subagents, for exampleaborted,cancelled, orclosed.close_agentshould be idempotent and return quickly for already-aborted agents.close_agentshould return a structured timeout/error instead of blocking the parent turn indefinitely.Additional information
Observed in a normal Codex Desktop subagent workflow; sharing reproduction details and local state evidence per the repo contributing guidance.
Adding one more concrete abort-path data point because it appears to match the same orphaned-subagent lifecycle failure.
What version of Codex CLI is running?
I did not capture the About Codex app dialog version during the repro.
What subscription do you have?
ChatGPT Pro.
What platform is your computer?
What issue are you seeing?
A spawned subagent entered an interrupted/aborted state and then behaved like a stale runtime handle:
close_agentsubmitted shutdown and marked the persisted spawn edge asclosed.active_turn_status=Interrupted.close_agentfor this subagent did not return before the parent turn was manually interrupted.Identifiers, partially redacted:
Subagent rollout tail:
Relevant logs for the subagent:
Persisted spawn edge state after the failed/partial cleanup:
So the local DB state and the runtime state appear inconsistent: the edge is already closed, but the app-server still sees an interrupted active turn for that thread.
What steps can reproduce the bug?
close_agentfor the interrupted subagent.closed, andclose_agentcan block instead of returning quickly.What is the expected behavior?
close_agentshould be idempotent and should return quickly for already-interrupted or already-persisted-closed subagents.close_agentshould return a structured timeout/error instead of blocking the parent turn indefinitely.Adding a current-version data point from Codex Desktop, but this appears to be the shared Codex core / app-server multi-agent lifecycle path rather than a GUI-only problem.
Version / surface
The installed Desktop app includes the bundled
codexCLI binary and exposes the same core/app-server protocol. The relevant symbols and source paths are incodex-rs/core/src/agent/*andcodex-rs/core/src/tools/handlers/multi_agents*, not only in the GUI layer.What I observed
In a long-running local project session that uses subagents heavily, completed subagents continued to count against the subagent pool until the parent explicitly called
close_agent.wait_agentobserved completion, but did not release the slot. After enough completed-but-not-closed workers accumulated, newspawn_agentcalls hit the pool limit even though the workers had already returned their final handbacks.This matters for real project workflows where subagents are used to isolate risky implementation/review slices. Some examples that do not require project-specific details:
I agree with earlier maintainer comments that a completed agent may still have background terminal processes or MCP connections, so blindly deleting thread state is not safe. The issue is that the only safe release path appears to be explicit
close_agent, and the parent model can miss that call.Code-level read
From
rust-v0.132.0:AgentControl::spawn_agent_internalreserves a pool slot withself.state.reserve_spawn_slot(config.agent_max_threads).AgentRegistry::reserve_spawn_slotchecks/incrementstotal_countand returnsAgentLimitReachedwhen the limit is hit.AgentRegistry::release_spawned_threadis the release path that removes the spawned agent from the active tree and decrementstotal_count.release_spawned_threadis called fromAgentControl::shutdown_live_agent, which is reached byAgentControl::close_agent, and from theInternalAgentDiedcleanup path.TurnCompletetoAgentStatus::Completed(...).is_finaltreatsCompleted,Errored,Shutdown, andNotFoundas final, but normal final status does not callrelease_spawned_thread.wait_agentobserves final status and emits waiting events; it does not close or release the agent.Relevant paths/functions:
Thread APIs are not an equivalent workaround
I also checked the app-server surface.
thread/unsubscribeonly unsubscribes a connection from a thread.thread/archivearchives and tears down normal thread state. Neither is exposed as the same operation asAgentControl::close_agent, and neither should be recommended as a slot-release workaround unless it is explicitly wired through the agent close/release path.Manual mutation/deletion/archive of thread state would risk losing UI/history/evidence and bypassing the only path that currently decrements the multi-agent pool counter.
Requested fix shape
The safest behavior would be to reap finalized spawned agents before enforcing the pool limit, while preserving history/result visibility and using the same release semantics as
close_agent/shutdown_live_agent.If automatic reaping is too risky because
Completedcan still have live terminals or MCP connections, a narrower fix would be a safe explicit command/API that closes completed agents throughAgentControl::close_agent, for example:That would avoid relying on the model remembering every
close_agentcall and would avoid unsafethread/archive/thread/unsubscribeworkarounds.Tests that would cover this
wait_agentcompletion observation does not silently lose result/history.close_agentremains the path that releases the pool slot.thread/archiveandthread/unsubscribeare not treated as equivalent slot release.I prepared a validated reference patch on a fork. Given the repository contribution policy, this is not an unsolicited PR request; it is included here so maintainers can inspect the proposed behavior concretely if useful:
https://github.com/openai/codex/compare/main...TacoRocket:codex:subagent-finalized-slot-recovery
Patch shape:
AgentControlbefore retryingagents.max_threadsslot reservation.close_completed_agentsto close every eligible finalized spawned leaf through the same close path.agent/closeCompletedwith{ closedCount }response, so cleanup is not only model-invoked.Validation in the fork:
just fmtjust write-app-server-schemacargo test -p codex-core close_completed_agentscargo test -p codex-core finalizedcargo test -p codex-core multi_agent_feature_selects_one_agent_tool_familycargo test -p codex-core multi_agent_v2_namespace_is_ignored_without_provider_namespace_supportcargo test -p codex-app-server-protocol agent_close_completedcargo test -p codex-app-server-protocol schema_fixturescargo test -p codex-app-server agent_close_completedHappy to adapt, split, or submit this as a PR if a Codex maintainer invites that path.
One fix shape that helped us on a similar runtime was to separate "thread still exists" from "child is still admitted to run again."
For interrupted children, we force a bounded terminal receipt even if full teardown is still in flight:
child_idlast_progress_atlast_verifier_deltahalt_reasonnext_attempt_allowedresource_reconciliation = pending|done|failedIf teardown cannot finish inside the close path, the child moves to a terminal-but-unreconciled state and stops counting against fresh work admission. A background janitor can then reconcile leftover MCP/process resources without leaving the operator stuck behind a handle that is both dead and still blocking capacity.
That design made two failure modes much easier to reason about for us:
We've been hitting the same family of issues in MartinLoop, and the biggest improvement came from making the terminal receipt explicit instead of letting "close" be the only source of truth.
It’s been almost two months since April 24, but it still doesn’t look fixed. Everything is the same in codex-cli 0.140.0
<img width="779" height="360" alt="Image" src="https://github.com/user-attachments/assets/435795e0-f200-4cf0-839a-0e6a568c1903" />
This issue maps closely to LS work on explicit agent lifecycle, execution leases, durable ownership, and replayable cleanup.
The central invariant may be:
agent no longer visible
≠ agent terminated
≠ resources released
≠ launch slot reclaimed
A subagent should remain represented in durable runtime state until it reaches a verified terminal state.
A possible lifecycle record:
{
"schema_version": "subagent-lifecycle/v0.1",
"dispatch_id": "dispatch-184",
"trajectory_id": "repository-task-001",
"parent_agent_id": "agent-root",
"child_agent_id": "agent-review-04",
"runtime_instance_id": "process-7712",
"task_ref": "task:review-auth-module",
"status": "RUNNING",
"lease": {
"owner": "agent-review-04",
"expires_at": "2026-06-24T20:14:00Z",
"last_renewed_at": "2026-06-24T20:09:00Z"
},
"resource_accounting": {
"counts_toward_limit": true
}
}
A useful lifecycle could be:
REQUESTED
→ STARTING
→ RUNNING
→ COMPLETING
→ COMPLETED
→ TERMINATING
→ TERMINATED
with explicit exceptional states:
FAILED
CANCELLED
TIMED_OUT
ORPHANED
TERMINATION_FAILED
UNKNOWN_RUNTIME_STATE
The important boundary is:
«"COMPLETED" describes task outcome, while "TERMINATED" describes runtime cleanup.»
A child may finish its work but still require resource cleanup:
task completed
→ result receipt stored
→ termination requested
→ runtime termination confirmed
→ slot released
The launch counter should be derived from verified live runtime instances, not from stale parent-thread memory.
A reconciliation process could compare:
durable lifecycle registry
↔ runtime process registry
↔ parent-visible agent list
↔ resource-limit accounting
and produce typed findings:
{
"child_agent_id": "agent-review-04",
"registry_status": "RUNNING",
"runtime_status": "NOT_FOUND",
"parent_visibility": "MISSING",
"classification": "ORPHANED_RECORD",
"recommended_action": "RECONCILE_AND_RELEASE_SLOT"
}
The opposite case should also be detected:
{
"child_agent_id": "unknown",
"runtime_instance_id": "process-8891",
"registry_status": "NOT_FOUND",
"runtime_status": "RUNNING",
"classification": "ORPHANED_RUNTIME",
"recommended_action": "QUARANTINE_OR_TERMINATE"
}
Manual lifecycle controls could operate on stable identities:
Each control should emit a durable receipt rather than only updating the TUI.
For example:
{
"event_type": "SUBAGENT_TERMINATION",
"child_agent_id": "agent-review-04",
"requested_by": "local-user",
"mode": "FORCE",
"status": "SUCCEEDED",
"runtime_instance_id": "process-7712",
"slot_released": true
}
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 "SubagentLifecycleRecord", "TerminationReceipt", and reconciliation fixture help make orphan detection, cleanup, and launch-slot accounting machine-testable?
Adding one current user-facing data point from Codex Desktop around the same lifecycle-control gap.
A user ended up with 100+ subagents shown in the sidebar after subagent-heavy work. From the model/tool surface available in the active thread, the agent could only see targeted lifecycle operations such as
close_agent(target)andresume_agent(id). It could not enumerate all currently open/live/completed subagents, so it could not safely do the obvious cleanup loop:This makes the current cleanup model fragile:
close_agentslot/resource cleanup, so users do not know which action is safe.The missing primitive seems smaller than a full orchestration redesign:
list_agentsorinspect_agentsthat returns all subagents for the current root/thread tree, including status and whether each counts toward the active limit.close_completed_agents/close_inactive_agentsthat uses the same release path asclose_agent.close_all_agentsorforce_close_agentpath for stale/interrupted agents, so cleanup cannot hang the parent turn indefinitely.Without this, the user can see the problem in the UI but neither the user nor the parent agent has a reliable supported way to enumerate and clean it up. The workaround becomes manual tracking of spawned ids or database/sidebar cleanup, which is not a safe lifecycle control.
Additional Windows data point: persistent edges remain open while runtime sees no agents
I observed the opposite state-divergence direction from #25426.
Environment:
0.144.0-alpha.40.144.5Observed behavior:
openinstate_5.sqlite.0.144.5and requesting closure of allremaining subagents reported that there were no subagents left to close.
status = 'open'.PRAGMA integrity_checkreturnedok, so this was not SQLite corruption.opentoclosedresulted in:ok,codex doctor: 0 warnings and 0 failures.This appears to be durable/runtime lifecycle divergence in the opposite
direction from #25426:
thread_spawn_edges: openExpected behavior:
runtime agents no longer exist.
transitioned to a terminal state.
state_5.sqlitemanually.I cannot confirm whether these persistent open rows were counted against
agents.max_threads; I did not observe an explicitagent thread limitlog for this case.reached
Thanks for the additional data points. These reports make it clear that this is not only a stale-UI issue or a single "close_agent" bug, but a broader lifecycle state divergence problem.
We now have evidence of several inconsistent states:
This suggests that "runtime registry", "thread_spawn_edges", and the UI/thread tree are being treated as separate sources of state without a reliable reconciliation step.
A minimal safe fix would likely need:
The key requirement is that users should never need to edit "state_5.sqlite" manually, and cleanup should never be able to block the parent turn indefinitely.
A useful regression matrix would include both divergence directions:
This issue appears to be the right umbrella for the lifecycle-control and reconciliation gap, while the narrower issues can document the individual failure paths.