[macOS Desktop] Recurrent main-process write EPIPE modal during IPC client churn

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

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

  • Codex Desktop: 26.814.41407 (build 6720)
  • Bundled CLI: codex-cli 0.148.0-alpha.15

Observed with an isolated macOS Codex profile.

What subscription do you have?

Not relevant to the failure: it occurs in the local Electron IPC path before a model request.

What platform is your computer?

Darwin 25.5.0 arm64 arm

What issue are you seeing?

Codex Desktop repeatedly shows a native Electron modal that blocks active tasks until the user clicks OK:

A JavaScript error occurred in the main process

Uncaught Exception:
Error: write EPIPE
    at WriteWrap.onWriteComplete [as oncomplete]
    (node:internal/stream_base_commons:87:19)

After one occurrence, the main-process log recorded a burst of ten IpcRouter EPIPE failures in approximately 57 ms. The captured stack identifies failed writes during IPC client registration:

[IpcRouter] Socket error errorCode=EPIPE errorMessage="write EPIPE"
    at Socket._write (...)
    at Writable.write (...)
    at <initial IPC write> (...)
    at IpcRouter.registerClient (...)
    at IpcRouter.handleRequest (...)

Those log lines are emitted by the router's attached socket-error listener. They prove that the router is writing to sockets while those connections are closing, but they do not prove that the handled error in the log is the same error object that reached Electron's process-level uncaught-exception dialog. The final unhandled writer may be a separate socket path.

The modal occurred during ordinary task/tool activity. Active tasks produced additional events, but the Desktop UI did not progress normally until the dialog was dismissed.

The stack is inside the Node/Electron runtime bundled with Codex Desktop rather than a system Node.js process.

What steps can reproduce the bug?

The modal is recurrent, but a deterministic minimal reproducer has not yet been isolated.

  1. Run Codex Desktop on macOS with several long-lived tasks.
  2. Keep the app running while IPC-backed clients disconnect or reconnect during ordinary use.
  3. Continue task/tool activity and switch focus to or from the Desktop window.
  4. Observe the native write EPIPE main-process modal.
  5. Inspect the main-process log for a burst of failed IPC writes during client registration or disconnection.

Remote-connection activity was present in the captured runs, but the evidence does not establish it as the sole trigger.

What is the expected behavior?

  • Closing an IPC socket during client registration should be handled as a client lifecycle event.
  • A socket write failure must not escape to Electron's main-process uncaught-exception handler.
  • Failure of one IPC client must not block unrelated tasks behind a native modal.
  • Remaining clients should continue operating.

Additional information

Packaged-build implementation observations

Inspection of the packaged main-process implementation found:

  • Router server sockets install a persistent error listener immediately when accepted, before registration writes occur. Client sockets also install an error listener when connected.
  • The shared write helper calls socket.write(frame) without a completion callback.
  • registerClient places a client in the active maps and broadcasts connected before the initialization response write has completed.
  • broadcastClientStatus writes to every mapped client without a socket-state filter or per-write completion handling. Cleanup currently waits for close or end; the error listener logs the failure but does not immediately remove the failed socket.
  • Some other broadcast paths precheck socket.writable, but that check cannot close the race between the check and an asynchronous peer disconnect.

This ordering can create bursts of writes to a failed connection and can publish a transient connected state for a client whose initialization response did not complete.

Exact-runtime validation

A focused harness was run with the packaged Node runtime (v24.19.0). It induced a peer close during a pending socket write and tested the presence or absence of both a persistent socket error listener and a write callback.

The result was important: a persistent socket error listener prevented uncaughtException; a write callback by itself did not. Adding a callback without retaining the socket listener still produced an uncaught socket error. Therefore, “add a write callback and swallow EPIPE” is not a sufficient or correct standalone fix.

Because the router sockets already have persistent error listeners, the logged router EPIPE events are handled events. Identifying the exact source of the modal requires instrumenting the process-level uncaught handler with the writer/socket identity, or auditing every main-process socket writer for a missing or prematurely removed error listener.

Minimum requirements for a robust repair
  1. Keep a persistent socket error listener for the entire lifetime of every socket. A per-write callback may record completion, but it cannot replace the socket listener.
  2. Give each client an explicit, idempotent lifecycle (registeringactivefailed/closed). On error, transition immediately, remove the client from routing maps, cancel its pending requests, and destroy/close the socket; do not merely log and wait for a later close event.
  3. Do not publish connected while the client is only registering. At minimum, commit the client to active routing after the initialization write callback succeeds and the socket is still active. If connected means the peer has actually received and accepted initialization, add a client acknowledgement; a write callback only proves local write completion.
  4. Route every status write through one state-aware writer. Snapshot active recipients before broadcasting, collect asynchronous failures, and perform idempotent cleanup after the iteration so failure handling cannot recursively mutate the broadcast being traversed.
  5. Add a regression test on the packaged Node major/minor that closes peers (a) before registration, (b) after write dispatch but before completion, and (c) during simultaneous registration/disconnection. Assert: no process-level uncaughtException, no phantom connected, exactly one terminal lifecycle transition per client, no recursive status storm, and continued service for unrelated clients.
  6. Separately instrument or test the process-level modal path. A router regression can explain the logged churn without proving that it is the socket writer responsible for the uncaught dialog.

Related reports:

  • #35985 captures the same class of uncaught stream error through a different router lifecycle stack.
  • #37690 covers remote SSH connection instability with a different primary failure mode.

Raw logs are not attached because they contain local paths, connection labels, task IDs, and request IDs. Sanitized excerpts can be provided if maintainers specify what they need.

View original on GitHub ↗

2 Comments

github-actions[bot] contributor · 9 days ago

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

  • #38360
  • #38202

Powered by Codex Action

tsuvic · 9 days ago

Independent evidence for this error class from macOS 26.5.2 arm64 — two occurrences with concrete churn triggers, on a sibling code path to the one in the report.

Both events carry the identical [IpcRouter] Socket error errorCode=EPIPE signature, but in my logs the failing write is inside unregisterClient → broadcastClientStatus (the report's stack shows registerClient):

  1. 2026-08-07T07:11:26.853Z — 8 ms after a browser-session window removal, immediately before the app-server transport stop; two ipc-connection-reset broadcasts were delivered to clients with no handler registered. Writes during the teardown window.
  2. 2026-08-11T23:55:48.846Z0.19 s after [sparkle] install_started: the auto-update install severs IPC clients while the router is still broadcasting.

So the class is "router writes to a socket whose peer is mid-disconnect", hit on both the register and unregister paths. Both of my events were absorbed by the socket-error listener (warning only, no modal), consistent with the report's observation that the logged handled EPIPE and the uncaught one reaching the dialog may be different writers.

A direction that would cover all of these: route every router socket write through one error-aware wrapper (check destroyed/writable before writing and catch EPIPE in the write callback itself), so no write path can escalate to the process-level uncaught-exception handler; including the client id in the EPIPE log line would identify which client churned.

Versions: both events on Desktop 26.803.41515 (build 6321); the second occurred during the Sparkle install that upgraded to build 6396.