chat open in new window shows something went wrong

Open 💬 6 comments Opened Aug 8, 2026 by lytruby

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

Version 26.721.81911

What subscription do you have?

chatgpt pro

What platform is your computer?

_No response_

What issue are you seeing?

<img width="900" height="270" alt="Image" src="https://github.com/user-attachments/assets/8b627add-697f-4dbb-be1c-79c2fcca7465" />
click on 'open in new window' will show this error: something went wrong

What steps can reproduce the bug?

click on 'open in new window' will show this error: something went wrong

What is the expected behavior?

_No response_

Additional information

_No response_

View original on GitHub ↗

6 Comments

0xdevalias · 15 days ago

While I don't currently know the root cause for this; I notice that it tends to show up when I have had Codex desktop open for a while (multiple days), with multiple windows open, etc.

My (currently unsubstantiated) theory is that maybe there is some correlation to this and the other issues where Codex doesn't clean up it's helper processes / MCP servers / etc; and that maybe maybe it has hit a soft limit of PIDs or FDs it can launch or similar.

---

Edit: From some further local codex CLI debugging of this, here are some relevant snippets from it's investigation:

The packaged implementation gives us a major narrowing: that exact page is not a generic renderer error boundary. It is hard-coded by the Electron main process and loaded only when the newly created BrowserWindow fails to load the packaged index.html/route. So the failing step is: BrowserWindow created → load local app bundle for the new window fails → catch logs “Failed to load bundle at” → load the “Something went wrong…” fallback HTML It happens before thread resume or backend activity. This substantially lowers stale-thread/auth hypotheses and makes local renderer/resource exhaustion the leading class. I’m now looking for the swallowed loadURL error and Electron renderer launch diagnostics.
• We now have the exact swallowed error, twice in today’s live session: - 08:30:48 and 08:50:06 local-action timestamps in the app log; - ERR_FAILED (-2) loading 'app://-/index.html'; - each failure immediately loads the fallback as a normal primary window (webContentsId 85/86, window IDs 51/52). This confirms the thread path itself was never reached. The remaining question is why Chromium’s custom app://protocol request failed. I’m correlating those two timestamps with unified logs and the protocol handler implementation.
• The FD evidence now separates the two processes: - MCP leak: app-server PID 73826 has 165 pipe FDs / 221 total. - Failing app:// file load: Electron main PID 4612 owns the protocol handler, with only 14 pipes / 375 total. - Chromium raises macOS’s default 256 FD soft limit (historically to at least 1024), which explains descriptors above 256. - Kernel file table is 12,894 / 491,520, so there is no system-wide exhaustion. That means the MCP leak is definitely present, but it is not a direct FD-exhaustion explanation for this exact failure snapshot. The app-server’s descriptors cannot make the Electron main process’s createReadStream(index.html) fail with EMFILE; only broader indirect pressure could connect them. I’m doing one last targeted filesystem trace attempt and then I’ll synthesize the best-supported root-cause boundary and next probe.

And here is a more substantial note from it's conclusion:

The investigation narrows this substantially: the visible “Something went wrong…” page is caused by Codex Desktop failing to load its own packaged UI, before it attempts to open or resume the selected thread. ## Confirmed failure path Codex Desktop 26.730.61639, build 6234, logged this repeatedly: ``text Failed to load bundle at errorCode=ERR_FAILED errorMessage="ERR_FAILED (-2) loading 'app://-/index.html'" ` For “Open in New Window”, the packaged implementation does roughly this: 1. Create a new Electron BrowserWindow. 2. Load app://-/index.html?initialRoute=/local/<thread-id>. 3. If that fails, catch the error. 4. Load a hard-coded HTML page containing only “Something went wrong…”. We captured failures on August 12 and 13. One included the requested thread route, but the failure was still loading index.html; the thread was never resumed: `text ERR_FAILED (-2) loading 'app://-/index.html?initialRoute=%2Flocal%2F019fa172-...' ` The current evidence is in the [August 13 Codex log](/Users/REDACTED/Library/Logs/com.openai.codex/2026/08/13/codex-desktop-6ef37613-40a4-47e7-8f21-3223b73d4efe-4612-t0-i1-001147-0.log:3373) and [August 12 Codex log](/Users/REDACTED/Library/Logs/com.openai.codex/2026/08/12/codex-desktop-6ef37613-40a4-47e7-8f21-3223b73d4efe-4612-t0-i1-000040-0.log:3542). This rules against authentication, backend availability, corrupt thread metadata, or thread/resume as the immediate cause. ## What the live resource evidence says Your MCP-leak theory is partly right, but the current snapshot does not show it directly causing this error. | Resource | Live result | Interpretation | |---|---:|---| | User processes | 633 / 10,666 | PID exhaustion ruled out | | Zombies | 0 | Ruled out | | Kernel file table | 12,894 / 491,520 | System-wide FD exhaustion ruled out | | app-server age | 5d 7h | Long-lived | | app-server children | 55 | Clearly accumulated | | app-server pipe FDs | 165 | Strong MCP/helper leak evidence | | app-server total FDs | 221 | Not near Chromium’s effective raised limit | | Electron main pipe FDs | 14 | MCP pipes are not held here | | Electron main total FDs | 375 | Above launchd’s nominal 256 because Chromium raises the limit | | Codex windows | 17 | High | | Renderer processes | 31 | High | | Renderer RSS total | 9.34 GiB | Significant accumulation | | System memory free | 44% | No immediate system memory exhaustion | The 55 app-server children consisted of: `text 25 node ./mcp/server.mjs --stdio 25 node_repl 4 npm exec xcodebuildmcp@latest mcp 1 codex-code-mode-host ` That aligns strongly with the lifecycle leak tracked in https://github.com/openai/codex/issues/26984. However, the app:// protocol handler runs in the Electron main process, while those leaked pipes belong to app-server. One process cannot consume the other process’s FD allowance. Chromium also deliberately raises macOS’s low default FD limit, explaining why both processes already have descriptors numbered above 256. [Chromium’s source documents that behavior directly.](https://chromium.googlesource.com/chromium/src/+/2025d00c3fa5270a0ea10c9ec7a18cfa69007beb/chrome/browser/chrome_browser_main_posix.cc) ## Best current root-cause assessment The immediate failure is: > A long-lived Electron main process can no longer successfully serve/read the packaged webview/index.html through Codex’s custom app:// protocol handler. Most likely classes, in order: 1. Accumulated Electron window/renderer/protocol state after extensive window churn. 2. A resource leak in the Electron main process other than ordinary file descriptors, such as Unix sockets, Mach ports, stream state, or renderer contexts. 3. A bug in the custom protocol.handle("app", ...) streaming implementation that appears after long uptime or repeated loads. 4. Indirect pressure from overall Codex helper/window accumulation. Straight MCP-pipe-driven EMFILE is presently unsupported for this occurrence. It remains a plausible sibling failure mode and possibly part of the broader longevity problem. There is also an instrumentation blind spot: the protocol handler catches fs.stat failures and turns them into a generic 404, while errors from createReadStream() emerge only as ERR_FAILED. It never logs the underlying filesystem exception or errno. That is precisely the missing evidence needed to distinguish an open/read failure from protocol cancellation or internal Chromium state failure. ## Most useful next A/B test Before restarting: 1. Close only the “Something went wrong…” fallback windows. 2. Retry opening a thread. 3. Close two or three ordinary Codex windows, retrying after each. 4. Record whether successful loading returns and how the renderer count changes. 5. Then restart Codex and immediately retry. If reducing window/renderer count restores loading while app-server and its leaked MCP children remain alive, that would strongly implicate Electron window/renderer state rather than app-server FDs. For a syscall-level check during one reproduction: `zsh sudo fs_usage -w -f filesys 4612 \ | rg --line-buffered 'app\.asar|index\.html|EMFILE|Too many open files' ` The most valuable product-side change would be to log errors separately around stat, createReadStream, and response streaming in the app:// handler. Right now Codex records only Chromium’s lossy ERR_FAILED (-2) result. So the revised theory I would put on https://github.com/openai/codex/issues/37551#issuecomment-5277969947 is: > Long-lived Codex sessions do accumulate MCP processes and pipes, but this UI symptom is specifically a failure by Electron main to load app://-/index.html for a newly created window. Current live evidence rules out user-wide PID exhaustion and does not show app-server EMFILE`. Window/renderer or custom-protocol resource degradation is currently the stronger immediate explanation.

---

Edit 2: Investigating deeper, with the context that it often happens after there is a new Desktop app update available, and that maybe the client is caching a reference to the app.asar and it somehow being changed/replaced/similar related to the update.

From the general investigation process:

We found the specific root cause, and it is remarkably close to your cached-link theory. The protocol handler is reached. Its stat() succeeds. But fs.createReadStream() for the ASAR entry fails with: ENOENT: no such file or directory, open /private/var/folders/.../T/.com.openai.codex.Ie5yNy Electron’s ASAR layer has resolved webview/index.html to a temporary extracted file and retained that temporary pathname. The temp file no longer exists. readFileSync() still succeeds because it reads through a different ASAR path, but streaming uses the missing cached extraction, producing net::ERR_FAILED, which Codex replaces with “Something went wrong…”.
The direct inspector test has isolated the failure: Codex can synchronously read app.asar/webview/index.html, but Electron’s streaming path repeatedly redirects that same file to a now-missing temp file. I’m checking Electron’s ASAR implementation and the updater timeline now, to separate the proven mechanism from the likely trigger.
The update correlation is real but narrower than “the installed app.asar was replaced”: Sparkle downloaded an update roughly 28 hours before the first recorded failure, while the running process still has the same physical archive inode it launched with. I’m checking whether Sparkle’s cleanup could plausibly remove Electron’s similarly named temp extraction file.
The implementation matches the runtime evidence exactly: Electron caches each copyFileOut() result and, on later calls, returns the cached pathname without checking whether the temp file still exists. Codex’s custom app:// handler uses createReadStream(), which reaches that cache; its direct readFileSync() path does not.

With it's final conclusion / summary being:

I found the immediate root cause. It is very close to your “cached link” theory, but the stale object is not app.asar itself. ## What is failing Inside the live Codex Electron main process: - app.asar/webview/index.html exists. - stat() succeeds. - readFileSync() successfully reads all 14,208 bytes. - But createReadStream() fails with: ``text ENOENT: no such file or directory, open '/var/folders/mj/lr13rswx62b6jfms8bz66d8w0000gn/T/.com.openai.codex.Ie5yNy' ` Codex’s custom app:// protocol handler serves app://-/index.html using fs.createReadStream(indexPath). Consequently: `text missing extracted temp file → createReadStream() fails → app:// request becomes net::ERR_FAILED → BrowserWindow.loadURL() rejects → Codex displays “Something went wrong…” ` This reproduces the exact errors recorded in the [first failure on August 12](/Users/REDACTED/Library/Logs/com.openai.codex/2026/08/12/codex-desktop-6ef37613-40a4-47e7-8f21-3223b73d4efe-4612-t0-i1-000040-0.log:3261) and the [August 13 failure](/Users/REDACTED/Library/Logs/com.openai.codex/2026/08/13/codex-desktop-6ef37613-40a4-47e7-8f21-3223b73d4efe-4612-t0-i1-001147-0.log:3373). ## Why Electron gets stuck Electron’s fs.open() ASAR wrapper calls archive.copyFileOut() for packed files. That creates a real temporary file so ordinary Node filesystem APIs can open it. The important part of Electron’s implementation is: 1. Cache the extracted path by ASAR-relative filename. 2. On subsequent requests, return the cached path immediately. 3. Do not check whether the temporary file still exists. You can see that behavior directly in Electron’s [Archive::CopyFileOut() implementation](https://github.com/electron/electron/blob/main/shell/common/asar/archive.cc#L354-L390), while the Node wrapper routes fs.open() through copyFileOut() in [asar-fs-wrapper.ts](https://github.com/electron/electron/blob/main/lib/node/asar-fs-wrapper.ts#L1226-L1229). The live process still returns the same missing .com.openai.codex.Ie5yNy pathname on repeated attempts. Therefore the archive cache believes the extracted file is alive, but something has deleted it from the macOS temporary directory. Restarting Codex should fix it because it discards the in-memory ASAR archive/cache and creates a fresh extraction. I deliberately did not restart it, so the broken state remains available. ## Was app.asar replaced? Not in this occurrence. The physical archive currently on disk is: `text inode: 62307328 size: 219945391 sha256: 3fea92820c0fb7a69473e7a8308a8e5b8e91524289a84181a33533ec6cb51d45 mtime: 2026-08-05 13:45:46 +1000 ` The long-running Electron main process holds FD 17 to that same physical device, inode and size. It launched on August 6, after that archive was installed. So there is no evidence that this process is holding an obsolete/deleted app.asar, nor that the archive was atomically replaced underneath it. The archive remains readable through Electron’s retained handle. There is nevertheless an update correlation: - Sparkle downloaded an available update at 2026-08-10T18:24:20Z. - It downloaded/checkpointed it again at 2026-08-11T00:29:43Z. - The first recorded window failure was 2026-08-12T04:12:47Z, about 27 hours 43 minutes later. That makes the updater a plausible trigger, but not a proven one. I did not find evidence showing Sparkle deleted this particular file. macOS temp cleanup, maintenance/security software, or another Codex/Electron cleanup path could produce the same result. So the evidence boundary is: - **Proven proximate cause:** stale Electron ASAR copyFileOut() cache pointing at a deleted temp extraction. - **Proven visible consequence:** createReadStream()ERR_FAILED → fallback error page. - **Not yet identified:** which component deleted .com.openai.codex.Ie5yNy. - **Disfavoured for this occurrence:** replacement of the running process’s physical app.asar. Interestingly, Electron recently landed a related fix for ASAR reads during live app replacement: [electron/electron@8215b5a](https://github.com/electron/electron/commit/8215b5aa536ec6ae121003bc618bdd825bdbbb0f). That fix makes its native ASAR URL loader use the retained archive handle, but it does not address Codex’s custom JavaScript protocol handler going through createReadStream() and the stale extracted-file cache. ## Likely fixes The strongest Codex-side fix would be to stop serving packed ASAR files through createReadStream(): - Use fs.readFile() and return a Response backed by the resulting buffer. - Package the webview assets unpacked under app.asar.unpacked. - Or use a loader that streams directly from the retained ASAR file descriptor. An Electron-side defensive fix would make CopyFileOut() validate cached paths and re-extract entries when the cached temp file has disappeared. ## Reproducing through Remote DevTools I used your existing electron-toolkit and connected directly to the Electron main process. For a future occurrence: `zsh pid="$(pgrep -f '/Applications/ChatGPT.app/Contents/MacOS/ChatGPT' | head -n 1)" ~/.dotfiles/bin/electron-toolkit inspect-enable "$pid" ~/.dotfiles/bin/electron-toolkit inspect-targets 9229 ` Then open chrome://inspect, configure localhost:9229, and inspect the target named electron/js2c/browser_init. In its console: `js const fs = process.mainModule.require('node:fs'); const indexPath = process.resourcesPath + '/app.asar/webview/index.html'; fs.readFileSync(indexPath).length; ` That should return 14208 in this build. Then: `js await new Promise((resolve, reject) => { let bytes = 0; const stream = fs.createReadStream(indexPath); stream.on('data', chunk => bytes += chunk.length); stream.on('end', () => resolve(bytes)); stream.on('error', reject); }); ` In the current broken state, the second expression exposes the underlying missing temp pathname rather than just ERR_FAILED. You can also reproduce Codex’s surfaced error directly: `js await process.mainModule.require('electron').net.fetch('app://-/index.html'); `` I closed the inspector afterward and verified that port 9229 is no longer listening. The temporary diagnostic scripts and runtime instrumentation were also removed/restored; Codex itself remains running in its original broken state.
0xdevalias · 15 days ago

Follow-up with a confirmed root cause and a validated process-local workaround.

Confirmed root cause

This was not PID/FD exhaustion, a corrupt thread, or the running process losing its app.asar file. The immediate cause was Electron retaining stale paths for ASAR entries that it had previously extracted into the macOS temporary directory.

Inside the live Electron main process:

  • stat() and readFileSync() succeeded for app.asar/webview/index.html.
  • fs.createReadStream() for the same ASAR entry failed with ENOENT while opening a generated path like:

``text
/var/folders/.../T/.com.openai.codex.Ie5yNy
``

  • Codex's custom app:// protocol handler uses createReadStream(), so that ENOENT became net::ERR_FAILED, causing BrowserWindow.loadURL("app://-/index.html...") to reject and display the hard-coded “Something went wrong…” page.

Electron's ASAR Archive::CopyFileOut() caches each extracted pathname. On a cache hit it returns that pathname without checking whether the temporary file still exists: https://github.com/electron/electron/blob/main/shell/common/asar/archive.cc#L354-L390. Electron's Node fs.open() wrapper reaches that path through copyFileOut(): https://github.com/electron/electron/blob/main/lib/node/asar-fs-wrapper.ts#L1226-L1229.

The cached archive object remained healthy and the original packed bytes remained readable. The stale object was the extracted temporary pathname.

Evidence that this affected the whole extracted-file cache

I initially recreated only the missing index.html temp file from the live archive. This changed the symptom from the immediate “Something went wrong…” fallback to a blank/loading renderer, proving that the initial document could now load.

Tracing createReadStream() while reloading then exposed missing temp paths for the main JS bundle, runtime, preload polyfill, CSS, fonts, and progressively loaded route chunks. Recreating each file advanced the renderer to the next import. Later missing dynamic imports caused the React “Oops, an error has occurred” boundary and a secondary exception:

TypeError: Failed to fetch dynamically imported module: app://-/assets/app-prefetch-impl-04p--jDk.js
Error: Missing AppServer request message handler

In total, 89 distinct missing extracted files were reconstructed manually/through bounded reload passes before installing the general workaround below. This strongly suggests that most or all .com.openai.codex.* ASAR extraction files had been purged while Electron's in-memory CopyFileOut() mappings survived.

The physical app.asar had not been replaced during this process. The running main process still held the same physical device/inode/size as the archive on disk, and the process started after that archive's modification time. A Sparkle update had been downloaded about 28 hours before the first recorded failure, so there is an update-time correlation, but I still do not have evidence identifying Sparkle, macOS cleanup, or another component as the process that deleted the temp files.

Validated process-local workaround

Using the Electron main-process Node inspector, I installed a narrowly scoped fs.createReadStream wrapper for app.asar/webview/*. If the underlying ASAR stream fails before yielding data with ENOENT for a .com.openai.codex.* temp path, it:

  1. Reads the original entry through fs.readFileSync() from the retained archive.
  2. Recreates the expected temp file with mode 0600.
  3. Retries the same stream into a PassThrough, allowing the original app:// request to complete without requiring another reload.

A controlled test moved one known extracted module aside and fetched its app:// URL. The same request recreated it and returned HTTP 200 with the expected size and identical SHA-256.

Afterward, normal interaction caused the patch to repair 28 additional real lazy assets. Current counters are:

intercepted ASAR webview streams: 120
repairs:                         29  (1 controlled test + 28 real)
successful same-request retries: 29
repair failures:                  0

With the patch active I successfully tested:

  • returning to the previously failing window;
  • opening an old thread;
  • creating a new window;
  • opening additional old threads in that new window.

All remained operational. The patch is in-memory only and disappears when Codex restarts.

For local reuse I added idempotent install/status/remove commands to my Electron debugging helper:

electron-toolkit codex-asar-self-heal ChatGPT
electron-toolkit codex-asar-self-heal-status
electron-toolkit codex-asar-self-heal-remove

The install command enables the loopback Node inspector with SIGUSR1, injects the wrapper, and leaves the inspector available for continued diagnosis.

Standalone installation (no helper required)

This is an unsupported, process-local diagnostic workaround for macOS. The Node inspector grants code execution in Codex's Electron main process, so keep it bound to loopback, do not expose port 9229 to a network, and close it when finished.

The Codex bundle is currently named ChatGPT.app. Find the Electron main process, enable its built-in Node inspector, and confirm the target is available:

pgrep -fl '^/Applications/ChatGPT\.app/Contents/MacOS/ChatGPT$'
pid="$(pgrep -f '^/Applications/ChatGPT\.app/Contents/MacOS/ChatGPT$' | head -n 1)"
kill -USR1 "$pid"
curl -fsS http://127.0.0.1:9229/json/list

If Codex is installed somewhere else, adjust the executable path. There should normally be one exact main-process match; do not signal a renderer or helper process.

Connect using either of these methods:

  • In Chrome, open chrome://inspect/#devices, choose Configure, add localhost:9229, then click inspect for the target usually titled electron/js2c/browser_init.
  • Or run npx --yes chrome-remote-interface --port 9229 inspect and use its JavaScript prompt.

Paste this complete expression into the main-process console/REPL:

(() => {
  const fs = process.mainModule.require('node:fs');
  const path = process.mainModule.require('node:path');
  const os = process.mainModule.require('node:os');
  const { PassThrough } = process.mainModule.require('node:stream');
  const key = Symbol.for('codex.asarStreamSelfHeal');
  const existing = globalThis[key];
  if (existing?.installed) return existing.status();

  const original = fs.createReadStream;
  const asarPrefix = path.join(process.resourcesPath, 'app.asar', 'webview') + path.sep;
  const tempPrefix = path.join(os.tmpdir(), '.com.openai.codex.');
  const stats = {
    installed: true,
    installedAt: new Date().toISOString(),
    intercepted: 0,
    repairs: 0,
    repairFailures: 0,
    retriesSucceeded: 0,
    lastRepair: null,
    recentRepairs: [],
  };

  function patchedCreateReadStream(...args) {
    const source = String(args[0]);
    if (!source.startsWith(asarPrefix)) return original.apply(this, args);

    stats.intercepted += 1;
    const output = new PassThrough();
    let retried = false;

    const attempt = () => {
      let bytesSeen = 0;
      let input;
      try {
        input = original.apply(this, args);
      } catch (error) {
        output.destroy(error);
        return;
      }
      input.on('data', chunk => { bytesSeen += chunk.length; });
      input.once('error', error => {
        const target = String(error.path || '');
        const repairable =
          !retried &&
          bytesSeen === 0 &&
          error.code === 'ENOENT' &&
          target.startsWith(tempPrefix);

        if (!repairable) {
          output.destroy(error);
          return;
        }

        retried = true;
        try {
          const bytes = fs.readFileSync(source);
          fs.writeFileSync(target, bytes, { mode: 0o600 });
          stats.repairs += 1;
          stats.lastRepair = {
            at: new Date().toISOString(),
            source,
            target,
            size: bytes.length,
          };
          stats.recentRepairs.push(stats.lastRepair);
          if (stats.recentRepairs.length > 50) stats.recentRepairs.shift();
          attempt();
        } catch (repairError) {
          stats.repairFailures += 1;
          output.destroy(repairError);
        }
      });
      input.once('end', () => {
        if (retried) stats.retriesSucceeded += 1;
      });
      input.pipe(output);
    };

    attempt();
    return output;
  }

  fs.createReadStream = patchedCreateReadStream;
  globalThis[key] = {
    installed: true,
    original,
    patchedCreateReadStream,
    stats,
    status: () => ({ ...stats, recentRepairs: [...stats.recentRepairs] }),
    uninstall: () => {
      if (fs.createReadStream === patchedCreateReadStream) {
        fs.createReadStream = original;
      }
      stats.installed = false;
      globalThis[key].installed = false;
      return globalThis[key].status();
    },
  };

  return globalThis[key].status();
})()

Retry the failed window/thread. If it is already sitting in the React error boundary from an earlier rejected import, use Try Again or reload that window once. The returned object should show repairs and retriesSucceeded increasing as missing assets are encountered.

Check status at any time:

globalThis[Symbol.for('codex.asarStreamSelfHeal')]?.status()

Remove the patch if needed:

globalThis[Symbol.for('codex.asarStreamSelfHeal')]?.uninstall()

When diagnosis is complete, close the inspector (the installed patch will continue running until removed or Codex exits):

process.mainModule.require('node:inspector').close()

Restarting Codex removes the patch, closes the inspector, and rebuilds Electron's in-memory ASAR extraction cache. In this failure mode, a restart is also the simpler immediate recovery if preserving the broken process for diagnosis is not important.

Suggested fixes

Possible Codex-side fixes:

  • Avoid serving packed ASAR webview resources through Node createReadStream(); read them through the retained archive and return a buffer-backed Response.
  • Package these resources under app.asar.unpacked.
  • At minimum, log the underlying stream error and pathname rather than only Chromium's lossy ERR_FAILED (-2).

Possible Electron-side defensive fix:

  • On a CopyFileOut() cache hit, verify that the cached temporary file still exists and re-extract it if missing.

So the remaining unknown is no longer the load failure itself; it is what deletes Electron's extracted .com.openai.codex.* files during a long-lived Codex session while leaving the process and ASAR cache alive.

0xdevalias · 15 days ago

Additional upstream Electron context after tracing this further:

This appears to be an Electron ASAR cache invariant bug which Codex's custom app:// resource loader happens to expose broadly.

The closest prior report is:

  • electron/electron#30911

It describes an ASAR-backed resource being extracted through Archive::CopyFileOut(), then becoming unavailable after the OS temp directory is cleared while Electron remains running. That issue concerned a Windows tray icon, but the underlying lifecycle is the same as the .com.openai.codex.* failures observed here. It was closed for inactivity, not by a generic CopyFileOut() fix.

Electron's current Archive::CopyFileOut() implementation stores each extracted file in external_files_. On a cache hit it returns the cached pathname immediately, without checking whether the temporary file still exists:

This is especially significant in light of this earlier issue:

  • electron/electron#22500

It was closed with the explicit observation that files under the OS temp directory can be cleaned at any time. In other words, Electron currently combines two incompatible assumptions:

  1. The OS may remove an extracted temp file while the app is alive.
  2. CopyFileOut() may trust the extracted pathname for the lifetime of the cached archive.

The relevant archive-cache bug and its fix are:

  • electron/electron#29292
  • electron/electron#29293

The fix changed Electron's ASAR archive cache from thread-local to process-wide. That correctly fixed archive/file-handle leaks, but it also means the Archive and its external_files_ pathname cache can now survive for the entire long-running process.

A recent related fix is:

  • electron/electron#52424

It explicitly describes CopyFileOut(), external_files_, and the process-wide archive cache. It avoided CopyFileOut() for one specific packed-ICO call site, but did not change the generic cache-hit behavior or add missing-file recovery.

Why Codex encounters this for its whole UI: Electron's Node filesystem wrapper routes fs.open() through archive.copyFileOut(), and fs.createReadStream() ultimately uses that wrapped open path. Codex's custom app:// handler serves packed webview resources with fs.createReadStream(), so HTML, JS, CSS, fonts, and lazy chunks all become dependent on these extracted temp paths.

Electron's own ASAR URL loader does not do that for packed resources; it streams them directly from a duplicate of the archive's retained file handle. That makes the responsibility split fairly clear:

  • Electron owns the stale cached-path behavior in CopyFileOut().
  • Codex's custom protocol implementation makes ordinary packed UI resources pass through that vulnerable extraction path.

The update/replacement theory also has a relevant upstream fix:

  • electron/electron#52283

It changed packed ASAR reads to use the archive's retained file handle when app.asar is atomically replaced by an updater or MDM tool. The Codex build examined here contains that retained-handle design, and the live process still held the original archive inode successfully. An update may correlate with removal of temporary extraction files, but replacement of app.asar itself was not the direct failure observed here.

I could not find a pre-existing open Electron issue or PR covering the exact generic failure: CopyFileOut() returning a cached extracted pathname after that temporary file has been deleted. The new narrowly scoped upstream report and deterministic reproduction are now available here:

  • electron/electron#52804
0xdevalias · 15 days ago

The upstream Electron bug is now filed as:

It includes a standalone two-file reproduction independent of Codex:

I verified the reproduction against stock Electron 43.4.0 on macOS 26.6 arm64. The first fs.createReadStream() extracts and reads the packed ASAR entry; after deleting only that extracted temp copy while Electron remains running, fs.readFileSync() still reads the packed entry successfully, but a second fs.createReadStream() fails with ENOENT for Electron's stale cached temp pathname.

That upstream report cross-links the directly relevant older Electron issues/PRs and this Codex investigation.

0xdevalias · 10 days ago

Updates from upstream in the electron repo, RE: https://github.com/electron/electron/issues/52804

It looks like @codebytere opened a PR with a bugfix for this, but later closed it: - https://github.com/electron/electron/pull/52807 Currently not sure of the context around that, but asked for clarity in this comment: - https://github.com/electron/electron/pull/52807#issuecomment-5323449643 --- Edit: Some further context: > @codebytere I see that #52811 looks like it was a testing bug you ran into after creating this PR, and that it landed; but from what I saw there, it didn't look like it contained this PR or a related fix; so I am curious why you closed this PR, if there is another newer/rebased/etc version of this fix I should be watching for; and/or whether you just found that this wasn't actually a good solution to #52804, etc > > --- > > Edit: After exploring with GitHub Copilot (GPT-5.6 Terra), I believe this may have been closed because there is a more broader PR related to ASAR file improvements/etc: > > > Best evidence: PR #52807 appears to have been closed in favor of the broader, still-open PR https://github.com/electron/electron/pull/52833, but there is no explicit closure explanation or cross-reference confirming that. The original issue, https://github.com/electron/electron/issues/52804, is still open, so the fix has not landed yet. > > > #52833 changes the relevant architecture instead of repairing the stale cache. It removes Archive::CopyFileOut() from fs.open, fs.createReadStream, fs.promises.open, descriptor reads, streams, and copy APIs. Packed ASAR entries are read directly from a duplicated retained archive FD, so there is no extracted temp file for macOS to sweep away. > > > > That directly solves the reported createReadStream / fs.open stale-temp-file failure in #52804, more comprehensively than #52807’s “check whether the cached extracted path still exists; re-extract if not” approach. > > > > However, #52833 is not a perfect one-for-one replacement. It explicitly leaves child_process.execFile*, process.dlopen, and native-module loading on the temp-copy path because they require a real OS path. So #52833 addresses the issue’s createReadStream case, but apparently does not address #52807’s broader claim that execFile should recover after its cached temp copy disappears. > > - https://github.com/electron/electron/pull/52833 > > _Originally posted by @0xdevalias in https://github.com/electron/electron/issues/52807#issuecomment-5323449643_ _Originally posted by @0xdevalias in https://github.com/electron/electron/issues/52804#issuecomment-5323469183_
0xdevalias · 4 days ago

Context from upstream, the root cause of this issue is fixed, and that will land in electron v45; as is too big to backport to v43/v44:

https://github.com/electron/electron/pull/52807 was closed because https://github.com/electron/electron/pull/52833 removes the temp-copy path entirely (fds, streams and copies are served from the archive now), which fixes this at the source. that's landed for 45; it's too large to backport, so 43/44 keep the current behaviour. _Originally posted by @codebytere in https://github.com/electron/electron/issues/52804#issuecomment-5385880422_

Looking at the electron release schedule:

That should hit stable around 2026-10-20:

Release | Alpha | Beta | Stable | End of Life | Chromium | Node.js
-- | -- | -- | -- | -- | -- | --
45.0.0 | Aug 27, 2026 | Sep 29, 2026 | ✨Oct 20, 2026✨ | Apr 27, 2027 | M156 | v24.18.1
44.0.0 | Jul 2, 2026 | Jul 28, 2026 | Aug 25, 2026 | Mar 2, 2027 | M152 | v24.18.1
43.0.0 | May 7, 2026 | Jun 2, 2026 | Jun 30, 2026 | Jan 5, 2027 | M150 | v24.17.0