Skip to content

<image_resize_notice> is written between tool outputs of the same batch, wedging threads on strict OpenAI-compatible providers #52834

Open

Description

@zhyuzhyu

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?

  1. Use the Codex App against a custom OpenAI-compatible provider that validates tool-call/tool-output ordering strictly.
  2. 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).
  3. 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.
  4. 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?

  1. Non-tool protocol items such as <image_resize_notice> must never be emitted between the function_call_output items of the same assistant batch - emit the notice after the batch's final tool output, or attach it to the image output it annotates.
  2. Before sending, the client should verify that each assistant tool_calls message is followed contiguously by tool messages for all of its call_ids (fail closed: wait or synthesize an output instead of sending an invalid body).
  3. This 400 should be recoverable: re-send once with a stand-in output for the offending call_id rather 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

  1. added
    bugSomething isn't working
    appIssues related to the Codex desktop app
    custom-modelIssues related to custom model providers (including local models)
    tool-callsIssues related to tool calling
    sessionIssues involving session (thread) management, resuming, forking, naming, archiving
    on Oct 10, 2026
  2. zhyuzhyu commented on Oct 10, 2026

    @zhyuzhyu
    Author

    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:

    09:42:01.447  function_call          call_00_thiuocduulpr6qocjh8a6ruf   (image view)
    09:42:01.448  function_call          call_01_f0d8saoq0xn55qfx3mz18sul   (exec_command)
    09:42:01.869  function_call_output   call_00_thiuocduulpr6qocjh8a6ruf   (image)
    09:42:01.869  message (developer)    <image_resize_notice> (3000x1880 -> 1992x1248)
    09:42:01.878  function_call_output   call_01_f0d8saoq0xn55qfx3mz18sul
    

    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:

    • A structural scan of all 149 rollout files in this installation found exactly one defective file: this thread. Rare, but deterministic whenever an image tool output that gets resized shares a batch with another tool call.
    • The bad ordering is persisted in the rollout, so restarting the app does not clear it; the thread stays dead until the file is repaired. (Any history copy - e.g. a fork - would copy the same ordering.)
    • Local workaround we now use: a small user-level script (~/.codex/bin/repair-rollout.py repair) that scans session rollouts and moves a developer notice sitting between sibling function_call_output items 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.
    • That workaround is further evidence that the compact client-side fix is to emit <image_resize_notice> (and any other non-tool item) after the batch's last function_call_output, or to attach it to the image item itself, rather than writing it between the tool messages.
  3. r266-tech commented on Oct 10, 2026

    @r266-tech

    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_items appends each prepared item and then its resize notice immediately, inside the per-item loop. Even supplying both sibling outputs in one invocation would produce output A → notice A → output B when A is resized.
    • record_conversation_items passes those prepared items to history/persistence.
    • normalize_history ensures 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.

  4. Edward4226 commented on Oct 12, 2026

    @Edward4226

    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:

    20.921  function_call          view_image      call_00_hC0DonBjeKHawzABcyLL1642
    21.127  function_call          exec_command    call_01_mNySew0rARWuQYQXPeIY2319
    21.216  function_call_output   call_00_hC0...  (image, 1024x9000)
    21.216  message (developer)    <image_resize_notice>  (1024x9000 -> 233x2048)
    21.465  function_call_output   call_01_mNy...  (command output)
    21.481  zstd "Compressed request body ..."    <- follow-up request assembled
    31.44   task_complete error: "No tool output found for tool call call_01_mNySew0rARWuQYQXPeIY2319."
    

    Two extra data points:

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

    2. 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 with No tool output found for tool call call_01_b for any other text - a plain sentence, <system_notice>x</system_notice>, <some_other_tag>x</some_other_tag>, image_resize_notice without tags, IMAGE_RESIZE_NOTICE, image_resiz_notice, or the identical notice text with role=user/system/assistant. That provider appears to whitelist the literal image_resize_notice token; 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.

  5. zhyuzhyu commented on Oct 14, 2026

    @zhyuzhyu
    Author

    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_output items of one batch, but a local-only event_msg item_completed record is interleaved right after the notice:

    4000  reasoning
    4001  function_call         call_01_ux0gn6r8kqdayv0hp5lgx1n8   (image view)
    4002  function_call         call_00_uw2vkj1pn9a8o06ux9afmlfz   (exec_command)
    4005  function_call_output  call_01_ux0gn6r8kqdayv0hp5lgx1n8   (image)
    4006  message (developer)   <image_resize_notice>
    4007  event_msg             item_completed                     (local-only, not sent to the model)
    4008  function_call_output  call_00_uw2vkj1pn9a8o06ux9afmlfz
    

    event_msg records 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:

    Messages with role 'tool' must be a response to a preceding message with 'tool_calls'
    

    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) and Messages with role 'tool' must be a response to a preceding message with 'tool_calls' (this one).

    Context and impact:

    • Session: 4020 raw lines, 1833 model-visible items, 609 tool calls; all calls and outputs are paired 1:1 by call_id in 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.
    • Two takeaways for the fix: (1) never emit <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.
    • Triage note for anyone scanning rollout files: requiring strict raw-line adjacency between the two tool outputs is not enough; filter to model-visible items first. Our scanner initially missed this occurrence for exactly that reason, and caught it once it ignored event_msg / token_usage_record entries.
    • Same local workaround applies (move the notice after the batch's final tool output). The thread resumes after a restart.
  6. MickxNT commented on Oct 15, 2026

    @MickxNT

    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_call call_00_J8IP5Z3POw6bHzJm0E7Q9410 (ordinal 4518) — an exec_command
    • message (developer) = the hookSpecificOutput.additionalContext returned by a PreToolUse hook (matcher Bash) for that same command (ordinal 4520)
    • function_call_output for 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, different request_ids). A fresh codex exec resume of 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 the function_call_output before the injected developer message, so the output is adjacent to its call; the same rollout then resumes and answers normally.

    Takeaways:

    • The output must be adjacent to its call: one injected message between them is enough, no second parallel call required.
    • Any PreToolUse hook (and likely PostToolUse as well) returning hookSpecificOutput.additionalContext can produce this shape — not specific to view_image.
  7. r266-tech commented on Oct 19, 2026

    @r266-tech

    The single-call hook report has a separate, identifiable insertion path on current public main 89461ea68338:

    • run_pre_tool_use_hooks records additional_contexts immediately after the hook completes, before returning either Continue or Blocked. record_additional_contexts writes those messages through record_conversation_items.
    • ToolRegistry dispatch 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.
    • There is already a focused mock-server fixture: 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 input ordering 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    appIssues related to the Codex desktop appbugSomething isn't workingcustom-modelIssues related to custom model providers (including local models)sessionIssues involving session (thread) management, resuming, forking, naming, archivingtool-callsIssues related to tool calling

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions