Windows Codex Desktop: opening/resuming historical subagent threads spawns duplicate MCP and node_repl process stacks

Open 💬 5 comments Opened Aug 7, 2026 by MarkoAvreliy
💡 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)?

Codex Desktop package: OpenAI.Codex_26.803.5235.0_x64

What subscription do you have?

This appears related to MCP lifecycle/refresh handling + sub agents

What platform is your computer?

_No response_

What issue are you seeing?

Summary

Codex Desktop on Windows creates duplicate local MCP and node_repl.exe process stacks when I open/resume historical chats that contain many completed subagents.

This occurs even when I do not send a new message or run a tool. Simply opening an old parent chat, and then opening a completed child thread in the Subagents panel, triggers thread/resume and starts another full set of local MCP processes.

The processes remain alive as children of the current codex.exe app-server instead of returning to a bounded baseline.

Environment

  • OS: Windows
  • Codex Desktop package: OpenAI.Codex_26.803.5235.0_x64
  • Main process:

codex.exe -c features.code_mode_host=true app-server --analytics-default-enabled

  • Configured local stdio MCP servers:
  • MarkItDown
  • brain-memory
  • brain-knowledge
  • Also enabled: node_repl / Browser / Computer Use

Reproduction

  1. Fully quit Codex Desktop.
  2. Launch Codex Desktop and do not open historical chats initially.
  3. Baseline is one process stack:
  • 1 node_repl.exe
  • 1 MarkItDown MCP wrapper
  • 1 brain-memory MCP wrapper
  • 1 brain-knowledge MCP wrapper
  1. Open one historical parent chat with many old subagents. Do not send a message.
  2. The Subagents panel initially shows many old children as Working.
  3. Open one or more old child threads. Their displayed state immediately changes from Working to Done.
  4. Observe duplicate process stacks under the current codex.exe.

Observed result

After opening a historical parent chat and inspecting old child threads, the current codex.exe had:

  • 8 node_repl.exe direct children
  • 8 MarkItDown MCP wrapper processes
  • 8 brain-memory MCP wrapper processes
  • 8 brain-knowledge MCP wrapper processes
  • 32 direct helper processes total
  • 48 Python processes in the Codex process tree

No new user task, subagent launch, or MCP tool call was requested.

The local app-server log shows thread/resume at the same timestamps as MCP startup / tool catalog initialization, including:

app_server.request ... otel.name="thread/resume"
MCP server ... Processing request of type ListToolsRequest
codex_mcp::connection_manager::tool_catalog

### What steps can reproduce the bug?

## Summary

Codex Desktop on Windows creates duplicate local MCP and `node_repl.exe` process stacks when I open/resume historical chats that contain many completed subagents.

This occurs even when I do not send a new message or run a tool. Simply opening an old parent chat, and then opening a completed child thread in the Subagents panel, triggers `thread/resume` and starts another full set of local MCP processes.

The processes remain alive as children of the current `codex.exe app-server` instead of returning to a bounded baseline.

## Environment

- OS: Windows
- Codex Desktop package: `OpenAI.Codex_26.803.5235.0_x64`
- Main process:
  `codex.exe -c features.code_mode_host=true app-server --analytics-default-enabled`
- Configured local stdio MCP servers:
  - MarkItDown
  - brain-memory
  - brain-knowledge
- Also enabled: `node_repl` / Browser / Computer Use

## Reproduction

1. Fully quit Codex Desktop.
2. Launch Codex Desktop and do not open historical chats initially.
3. Baseline is one process stack:
   - 1 `node_repl.exe`
   - 1 MarkItDown MCP wrapper
   - 1 brain-memory MCP wrapper
   - 1 brain-knowledge MCP wrapper
4. Open one historical parent chat with many old subagents. Do not send a message.
5. The Subagents panel initially shows many old children as `Working`.
6. Open one or more old child threads. Their displayed state immediately changes from `Working` to `Done`.
7. Observe duplicate process stacks under the current `codex.exe`.

## Observed result

After opening a historical parent chat and inspecting old child threads, the current `codex.exe` had:

- 8 `node_repl.exe` direct children
- 8 MarkItDown MCP wrapper processes
- 8 brain-memory MCP wrapper processes
- 8 brain-knowledge MCP wrapper processes
- 32 direct helper processes total
- 48 Python processes in the Codex process tree

No new user task, subagent launch, or MCP tool call was requested.

The local app-server log shows `thread/resume` at the same timestamps as MCP startup / tool catalog initialization, including:

```text
app_server.request ... otel.name="thread/resume"
MCP server ... Processing request of type ListToolsRequest
codex_mcp::connection_manager::tool_catalog

### What is the expected behavior?

_No response_

### Additional information

Additional context
This appears related to MCP lifecycle/refresh handling. PR #19753 added explicit shutdown handling for MCP refresh and session shutdown:
https://github.com/openai/codex/pull/19753
Potentially related report:
https://github.com/openai/codex/issues/26869

View original on GitHub ↗

5 Comments

github-actions[bot] contributor · 20 days ago

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

  • #37426
  • #37402

Powered by Codex Action

MarkoAvreliy · 20 days ago

Additional confirmed reproduction:

A clean Codex Desktop launch starts with one MCP/node_repl stack.

Opening one historical parent chat with 51 stale subagents, without sending a message, causes thread/resume and creates additional full MCP stacks.

Opening old child threads changes their UI state from Working to Done and triggers further thread/resume calls.

Observed after one parent-chat load and inspection of a few children:

  • 8 node_repl.exe direct children
  • 8 MarkItDown MCP wrappers
  • 8 brain-memory MCP wrappers
  • 8 brain-knowledge MCP wrappers
  • 48 Python processes in the Codex process tree

Codex logs correlate the launches with app_server thread/resume requests and MCP tool catalog initialization.

This is reproducible without launching a new task, a new subagent, or an MCP tool call.

leadingproblemsolver · 20 days ago

The strongest invariant here seems to be that opening historical state is not an execution transition.

thread/resume currently appears to conflate two operations:

hydrate/read historical thread for UI
activate executable thread runtime + tools/MCP

Those should be separate lifecycle intents. A historical child that is terminal should be loadable with something like:

thread/open(view_mode='history')
  -> read rollout/state
  -> reconcile terminal status
  -> NO runtime lease
  -> NO node_repl
  -> NO MCP process/catalog startup

Only an explicit continuation/new-turn operation should acquire an execution generation and materialize tool resources.

That gives each thread instance a resource lease identity:

thread_id
execution_generation
runtime_lease_id
resource_set_hash
lease_state = absent | active | released

A terminal historical thread should have no active lease. Reopening it 20 times may create 20 view sessions, but must still create zero new MCP/runtime stacks.

The reported Working->Done flip on opening a child is also a useful sign that read-time reconciliation is happening. That repair should stay read-only: discovering that historical state is terminal must not activate the runtime in order to prove it.

Regression cases:

open terminal child for inspection N times => 0 new MCP/node_repl stacks
open parent containing 51 terminal children => 0 child runtime leases
send new user turn to terminal/resumable thread => exactly 1 new execution generation/resources
close/release generation                 => resources return to baseline
restart app + inspect history            => still read-only

The invariant is: history hydration may reconstruct evidence and UI state; only explicit execution intent may allocate live execution resources.

MarkoAvreliy · 19 days ago

Follow-up: local mitigation, validation, and architectural request

This is not a UI-only annoyance in my workflow.

I reproduced the historical-hydration path where completed children can be presented as Working again and local tool/runtime activity can be re-materialized without any new user message. My earlier per-child stdio MCP topology amplified the process symptom, but it was not the root cause.

Investigation cost

Approximate workflow telemetry for diagnosis, reproduction, patching, and validation:

  • 10h 51m active investigation;
  • 7.36M internal agent-processing tokens.

These are not exact billed API tokens or exact ChatGPT subscription-quota consumption; that cannot be reconstructed client-side. However, the incident materially affected productivity and forced a subscription-tier increase during the investigation period.

Practical comparison

I moved from Claude Code expecting comparable multi-agent capability with a more economical workflow. In my day-to-day Claude Code workflow, bounded subagent orchestration had been predictable and practical.

In Codex Desktop, the equivalent workflow became operationally unsafe: reopening historical chats could restore stale child state and create uncertainty about whether completed work had truly stopped. This is my field experience, not a universal benchmark claim, but it is a major regression for a user relying on many small workers.

Experimental local mitigation

I published a sanitized, reproducible checkpoint:

It does not disable MCP, browser control, computer use, Node tools, or subagents. Instead it keeps lifecycle boundaries intact:

  • historical display does not start MCP/tool runtime;
  • completed child status is restored without loading its runtime;
  • completed V2 child runtimes unload through the normal shutdown path;
  • stale legacy child edges are repaired without eagerly resuming the child;
  • tools still start normally for a real new foreground action.

Validation: 4 lifecycle tests and 3 lazy-resume MCP integration tests passed. Opening four historical parent chats with completed children produced 0 new Python, 0 new Node, and 0 new node_repl processes, and no history-triggered MCP, child-spawn/resume, or model-transport references.

This is an experimental local mitigation, not an official OpenAI fix or distributable Codex build.

IMPORTANT FOR CODEX USERS AND MAINTAINERS

HISTORICAL CHAT VIEWING MUST BE PASSIVE. A COMPLETED CHILD MUST NOT BE RESTORED AS A RUNNABLE RUNTIME JUST BECAUSE ITS PARENT CHAT WAS OPENED.

DEFAULT CHILDREN SHOULD BE LIGHTWEIGHT AND DENY-BY-DEFAULT FOR MCP SERVERS, PLUGINS, SKILLS, AND OTHER HEAVY TOOLING.

The orchestrator can retain its full toolset. Ordinary child workers should start with a minimal profile and bounded context; a specific MCP or plugin should be enabled only for the specialized child that actually needs it.

Suggested model:

  • orchestrator -> required full toolset;
  • general research / coding / QA child -> no heavy MCP by default;
  • document child -> only document MCP;
  • memory child -> only memory MCP;
  • knowledge child -> only knowledge MCP;
  • browser/computer child -> only the relevant capability.

If every child inherits the parent’s full tool surface and history by default, it creates an amplification risk: unnecessary process churn, configuration conflicts, avoidable context overhead, and potentially unnecessary agent/model usage.

Codex needs first-class lifecycle isolation and a safe lightweight-child default, while preserving deliberate opt-in to specialist tools.

dingaiminGIT · 8 days ago

I reproduced the same restart/resume amplification on macOS. This appears to be the cross-platform counterpart of this issue, with a related live-session retention symptom tracked in #38925.

I also reviewed the experimental mitigation described above. Its passive-history invariant matches my findings. The prototype below targets complementary paths that were still visible in my macOS restart captures: restored root sessions, internal summary sessions, concurrent cold tool-catalog discovery, and safe local-stdio retirement after a real turn.

Environment

  • macOS arm64
  • ChatGPT Desktop 26.814.41407 (build 6720)
  • bundled codex-cli 0.148.0-alpha.15
  • 16 local stdio MCP servers, mostly uv + Python process pairs

Observed behavior

Before the mitigation, one capture contained 96 uv and 102 Python processes, using about 5.7 GB RSS in aggregate. During restarts the ChatGPT process reached 18–26 GB and macOS displayed the system out-of-memory dialog.

The configuration contained one entry per MCP server. The duplicates were descendants of the same live app-server. In a bounded restart capture, five resumed threads produced eight complete MCP generations; each configured server appeared eight times as a launcher/runtime pair.

A minimal observation sequence was:

  1. configure multiple local stdio MCP servers;
  2. fully quit and relaunch Codex Desktop with historical threads available for restoration;
  3. do not send a message;
  4. sample descendants of the new codex ... app-server by PPID and creation time;
  5. send one user message, then sample again after the turn completes.

Before the prototype, multiple complete MCP groups appeared during step 3. After the prototype, the count remained zero until step 5.

This is not only a final app-server shutdown problem. The amplification has multiple sources:

  1. restored threads construct an MCP runtime and eagerly discover optional local stdio tools before a user turn needs them;
  2. a cold tool-catalog cache permits concurrent sessions to probe the same local stdio configuration;
  3. the local session Environment object identity prevents otherwise identical host-local catalogs from sharing across sessions;
  4. internal ephemeral system sessions used for thread summaries can receive the normal local stdio MCP configuration;
  5. superseded/idle local stdio connection sets can remain alive after a turn unless retirement is tied to the actual turn owner and in-flight bindings.

Prototype and validation

I built a version-matched prototype with the following behavior:

  • restored threads publish local stdio MCP connections as dormant until a real turn;
  • optional host-local stdio discovery is process-scoped singleflight using a complete identity (server, transport, args/env, cwd, environment/capability fingerprint); different cwd values and remote environments remain isolated;
  • the known Desktop thread-summary contract skips only host-local stdio MCPs for ephemeral=true, threadSource=Feature("system"), and plugins disabled; explicit subagents, HTTP MCPs, Codex Apps, and remote stdio are preserved;
  • turn completion performs a cold swap: it publishes a dormant set first, lets existing bindings retain the old set, and retires only host-local stdio after the old references drain;
  • normal and abort paths use a turn generation/owner, so an old cancellation cannot finish a newer turn;
  • probe failure/cancellation releases only its own generation lease, avoiding both permanent suppression and duplicate slow starts.

The prototype maps these behaviors to existing ownership points rather than to Code Mode cell teardown:

  • core::session::mcp_runtime selects the startup policy from thread restore/ephemeral context and owns turn begin/finish transitions;
  • codex_mcp::runtime::McpRuntime publishes connection generations and performs the dormant cold swap;
  • McpToolCatalogCache coordinates an optional-startup owner per complete local identity;
  • McpConnectionManager::shutdown_local_stdio retires only host-local stdio transports after old bindings drain.

I initially considered shutting MCP down after each Code Mode execution cell, but source and PID tests showed that the MCP runtime is session/thread-owned. Cell-scoped shutdown would be too early and could break later MCP calls in the same turn or session.

Focused validation included:

  • 184/184 codex-mcp unit tests;
  • real PID tests for resume-without-start, first-turn activation, superseded-generation cleanup, and hidden summary sessions;
  • negative coverage for explicit subagents/root sessions, different cwd values, HTTP MCP, and remote stdio;
  • startup failure, cancellation, and unrelated-connection failure races;
  • Clippy with -D warnings for the affected crates and an arm64 release build.

In the post-change restart capture, the new app-server had zero local MCP/uv processes until the first user turn. That turn created one MCP group for its cwd. Three internal summary sessions created no additional groups. A concurrent task in a different cwd created a second group as expected, and that group disappeared after its turn. A normal quit followed by an immediate reopen did not require waiting for Python/uv processes manually; the old group reached zero before the new app-server activated a group.

Proposed upstream shape

I have a working prototype, but it is cumulative, version-bound, and based on an older bundled alpha. I am intentionally not posting that diff as an unsolicited PR. If this direction matches the intended ownership model, I can rebase/reimplement it on current main and split it into focused changes:

  1. explicit shutdown for temporary resource/status managers with stdio PID tests;
  2. a dormant startup policy for restored threads;
  3. process-scoped optional-startup singleflight with complete identity and race tests;
  4. safe turn-end retirement of host-local stdio connections;
  5. an explicit generic MCP policy for Desktop internal generation, rather than upstreaming a hard-coded Feature("system") check.

Would maintainers be interested in this direction, and if so, which slice would you prefer as the first invited PR?

No internal server names, credentials, command arguments, or private paths need to be included in an upstream change.