send_message_to_thread omits the target turn ID, preventing exact completion correlation
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 125k
- Forks
- 19.4k
- PR merge metrics
- PR metrics pending
Description
What version of the Codex App are you using (From “About Codex” dialog)?
macOS desktop app host, installed bundle version 26.803.61601 (build 6396). Local Codex CLI: codex-cli 0.147.0-alpha.6.5.
What subscription do you have?
Not recorded for this reproduction; the completion-correlation contract should not depend on plan tier.
What platform is your computer?
macOS 26.5.2 (build 25F84), Apple Silicon / arm64.
What issue are you seeing?
The first-party task tools do not expose a stable way to correlate one successful
send_message_to_thread call with the target turn whose completion should later
be observed by wait_threads.
send_message_to_thread returns only threadId. wait_threads accepts only a
target threadId and an optional thread cursor. If two turns complete after the
saved cursor before the caller begins waiting, wait_threads reports the latest
turn, even when the caller needs the completion created by the first send.
This is a completion-correlation problem. It is not a claim that the later turn
failed, nor is it the delivery-acknowledgement failure reported in #29886.
What steps can reproduce the bug?
Use a harmless projectless target task B. Each prompt below only asks for an
exact text response and performs no tools or file changes.
-
Let B become idle, then call:
{ "targets": [{"threadId": "<B>"}], "timeoutMs": 0 }Save the returned
polls[0].cursorasC0. -
From task A, call
send_message_to_threadtargeting B with:Reply exactly: X_DONE_20260824_C91E -
Observe the complete send result:
{"threadId":"<B>"}No target turn ID or message/dispatch ID is returned.
-
With
read_thread, confirm X completed. In this run its turn ID ended in
a952and its final text wasX_DONE_20260824_C91E. -
From task A, call
send_message_to_threadagain, targeting the same B, with:Reply exactly: Y_DONE_20260824_5B42 -
With
read_thread, confirm Y completed. In this run its different turn ID
ended indb7aand its final text wasY_DONE_20260824_5B42. -
Only now call
wait_threadswith the cursor saved before X:{ "targets": [{"threadId":"<B>", "afterCursor":"<C0>"}], "timeoutMs": 0 }
Actual behavior
The late wait returns reason: "turnCompleted", Y's turn ID (…db7a), and the
Y response. Relevant fields from the returned result:
{
"wake": {
"reason": "turnCompleted",
"turnId": "…db7a",
"threadId": "<B>"
},
"polls": [
{
"latestAssistantMessage": {
"turnId": "…db7a",
"text": "Y_DONE_20260824_5B42"
}
}
]
}
The caller has no field with which to express “wait for the target turn created
by the X send.” Both sends have the same source and target thread IDs, so
thread-level provenance cannot disambiguate them. A read_thread immediately
after sending can sometimes infer the latest turn, but that is itself a
latest-state read and remains racy.
What is the expected behavior?
A successful send should expose a stable correlation handle, and the wait tool
should optionally wait for that exact unit without substituting a newer turn.
One minimal backward-compatible shape would be:
send_message_to_thread(...) -> { threadId, turnId }
wait_threads({
targets: [{ threadId, turnId? }]
})
When turnId is provided, completion of a newer turn must not satisfy the wait.
When it is omitted, the existing thread-level/latest-turn behavior can remain
unchanged.
Additional information
How this issue was found
We found this issue by applying KFD's Work-level semantic invariants to the
task-management contracts in the Codex repository, then testing the predicted
gap against the live codex_app tool surface.
KFD is an open semantic model for durable agent Work. It keeps Work identity,
agent/thread execution, delegation receipts, provenance, and completion
authority distinct, rather than reconstructing them from session or thread
state.
One of those invariants is that a durable thread cannot serve as the identity
of an individual delegated unit of Work. Applying it here led us to check
whether send_message_to_thread returns the identity of the turn it creates,
and whether wait_threads can wait on that same identity. The black-box
reproduction above and the source analysis below confirm that the correlation
handle is missing.
The broader protocol analysis and six related boundary tests are discussed in
KFD #427.
Source analysis
Public commit 339751715c64496cb86246bfb3935f40e309dd3d shows the same
contract gap in the TUI-hosted task tools:
send_message_to_threadawaitsstart_turn(...)but discards the
TurnStartResponse, then returns onlythreadId:
https://github.com/openai/codex/blob/339751715c64496cb86246bfb3935f40e309dd3d/codex-rs/tui/src/dynamic_tools.rs#L739-L777TurnStartResponsealready contains the createdturn:
https://github.com/openai/codex/blob/339751715c64496cb86246bfb3935f40e309dd3d/codex-rs/app-server-protocol/src/protocol/v2/turn.rs#L163-L168WaitTargetcontains onlythreadIdandafterCursor:
https://github.com/openai/codex/blob/339751715c64496cb86246bfb3935f40e309dd3d/codex-rs/tui/src/dynamic_tools.rs#L132-L142wait_threadsreads the latest turn and reports that turn on completion:
https://github.com/openai/codex/blob/339751715c64496cb86246bfb3935f40e309dd3d/codex-rs/tui/src/dynamic_tools.rs#L826-L984
The black-box observation above is from the codex_app tool surface. I have not
verified that the desktop handler and the public TUI host share the identical
implementation; the source links show that the same externally visible contract
shape also exists in that public commit.
Related issues
- #29886 requests reliable delivery acknowledgement when a send succeeds but a
handler reports failure. This report assumes successful sends and concerns
correlation with the later completion. - #35846 proposes a broader persistent work-thread / command-ID abstraction.
This report does not require that abstraction; the minimal request is an exact
target-turn handle for the already shipped send/wait tools.
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.
Research direction
Start in codex-rs/tui/src/dynamic_tools.rs, reading send_message_to_thread, WaitTarget, and wait_threads alongside TurnStartResponse in codex-rs/app-server-protocol/src/protocol/v2/turn.rs. Done means a successful send exposes its created turn ID, wait_threads can target that turn without accepting a newer completion, and existing thread-level behavior remains unchanged.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- api, backend-api-design
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100