send_message_to_thread omits the target turn ID, preventing exact completion correlation
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 successfulsend_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:
``json``
{
"targets": [{"threadId": "<B>"}],
"timeoutMs": 0
}
Save the returned polls[0].cursor as C0.
- From task A, call
send_message_to_threadtargeting B with:
``text``
Reply exactly: X_DONE_20260824_C91E
- Observe the complete send result:
``json``
{"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
a952 and its final text was X_DONE_20260824_C91E.
- From task A, call
send_message_to_threadagain, targeting the same B, with:
``text``
Reply exactly: Y_DONE_20260824_5B42
- With
read_thread, confirm Y completed. In this run its different turn ID
ended in db7a and its final text was Y_DONE_20260824_5B42.
- Only now call
wait_threadswith the cursor saved before X:
``json``
{
"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 only threadId:
https://github.com/openai/codex/blob/339751715c64496cb86246bfb3935f40e309dd3d/codex-rs/tui/src/dynamic_tools.rs#L739-L777
TurnStartResponsealready contains the createdturn:
WaitTargetcontains onlythreadIdandafterCursor:
wait_threadsreads the latest turn and reports that turn on completion:
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.