Repository navigation
[macOS App] Completed subagents stay open in thread_spawn_edges and restore stale MCP stacks #33700
Description
What version of the Codex App are you using (From “About Codex” dialog)?
26.707.91948 (build 5440), bundled codex-cli 0.144.5
What subscription do you have?
ChatGPT subscription. The exact tier is not exposed by the local diagnostics used for this report.
What platform is your computer?
Darwin 25.5.0 arm64 arm
What issue are you seeing?
Completed/interrupted subagents remain persisted as open rows in state_5.sqlite.thread_spawn_edges. When Codex Desktop restores the parent task, it starts one complete MCP/plugin helper generation for the root task and for every stale descendant edge—even though the children no longer appear in the live agent listing.
This creates a direct, measurable multiplier:
root task + 4 direct subagents + 1 nested subagent
= 6 restored contexts
= 6 MCP/plugin helper generations
Before the focused reproduction, a single app-server had reached:
generations=11
matched MCP/plugin direct children=99
matched child RSS=3798 MiB
app-server RSS=692 MiB
After a full application relaunch, the same task restored exactly six generations:
generations=6
matched MCP/plugin direct children=48
matched child RSS=3152 MiB
app-server RSS=632 MiB
The five descendant rows were still open, despite the agents being completed/interrupted and absent from the current live-agent listing:
parent child-1 open depth=1
parent child-2 open depth=1
parent child-3 open depth=1
parent child-4 open depth=1
child-3 nested-child open depth=2
There is a second lifecycle/config problem: restored contexts can start helpers from stale tool policy. xcodebuildmcp started four times even though the current plugin MCP override resolved it as disabled. A removed GitHits MCP definition did take effect and did not restart, so persisted tool policy appears inconsistent by source/layer.
What steps can reproduce the bug?
-
In Codex Desktop, start one task with several subagents and at least one nested subagent. Use MCP-bearing plugins or configured stdio MCP servers.
-
Let the children finish or interrupt them. Confirm they no longer appear as live agents.
-
Inspect
thread_spawn_edgesfor the root task:WITH RECURSIVE tree(id, depth) AS ( SELECT '<root-thread-id>', 0 UNION ALL SELECT e.child_thread_id, t.depth + 1 FROM thread_spawn_edges e JOIN tree t ON e.parent_thread_id = t.id ) SELECT e.parent_thread_id, e.child_thread_id, e.status, t.depth + 1 AS depth FROM thread_spawn_edges e JOIN tree t ON e.parent_thread_id = t.id;
-
Fully quit and reopen Codex Desktop.
-
Count repeated direct children under the new
codex app-serverPID (node_repl, pluginserver.mjs/server.cjs, Workshop, Pencil, XcodeBuildMCP, etc.). -
Observe that the number of helper generations matches the root task plus the stale
opendescendant edges. -
Disable a plugin-provided MCP in the current config, relaunch, and observe that restored contexts may still start that server from stale policy.
Controlled diagnostic
After an online SQLite backup, I changed only these five descendant edge rows from open to closed; PRAGMA integrity_check returned ok.
Restarting only app-server was insufficient: the still-running Electron main process retained cached task state and recreated five helper generations. This suggests there are two sources that must converge:
- persisted
thread_spawn_edgeslifecycle state; - cached restored-task/tool state owned by the long-lived desktop main process.
This database edit is diagnostic evidence, not a recommended user workaround.
What is the expected behavior?
- A completed, interrupted, or stopped subagent must transition its spawn edge to
closedexactly once. - Desktop restore must not treat completed descendants as live task contexts.
- Restored tasks must re-resolve the current MCP/plugin policy before launching helpers.
enabled = falsemust prevent launch from persisted or cached task state.- Finished descendants' MCP process groups must be reaped without requiring a full desktop restart.
- The UI/diagnostics should expose the owning task/subagent for every MCP helper and why it is still alive.
Additional information
Sanitized logs, state-table evidence, and active-config summary:
https://gist.github.com/ardjo-s/aec95bc245439bd447beb179b0bfb049
Related open issues:
- [macOS App] Restored tasks fan out plugin MCP servers (~30x) and crash/reload the renderer #32942 — restored tasks fan out plugin MCP servers and can ignore current Xcode MCP policy
- Persistent orphaned subagents, missing lifecycle controls, and eventual session freezes #19197 — persistent orphaned subagents and missing lifecycle controls
- Subagents leak stdio MCP helper trees in Codex App; xcodebuildmcp and chrome-devtools-mcp accumulate indefinitely #17574 — subagents retain stdio MCP helper trees
- MCP server processes leak: per-thread processes never cleaned up (9+ GB RSS) #30408 — per-thread MCP processes are not cleaned up
- MCP servers eat up memory when multi-tasking in Codex App #11324 — MCP memory growth while multitasking in the App
- MCP servers eagerly start per session, causing headed browser MCP processes to accumulate #21984 — eager per-session MCP startup
This report is intentionally focused on the persisted thread_spawn_edges.status = open invariant and the exact 1:1 mapping between stale descendant edges and restored MCP generations.
During diagnostics I intentionally sent SIGTERM to app-server to test teardown. The app displayed its generic crash page; that signal was operator-initiated and is not being reported as a spontaneous crash. The page also displayed an unrelated Semrush remote-plugin name-mismatch warning; it may deserve a separate report but is not presented here as the cause of the MCP/subagent lifecycle defect.
Activity
github-actions commentedon Jul 16, 2026
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
ardjo-s commentedon Jul 18, 2026
Cross-reference to my related quota report: #34066.
#33700 documents the process-side mechanism that may explain hidden background usage: completed/interrupted descendants remain
openinstate_5.sqlite.thread_spawn_edges, and Desktop restore launches one MCP/plugin helper generation per stale descendant. In the controlled reproduction, 6 restored contexts produced 6 generations, 48 direct child processes, and 3152 MiB RSS; the earlier state reached 11 generations, 99 children, and 3798 MiB RSS.#34066 reports immediate weekly-limit exhaustion and unusually large local token estimates without session-level attribution. The MCP/subagent multiplier is therefore a plausible contributor to background usage, but this does not yet prove that the process leak alone caused the quota exhaustion. The two reports should be investigated together across restored-task state, MCP lifecycle, subagent accounting, and quota attribution.
KoiosSG commentedon Aug 9, 2026
Windows A/B reproduction on Codex Desktop
26.803.5235.0with bundled Codex CLI0.147.0, using only the official archive operation and read-only aggregate measurements. No database edits, restarts, deletions, or process termination were performed.notLoaded; all evidence remained readable. Helpers stayed at 4 and Node workers at 20 after every archive, so this class produced no process benefit.Interpretation: official archiving can unload a currently loaded idle task, but it is not child-edge finalization and does not repair retained ownership for completed tasks that are already unloaded. This reproduces the archive/open-edge divergence on Windows and distinguishes a transient unload effect from a durable lifecycle fix.
The requested product behavior is an idempotent, bounded terminal transition for completed/failed/interrupted/abandoned child edges; release of helper trees owned only by those terminal children without a desktop restart; parent restore that ignores terminal descendants; and owner-attributed diagnostics plus a supported non-destructive repair path for already-stale edges.
wxyJohnsonAI commentedon Aug 9, 2026
Additional Windows persistence evidence (aggregate-only; complements the existing Windows archive A/B comment):
Codex Desktop for Windows
26.803.5235.0, read-only snapshot at2026-08-09T13:13:09Z:thread_spawn_edges: 692open, 275closed.final_answerat or after the latest user message, and 639 hadtask_completeat or after the latesttask_startedevent.codex_otel.agent_communicationtelemetry contained 263kind=result / state=sendrecords from 197 unique sender threads.openspawn edges; 194 also had terminal final answers after their latest user messages.Method: read the state database in SQLite read-only mode; classify a rollout as terminal only when its latest
final_answeris at or after its latest user message; correlate result-sender UUIDs with open child UUIDs in memory; emit aggregate counts only.No database mutation was performed for this report. The report contains no thread UUIDs, rollout paths, prompts, outputs, task titles, workspace paths, account identifiers, or machine identifiers.
This narrows the persisted-state invariant on Windows: a result can be sent back to the parent and a terminal final answer can be recorded while the corresponding spawn edge remains
openfor days or weeks. Desktop rehydration should not interpret those rows as live descendants.teamleaderleo commentedon Aug 10, 2026
Confirming this on a current ChatGPT/Codex Desktop build:
26.803.41515build6321codex-cli 0.147.0-alpha.6.526.5.1 (25F80)arm64~/.codex/state_5.sqlitecurrently has:thread_spawn_edgesonly has:I derived the ages from the timestamps embedded in the UUIDv7 thread IDs. Codex's current
ThreadIdimplementation says generated thread IDs are UUIDv7 and usesUuid::now_v7().The same long-lived app-server also had a bunch of repeated direct-child generations of:
Several had been alive for more than a day.
Those children were only about 148 MiB RSS combined during this capture, so they were separate from the big app-server memory spike I was looking at.
I haven't mapped all 79 persisted edges 1:1 to the currently live helper generations, but the stale
openrows and repeated helper generations look like the same lifecycle problem described in this issue.tsuvic commentedon Aug 12, 2026
Additional macOS evidence from a current local reproduction (read-only; no database mutation).
Environment:
Scope:
019fe3e6-ac7d-7452-b67d-3cfddb7459e7is the parent thread (source=vscode, with no incoming spawn edge). The reported symptom is in its direct child SubAgents; this parent ID is only the root used to enumerate them.Observed:
open, 1closed.openchild rollouts contain a terminalevent_msg.payload.type=task_complete; each timestamp matches that child’sthreads.updated_at_ms.closedchild is a separate interrupted/closed case and has notask_completeevent.task_complete; it is not the affected SubAgent.PRAGMA integrity_checkreturnsok.latestTurnStatus=completedtogether withmarkedStreaming=true; later the stream activity was cleared. Opening/selecting an affected child makes the UI show Done.Interpretation:
The completed child SubAgents are the affected objects. Their terminal rollout state is complete, but the persisted parent→child
thread_spawn_edges.statusremainsopen. The parent reopen/resume is only the trigger that rehydrates the child list; it does not mean the parent was still running.Hypothesis / fix target:
On every child terminal transition (completed, failed, interrupted, or stopped), persist the incoming spawn edge as
closedidempotently. Desktop SubAgent/Agent projections should also reconcile terminal child state instead of continuing to display the child as active or counting elapsed time.socialperks commentedon Aug 16, 2026
I reproduced this on macOS with the Codex desktop build bundled with
codex-cli 0.148.0-alpha.9.The core lifecycle gap is that multi-agent V2 exposes
interrupt_agent, which intentionally keeps the target resumable, but does not expose the existing permanentAgentControl::close_agentpath. An interrupted or completed child can therefore retain an open spawn edge and be restored as open after the app restarts. This is distinct from every stale child actively making new model calls, but it makes the UI state unreliable and can retain live runtime/MCP resources.Verified local impact
For one V2 orchestration tree running from 2026-08-15 21:12 CEST to 2026-08-16 09:52 CEST:
token_countevents.wait_agent1,009 times; 773 returnedtimed_out: true.token_countevents.These are local cumulative runtime counters, not a claim about the exact subscription-weighting formula. The correlation is strong, but only OpenAI can confirm how cached input is weighted against the weekly allowance.
The current database snapshot contains 998
openand 273closedspawn edges. Every linked child is archived. Of the 998 open edges, 966 child rollouts contain atask_completeevent.Current upstream also contains an explicit regression named
multi_agent_v2_spawn_agent_ignores_configured_max_depth; V1 checksagent_max_depth, while the V2 spawn handler does not. The observed tree reached depth 2 despite the default configured depth of 1.Proposed bounded core change
The revised patch exposes V2
close_agent, resolves ids or canonical task paths, and strengthens the durable close path so it marks the selected agent and every persisted descendant edge closed deepest-first before the parent. It still shuts down the live subtree if persistence reports an error, emits the existing close lifecycle event for app-server watch cleanup, and adds regressions proving that a nested grandchild moves from open to closed and does not rehydrate when its parent is resumed.Focused lifecycle and tool-family tests pass. I attempted to open a draft PR from a registered fork, but GitHub returned
FORBIDDEN: socialperks does not have the correct permissions to execute CreatePullRequest. The OAuth token hasreposcope, so I cannot confirm that this is merely a missing token scope. The browser comparison link may still permit manual creation.This commit is intentionally the smallest core termination boundary. It does not auto-close agents on terminal completion, reconcile already-stale archived edges at startup, enforce V2 depth, impose per-tree token/time budgets, or by itself fix the desktop renderer. Those need follow-up changes.
Related: #19197 and #37563.
eliotgevers commentedon Aug 20, 2026
Confirming similar behavior on macOS with ChatGPT Desktop
26.814.41407(build6720) and bundledcodex-cli 0.148.0-alpha.15.Even with Convex operations serialized, helpers started by the official Convex plugin accumulated instead of being cleaned up: persistent
convex mcp startprocesses grew from 21 to 33, then reached 36 processes using roughly 2.2 GiB RSS combined.This looks consistent with MCP/plugin helpers being retained across task or subagent lifecycles after the work that spawned them has finished.
hasbai commentedon Sep 17, 2026
Additional macOS reproduction: completed subagents and MCP/CUA cleanup
This is additional evidence for the lifecycle issue tracked here and overlaps with the hidden-thread/MCP-pool behavior in #43971.
Environment
codex.opencodex-real, running as the Codex app-serverObserved before a full app quit
chrome-devtools-mcpprocesses and 141cua_nodeprocesses; combined descendant RSS was about 5.6 GiBthread-writer-lockswere openevent_msg.payload.type=task_complete; 23 completed more than 6 hours earlier and 9 more than 24 hours earlierWhy this points to lifecycle retention rather than one malloc leak
The heap was dominated by approximately 7.2 million non-object malloc nodes (about 1.3 GiB). The macOS
leakstool reported only about 0.6 MiB of unreachable memory, so this does not look like one conventional pointer leak. It looks like completed task/session state is retained and the MCP/CUA child process trees are not reaped.The app log also contained repeated
responses_websocketerrors (111),codex_mcp::rmcp_clientwarnings (114), and thread-history protocol warnings (35). These may exacerbate the cleanup path, although I cannot prove causality from this observation alone.Reproduction context
~/.codex/thread-writer-locks.After the full restart, ChatGPT was about 214 MiB RSS and no Codex app-server was present until a new task started. This restart-only recovery is a strong control.
Expected behavior
Completed, interrupted, or failed subagents should close their session resources, transition lifecycle state, and reap all MCP/CUA descendants without requiring a full desktop restart.