Codex Desktop composer can double-dispatch one submit, creating two threads or duplicate steered messages
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
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
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:
- New-task composer: two independent
thread/startrequests were emitted fromoriginWebcontentsId=1with 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 frontendclient-new-thread:<uuid>draft, and local prompt history recorded the same prompt twice. - Existing thread: two independent
turn/startrequests 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.
- Open Codex Desktop on macOS and select an SSH remote project.
- Open the new-task composer.
- Enter a prompt and activate Send once.
- Occasionally, two tasks with the same title and prompt are created.
- In an existing idle thread, enter a follow-up and activate Send once.
- Occasionally, two
turn/startcalls 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:
- In
rust-v0.144.1,ThreadStartParamshas no client idempotency key:
https://github.com/openai/codex/blob/rust-v0.144.1/codex-rs/app-server-protocol/src/protocol/v2/thread.rs#L56-L148 - Every
thread/startdirectly creates a new thread:
https://github.com/openai/codex/blob/rust-v0.144.1/codex-rs/app-server/src/request_processors/thread_processor.rs#L1212-L1244 turn/start.clientUserMessageIdis copied into pending input but is not used for deduplication:
https://github.com/openai/codex/blob/rust-v0.144.1/codex-rs/core/src/session/mod.rs#L3931-L3946
Suggested defense-in-depth fix
- Set a synchronous
useRef/mutex at the first line of the composer submit handler, before anyawait. - Generate one stable client submission ID per user gesture/draft and reuse it across retries.
- Pass that ID through
thread/start; repeated requests should return the original thread. - Deduplicate
turn/startbyclientUserMessageIdand return the original turn instead of enqueueing again. - 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from openai/codex
-
enhancement remote
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
-
bug CLI windows-os
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
-
macOS sandbox blocks hw.optional.arm64 sysctl, causing Flutter to misdetect Apple Silicon as x64 Openbug CLI sandbox
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
-
bug CLI TUI
Difficulty 2/5 1-3 hours Newbie friendliness 90/100
-
CLI config enhancement skills
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 86/100
kwakseongjae/auto-hwp#319 ·
-
area:cli bug filter-quality good first issue priority:medium
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
-
Difficulty 1/5 Under an hour Newbie friendliness 72/100
bevyengine/bevy#25861 ·
-
comp-datalake
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
ClickHouse/ClickHouse#121222 ·
-
A-linter
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
oxc-project/oxc#26863 ·