Skip to content

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

Open

Description

@ardjo-s

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:

    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;
  4. Fully quit and reopen Codex Desktop.

  5. Count repeated direct children under the new codex app-server PID (node_repl, plugin server.mjs/server.cjs, Workshop, Pencil, XcodeBuildMCP, etc.).

  6. Observe that the number of helper generations matches the root task plus the stale open descendant edges.

  7. 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:

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

  1. added
    bugSomething isn't working
    appIssues related to the Codex desktop app
    subagentIssues involving subagents or multi-agent features
    mcpIssues related to the use of model context protocol (MCP) servers
    app-serverIssues involving app server protocol or interfaces
    on Jul 16, 2026
  2. github-actions commented on Jul 16, 2026

    @github-actions
    Contributor

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

    Powered by Codex Action

  3. ardjo-s commented on Jul 18, 2026

    @ardjo-s
    Author

    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.

  4. KoiosSG commented on Aug 9, 2026

    @KoiosSG

    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.

  5. wxyJohnsonAI commented on Aug 9, 2026

    @wxyJohnsonAI

    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.

  6. teamleaderleo commented on Aug 10, 2026

    @teamleaderleo

    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.

  7. tsuvic commented on Aug 12, 2026

    @tsuvic

    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.

  8. socialperks commented on Aug 16, 2026

    @socialperks

    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.

  9. eliotgevers commented on Aug 20, 2026

    @eliotgevers

    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.

  10. hasbai commented on Sep 17, 2026

    @hasbai

    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

    • macOS 26.5.2 (25F84), Apple Silicon arm64, 24 GiB RAM
    • Incident client version: ChatGPT Desktop 26.908.40834
    • After restart, the app auto-updated to 26.908.70816 (build 9275)
    • The affected process was the OpenAI-signed ChatGPT child process named codex.opencodex-real, running as the Codex app-server

    Observed before a full app quit

    • app-server uptime: about 25 hours
    • app-server physical footprint: 1.8–2.3 GiB, peak 2.3 GiB; 1.1–1.8 GiB was swapped
    • 266 descendants in the process tree: 99 chrome-devtools-mcp processes and 141 cua_node processes; combined descendant RSS was about 5.6 GiB
    • many helper groups had been alive for more than 20 hours
    • 47 unique thread-writer-locks were open
    • 33 of the corresponding rollout JSONL files ended with event_msg.payload.type=task_complete; 23 completed more than 6 hours earlier and 9 more than 24 hours earlier
    • about 63 session JSONL files and 458 pipes were open

    Why 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 leaks tool 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_websocket errors (111), codex_mcp::rmcp_client warnings (114), and thread-history protocol warnings (35). These may exacerbate the cleanup path, although I cannot prove causality from this observation alone.

    Reproduction context

    1. Use several Codex subagents with MCP, browser, computer-use, or visualization helpers.
    2. Let children complete or fail, while keeping the desktop app open for several hours.
    3. Inspect the app-server descendants and ~/.codex/thread-writer-locks.
    4. Completed child rollouts remain marked complete in their JSONL, but locks and helper processes remain open.
    5. Fully quitting and reopening ChatGPT releases the memory and removes the accumulated helper tree.

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    appIssues related to the Codex desktop appapp-serverIssues involving app server protocol or interfacesbugSomething isn't workingmcpIssues related to the use of model context protocol (MCP) serversperformancesubagentIssues involving subagents or multi-agent features

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions