Windows Codex Desktop: opening/resuming historical subagent threads spawns duplicate MCP and node_repl process stacks
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
- Fully quit Codex Desktop.
- Launch Codex Desktop and do not open historical chats initially.
- Baseline is one process stack:
- 1
node_repl.exe - 1 MarkItDown MCP wrapper
- 1 brain-memory MCP wrapper
- 1 brain-knowledge MCP wrapper
- Open one historical parent chat with many old subagents. Do not send a message.
- The Subagents panel initially shows many old children as
Working. - Open one or more old child threads. Their displayed state immediately changes from
WorkingtoDone. - 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.exedirect 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
5 Comments
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
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/resumeand creates additional full MCP stacks.Opening old child threads changes their UI state from Working to Done and triggers further
thread/resumecalls.Observed after one parent-chat load and inspection of a few children:
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.
The strongest invariant here seems to be that opening historical state is not an execution transition.
thread/resumecurrently appears to conflate two operations:Those should be separate lifecycle intents. A historical child that is terminal should be loadable with something like:
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:
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:
The invariant is: history hydration may reconstruct evidence and UI state; only explicit execution intent may allocate live execution resources.
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
Workingagain 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:
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:
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_replprocesses, 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:
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.
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
uv+ Python process pairsObserved behavior
Before the mitigation, one capture contained 96
uvand 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:
codex ... app-serverby PPID and creation time;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:
Environmentobject identity prevents otherwise identical host-local catalogs from sharing across sessions;Prototype and validation
I built a version-matched prototype with the following behavior:
ephemeral=true,threadSource=Feature("system"), and plugins disabled; explicit subagents, HTTP MCPs, Codex Apps, and remote stdio are preserved;The prototype maps these behaviors to existing ownership points rather than to Code Mode cell teardown:
core::session::mcp_runtimeselects the startup policy from thread restore/ephemeral context and owns turn begin/finish transitions;codex_mcp::runtime::McpRuntimepublishes connection generations and performs the dormant cold swap;McpToolCatalogCachecoordinates an optional-startup owner per complete local identity;McpConnectionManager::shutdown_local_stdioretires 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:
codex-mcpunit tests;-D warningsfor the affected crates and an arm64 release build.In the post-change restart capture, the new app-server had zero local MCP/
uvprocesses 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/uvprocesses 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
mainand split it into focused changes: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.