openai / openai/codex

send_message_to_thread omits the target turn ID, preventing exact completion correlation

Open
#40,397 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

app bug subagent
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.

  1. Let B become idle, then call:

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

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

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

    Reply exactly: X_DONE_20260824_C91E
    
  3. Observe the complete send result:

    {"threadId":"<B>"}
    

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

  4. With read_thread, confirm X completed. In this run its turn ID ended in
    a952 and its final text was X_DONE_20260824_C91E.

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

    Reply exactly: Y_DONE_20260824_5B42
    
  6. 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.

  7. Only now call wait_threads with 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:

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

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.

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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.