Windows Desktop: bundled node_repl MCP processes leak one-per-thread, never reaped until app-server exit

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

What version of Codex is running?

  • Codex Desktop 26.721.4979.0 (native Windows, no WSL)
  • app-server: codex-cli 0.146.0-alpha.3.1
  • Bundled runtime: cua_node/f8d2abcb7481383b

What operating system are you using?

Windows 11 x64, 24 GB RAM.

What is the issue?

node_repl.exe MCP server child processes accumulate over the life of the codex.exe app-server process and are never reaped. One new node_repl.exe appears roughly per opened conversation/thread, stays alive after the thread is idle or closed, and only goes away when the app-server itself exits.

Snapshot after ~8 hours of normal Desktop usage — 14 live node_repl.exe, all children of the single app-server PID, all idle (0 CPU since spawn):

ProcessId CreationDate            Parent
17248     12:41  (app start)      codex.exe (1772)
16888     13:12                   codex.exe (1772)
19040     13:43                   codex.exe (1772)
 5508     15:18                   codex.exe (1772)
21312     18:40                   codex.exe (1772)
 1668     19:02                   codex.exe (1772)
21144     19:47                   codex.exe (1772)
 9004     19:58                   codex.exe (1772)
20704     19:58                   codex.exe (1772)
21984     20:04                   codex.exe (1772)
20664     20:12                   codex.exe (1772)
14232     20:18                   codex.exe (1772)
20508     20:18                   codex.exe (1772)
20588     20:19                   codex.exe (1772)

Each holds ~7–9 MB working set and ~115–125 handles. Individually small, but they accumulate monotonically (verified creation timestamps map to thread-open events) and add up alongside the other per-thread state. Killing the app-server reaps all of them (verified: after terminating the app-server PID, node_repl.exe count went from 14 to 0).

Relevant config: [mcp_servers.node_repl] pointing at the bundled node_repl.exe, features.computer_use = true, features.multi_agent = true.

What is the expected behavior?

node_repl MCP processes for a thread should be terminated when the thread is closed/idle-expired, or at minimum pooled/reused, instead of leaking one process per thread until app exit.

Additional context

Possibly related to #30408 (per-thread MCP server processes never cleaned up), but this one is specifically the bundled node_repl on native Windows, where each thread spawns a distinct child that outlives the thread.

View original on GitHub ↗

7 Comments

github-actions[bot] contributor · 1 month ago

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

  • #34251
  • #34410
  • #34704
  • #34614

Powered by Codex Action

misosiruda · 23 days ago

Current Windows reproduction and lifecycle analysis

Update: I initially scoped the likely cause too narrowly to subscribed-thread retention. After reviewing prior unmerged work, current main still has at least one additional plausible lifecycle-retention path around detached MCP status/resource/tool RPC work. The analysis and proposed order below now include that prior art.

I reproduced the same process-retention pattern on a newer Windows Desktop build:

  • Codex Desktop: 26.730.7989.0
  • bundled app-server: codex-cli 0.147.0-alpha.1.2
  • Windows 11 x64
  • root Desktop app-server uptime at the latest capture: about 3h48m

At an earlier point in the same app-server lifetime I observed 7 direct node_repl.exe children using about 143.5 MiB combined. A fresh read-only capture at 2026-08-05T13:56+09:00 showed 4 direct children using 81.3 MiB and 532 handles. Three of the four used at most 0.02 CPU seconds during a three-second sample; one was active and owned a nested stdio app-server.

This is an important distinction: these are not Windows PPID-orphans. They remain direct children of the live Desktop app-server (and were inside its Job in the earlier snapshot). Also, my count did decrease from 7 to 4 during the investigation, so my reproduction supports long-lived/logically retained runtimes, but does not support claiming that every instance is literally never reaped under every lifecycle path. Process inspection alone also cannot attribute every retained node_repl.exe to a particular thread, status snapshot, hidden task, or other runtime owner.

Relevant current-main behavior

I traced current main at 5d89ab65dc9d4d0c55796c11df112b54157922b4.

The per-thread path behaves as follows:

  • The app-server unload delay is fixed at 30 minutes, and an unload target exists only when the thread is both inactive and has no subscribers:

https://github.com/openai/codex/blob/5d89ab65dc9d4d0c55796c11df112b54157922b4/codex-rs/app-server/src/request_processors/thread_lifecycle.rs#L5-L62

  • thread/unsubscribe removes the connection subscription but does not immediately shut down the thread:

https://github.com/openai/codex/blob/5d89ab65dc9d4d0c55796c11df112b54157922b4/codex-rs/app-server/src/request_processors/thread_processor.rs#L887-L912

  • Once thread shutdown is actually entered, it calls mcp_runtime.shutdown():

https://github.com/openai/codex/blob/5d89ab65dc9d4d0c55796c11df112b54157922b4/codex-rs/core/src/session/handlers.rs#L587-L609

There is also a separate RPC/temporary-manager path that should not be conflated with thread subscription retention:

  • Current app-server code still detaches mcpServerStatus/list, resource reads, and tool calls with tokio::spawn:

https://github.com/openai/codex/blob/5d89ab65dc9d4d0c55796c11df112b54157922b4/codex-rs/app-server/src/request_processors/mcp_processor.rs#L234-L305
https://github.com/openai/codex/blob/5d89ab65dc9d4d0c55796c11df112b54157922b4/codex-rs/app-server/src/request_processors/mcp_processor.rs#L391-L480

  • The threadless status snapshot creates a fresh McpConnectionSet, then cancels its startup token and returns without explicitly awaiting shutdown():

https://github.com/openai/codex/blob/5d89ab65dc9d4d0c55796c11df112b54157922b4/codex-rs/codex-mcp/src/mcp/mod.rs#L393-L451

  • The Windows local-stdio terminator uses taskkill /PID ... /T /F, but current code discards the command status:

https://github.com/openai/codex/blob/5d89ab65dc9d4d0c55796c11df112b54157922b4/codex-rs/rmcp-client/src/stdio_server_launcher.rs#L373-L386

The evidence therefore points to at least two plausible retention mechanisms:

  1. loaded per-thread MCP runtimes that never become eligible for the no-subscriber idle-unload path because Desktop keeps background/sidebar threads subscribed;
  2. temporary or thread-associated MCP work whose detached RPC task outlives the serialized request lifecycle or is not explicitly drained.

The first mechanism is consistent with the controlled subscribed-thread analysis reported in #20883:
https://github.com/openai/codex/issues/20883#issuecomment-4976823228

Relevant prior work that was not merged

Three earlier PRs overlap materially with this issue and should be evaluated before designing a new fix:

  • #20447, “Fix MCP status/list lifecycle leak”, awaited status/resource/tool work inside the request future, explicitly shut down temporary managers, surfaced Windows taskkill failures, and added serialization coverage. It was closed as stale without being merged:

https://github.com/openai/codex/pull/20447

  • #14997, “Reap orphaned idle threads on websocket disconnect”, immediately unloaded an inactive persisted thread after its final websocket disconnected and included a real stdio-helper before/after regression. It was also closed as stale without being merged:

https://github.com/openai/codex/pull/14997

  • #15258, “pool MCP backends in thread manager”, explored sharing local stdio MCP backends across loaded threads and added unsubscribe/archive/resume lifecycle coverage. It was closed without being merged:

https://github.com/openai/codex/pull/15258

These PRs target different layers, so none should be assumed to be a complete standalone fix for the current architecture. In particular, #20447 addresses detached temporary-manager/RPC lifetime, while #14997 and #15258 address per-thread retention and duplication.

Proposed fix direction

My preferred order would now be:

  1. Re-evaluate and port the narrow intent of #20447 onto current mcp_processor.rs:
  • keep serialized MCP RPC work inside the tracked request future where possible;
  • explicitly drain temporary MCP connection sets;
  • surface failed Windows process-tree termination;
  • add a regression that distinguishes thread-owned runtimes from temporary status/resource runtimes.
  1. Fix the loaded-thread ownership path:
  • have Desktop unsubscribe from background threads when it no longer needs live turn/item events;
  • consider an explicitly negotiated or initially opt-in app-server subscribed-idle safety valve;
  • never evict an active turn/agent or a thread with pending server requests;
  • reset the timer on real thread activity;
  • use the existing shutdown path, emit thread/closed, and preserve resume from the persisted rollout.
  1. Treat project/app-server-scoped MCP pooling as a separate, larger design decision after the two cleanup paths above are bounded.

Regression coverage should include:

  • serialized status/resource/tool requests do not leave temporary MCP processes behind;
  • subscribed idle eviction;
  • active-turn protection;
  • activity resetting the deadline;
  • resume after eviction;
  • actual stdio MCP child-tree exit on Windows.

The last item is currently a coverage gap: the existing real-process cleanup integration test is Unix-only:
https://github.com/openai/codex/blob/5d89ab65dc9d4d0c55796c11df112b54157922b4/codex-rs/rmcp-client/tests/process_group_cleanup.rs#L1-L119

Would maintainers prefer reviving the narrow #20447 lifecycle fix first, a Desktop subscription fix, or a combined staged approach? If one of those directions aligns with the intended lifecycle, I would be happy to prepare a small, test-first PR if invited.

vbzgn2rc98-ctrl · 10 days ago

Additional Windows reproduction on current Desktop 26.810.7004.0, with read-only lifecycle/performance measurements.

This matches the lifecycle nuance already described above: the helpers are not literally never reaped. On this machine, a true idle period of roughly 30 minutes reclaimed most of a retained batch (28 -> 7; 22/28 baseline helper PIDs exited). However, under sustained multi-thread Desktop use, helper creation outpaced that delayed cleanup and the process tree accumulated rapidly.

Environment / current build

  • Windows 11 x64
  • Codex Desktop AppX: 26.810.7004.0
  • bundled Chrome target observed separately: 26.810.52044
  • the previous Chrome native-host/junction reconciliation bug did not reproduce on this build (junction identity/target/creation time remained stable)

Helper lifecycle observations

Across normal thread/task activity:

  • node_repl.exe: 4 -> 6 -> 10 -> 12 -> 22 -> 23 -> 26 -> 25
  • after a later true-idle ~30 minute window: baseline 28 -> 7
  • 22/28 baseline helper PIDs exited during that idle window
  • before that reclamation window, some helpers had remained alive for >1 hour and two for >24 hours
  • retained helpers were almost completely idle (~3.4 MiB private memory each, negligible CPU/I/O)
  • all observed helpers shared the same sanitized command signature and were direct children of the root codex.exe
  • several helper groups also owned nested codex.exe children
  • existing logs contained rmcp::service / rmcp::transport::child_process plus occurrences involving failed to terminate, failed to kill, orphan, and cleanup

A particularly narrow trigger was observed: resuming another thread and asking only a design/conversation question (no requested local file, shell, browser, or computer action) increased node_repl.exe from 10 -> 12. Previously observed helpers were not reclaimed at that point. This suggests helper/runtime creation can occur during thread/work-context initialization, not only after an explicit tool call.

Performance degradation while active workload was running

During a later sustained-workload bad state, the root codex.exe showed a persistent cache-backed logical-read storm across three consecutive 5-second windows:

  • 265.752 MiB/s
  • 288.004 MiB/s
  • 286.842 MiB/s
  • average: ~280.2 MiB/s
  • peak: ~288.0 MiB/s
  • read operations: ~34k-37k/s

At the same time:

  • physical disk read: 0-0.079 MiB/s
  • disk busy: max 0.24%
  • disk queue: 0
  • total CPU average: 11.04%, max 19.36%
  • available RAM: ~41.7 GiB
  • committed memory: 33.5%
  • DPC time: 0.13%
  • context switches: ~181,208/s average, ~200,951/s peak
  • node_repl.exe: idle baseline 7, then 22 -> 23 after work resumed
  • root codex.exe private memory grew from ~992 MiB -> 2263 MiB in ~10 minutes, with large short-window allocation/free churn

User-visible impact was system-wide mouse/input/window-animation stutter despite low total CPU, essentially idle physical disk, ample RAM, and low DPC activity. Fully exiting/restarting Codex cleared the bad state temporarily.

File-level trace follow-up

An 8.139-second PID-filtered kernel File I/O trace was later captured after the storm had subsided:

  • ETW events lost: 0
  • ordinary file reads: 799 / 3.297 MiB total (0.405 MiB/s)
  • top files were %USERPROFILE%\.codex\logs_2.sqlite and logs_2.sqlite-wal, read in short ~2-second bursts

That calm-state trace did not reproduce the prior ~280 MiB/s process-level logical-read storm, so it does not attribute the storm to those SQLite files. The high-read state appears state-dependent/intermittent and would need another trace captured while the stutter is actively present.

Current interpretation

This machine supports:

  • delayed (~30 min) helper reclamation exists;
  • under sustained multi-thread use, helper creation can outpace cleanup and produce large net accumulation;
  • the accumulated/active runtime state correlates with severe cache-backed codex.exe logical-read activity and a ~180k-200k/s context-switch storm;
  • the previous Chrome junction reconciliation loop is not the explanation on 26.810.7004.0 here.

I am intentionally not uploading the raw ETL because it contains local workstation metadata/paths. Sanitized counters or another trace captured during the active bad state can be provided if maintainers need a specific additional measurement.

wakasellahi · 10 days ago

Additional Windows Desktop sample, non-urgent:

  • App package: OpenAI.Codex 26.810.7004.0
  • Same long-lived codex.exe parent PID 19152, started 07:49:07 local
  • node_repl direct children, all parented by that process:
  • 09:07: 25
  • 09:10:58: 23
  • 09:15:10: 22
  • 09:18:38: 20, combined working set 277 MB

Counts declined throughout the sample, so this is not urgent or evidence of current growth. It still supports the lifecycle issue: child instances persist across task or tool activity rather than being retired per thread. No local process termination was used for this sample; the user will close and relaunch the app normally.

uto96322 · 10 days ago

Reproduced on a newer Windows Desktop build; this issue is still present.

Directly measured environment

  • Windows 10 Pro 22H2, version 10.0.19045 (build 19045), x64
  • ChatGPT/Codex Desktop AppX: OpenAI.Codex_26.814.5167.0_x64__2p2nqsd0c76g0
  • App server PID: 10976
  • Bundled runtime: cua_node/2fb562745e6d66f0/bin/node_repl.exe

Observed behavior

All observed node_repl.exe instances are direct children of the same long-lived codex.exe app-server.

  • Initial measurement: 15 live node_repl.exe processes
  • Immediate recheck: still 15
  • After the next chat turn / tool activity: 17
  • After several additional normal assistant tool calls: 25
  • Oldest process creation time: 15:52:17 local
  • Newest process creation time: 16:21:18 local
  • None of the earlier instances were reaped during the app-server lifetime

This confirms the leak/retention also reproduces on Windows 10 and on Desktop build 26.814.5167.0, not only the older Windows 11 build reported in the original issue.

The same user separately observes a more severe reproduction on a Windows 11 home PC: the count reportedly increases on every chat turn. Disabling node_repl.exe prevents the accumulation, but Browser/Web work and Computer Use then stop functioning, so disabling the runtime is not a viable workaround.

Expected

The process count should remain bounded through reuse/pooling, or thread-scoped runtimes should be reaped after the owning turn/thread becomes idle. Long-lived Desktop sessions should not monotonically accumulate node_repl.exe children.

A full app-server restart remains the only reliable cleanup observed.

grtninja · 3 days ago

Additional sanitized Windows observation from newer builds:

  • VS Code OpenAI extension build 26.818.41705 was the measured surface. Codex Desktop AppX 26.818.8289.0 was also installed, but I am not attributing the extension process to the AppX package.
  • One extension codex.exe process had 11 direct bundled node_repl.exe children.
  • The children matched the same bundled runtime, had no children of their own, and had no observed listening sockets.
  • Each child held approximately 130–320 handles and 20–22 threads; the parent held approximately 1,904 handles and 276 threads.
  • A later passive whole-machine snapshot had lower totals than the first, so this is not a controlled monotonic-growth measurement and I am not claiming that all 11 children were leaked.
  • I could not safely run a create/close/reap correlation test: subsequent attempts to start bounded one-line PowerShell and cmd probes from the task failed before launch with Win32 error 1816 (ERROR_NOT_ENOUGH_QUOTA).
  • No process was terminated because per-task ownership could not be established.

The direct codex.exe -> node_repl.exe shape appears related to this report, but this snapshot does not map a child to a specific open or closed thread and therefore does not prove missed cleanup. Is there a supported diagnostic field or procedure for correlating each bundled node_repl child with its owning thread/session and terminal cleanup event?

twk0672005 · 2 days ago

Additional native Windows Desktop reproduction on OpenAI.Codex 26.818.8289.0. This measurement is from the Desktop app-server, not the VS Code extension.

Read-only capture at 2026-08-26T00:21:49Z:

  • one codex.exe app-server, started at 2026-08-25T15:50:19Z;
  • 12 bundled node_repl.exe processes, all direct children of that app-server;
  • combined node_repl.exe working set: approximately 110 MiB;
  • the oldest child started about 41 seconds after the app-server;
  • the newest child appeared roughly 8.4 hours later.

No processes were terminated.

This is a point-in-time persistence/accumulation snapshot. It confirms the direct parent-child shape on the current native Desktop build, but it does not map each child to a specific thread or prove that every individual child missed its cleanup deadline.

For a narrow first fix, I would favor re-evaluating the lifecycle intent of #20447 on current main before attempting MCP pooling:

  1. keep temporary status/resource/tool RPC work inside the tracked request future rather than detached tasks;
  2. explicitly cancel, drain, and shutdown().await temporary McpConnectionSet owners on success, error, and cancellation;
  3. wait for Windows child-tree exit and surface termination failures instead of discarding them;
  4. add a real Windows stdio regression that distinguishes temporary/request-owned runtimes from thread-owned runtimes.

The subscribed-thread retention path can then be handled separately: Desktop should unsubscribe background threads it no longer needs, and idle unload must protect active turns and pending requests while preserving resume from the rollout. Project/app-server MCP pooling seems better treated as a later architecture change, not a substitute for deterministic teardown.

Is there a supported diagnostic field or procedure for mapping each bundled node_repl child to its owning thread/request and terminal cleanup event? I can provide the sanitized health JSON or run a controlled create/close/reap correlation test if maintainers specify the intended procedure.