send_message_to_thread omits the target turn ID, preventing exact completion correlation

Open 💬 0 comments Opened Aug 24, 2026 by dongkeren

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.

  1. Let B become idle, then call:

``json
{
"targets": [{"threadId": "<B>"}],
"timeoutMs": 0
}
``

Save the returned polls[0].cursor as C0.

  1. From task A, call send_message_to_thread targeting B with:

``text
Reply exactly: X_DONE_20260824_C91E
``

  1. Observe the complete send result:

``json
{"threadId":"<B>"}
``

No target turn ID or message/dispatch ID is returned.

  1. With read_thread, confirm X completed. In this run its turn ID ended in

a952 and its final text was X_DONE_20260824_C91E.

  1. From task A, call send_message_to_thread again, targeting the same B, with:

``text
Reply exactly: Y_DONE_20260824_5B42
``

  1. 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.

  1. Only now call wait_threads with 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_thread awaits start_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

  • TurnStartResponse already contains the created turn:

https://github.com/openai/codex/blob/339751715c64496cb86246bfb3935f40e309dd3d/codex-rs/app-server-protocol/src/protocol/v2/turn.rs#L163-L168

  • WaitTarget contains only threadId and afterCursor:

https://github.com/openai/codex/blob/339751715c64496cb86246bfb3935f40e309dd3d/codex-rs/tui/src/dynamic_tools.rs#L132-L142

  • wait_threads reads 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.

View original on GitHub ↗