openai / openai/codex

Desktop: app-server death mid-turn leaves every thread permanently unsendable (latestTurnStatus=interrupted + markedStreaming=true, zero turn/start afterwards)

Open
#40,766 5 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

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

Description

Summary

When an OAuth-backed Streamable HTTP MCP server returns 401 with a WWW-Authenticate: Bearer resource_metadata=... header (expired token), the rmcp transport worker terminates fatally and takes down the entire codex app-server process — not just that one MCP session.

On Codex Desktop the blast radius is total: after the process dies mid-turn, the rollout is left without a turn-completion record, and on every subsequent launch the renderer resumes the thread with latestTurnStatus=interrupted and markedStreaming=true. The composer then believes a turn is permanently in flight and submits nothing. New conversations cannot be started either. Restarting the app does not help, because the state is re-derived from the truncated rollout on each resume. The only recovery is hand-editing ~/.codex/config.toml to disable the offending plugin.

This looks like the missing trigger requested in #36863, which observed the identical worker quit with fatal: Transport channel closed symptom but concluded that the 429/503 path in rmcp 3.0.0 was not sufficient to explain a fatal worker shutdown, and asked for the "concurrent or subsequent event". Here the trigger is AuthRequired, not a transient 5xx.

Worth stressing: the offending server is an optional third-party plugin. A single non-official MCP server with an expired token should not be able to brick the whole client.

Environment
  • Codex Desktop 26.820.7780.0 (MSIX), Windows 11 build 26100
  • bundled codex-cli 0.149.0, rmcp 3.0.0
  • MCP server: third-party agentkey plugin — {"type": "http", "url": "https://api.agentkey.app/v1/mcp"}, OAuth-backed, access token expired
Log evidence

Fatal worker exit escalating to full process death:

app_server_connection.closed code=4294967295 connectionId=1 transport=stdio
  reason="{\"level\":\"ERROR\",\"fields\":{\"message\":\"worker quit with fatal:
    Transport channel closed, when AuthRequired(AuthRequiredError {
      www_authenticate_header: \\\"Bearer resource_metadata=\\\\\\\"https://api.agentkey.app/.well-known/oauth-protected-resource\\\\\\\"\\\"
    })\"},\"target\":\"rmcp::transport::worker\"}"

error [AppServerConnection] Codex CLI process exited classifiedAsExpected=false code=4294967295
error [AppServerConnection] fatal_error_broadcasted initialized=false listenerCount=2 pendingRequestCount=0
info  [electron-message-handler] remote_connections.manager_state_set nextState=error

The same crash had already fired twice earlier the same morning, surfacing with a different exit code when the watchdog terminated the dying process:

error [AppServerConnection] Codex CLI process exited classifiedAsExpected=false code=1073807364

(0x40010004 = DBG_TERMINATE_PROCESS.)

Resulting stuck state (the user-visible bug)

The in-flight turn is never finalized. The rollout JSONL ends mid-turn on a custom_tool_call_output, with no turn-completion and no interrupt record. On the next launch:

maybe_resume_success conversationId=<redacted> latestTurnId=<redacted>
  latestTurnStatus=interrupted markedStreaming=true turnCount=1
  hasLatestTurnParams=false hasCurrentPermissions=false hasLatestThreadSettings=false

With markedStreaming=true the composer treats a turn as in flight. Across the whole following app session there are zero turn/start events despite repeated send attempts — in that thread and in unrelated threads. Esc / stop does not clear it. Restarting the app does not clear it.

Reproduction
  1. Configure an OAuth-backed Streamable HTTP MCP server and authenticate successfully.
  2. Let the access token expire, or revoke it server-side, so the endpoint answers 401 with WWW-Authenticate: Bearer resource_metadata=....
  3. Start a turn in Codex Desktop that reaches the MCP layer.
  4. Observe the app-server process exit, rather than an MCP-scoped tool error.
  5. Relaunch Codex. The thread resumes as interrupted / markedStreaming and the composer accepts no input, in any thread.
Expected behaviour
  1. AuthRequired should surface as a per-server re-auth prompt, or at worst as a failed tool call scoped to that server. It must never terminate the app-server process. One optional MCP server should be isolated from the rest of the client.
  2. When the app-server dies mid-turn, the orphaned turn should be finalized as failed on resume, and the composer must unlock. A thread whose last turn is interrupted should be immediately writable.
  3. Starting a new conversation must never be blocked by another thread's stale streaming state.
Workaround

Set enabled = false for the offending plugin in ~/.codex/config.toml and restart. There is no in-app recovery path, because the UI itself is unresponsive by the time the user notices.

Related

#36863 (same symptom, trigger unidentified), #27165, #33403, #35006

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 by reproducing the expired OAuth-backed MCP request and trace the rmcp transport worker through the app-server connection and renderer resume path. Inspect how AuthRequired becomes a fatal process exit, how interrupted rollouts set markedStreaming, and how new conversations are gated. Done means the server failure is isolated and resumed or new threads remain writable without manual config edits.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
authentication, backend, desktop
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.