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
Nobody has claimed this yet.
- 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*_callhas a matching*_outputbycall_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 →notLoadedrecovery noted in #41338 is consistent with that. - The app does not use provider-side continuation here: the captured request shows
store=falseand 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?
- 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*_outputitem appended (the gap above is 68 ms; a
command that exceeds the tool wait limit leaves a 30 s window). - 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. - Treat this 400 as recoverable. If the provider does reject it, re-send once with a stand-in output for
the namedcall_idrather 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_imageout of any batched message, and never batch a
command that can exceed the 30 s tool wait. Additionally, a small local proxy on the providerbase_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/4end-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 (orphanedexec_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
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
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