Repository navigation
[macOS] Codex Desktop creates hidden threads and leaks one MCP process pool approximately every 5 minutes while idle #43971
Description
What version of the Codex App are you using (From “About Codex” dialog)?
Codex Desktop: 26.901.51231
What subscription do you have?
pro 20x
What platform is your computer?
MacOS
What issue are you seeing?
After a clean restart, Codex Desktop repeatedly creates a new hidden thread while the application is idle. Each hidden thread starts a complete MCP process pool, including XcodeBuildMCP, Context7, Computer Use, node_repl, and app-tool servers. The previous pool is not terminated.
The behavior is unbounded: process count and memory usage grow continuously until the application is fully quit.
What steps can reproduce the bug?
Steps to reproduce
- Start Codex Desktop after fully quitting it.
- Open or resume one local Codex task.
- Leave the application open and do not interact with it.
- Periodically inspect the app log and the descendants of the Codex
app-serverprocess. - Observe that a new hidden
thread/startoccurs approximately every 5 minutes 20 seconds and starts another complete MCP pool.
No scheduled automation or user action is required to trigger the growth.
What is the expected behavior?
- An idle application should not continuously create new threads.
- If an internal draft, prewarm, or code-mode thread is periodically replaced, its MCP child processes should be shut down.
- MCP process counts and memory usage should stabilize while the application is idle.
Additional information
[macOS] Codex Desktop creates hidden threads and leaks one MCP process pool approximately every 5 minutes while idle
Summary
After a clean restart, Codex Desktop repeatedly creates a new hidden thread while the application is idle. Each hidden thread starts a complete MCP process pool, including XcodeBuildMCP, Context7, Computer Use, node_repl, and app-tool servers. The previous pool is not terminated.
The behavior is unbounded: process count and memory usage grow continuously until the application is fully quit.
Environment
- macOS 26.6.2 (25G83), Apple Silicon (
arm64) - Physical memory: 36 GiB
- Codex Desktop:
26.901.51231 - Bundled Codex CLI / app-server:
0.153.4 - One visible Codex task was open
- No scheduled Codex automations were configured
- Global
openclawMCP: disabled - Global
XcodeBuildMCP: disabled context7: enabled- The build-ios-apps plugin supplied the runtime
xcodebuildmcpserver
Steps to reproduce
- Start Codex Desktop after fully quitting it.
- Open or resume one local Codex task.
- Leave the application open and do not interact with it.
- Periodically inspect the app log and the descendants of the Codex
app-serverprocess. - Observe that a new hidden
thread/startoccurs approximately every 5 minutes 20 seconds and starts another complete MCP pool.
No scheduled automation or user action is required to trigger the growth.
Expected behavior
- An idle application should not continuously create new threads.
- If an internal draft, prewarm, or code-mode thread is periodically replaced, its MCP child processes should be shut down.
- MCP process counts and memory usage should stabilize while the application is idle.
Actual behavior
- The renderer sends
thread/startwithconversationId=nullapproximately every 5 minutes 20 seconds. - Each new thread starts the same MCP servers.
- These thread IDs do not have corresponding session files and do not appear as user-visible tasks.
- No MCP stop, shutdown, reap, or thread-close events were found in the app log.
- Every recorded XcodeBuildMCP startup still had a corresponding live server process several hours later.
Observed timeline
All timestamps below are UTC:
21:51:20 hidden thread/start; MCP pool started
21:56:22 hidden thread/start; another MCP pool started
22:01:36 hidden thread/start; another MCP pool started
22:06:54 hidden thread/start; another MCP pool started
...
01:24:59 hidden thread/start; another MCP pool started
01:30:23 hidden thread/start; another MCP pool started
During the idle interval, the number of live XcodeBuildMCP server instances increased from 3 to approximately 43. No matching shutdown events occurred.
Sanitized log excerpt
2026-09-09T01:24:59.190Z ... conversationId=null ... method=thread/start originWebcontentsId=1
2026-09-09T01:24:59.192Z ... server=context7 status=starting threadId=<ephemeral-thread-A>
2026-09-09T01:24:59.192Z ... server=node_repl status=starting threadId=<ephemeral-thread-A>
2026-09-09T01:24:59.192Z ... server=cua_repl status=starting threadId=<ephemeral-thread-A>
2026-09-09T01:24:59.192Z ... server=xcodebuildmcp status=starting threadId=<ephemeral-thread-A>
2026-09-09T01:30:23.852Z ... conversationId=null ... method=thread/start originWebcontentsId=1
2026-09-09T01:30:23.852Z ... server=xcodebuildmcp status=starting threadId=<ephemeral-thread-B>
2026-09-09T01:30:23.854Z ... server=node_repl status=starting threadId=<ephemeral-thread-B>
2026-09-09T01:30:23.854Z ... server=context7 status=starting threadId=<ephemeral-thread-B>
Process snapshot after several hours
After returning to the app and collecting diagnostics, the process table contained:
| Process group | Live processes | Summed RSS |
|---|---|---|
| XcodeBuildMCP | 48 npm launchers + 48 Node servers | about 5.46 GiB |
| Context7 | 49 npm launchers + 49 Node servers | about 4.86 GiB |
node_repl |
96 | about 1.16 GiB |
| Computer Use launchers | 47 | about 1.76 GiB |
App-tool server.mjs processes |
47 | about 1.88 GiB |
| All app-server descendants | 538 | about 20.8 GiB |
The summed RSS values are not equivalent to unique physical memory because shared pages may be counted more than once. The process-count growth and lack of cleanup are independent of that limitation.
Configuration control results
- Disabling global OpenClaw worked: no
openclaw mcp serveprocesses were created after restart. - Disabling global
XcodeBuildMCPremoved the duplicate global definition. - The lower-case
xcodebuildmcpsupplied by the build-ios-apps plugin still started once for every hidden thread. - Therefore, individual MCP configuration changes reduce the memory cost per leaked thread but do not stop the hidden-thread creation loop.
Impact
- Continuously increasing process count and memory use while the app is idle
- Heavy swap or memory pressure on long-running sessions
- Hundreds of orphaned MCP-related descendants after a few hours
- Full application restart required to recover resources
Workaround
- Fully quit and reopen Codex Desktop to reclaim the leaked process pools.
- Disabling heavy MCP servers or plugins reduces the growth rate, but it does not fix the lifecycle issue.
Suspected area
The evidence points to a renderer-initiated draft/prewarm or code-mode-host lifecycle path: a new internal thread is periodically created, but the previous thread and its MCP process pool are not disposed. This is an inference from the log sequence, not a confirmed implementation-level root cause.
Additional evidence available
Raw app logs are available locally, but they are not attached because they may contain task names, local paths, prompts, or other sensitive information. A further sanitized excerpt can be provided if needed.
Activity
github-actions commentedon Sep 9, 2026
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
argszero commentedon Sep 9, 2026
This looks like the same underlying "idle app-server spawns/leaks MCP process pools" family as #43471 (ChatGPT 26.901 / codex-cli 0.153.4: idle app-server leaks SkyComputerUseClient mcp helpers — pair per 5 min → 6.4 GB RSS + kernel panics). Your macOS report adds a fresh angle I hadn't seen on the family — the hidden threads that each spawn their own full job pool (XcodeBuildMCP, Context7, Computer Use, node_repl, app-tool servers) with the prior pool never torn down.
#43471 tracks the reconnect/keepalive spawn path in
codex-rs(and links the turn-ended variant #26293/#29157). If the idle timer is creating a new hidden thread (rather than just reconnecting the same one), the unbounded growth you describe would be the pool-per-thread accumulation on top of the pair-per-5-min keepalive leak — worth confirming whether it's one new hidden thread each cycle (pool-per-thread) or the same thread reconnecting (pair-per-cycle). Either way this is the same class and I'd suggest coalescing the repro + memory/proc observations there so maintainers see the combined picture.chiuser commentedon Sep 9, 2026
Confirmed: this is one new hidden thread per cycle, not the same thread reconnecting.
Verification summary
2026-09-08T21:51:20Zthrough2026-09-09T01:30:23Z.method=thread/startevents withconversationId=null.thread/startwith the immediately followingserver=xcodebuildmcp status=startingevent produced 42 distinct hidden thread IDs.method=thread/resume, notthread/start.The word “inference” in my original report referred only to whether the responsible implementation path is draft/prewarm or code-mode-host. The creation of a distinct hidden thread on each cycle is directly confirmed by the logs.
Sanitized log excerpt
Real thread IDs and request IDs have been replaced with stable placeholders. Usernames, local paths, task names, project data, and prompt content are omitted.
The different placeholders represent different original thread IDs; they are not repeated or reconnected IDs.
Sanitized mapping of all 42 hidden-thread cycles
Each
Hnnnmaps to a different original thread ID observed on the corresponding XcodeBuildMCP startup event.This confirms the pool-per-new-thread accumulation variant. It is not merely a pair-per-reconnect leak on one stable thread.
jorgeschnura commentedon Sep 15, 2026
Windows reproduction, plus the prewarm mechanism in the desktop bundle and evidence that discarded prewarmed threads leak their MCP children
Same family as #44525 and #30408. Adding a Windows data point and, more usefully, where the periodic hidden
thread/startcomes from in the Electron app, plus what I can and cannot tell about why the app-server never reaps those pools.Environment
26.908.4834.0(app-server seesclient_version="26.908.40834"), bundled codex0.154.0node_repl,cua_repl,codex-security,codex-app-tools,artifact-template-picker) and one personal plugin providing a stdio server (a PowerShell launcher that spawns a node child; both stay alive). No automations were running (two exist, bothPAUSED,automation_runsempty), so they are not the source.Observation
The app-server started at 23:56. At 08:47 it had 156 direct children: 30 PowerShell launchers from my plugin, 34
node, 30node_repl.exe, 60cmd.exe(two per pool), onecodex-code-mode-host.exe, oneconhost: one full set per spawn. From 00:12 to 03:52, with the window visible, no user activity and zero turns (the account had no usage credits left), a complete stdio MCP pool (five process groups, six whenartifact-template-pickeris included) was spawned every 10 minutes atxx:x2:11, and none were ever reaped. Each pool correlates with onethread/startfromclient_name="Codex Desktop"carrying a new thread id. In~/.codex/logs_2.sqlitethe only row most of those threads ever get is the WARNcodex_core::shell_snapshotline emitted duringsession_init(thread_spawn ... session_init ... shell_snapshot{thread_id=...}); for those hours the log has no INFO/DEBUG/TRACE rows at all, so that WARN is the only trace of them. None of those thread ids ever get a rollout undersessions/; the ones I spot-checked also have nothread-writer-locks/<id>.lockand no row instate_5.sqlite(a real thread has all three).It is not idle-only: over the previous working day 61 of 68
thread/startrequests were hidden threads that never received a turn, so the pools also pile up while the app is in use. After ~9.5 hours: ~5.5 GB working set in my plugin's launcher+node pairs alone, ~11 GB counting the bundled servers, machine at 84 % RAM.The cadence stopped at 03:52 and did not simply resume in the morning: the app woke up at 07:35 (plugin bundle sync, an
mcpServerStatus/listfrom the Desktop connection, athread/resumefrom the Android client, which respawned MCP servers without anysession_init), the next hiddenthread/startcame at 08:00, and the child count then stayed flat until at least 09:30. The overnight stop is consistent with a renderer timer that pauses while the window is occluded, but I have not confirmed that, nor why the idle cadence is 10 minutes here versus the ~5 min 20 s reported above.Mechanism (from
resources/app.asar, Electron main bundle.vite/build/main-*.js)this.prewarmedThreadManager, class minified asYxe) keeps one prewarmed thread per cwd (prewarmedThreadByCwd).prewarmConversationsendsthread/startviaprewarmThreadStartahead of use so that starting a conversation is instant; the renderer asks for it through thethread-prewarm-startIPC message.Jxe = 4.75 * 60seconds;isFresh(createdAtSeconds)isnow/1e3 - createdAt < Jxe.hasPrewarmedThreaddiscards a stale entry (or one whoseworkspaceRoots,serviceNameorrequestedHistoryModeno longer match) withdiscardPrewarmedThread(e)→deletePrewarmedThread(threadId)→sendRequest('thread/delete', {threadId}), and a new prewarmthread/startfollows.setPrewarmedThreadPromisealso discards the previous entry when a new one is set.What I can and cannot tell about the leak
The app-server does know how to tear MCP children down: a real thread that was closed (
op: Shutdown→Shutting down Codex instance/clearing thread listener during thread-state teardown) left no child processes, and a session init that got cancelled during startup cleaned up too (rmcp::servicelogstask cancelled/serve finished quit_reason=Cancelled). A prewarmed thread that never had a turn and gets discarded never goes through that path: its MCP children stay alive indefinitely (about ten hours in my case, until I killed them). Whetherthread/deleteis not sent for them or is handled without shutting down the session-owned MCP runtime I cannot tell from my logs (only 10 of my 170thread/startrequests even have a TRACE row and nothread/deletedoes); if there is a log filter that captures every app-server request I will rerun and confirm.Killing the orphaned plugin pairs externally (26 of them, at 09:51) was handled cleanly: the app-server logged
input stream terminated/serve finished quit_reason=Closedat INFO for each one, no WARN/ERROR, no respawn, and the live threads kept their own MCP processes.Suggested fix
thread/deleteof a thread that never had a turn, run the same shutdown a closed thread gets (op: Shutdown→Shutting down Codex instance, which does take the MCP children down), orWorkaround on Windows
A scheduled task that kills MCP children whose
session_initthread never got a rollout or lock, is older than an hour and is not the newest prewarmed one keeps this in check (children of one init are born in the same second or the next, so they can be attributed by creation time); the app-server has been unaffected so far. Happy to share it, or targeted log excerpts, if useful.