openai / openai/codex

[Desktop] Unobserved MCP extension-host query is evicted after 5 minutes, retaining old sessions and spawning new MCP processes

Open
#43,122 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

app app-server bug mcp windows-os
Dominant language
Rust
Stars
125k
Forks
19.4k
PR merge metrics
PR metrics pending

Description

What version of the Codex App are you using (From “About Codex” dialog)?

Desktop Windows package 26.901.5280.0; desktop app-server client version 26.901.41600; local app-server codex-cli 0.153.4 (package/runtime versions verified locally).

What subscription do you have?

ChatGPT Pro (20x)

What platform is your computer?

Windows 11 Home 25H2, build 26200.9168, x64; native Windows backend. Requested PowerShell output: Microsoft Windows NT 10.0.26200.0 x64.

What issue are you seeing?

Summary

The Desktop renderer caches its internal mcp_extension_host thread ID in a query with staleTime: Infinity but no gcTime override. On the imperative tool/resource path without a persistent query observer, the entry is garbage-collected after 300000 ms. Cache eviction does not send thread/unsubscribe. The next lookup starts another ephemeral thread and MCP process set while the previous session remains loaded and subscribed.

This report isolates the five-minute query-cache eviction trigger. It is related to #39783, #38925, #38754 and #38825, and identifies a specific cache-lifetime trigger alongside those broader lifecycle reports. The downstream Windows blocking-pool failure is already tracked separately in #42740.

The reproduction package described below tests the actual extracted renderer Scope/query path against the installed app-server with real timers. After five minutes, the original case has two simultaneously loaded sessions and two healthy diagnostic MCP processes; both controls (gcTime: Infinity and an actual mounted Scope observer) retain one session/process each. This finding is conditional on the unobserved path; observer state in the original live UI was not directly captured.

User-visible impact

During ordinary local Desktop use, duplicate MCP process groups accumulated under one live app-server. Message submission then failed with:

App server request queue is full
App server request expired while queued

The first rejection recorded 6 in-flight RPCs and 16 queued critical requests; a later rejection recorded about 30005 ms in the queue. These are local RPC scheduling failures. The isolated reproduction below requires no model turn or user prompt.

The original logs contain 37 non-persisted IDs used by renderer-originated MCP calls, with 992 routed responses and no recorded thread/unsubscribe for those IDs. For 36 IDs, 239 unique direct child processes started 43–127 ms after their ID creation timestamps. 21 of 36 consecutive ID intervals were 300–340 seconds (median 328.511 seconds). No local app-server restart/reconnect recovery was recorded during this sequence.

Historical logs do not preserve each thread/start payload or cache-eviction event. Attribution of all historical IDs to this helper is therefore strong correlation, not a direct capture of every eviction. The precise causal chain from accumulation to the original queue failure was not reproduced synchronously.

What steps can reproduce the bug?

Runnable reproducer

The mcp-cache-repro.zip package contains the extractor, harness, tiny healthy stdio MCP probe, package-lock.json, English README, validation report, and selected evidence from the final run.

  1. Extract the ZIP and open a terminal in its mcp-cache-repro directory. Install dependencies with the command below.
  2. Supply absolute paths to the affected Desktop package's app.asar and its installed app-server executable. The adapter verifies the renderer/runtime SHA-256 and supports the examined Desktop package 26.901.5280.0; other hashes fail before test servers start.
  3. Run the full comparison and allow approximately 5–6 minutes plus extraction/startup.
npm ci --ignore-scripts --no-audit --no-fund
node reproduce.mjs --asar "C:\path\to\app.asar" --codex "C:\path\to\codex.exe" --out "./results"

Use the actual installed paths in place of the placeholders. The README includes Windows package-location instructions. Node.js 22+ is required (tested with 24.14.0). The output directory must not already exist; omit --out for a unique temporary directory.

The harness starts three isolated app-servers with separate temporary CODEX_HOME directories, each configured with one healthy local MCP probe. It does not copy user authentication/configuration, start model turns, or modify the running Desktop. Each test connection stays alive across the five-minute boundary.

What the test does

  1. Extract the installed query descriptor, Scope, querySignal, Scope.query.getOrFetch, QueryClient and module helpers; route RPC to the isolated test backend. Browser-mode cache defaults are selected without launching a DOM/React UI.
  2. In the original case, call scope.query.getOrFetch(extensionHostQuery, "local") without a retained observer. Immediate reuse and lookups at minutes 1–4 return T1.
  3. Observe cache removal after 300000 ms, without sending thread/unsubscribe. Before the next lookup, assert that T1 is still loaded and its MCP probe is alive.
  4. Look up the ID again. Assert that T2 differs from T1, both IDs are loaded, and both probes have initialized and are alive.
  5. Compare two separate real-time controls: add only gcTime: Infinity, or retain an actual observer through scope.watch. Each must retain one ID and one probe.
  6. Explicitly unsubscribe the test IDs (asserting status: unsubscribed), close the isolated servers, and assert that no test probes remain.

This is an extracted-code integration reproduction. The adaptations and limits are documented in the README; it does not automate the complete Desktop UI.

The installed descriptor is equivalent to this readable fragment (identifiers normalized):

const extensionHostQuery = querySignal(scopeToken, (hostId, { scope }) => ({
  queryKey: ["mcp-extension-host-thread", hostId],
  staleTime: Infinity,
  // No gcTime override.
  queryFn: async () => {
    const { thread } = await connection(scope, hostId).sendRequest(
      "thread/start",
      { ephemeral: true, permissions: ":read-only",
        threadSource: "mcp_extension_host" },
      { timeoutMs: requestTimeout }
    );
    return thread.id;
  }
}));
const id = await scope.query.getOrFetch(extensionHostQuery, hostId);

Final real-time result, 2026-09-05 UTC

Configuration Cache eviction before next lookup Loaded threads Live MCP probes
Original descriptor, no observer Yes, after 300.007 seconds 2 2
Add only gcTime: Infinity No 1 1
Retain an actual Scope observer No 1 1

In the original case, T1 was returned at 22:40:39.362 UTC, its cache entry was removed at 22:45:39.369 UTC, and both T1/T2 and both initialized probes were present at 22:45:44.469 UTC. The final harness exited naturally with code 0. All three servers exited with zero surviving probes and no fallback termination by the harness. A separate Windows process check confirmed that the test servers, probes and harness were gone.

PASS means that the reported defect was reproduced and both controls behaved as expected. Failed assertions, backend errors or failed cleanup produce a nonzero exit status.

Additional validation

  • Clean dependency installation and final full execution from a separate directory containing spaces; distributed source files match the tested copy.
  • Fast deterministic-clock check using the actual Scope path: 12 minute-spaced reads created 3 / 1 / 1 IDs in the original / GC-control / observer-control variants. Fresh reads did not postpone GC. This optional --quick mode uses a stub transport and does not test backend retention on its own.
  • Unsupported bundle hash and deliberately wrong backend executable were rejected; no false PASS.
  • Cancellation through the final harness's SIGINT handler cleaned up its test servers and exited with code 1.

No model turn was started, so model token-limit/context-window usage is not applicable. Selected evidence uses synthetic aliases T1/T2; private transcripts, raw run directories and extracted application bundles are excluded.

mcp-cache-repro.zip

What is the expected behavior?

Reuse the internal MCP extension-host session for its intended host/connection lifetime, or explicitly release its subscription when replacing or disposing of it. Query-cache eviction must not silently permit a new session to be created while leaving the old server resource owned and loaded.

Handle connection restart, host changes and scope teardown explicitly. gcTime: Infinity prevents the demonstrated periodic recreation, but should not be treated as a complete fix for all lifecycle transitions.

A regression test should cover the imperative path without an observer, repeated fresh lookups beyond five minutes, the mounted-observer path, and connection/scope teardown.

Additional information

Source provenance and scope

Examined installed renderer webview/assets/app-initial-ffce11d82782.js: descriptor Xzi, accessor Yzi, imperative query controller IVt, and tool/resource callers Jzi/Kzi. The global renderer QueryClient has no overriding gcTime. The sidebar path iBi can mount an observer, which prevents eviction in the observer control.

The examined renderer, main/shared code and rolldown helper copies were compared byte-for-byte against the installed app.asar. Renderer SHA-256:

30bfcbdbc47b90bc72fa8853e949dce2eee6b519d3876113260c939801f17203

Other thread-cleanup paths exist in the application; this report concerns the missing release at this query's eviction/replacement boundary. The main process retains ephemeral IDs for notification filtering, so the claim is loss of the cache entry used for reuse, not loss of every copy of the ID.

The server's documented 30-minute unload grace after the last unsubscribe is not itself reported as a bug. No unsubscribe occurs at query eviction here. The test did not measure the full 30-minute post-unsubscribe interval.

A later snapshot of the affected server found 510 OS threads waiting on pipe reads in a nearly exhausted shared pool. This is consistent with #42740, but it was not captured at the exact original queue rejection. Windows is the tested platform; this report does not establish that the renderer cache defect is Windows-only.

Related issues / duplicate search

  • #42740: Windows stdio reads exhaust the shared blocking pool; already tracks that lower-level failure.
  • #39783: summary/title/description ephemeral sessions retain MCP stacks after unsubscribe; a different owner/trigger from this unobserved extension-host query.
  • #38925: MCP generations accumulate during a live app-server connection.
  • #38754 and #38825: repeated MCP process groups during one Windows task.

Searched open and closed issues for the exact queue errors, MCP process accumulation, mcp_extension_host, mcp-extension-host-thread, and MCP gcTime; reviewed the closest reports and discussions. At the time of the search (2026-09-05 UTC), no exact report of this five-minute query-eviction mechanism was found. Filing separately to identify the concrete renderer trigger while cross-linking the broader lifecycle reports.

This diagnosis was developed with AI-assisted code/log analysis and checked by two independent agent reviews. The conclusions above rely on source inspection and recorded reproduction results; the historical attribution and downstream causality limitations are stated explicitly. Raw user transcripts, credentials, private paths and full application bundles are omitted.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the extracted renderer asset webview/assets/app-initial-ffce11d82782.js, focusing on descriptors Xzi/Yzi, controller IVt, and callers Jzi/Kzi. Run reproduce.mjs from mcp-cache-repro.zip against the specified app.asar and codex executable to confirm the five-minute unobserved-query behavior. Done means repeated lookups do not leave duplicate sessions or MCP probes, while observer, teardown, and regression-test cases pass.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, node.js
Domain
desktop, frontend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.