Codex Desktop composer can double-dispatch one submit, creating two threads or duplicate steered messages

Open
#33,558 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

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

Research direction

Start with the packaged Desktop submit handler in app.asar and the referenced Rust files: app-server-protocol/src/protocol/v2/thread.rs, app-server/src/request_processors/thread_processor.rs, and core/src/session/mod.rs. Review concurrent-submit behavior and add or run Promise.all-style tests for duplicate submissions. Done means one gesture produces one thread or turn, while a later intentional submission remains distinct.

Written by the indexing model from the issue text.

Description

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

26.707.91948 (build 5440)

What subscription do you have?

ChatGPT Enterprise / managed workspace (exact plan label is not shown in the Codex App)

What platform is your computer?

Darwin 25.5.0 arm64 arm

What issue are you seeing?

On macOS, one user-visible submit intermittently emits two independent RPCs from the same focused renderer.

I captured two manifestations in the same app session:

  1. New-task composer: two independent thread/start requests were emitted from originWebcontentsId=1 with different request IDs. The responses were 14 ms apart, and two persistent threads were created about 1 ms apart. Both threads came from the same frontend client-new-thread:<uuid> draft, and local prompt history recorded the same prompt twice.
  2. Existing thread: two independent turn/start requests were emitted from the same renderer. The app-server accepted two distinct UserInput submissions 30.4 ms apart. The second input was queued/steered into the active turn and only became visible about 31 seconds later, making it look as if the message had been sent again later.

There was no submission error, timeout, reconnect, or retry warning. Both requests came from the same focused primary window and the same single app-server process. Prompts, usernames, hostnames, IPs, project paths, and session IDs are intentionally redacted.

What steps can reproduce the bug?

The issue is intermittent rather than deterministic, but it was captured twice with local Desktop and app-server logs.

  1. Open Codex Desktop on macOS and select an SSH remote project.
  2. Open the new-task composer.
  3. Enter a prompt and activate Send once.
  4. Occasionally, two tasks with the same title and prompt are created.
  5. In an existing idle thread, enter a follow-up and activate Send once.
  6. Occasionally, two turn/start calls are emitted within tens of milliseconds. The second message may appear later because it is delivered through the active-turn steer/input queue.

Observed app: 26.707.91948 (build 5440).
Embedded Codex CLI: 0.144.5.
Remote app-server in the captured case: 0.144.1.

What is the expected behavior?

One user submit gesture must result in exactly one logical submission:

  • A new-task submit creates one persistent thread.
  • An existing-thread submit adds one user message.
  • Duplicate callbacks or transport retries must be idempotent and return the original thread/turn.
  • Sending the same text again in a later, intentional gesture must remain possible because it receives a new submission ID.
Additional information
Source-level evidence

The packaged Desktop submit handler has no synchronous single-flight guard. In:

app.asar :: webview/assets/app-initial~app-main~new-thread-panel-page~appgen-library-page~hotkey-window-thread-page~ho~iufn7mg3-DtuASjaM.js

the async submit path is effectively:

await prepareGoalSubmit(...)
appendPromptToHistory(...)
await Promise.all([buildLocalContext(...), collectThreadReferences(...)])
setIsSubmitting(true)
await submitTarget.submit(...)

The handler entry is around byte 338820; the first normal-path setIsSubmitting(true) is around byte 341182. A second callback can therefore enter while the first is awaiting preparation. React state/button disabling is too late to be a same-tick mutex.

The app-server has no backstop:

Suggested defense-in-depth fix
  1. Set a synchronous useRef/mutex at the first line of the composer submit handler, before any await.
  2. Generate one stable client submission ID per user gesture/draft and reuse it across retries.
  3. Pass that ID through thread/start; repeated requests should return the original thread.
  4. Deduplicate turn/start by clientUserMessageId and return the original turn instead of enqueueing again.
  5. Add concurrent-submit tests such as Promise.all([submit(), submit()]).

Related but materially different: #19951 involves visible submission errors and explicit user retries. This report has no error or manual resubmit; the two RPCs originate from the same renderer within tens of milliseconds.

The exact physical event source (keyboard/IME/button) is not present in current logs, but a synchronous frontend guard plus protocol-level idempotency would make that source irrelevant.

Dominant language
Rust
Stars
125k
Forks
19.5k
Avg merge
1m
Merged PRs (30d)
1k

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.

More from openai/codex

All issues in openai/codex

Similar issues

More Rust issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.