openai / openai/codex

Next sampling request is sent before the last tool output of a batch is appended, so the provider rejects it with "No tool output found for tool call X" and the thread stays wedged

Open
#44,604 4 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

app app-server bug custom-model tool-calls
Dominant language
Rust
Stars
125k
Forks
19.4k
PR merge metrics
PR metrics pending

Description

What version of Codex App are you using?

Codex desktop app (app-server), cli_version 0.153.4 as recorded in the session's session_meta.

What platform is your computer?

Windows 11 (x64).

What issue are you seeing?

A thread becomes permanently unable to continue: every later user turn — and every fork of it — fails with

{"error":{"message":"No tool output found for tool call call_01_5kk0GRUtk7tY2lEwSKhY0823.","type":"invalid_request_error","param":null,"code":"invalid_request_error"}}

The trigger is a race inside a single assistant message that batches several tool calls. The follow-up
sampling request goes out before the last tool result of that batch has been appended to the input, so the
serialized request contains a function_call with no matching function_call_output. The provider rejects
the whole request, and the rejected turn is now part of the thread's live history, so the same 400 repeats
forever (a restart is the only thing that clears it).

I have the rollout JSONL for three wedged threads. The timelines below are the tool-call / tool-result
timestamps from those files plus the Turn error timestamp from the logs.

What steps can reproduce the bug?

Variant A — two view_image calls in one message (race window ≈ 70 ms). One assistant message issued two
image views; the two results were written back 68 ms apart, and the next request went out inside that gap:

17:10:33.270  function_call          view_image    call_00_si6t6wrnjTkCBhCbg89w4748
17:10:33.348  function_call          view_image    call_01_5kk0GRUtk7tY2lEwSKhY0823
17:10:34.115  item_completed         ImageView     call_00_si6t6wrnjTkCBhCbg89w4748
17:10:34.164  item_completed         ImageView     call_01_5kk0GRUtk7tY2lEwSKhY0823   <- batch now looks "done"
17:10:34.188  function_call_output   call_00_si6t6wrnjTkCBhCbg89w4748 (image)
17:10:34.189  message (developer)    <image_resize_notice>
17:10:34.256  function_call_output   call_01_5kk0GRUtk7tY2lEwSKhY0823 (image)       <- 68 ms too late
17:10:37.260  task_complete          error: No tool output found for tool call call_01_5kk0GRUtk7tY2lEwSKhY0823

The follow-up request therefore had to be issued between 34.189 and 34.256 — it was accepted by the transport
and rejected by the server ~3 s later.

Variant B — one view_image batched with a command that exceeded the 30 s tool wait. Here the race
window is ~30 s wide, so it reproduces every time:

16:53:36.098  function_call          view_image    call_00_xOnP62dcYl3n9S9KIgyj0114
16:53:36.981  function_call          exec_command  call_01_bAkMrxrT9wYGu6WvjHjs2192   (ran ~30 s)
16:53:37.052  function_call_output   call_00_xOnP62dcYl3n9S9KIgyj0114 (image)
16:53:37.052  message (developer)    <image_resize_notice>                       <- request goes out here
16:54:07.421  function_call_output   call_01_bAkMrxrT9wYGu6WvjHjs2192 (command result, 30 s late)
16:54:11.683  task_complete          error: No tool output found for tool call call_01_bAkMrxrT9wYGu6WvjHjs2192
Why this is a single-turn race and not a corrupt rollout
  • After the fact, the on-disk rollout for these threads is consistent — I scanned every response_item
    in the affected session files (>8k items) and every *_call has a matching *_output by call_id. The
    tool result is written a few tens of milliseconds after the request that needed it.
  • The wedge lives in the in-memory conversation: the unmatched call stays in the thread's live history,
    which is why every later turn (and every fork) reproduces the identical 400 within ~1 s without hitting the
    network. This matches #31579 ("app-server keeps stale in-memory history until unload") — and the
    archive → unarchive → notLoaded recovery noted in #41338 is consistent with that.
  • The app does not use provider-side continuation here: the captured request shows store=false and no
    previous_response_id, i.e. the full history is re-sent every turn, so the unmatched call really is in the
    body (it is just missing its output).
What is the expected behavior?
  1. Gate the follow-up request on the whole batch. Do not issue the next sampling request until every tool
    call from the current assistant message has its *_output item appended (the gap above is 68 ms; a
    command that exceeds the tool wait limit leaves a 30 s window).
  2. Re-validate pairing on the serialized input immediately before sending (and fail closed: if a call has
    no output, either wait or synthesize one) instead of letting an unpaired call reach the provider.
  3. Treat this 400 as recoverable. If the provider does reject it, re-send once with a stand-in output for
    the named call_id rather than ending the turn — ending the turn is what makes the thread permanently
    dead. (This is what my local workaround does, see below; the retry succeeds.)
Additional information
  • Provider: a third-party OpenAI-compatible endpoint (wire_api = "responses", DeepSeek). The provider
    enforces "every tool call needs a tool output" strictly; I have not tested whether the OpenAI endpoint
    accepts the same unpaired body.
  • Impact: 3 threads wedged in one evening of ordinary work (image-heavy review sessions), each with the same
    error, and each unrecoverable in place. The work in those threads was only recoverable by redoing it in a
    new thread.
  • Immediate workaround that works for us: keep view_image out of any batched message, and never batch a
    command that can exceed the 30 s tool wait. Additionally, a small local proxy on the provider base_url
    that (a) injects a stand-in output for an unpaired call and (b) retries once when the upstream returns this
    400 has been verified to turn the failing request into a 200 (4/4 end-to-end cases, plus the original
    10 unit tests for the unrelated duplicate-namespace repair it also carries).
  • Related: #31053 (same error via tool_search), #36827 (orphaned exec_command), #31579, #41338, #18629.
  • Happy to provide the raw rollout files (or a trimmed excerpt) if that helps — say the word and I will
    attach them with the local paths and identifiers redacted.

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

The issue names no source files or tests. Start by locating the app-server path that batches tool calls, appends outputs, and serializes the next sampling request; reproduce with multiple tools or a tool exceeding the wait limit. Done means follow-up requests are gated until every call has an output, serialized inputs are revalidated, and the reported 400 does not permanently wedge the thread.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
backend-api-design, tooling
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.