[macOS App] Completed subagents stay open in thread_spawn_edges and restore stale MCP stacks

Open 💬 9 comments Opened Jul 16, 2026 by ardjo-s
💡 Likely answer: A maintainer (github-actions[bot], contributor) responded on this thread — see the highlighted reply below.

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?

  1. In Codex Desktop, start one task with several subagents and at least one nested subagent. Use MCP-bearing plugins or configured stdio MCP servers.
  2. Let the children finish or interrupt them. Confirm they no longer appear as live agents.
  3. Inspect thread_spawn_edges for the root task:

``sql
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;
``

  1. Fully quit and reopen Codex Desktop.
  2. Count repeated direct children under the new codex app-server PID (node_repl, plugin server.mjs/server.cjs, Workshop, Pencil, XcodeBuildMCP, etc.).
  3. Observe that the number of helper generations matches the root task plus the stale open descendant edges.
  4. 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:

  1. persisted thread_spawn_edges lifecycle state;
  2. 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 closed exactly 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 = false must 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:

  • #32942 — restored tasks fan out plugin MCP servers and can ignore current Xcode MCP policy
  • #19197 — persistent orphaned subagents and missing lifecycle controls
  • #17574 — subagents retain stdio MCP helper trees
  • #30408 — per-thread MCP processes are not cleaned up
  • #11324 — MCP memory growth while multitasking in the App
  • #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.

View original on GitHub ↗

9 Comments

github-actions[bot] contributor · 1 month ago

Potential duplicates detected. Please review them and close your issue if it is a duplicate.

  • #32942

Powered by Codex Action

ardjo-s · 1 month ago

Cross-reference to my related quota report: #34066.

#33700 documents the process-side mechanism that may explain hidden background usage: completed/interrupted descendants remain open in state_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 · 19 days ago

Windows A/B reproduction on Codex Desktop 26.803.5235.0 with bundled Codex CLI 0.147.0, using only the official archive operation and read-only aggregate measurements. No database edits, restarts, deletions, or process termination were performed.

  • Canary: archived one completed, idle, loaded review task; its evidence remained readable. The Codex process family changed from 81 to 73 members, helper processes from 4 to 3, Node workers from 19 to 14, and mean private memory from about 10.00 GB to 9.50 GB in the settled after sample.
  • Control: archived five completed tasks already reported by Codex as notLoaded; all evidence remained readable. Helpers stayed at 4 and Node workers at 20 after every archive, so this class produced no process benefit.
  • Lifecycle: archive counters changed as expected, but all 1,433 observed spawn edges remained open, including all 382 in the measured parent group. While active work continued, aggregate counts later returned to 87–89 family members, 4 helpers, and 20 Node workers.

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 · 18 days ago

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 at 2026-08-09T13:13:09Z:

  • thread_spawn_edges: 692 open, 275 closed.
  • All 692 open child rollouts were present. 642 had a terminal final_answer at or after the latest user message, and 639 had task_complete at or after the latest task_started event.
  • Of the 642 open children with terminal final answers, 622 were older than one day, 528 older than seven days, and 179 older than 30 days.
  • Retained codex_otel.agent_communication telemetry contained 263 kind=result / state=send records from 197 unique sender threads.
  • All 197 unique result senders still had open spawn 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_answer is 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 open for days or weeks. Desktop rehydration should not interpret those rows as live descendants.

teamleaderleo · 18 days ago

Confirming this on a current ChatGPT/Codex Desktop build:

  • ChatGPT Desktop 26.803.41515 build 6321
  • bundled codex-cli 0.147.0-alpha.6.5
  • macOS 26.5.1 (25F80) arm64

~/.codex/state_5.sqlite currently has:

open_edges: 79
distinct_parents: 11

oldest_child_utc: 2026-07-09T19:51:22.217Z
newest_child_utc: 2026-08-09T01:28:08.185Z

open_edges_older_than_1d: 79
open_edges_older_than_7d: 33
open_edges_older_than_14d: 29
open_edges_older_than_30d: 20

thread_spawn_edges only has:

parent_thread_id
child_thread_id
status

I derived the ages from the timestamps embedded in the UUIDv7 thread IDs. Codex's current ThreadId implementation says generated thread IDs are UUIDv7 and uses Uuid::now_v7().

The same long-lived app-server also had a bunch of repeated direct-child generations of:

node ./mcp/server.mjs
node ./mcp/server.cjs --stdio
/Applications/ChatGPT.app/Contents/Resources/cua_node/bin/node_repl

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 open rows and repeated helper generations look like the same lifecycle problem described in this issue.

tsuvic · 16 days ago

Additional macOS evidence from a current local reproduction (read-only; no database mutation).

Environment:

  • macOS 26.5.2 (25F84), arm64
  • Codex Desktop log release 26.803.61601
  • standalone codex-cli 0.147.0

Scope: 019fe3e6-ac7d-7452-b67d-3cfddb7459e7 is 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:

  • The parent has 7 direct child edges: 6 open, 1 closed.
  • All 6 open child rollouts contain a terminal event_msg.payload.type=task_complete; each timestamp matches that child’s threads.updated_at_ms.
  • The single closed child is a separate interrupted/closed case and has no task_complete event.
  • The parent itself also has task_complete; it is not the affected SubAgent.
  • PRAGMA integrity_check returns ok.
  • In the Desktop log, reopening the parent showed latestTurnStatus=completed together with markedStreaming=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.status remains open. 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 closed idempotently. Desktop SubAgent/Agent projections should also reconcile terminal child state instead of continuing to display the child as active or counting elapsed time.

socialperks · 12 days ago

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 permanent AgentControl::close_agent path. 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:

  • 7 user turns produced 1,399 parent token_count events.
  • The parent called wait_agent 1,009 times; 773 returned timed_out: true.
  • 1,007 of those waits requested only 30 or 60 seconds even though current core supports waits up to one hour and its usage hint recommends waits measured in minutes.
  • The final local tree contained 69 archived descendants and 42,668 descendant token_count events.
  • Descendant cumulative counters recorded 4,985,014,061 input tokens, of which 4,903,650,816 were cached (98.37%), plus 9,379,576 output tokens.
  • One child spawned nine children; those nine alone recorded 1,905,536,556 cumulative total tokens.
  • Weekly usage metadata recorded 18% used at 2026-08-15 21:58 CEST and 77% used at 2026-08-16 10:02 CEST.

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 open and 273 closed spawn edges. Every linked child is archived. Of the 998 open edges, 966 child rollouts contain a task_complete event.

Current upstream also contains an explicit regression named multi_agent_v2_spawn_agent_ignores_configured_max_depth; V1 checks agent_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 has repo scope, 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.

shleder · 11 days ago

This is useful for validating the rollout/lifecycle side of codex-rescue==0.1.0a4. It will not close thread_spawn_edges, reap MCP processes, or alter Desktop state, but a read-only doctor pass on one representative stale child rollout can tell us whether its durable transcript is classified as terminal/unfinished and whether any schema/tool-state findings disagree with the persisted open edge.

codex-rescue doctor --json "/path/to/one-stale-child-rollout.jsonl"

If you try it, a sanitized summary of status/findings/aggregate counts is sufficient. Please do not post raw rollouts/SQLite, MCP or tool payloads, prompts, repository data, thread IDs, credentials, or private paths.

eliotgevers · 7 days ago

Confirming similar behavior on macOS with ChatGPT Desktop 26.814.41407 (build 6720) and bundled codex-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 start processes 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.