Repository navigation
Description
What version of the Codex App are you using (From “About Codex” dialog)?
26.1007.21159 (bundled codex-cli 0.162.0-alpha.17.2)
What subscription do you have?
API - custom OpenAI-compatible provider (a self-hosted relay, wire_api = "responses", which validates tool pairing strictly). Not the ChatGPT/OpenAI backend.
What platform is your computer?
macOS 26.6 (Darwin 25.6.0), arm64 (Apple Silicon)
What issue are you seeing?
A thread became permanently unusable. The first request after a batched tool call failed with:
{"error":{"code":null,"message":"An assistant message with 'tool_calls' must be followed by tool messages responding to each 'tool_call_id'. (insufficient tool messages following tool_calls message)","param":null,"status":400,"type":"invalid_request_error"}}
Every later user turn in the same thread then failed with the same 400 within ~4 s.
Root cause, taken from the session rollout JSONL: in one assistant turn that contained two parallel tool calls, the client appended the image tool output, then a developer <image_resize_notice> message, then the second tool output:
09:01:10.383 function_call js call_00_sklbnyw...
09:01:10.384 function_call exec_command call_01_asvwdd...
09:01:11.838 function_call_output call_00_sklbnyw... (screenshot image, ~438 KB)
09:01:11.838 message (developer) <image_resize_notice>
09:01:11.839 function_call_output call_01_asvwdd... (command output)
09:01:16.923 task_complete error: the 400 above
The notice text is:
<image_resize_notice>
Image 1 of 1 in the preceding tool output was resized from 3024x1898 to 1989x1248 pixels.
</image_resize_notice>
So the assistant message with two tool_calls is not followed contiguously by its two tool messages: the developer notice splits the block. Strict OpenAI-compatible validators reject that shape (this is what produces insufficient tool messages following tool_calls message). Because the interleaved order is persisted in the rollout, the malformed block is replayed on every following request, and the thread can never make progress.
Session scan of the affected rollout (856 response items, 129 tool calls): every function_call has a matching function_call_output, and this was the only place where a non-tool item sits between two tool outputs of the same batch. Two later user turns (09:03:10 and 09:03:19 UTC) failed with the identical error in <4 s, without reaching the model.
How this differs from #44604 (inference): there, the wedge is attributed to the follow-up sampling request going out before the last tool output of the batch was appended (a race; gaps of 68 ms / ~30 s). Here the two tool outputs are 1 ms apart and the bad ordering is written into the rollout itself, so the stored history is invalid even with all outputs present; request gating alone would not correct the on-disk ordering.
What steps can reproduce the bug?
- Use the Codex App against a custom OpenAI-compatible provider that validates tool-call/tool-output ordering strictly.
- Have one assistant turn issue two parallel tool calls where the first one returns a screenshot image large enough to be resized (e.g. a computer-use/MCP tool returning a 3024x1898 image) and the second is any other tool (e.g.
exec_command). - The app writes: tool output update summary to auto #1 ->
<image_resize_notice>developer message -> tool output (cleanup) add link to https://cookbook.openai.com for apply_patch and minor tidy up #2, in that order, into the rollout. - The next sampling request is rejected with the 400 above, and the thread stays wedged afterwards.
Affected session (truncated): 01a124e4-...-9c9dbc7b37ea, 2026-10-10 09:01-09:03 UTC.
What is the expected behavior?
- Non-tool protocol items such as
<image_resize_notice>must never be emitted between thefunction_call_outputitems of the same assistant batch - emit the notice after the batch's final tool output, or attach it to the image output it annotates. - Before sending, the client should verify that each assistant
tool_callsmessage is followed contiguously by tool messages for all of itscall_ids (fail closed: wait or synthesize an output instead of sending an invalid body). - This 400 should be recoverable: re-send once with a stand-in output for the offending
call_idrather than ending the turn and wedging the thread.
Additional information
Related: #44604 (same trigger - <image_resize_notice> after an image output; attributed to a request race), #38365 (notes a developer message between a tool call and its output breaking custom providers), #26865 and #25380 (same 400 / wedged-thread symptom).
Happy to attach a redacted excerpt of the rollout JSONL if that helps.
Activity
zhyuzhyu commentedon Oct 10, 2026
Recurrence update - same thread wedged a second time, ~40 minutes after the first repair.
The defect reproduced with the same shape, this time with a 3000x1880 image resized to 1992x1248:
The next request failed with
insufficient tool messages following tool_calls message, and every later turn in the thread kept failing until the stored history was repaired again (same fix: move the notice after the batch's final tool output).Details that may help triage:
~/.codex/bin/repair-rollout.py repair) that scans session rollouts and moves a developer notice sitting between siblingfunction_call_outputitems to after the batch's last output, then re-verifies the result. It is a pure reorder - identical content, same line count, one message moved - and the thread resumes after a restart.<image_resize_notice>(and any other non-tool item) after the batch's lastfunction_call_output, or to attach it to the image item itself, rather than writing it between the tool messages.r266-tech commentedon Oct 10, 2026
I traced the corresponding paths in current public main (
806d9732c974bc8a51b8317c1bd8985544fe627c). This is source analysis, not an independent reproduction of the bundled App or its relay.prepare_response_itemsappends each prepared item and then its resize notice immediately, inside the per-item loop. Even supplying both sibling outputs in one invocation would produceoutput A → notice A → output Bwhen A is resized.record_conversation_itemspasses those prepared items to history/persistence.normalize_historyensures outputs exist and removes orphans. The presence check collects output IDs across the whole history; it does not require adjacency or move a notice when all outputs are present. Therefore that normalization does not repair this ordering on replay.A regression can target the preparation boundary with two synthetic calls/outputs and a resized first image, then check the normalized request on initial send and replay. Waiting for both outputs alone would not address the per-item insertion above. It would also help to capture a redacted outbound Responses request and the relay's translated message ordering: the rollout ordering establishes the client path, while the strict rejection occurs at the compatible provider/relay boundary.
Edward4226 commentedon Oct 12, 2026
Independent occurrence on a different setup, plus a provider-side A/B that may be useful here.
Setup: Windows 11, Codex desktop app 26.1007.21434 (codex-cli 0.162.0-alpha.17.2), custom provider = OpenCode Zen (
wire_api = "responses", DeepSeek) behind a local OpenAI-compatible relay.Same shape, same symptom - one assistant step, two parallel tool calls, notice written between the two outputs:
Two extra data points:
The stored history is not the problem here: re-sending the items exactly as recorded in the rollout (same order, same 155 KB image, same notice) to the same provider endpoint returns 200, 4/4. So on this setup the interleaved order alone is not sufficient to make the provider reject - and the command output is appended 16 ms before the request body is assembled, i.e. inside the race window described in 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 #44604.
A strict provider's rejection can be content-dependent: with pure HTTP requests to that endpoint (no Codex involved), an identical item array is accepted when the interleaved text is
<image_resize_notice>...</image_resize_notice>, and rejected withNo tool output found for tool call call_01_bfor any other text - a plain sentence,<system_notice>x</system_notice>,<some_other_tag>x</some_other_tag>,image_resize_noticewithout tags,IMAGE_RESIZE_NOTICE,image_resiz_notice, or the identical notice text with role=user/system/assistant. That provider appears to whitelist the literalimage_resize_noticetoken; anything else becomes a group-breaking message.I still think your asks are right (never emit a non-tool item between the batch's tool outputs; verify pairing at the send boundary; treat this 400 as recoverable), and point 2 is just a caution: "it works against another provider" is not evidence that the ordering is safe. Filed the provider-side report as anomalyco/opencode#54698. Happy to supply a redacted excerpt of the rollout (paths/account identifiers removed) if it helps - I would rather not attach the raw session file.
fix: tool outputs keep their batch, base64 files stay out of text, tw…
fix: tool outputs keep their batch, blank Anthropic text, duplicate r…
zhyuzhyu commentedon Oct 14, 2026
Third occurrence, new shape: the notice can also land between the tool outputs with a local-only event in between.
Same defect, different signature. In this session the developer notice sits between the two
function_call_outputitems of one batch, but a local-onlyevent_msg item_completedrecord is interleaved right after the notice:event_msgrecords are not part of the provider request, so the serialized history still contains the split batch - but this time the provider reported a different message for the same defect:So the same underlying ordering bug surfaces as at least two different 400s depending on the validator:
insufficient tool messages following tool_calls message(first two occurrences) andMessages with role 'tool' must be a response to a preceding message with 'tool_calls'(this one).Context and impact:
call_idin the file. The session spans several days and includes a compaction (line 2826) before this occurrence - noting it in case the post-compaction replay path is relevant.<image_resize_notice>(or any non-tool item) between the tool outputs of a batch; (2) validate the serialized message list, not the raw item stream, right before sending - the two diverge because local-only records can sit between tool outputs.event_msg/token_usage_recordentries.MickxNT commentedon Oct 15, 2026
New trigger for the same wedge class — and a single tool call is enough. Here the interleaved item is hook-injected context, not the resize notice.
Setup: Codex app +
codex exec(bundled CLI 0.162.1), provider = DeepSeek Responses (wire_api = "responses"), strict pairing.Rollout sequence:
function_callcall_00_J8IP5Z3POw6bHzJm0E7Q9410(ordinal 4518) — anexec_commandmessage(developer) = thehookSpecificOutput.additionalContextreturned by aPreToolUsehook (matcherBash) for that same command (ordinal 4520)function_call_outputfor the same call (ordinal 4522)The follow-up sampling request returned
400 No tool output found for tool call call_00_J8IP5Z3POw6bHzJm0E7Q9410.and every later turn kept failing with the same 400 (three turns over ~15 minutes, differentrequest_ids). A freshcodex exec resumeof the persisted rollout in a clean process reproduced the 400, so it is stored history, not stale app-server memory. Repair was the same as here: move thefunction_call_outputbefore the injecteddevelopermessage, so the output is adjacent to its call; the same rollout then resumes and answers normally.Takeaways:
PreToolUsehook (and likelyPostToolUseas well) returninghookSpecificOutput.additionalContextcan produce this shape — not specific toview_image.r266-tech commentedon Oct 19, 2026
The single-call hook report has a separate, identifiable insertion path on current public main
89461ea68338:run_pre_tool_use_hooksrecordsadditional_contextsimmediately after the hook completes, before returning either Continue or Blocked.record_additional_contextswrites those messages throughrecord_conversation_items.ToolRegistrydispatch awaits that pre-hook before executing the tool. The post-hook path also records its context inside dispatch, before returning the tool result. Moving resize notices alone would leave these insertion paths in place.pre_tool_use_records_additional_context_for_exec_command. Its follow-up-request assertions independently check that the developer context and matching output exist, but do not check their relative positions. The blocked-pre-hook and post-hook fixtures have the same opportunity for an ordering assertion.A useful regression would inspect the serialized
inputordering in those existing fixtures, including a resumed turn and a parallel-call case. Keep the hook text intact while checking where it appears relative to the matching tool output(s).This is source analysis only; I have not run these Rust fixtures or independently reproduced the bundled App/DeepSeek relay failure. The provider-specific A/B above remains relevant when interpreting a successful request.