Skip to content

Inline image output costs ~230 tokens but 4.2 MB on the wire, so token-based context management never sees the payload that wedges the thread #41338

Open

Description

@ramuroy

Related issues

I searched first. This overlaps #18629 (inline base64 tool images poisoning threads) and #31579 (thread stuck after a failed auto-compact), and I have 👍'd both.

I am filing separately because I have a measurement that contradicts a stated hypothesis in #18629 and that neither issue reports: the images are nearly free in tokens and enormous on the wire. #18629 says the payload is "likely inflating token usage." Measured here, it does not — and that asymmetry looks like the actual reason nothing in the client notices before the thread dies. Happy for this to be folded into #18629 if you'd rather keep it in one place.

New here: the per-image token cost, a 34.78 MB payload (32× #31579's 1.08 MB), and view_image on a local file as the producer rather than a browser/screenshot plugin.


What version of the Codex App are you using?

Codex App 26.810.52044; codex-cli 0.150.1

What subscription do you have?

Not specified here. Observable from the session's own telemetry: limit_id: "codex", a 10,080-minute (weekly) primary window at 81% used, credits.balance: "0".

What platform is your computer?

Linux 7.0.0-30-generic x86_64 x86_64

What issue are you seeing?

A thread became permanently unable to continue. Three consecutive user turns each returned:

{"detail":"Bad Request"}

Immediately before that, three pre-turn auto-compactions failed:

16:14:50 WARN  remote compaction v2 stream failed; retrying request after delay retries=1 max_retries=2
               compact_error=stream disconnected before completion: failed to send websocket request: Connection closed normally
16:14:52 WARN  remote compaction v2 stream failed; retrying request after delay retries=2 max_retries=2
               compact_error=stream disconnected before completion: websocket closed by server before response.completed
16:14:54 WARN  falling back to HTTP
16:14:54 DEBUG Compressed request body with zstd pre_compression_bytes=34777589 post_compression_bytes=492064
16:14:55 ERROR session_task.run:run_turn: Failed to run pre-sampling compact

After the compaction failed, the turn proceeded on the uncompacted history: five websocket retries, all closed by server before response.completed, HTTP fallback, then Turn error: {"detail":"Bad Request"}. Every later message repeated it, with the body growing slightly each time as the failed turns appended (34,777,589 → 34,779,905).

The measurement

The request body was 34.78 MB. Of that, eight view_image outputs were 4,200,348 bytes each:

response_item type Count Bytes Share
custom_tool_call_output 226 35,243,778 96.3%
reasoning 281 771,319 2.1%
custom_tool_call 226 375,302 1.0%
message 56 150,834 0.4%
everything else 4 48,958 0.1%

The eight images alone are 33.6 MB, 92% of the entire request.

Now the part I think matters. Taking last_token_usage.input_tokens on the turn immediately after each image entered history:

Image Bytes on the wire Prompt-token delta
1 4,200,348 +239
2 4,200,348 +238
3 4,200,348 +230
4 4,200,348 +232
5 4,200,348 +227
6 4,200,348 +216
7 4,200,347 +235
8 4,200,346 +226
total 33.6 MB 1,843

1,843 tokens — 0.71% of the 258,400 context window — for 92% of the request body. For comparison, ordinary exec outputs in the same session cost +18,516, +11,000 and +7,240 tokens each while contributing almost nothing to the byte count.

Why this looks like the root cause

Context management is token-denominated; the transport constraint is byte-denominated. Only one of those is tracked.

  1. The client cannot see the problem. Its own accounting said 233,226 / 258,400 — a normal 90%, the routine trigger for compaction — while the actual body was 34.78 MB. pre_compression_bytes is computed and logged immediately before the POST and feeds no decision.
  2. Compaction cannot fix it, even when it works. A token-budget compactor has no incentive to evict images: they are the cheapest items per token and the most expensive per byte. It will evict the 18,516-token text output and keep the 4.2 MB picture. A textbook-perfect compaction could leave the body essentially unchanged.
  3. Compaction cannot run at all. It is itself a model request carrying the same payload, so when the payload is what fails, the only shrink mechanism fails with it — then the turn proceeds uncompacted anyway. That converts one bad turn into a permanently dead thread.
  4. The error says nothing. {"detail":"Bad Request"} gives no size, no token count, no offending item, no remedy.

What steps can reproduce the bug?

  • Session id: 01a04764-a97d-71c2-ace0-6fcf5816cc42
  • Model: gpt-5.6-sol, reasoning_effort=max
  • Context window: 258,400. Last successful turn: 233,226 input tokens (90.3%).
  • Two earlier auto-compactions in the same session succeeded — both before any image entered history.
  1. Run a long session with ordinary tool use until it sits near the auto-compact threshold.
  2. Have a tool write a large PNG to disk. Ours was 1280×820 at 3,149,928 bytes.
  3. Inline it: const r = await tools.view_image({path, detail:"original"}); image(r.image_url); — repeat ~8 times.
  4. Send another message. Pre-turn auto-compact fires, fails, and every turn from then on returns {"detail":"Bad Request"}.

The image producer does not matter — #18629 collects browser-use, Playwright, image_gen and plugin cases. What matters is an unbounded artifact reaching history via base64.

What is the expected behavior?

  1. Bound the inlined artifact. A 1280×820 frame tiles to the same image tokens whether the file is 13 KB or 3.1 MB — the measurement above shows the extra bytes buy the model nothing. Downscale or re-encode to a byte ceiling before the item enters history. This is lossless in every sense that matters and removes ~99% of the payload on its own.
  2. Track bytes as a budget, not only tokens. The number is already computed at pre_compression_bytes. Evicting by bytes-per-token would have dropped eight items worth 1,843 tokens and reclaimed 33.6 MB.
  3. Give compaction a local fallback, and never proceed uncompacted. If the compaction request fails, elide the heaviest items client-side and retry without the network. Running the turn anyway on the uncompacted history is what makes the state permanent rather than transient.
  4. Report the real reason. A 400 on a 34.78 MB body should say so.

Additional information

  • detail: "original" was requested; the stored item records detail: "high". Nothing downscaled or capped it either way.
  • On the producer side: the oversized PNG came from a local MCP server we maintain, which was writing PNGs as stored deflate blocks — 3,149,928 bytes for a line drawing that compresses to 16,687. We have fixed that (299× smaller, byte-identical pixel data), so this particular trigger is gone for us. It only removes one producer, though: nothing client-side stops the next large image from any source.
  • We depend on these images and are not going to stop sending them. The text alternatives our tooling exposes — a 400×200 character grid, a 32-entry colour ranking, single-pixel reads — answer "what colour is at this point", not "what does this drawing show". The right fix is bounding the payload, not rationing the capability.
  • On whether this is a size limit: I want to be careful. Thread gets stuck after failed auto-compact; app-server keeps stale in-memory history until unload #31579 hit the same 400 at 1,084,457 bytes, ~32× smaller than ours, alongside a dangling No tool output found for tool search call, so the rejection may be structural rather than a byte cap. What I can state from this session is the payload composition, the token/byte asymmetry, and that the client had no recovery path. I could not observe the server's reason.
  • Possible recovery, from Thread gets stuck after failed auto-compact; app-server keeps stale in-memory history until unload #31579: archiving and immediately unarchiving the thread moved it from systemError to notLoaded, unloading the stale in-memory history, after which the same prompt succeeded. Worth documenting if confirmed — right now a user's only visible option is to abandon the thread.

Activity

  1. added
    bugSomething isn't working
    appIssues related to the Codex desktop app
    contextIssues related to context management (including compaction)
    tool-callsIssues related to tool calling
    on Aug 28, 2026
  2. MR-Wayne commented on Aug 31, 2026

    @MR-Wayne

    I can reproduce a closely related failure on Windows with a single local image. In this environment, the same unbounded inline-image hazard manifests as a hard token-limit rejection rather than only a transport-level 400.

    Environment

    • Observed: 2026-08-31
    • Codex App: 26.825.6671.0
    • codex-cli: 0.151.0-alpha.7.2
    • Platform: Windows x64
    • Model: gpt-5.6-sol through a custom Responses-compatible provider
    • Effective context window: 272,000 tokens

    Sequence and measurements

    1. The user attached a PNG measuring 2012x938, 1,174,733 bytes.
    2. Codex called:
      tools.view_image({ path, detail: "original" })
    3. The tool result placed the image back into history as an input_image containing a data:image/png;base64,... URL, roughly 1.57 MB inline.
    4. The last successful model request before that tool result reported 27,153 input tokens.
    5. The very next request was rejected before generation:

    Your input exceeds the context window of this model. (prompt is too long: 498792 tokens > 272000 maximum) (trace: t22ffb61nrl)

    A forked subagent inheriting the same history failed similarly at 497,711 tokens.

    Why this seems relevant

    This looks like the same client-side hazard reported here, with an additional failure mode: a Responses-compatible provider may account for or serialize the inline data URL as prompt text. A single tool result can therefore jump from about 27k to about 499k tokens, completely skipping the auto-compaction threshold. At that point, no recovery or compaction request can be assembled within the model window.

    The provider is custom, so its image-token accounting may differ from OpenAI's service. However, the unbounded Base64 artifact is still produced and retained by the Codex client, and there is no local guardrail before the next request.

    Expected safeguards

    • Downscale/re-encode images to a sensible pixel and byte ceiling before adding them to history.
    • Keep the original out of band as a blob/file reference; send a preview or bounded crop to the model.
    • Content-hash deduplicate an existing attachment and a later view_image result.
    • Preflight both serialized bytes and provider-estimated tokens before sending.
    • If remote compaction cannot run, locally elide or replace the heaviest inline image items and retry.

    No image, local path, session ID, provider URL, or credential is included in this report.

  3. a18846013774-blip commented on Sep 1, 2026

    @a18846013774-blip

    I can reproduce the same byte-vs-token asymmetry on the current macOS desktop build, with an additional failure mode: compaction succeeds and the thread remains usable, but every subsequent turn takes 2–3 minutes before the first reasoning item because the compacted history still resends all retained image data URLs.

    Environment

    • Codex App: 26.825.51511
    • Bundled Codex: 0.151.0-alpha.7.2
    • Platform: macOS 26.3.2 arm64
    • Model: gpt-5.6-sol, reasoning_effort=max
    • Transport: Responses HTTP with zstd request compression
    • Relevant features reported enabled: compaction_image_budget, remote_compaction_v2, enable_request_compression

    Compaction still retains the full image bytes

    Across five successful compactions, the compacted record grew as follows:

    Window Retained PNG data URLs Compacted record
    1 3 7.7 MB
    2 6 14.0 MB
    3 10 24.7 MB
    4 19 47.8 MB
    5 23 57.9 MB

    The latest replacement_history contains 134 items and 23 complete data:image/png;base64,... values totaling 57,737,086 characters. These are not summaries or file references.

    This happened after the app had already updated to the bundled version above, with compaction_image_budget=true.

    Low token count after compaction, but ~140 s to first reasoning

    Immediately after successful compaction:

    • Window 4: the context counter reset to 70,161 tokens; the first real request used 85,236 input tokens (uncached); first reasoning appeared after 136.7 s.
    • Window 5: the counter reset to 79,983 tokens; the first real request used 96,325 input tokens (uncached); first reasoning appeared after 140.5 s.

    So the UI/token accounting says the context is small, but the wire payload remains very large.

    Same machine/network/model comparison

    Compacted long thread Fresh thread
    Request before zstd 58,849,614 bytes 927,878 bytes
    Request after zstd 28,795,422 bytes 640,692 bytes
    Local zstd duration 298 ms 1 ms
    POST to HTTP 200 181.6 s 5.48 s
    Submit to first reasoning 190.5 s 24.1 s
    Input tokens 286,326 31,883

    Local history parsing/compression was not the multi-minute bottleneck: the long-thread request was dispatched about 1.6 seconds after submission. Nearly all of the delay occurred after the 28.8 MB compressed request was sent and before response headers arrived.

    A later call still had 357,632 / 361,443 input tokens cached, but the serialized request was about 62.9 MB (30.8 MB after zstd) and HTTP 200 still took about 153 seconds. Prompt caching therefore does not mitigate repeatedly transmitting/deserializing the inline image bytes.

    Expected safeguard

    In addition to token budgeting:

    • Deduplicate retained images by content hash.
    • Store images out of band and replay stable file/blob references instead of full data URLs.
    • Apply a serialized-byte budget during compaction; pre_compression_bytes is already measured.
    • Evict or replace old image items even when their model-token cost is low.
    • Avoid resending unchanged image bytes on every sampling request.

    This is the slow-but-not-hard-failed version of the issue: the server accepts the payload, but the thread becomes practically unusable despite successful compaction and a low post-compaction token count.

    No session ID, repository content, local paths, images, credentials, or raw logs are included here.

  4. huntario commented on Sep 6, 2026

    @huntario

    Three ordinary view_image calls produced ~2.74 MB of persisted inline image data in a 3.0 MB rollout. Subsequent requests replayed all three input_image tool outputs with reasoning.context: "all_turns", eventually producing 413 Failed to buffer the request body. Removing those image objects reduced the rollout to ~438 KB.

    Workaround (remove images from conversation history)

    ROLL=$(ls -t ~/.codex/sessions/*/*/*/rollout-*.jsonl | head -n 1)
    cp "$ROLL" "$ROLL.backup"
    
    python3 - "$ROLL" <<'PY'
    import json, sys
    p=sys.argv[1]
    
    def clean(x):
        if isinstance(x, list):
            return [
                clean(v) for v in x
                if not (
                    isinstance(v, dict)
                    and v.get("type") == "input_image"
                    and isinstance(v.get("image_url"), str)
                    and v["image_url"].startswith("data:image")
                )
            ]
        if isinstance(x, dict):
            return {k: clean(v) for k,v in x.items()}
        return x
    
    out=p+".fixed"
    with open(p) as src, open(out,"w") as dst:
        for line in src:
            dst.write(json.dumps(clean(json.loads(line)), separators=(",",":"))+"\n")
    print(out)
    PY
    
    mv "$ROLL.fixed" "$ROLL"
    

    codex resume --last


    Reproduce:

    1. Start a Codex Desktop/CLI thread.
    2. Give it the path to a screenshot and ask it to inspect it.
    3. Repeat with 2–3 additional screenshots in separate turns.
    4. view_image returns each screenshot as an inline data:image/...;base64,... input_image.
    5. Send another normal text message.
    6. Codex replays all previous inline image tool outputs in the request body.
    7. Eventually the request fails with 413 Payload Too Large: Failed to buffer the request body.

    Results in this in the request

    {
      "type": "custom_tool_call",
      "name": "exec",
      "input": "const r=await tools.view_image({path:\"...Screenshot-2026-09-05-at-10-40-05-PM...png\"}); image(r.image_url);"
    }
    

    Then Codex stores the result as:

    {
    
      "type": "custom_tool_call_output",
      "output": [
        {
          "type": "input_image",
          "image_url": "data:image/png;base64,..."
        }
      ]
    }
    

    Basically... just don't use screenshots or your conversation will be corrupted and unusable.

    !!! AGI IN 2 WEEKS!!!! !!!

  5. pang-cream commented on Sep 6, 2026

    @pang-cream

    I can confirm that this issue is still reproducible on codex 0.153.4.

    I inspected one affected Codex Desktop thread using a custom Responses provider over HTTP. The thread had been successfully compacted earlier, and immediately after that compaction there were no inline images left in the active reconstructed history. However, after subsequent view_image calls, the image payload accumulated again.

    Measurements from the affected thread:

    • Codex version: 0.153.4
    • Transport: HTTP Responses API through a custom provider
    • Historical inline images currently retained: 20
    • Total size of the image data URLs: ~25.65 MB
    • Reconstructed active history size: ~26.47 MB
    • Image data accounts for ~96.9% of that active history
    • Exact duplicate images: 0
    • My gateway observed subsequent individual requests in the 20+ MB range

    The important detail is that this is not one image being duplicated many times inside the same request.

    These are 20 different images produced by earlier view_image calls. The problem is that old image payloads remain in active conversation history and continue to be carried by later model requests, including turns where no new image is added and the old images are no longer relevant.

    In this thread, image history grew approximately like this after the last successful compaction:

    • after compaction: no inline images in the reconstructed active history
    • later: ~13.09 MB of image data
    • later: ~20.28 MB
    • eventually: ~25.65 MB

    After reaching ~25.65 MB, later model calls continued to retain those images.

    A manual /compact has been an effective workaround in this thread: previous successful compactions removed the inline images from the reconstructed active history. But the images start accumulating again as soon as more view_image calls are made, so this does not appear to fix the underlying issue.

    For accuracy: the 25.65 MB image size and 26.47 MB active-history size are measurements reconstructed from the local session history, not packet-captured HTTP Content-Length values. The 20+ MB per-request size is what I observed separately at the gateway, so I do not want to claim the reconstructed size is an exact wire-size measurement.

    I have also seen some 502/520/send/retry failures in the affected session, but I cannot establish that all of those failures were caused by the retained images.

    This seems to reinforce the core issue described here: image payload size is largely invisible to token-based context management, while the same inline Base64 data can remain expensive at the transport layer across subsequent requests.

    Ideally, historical view_image results should either be stored/referenced out-of-line, aggressively byte-bounded, or removed from active request history once they are no longer needed, rather than repeatedly serializing the full Base64 payload.

    I can provide additional sanitized measurements if useful. I can also submit /feedback from the affected thread for internal diagnostics without posting the raw session file, image Base64, provider URL, or credentials publicly.

  6. xuanmingguo commented on Sep 8, 2026

    @xuanmingguo

    Additional measurements and an isolated reproduction of the error-classification gap on Linux, with Codex 0.151.0 and 0.153.4.

    Separating the causes

    Our initial incident included a bug in our own custom Responses proxy: a compatibility transform duplicated tool-result images. That duplication was not a Codex bug, and we fixed it separately. Nevertheless, the byte/token mismatch and inability to recover from an ordinary HTTP 413 remain relevant here.

    After removing duplication without changing image bytes or quality, a real request containing 14 synthetic tool images was 29,803,115 serialized bytes, while reported input usage was only 11,741 tokens. The model correctly read the image marker and the stream ended with response.completed. Thus low token usage did not imply a small HTTP body. The gateway in our setup reported a 52,428,800-byte limit; this is an observed provider-specific limit, not a proposed universal Codex limit.

    Reproduction independent of any model or credentials

    Using an isolated CODEX_HOME, a localhost mock Responses HTTP server, and codex exec --ephemeral --json with a custom provider (wire_api = "responses", requires_openai_auth = false, HTTP/stream retries set to zero), submit a short text prompt. Return one of these responses for POST /v1/responses:

    1. HTTP 413, Content-Type: text/plain:
      Request body too large. Maximum size is 52428800 bytes

    2. HTTP 400, Content-Type: application/json:
      {"error":{"type":"invalid_request_error","code":"context_length_exceeded","message":"Synthetic context limit for local reproduction."}}

    3. HTTP 200, Content-Type: text/event-stream, followed by a blank line:

      event: response.failed
      data: {"type":"response.failed","response":{"status":"failed","error":{"type":"invalid_request_error","code":"context_length_exceeded","message":"Synthetic context limit for local reproduction."}}}
      

    Both installed versions produced the same results, with one request per case:

    Response Observed CLI error
    HTTP 413 unexpected status 413 Payload Too Large: ...
    HTTP 400 with explicit context error code Raw JSON error body
    SSE response.failed with the same code Codex ran out of room in the model's context window...

    All three end the current turn. The mock test establishes classification, not successful compaction. Separately, with the resident 0.151.0 App Server and a real custom provider, translating a known body-budget rejection into the third form caused contextWindowExceeded; the next turn performed native contextCompaction, completed with a retained text marker, and subsequently used view_image successfully. This was not same-turn recovery and did not retain every historical image in active context.

    Current source and possible fix direction

    Static inspection of main at 6fee98cc85a69ee44a856121cac0f64505cf48d0 still shows the distinction between HTTP error mapping and SSE context-error recognition. This is source inspection, not a runtime test of that commit.

    It would help to recognize structured HTTP context-limit errors and provide bounded, byte-aware recovery for request-size rejections. HTTP 413 need not mean token-window exhaustion: recovery should avoid retrying an identical oversized body, ensure the compaction request itself fits, and preserve current image inputs or explain when they require splitting. A provider-specific serialized-body budget should be separate from token accounting. Related recovery discussion: #7808.

    Submitting these measurements here under the current contribution policy rather than opening a duplicate issue or external PR. Only synthetic image measurements and sanitized errors are included.

  7. mycyg commented on Sep 8, 2026

    @mycyg

    Additional measurements on Codex core 0.153.4 / macOS arm64, using the built-in OpenAI provider with ChatGPT authentication. This case adds a large compressed request payload, repeated compaction retries, and a small WebP encoding comparison.

    Environment and workload

    • Desktop app: 26.901.51231; bundled core: 0.153.4.
    • Model: gpt-6-astra, reasoning effort xhigh.
    • remote_compaction_v2, compaction_image_budget, and request compression were already enabled.
    • Workload: a long slide-creation / visual-QA task with repeated image inspection and screenshot/tool outputs.
    • These failures occurred with the built-in openai provider, before subsequent timeout and image-preprocessing experiments.

    Measurements

    Across the recorded conversation, 97 distinct images entered as original response items: 85 from tool outputs and 12 from user messages. This count excludes duplicated event records and historical compaction snapshots.

    At the failing compaction, reconstructed active history contained 53 images, with approximately 32.86 MB of image data URLs. The compaction request was 33.58 MB before zstd and 24.39 MB after zstd. The history measurement strongly suggests images account for most of the request size; I did not capture and independently parse the wire request.

    Sanitized request-compression measurements for the initial attempt and its two retries:

    pre_compression_bytes=33576568 post_compression_bytes=24394918 compression_duration_ms=67
    pre_compression_bytes=33576568 post_compression_bytes=24394934 compression_duration_ms=66
    pre_compression_bytes=33576568 post_compression_bytes=24394920 compression_duration_ms=62
    

    Local zstd compression was fast. The long delay was in the remote request lifecycle:

    Attempt Approx. time until HTTP 200 response headers Outcome
    Initial 93 seconds Stream failed after 6m29s total
    Retry 1 100 seconds Stream failed after 6m02s total
    Retry 2 108 seconds Still unfinished when interrupted

    The whole compaction cycle lasted 18m42s before interruption, with no successful compaction record. The final attempt was interrupted, not a third confirmed network failure.

    remote compaction v2 stream failed; retrying request after delay
    retries=1 max_retries=2
    compact_error=stream disconnected before completion:
    Transport error: network error: error decoding response body
    

    The same error recurred with retries=2 max_retries=2. Each retry reconstructed essentially the same large payload; no meaningful reduction in request bytes was observed.

    A separate affected conversation sent a 29.27 MB raw / 21.48 MB zstd compaction request and, after approximately 15m08s, received:

    408 Request Timeout
    {"detail":"Request body read timed out"}
    

    One important distinction: an earlier successful compaction in the main case did reduce the replacement history to 15 items containing one user image. Subsequent image inspection then grew the active image count back to 53. This case is therefore about image accumulation within the next window and failure to complete the next compaction, rather than proof that every successful compaction retains every old image.

    Reproduction shape and uncertainty

    The observed workflow is: run a long visual task, repeatedly inspect generated pages/screenshots, accumulate dozens of distinct tool images, and allow mid-turn auto-compaction to trigger. I do not yet have a standalone deterministic reproducer.

    There were confirmed proxy/connection failures earlier in one run, including WebSocket failure followed by HTTP fallback. I also experience similar compaction stalls when ordinary network use appears normal, but that does not establish that the long-lived inference connection is healthy. These measurements do not prove a universal byte limit or identify which network hop/server terminated every stream. They do show that the recovery path repeatedly carries a substantial image payload.

    Proposed improvement

    1. Prepare images before they accumulate in history. Offer suitable re-encoding and dimension limits, with lossless handling for dense text/screenshots and preservation of coordinate-sensitive images. Skip conversion if it increases the payload.
    2. Make the existing image budget observable and configurable in bytes as well as count/tokens. Surface raw/compressed request sizes and enforce a useful bound before starting compaction.
    3. Summarize older tool images incrementally. Preserve grounded visual findings and retrievable asset references, allowing older raw screenshots to be omitted from compaction when safe. Keep essential user references and make loss of visual detail explicit; avoid silently discarding task-critical images.
    4. Provide a lighter recovery path. After a compaction stream fails, offer a bounded-image retry or local history reduction that preserves originals, rather than always resending the same large payload. Asset references/cache reuse could also reduce repeated uploads where the backend supports them.

    WebP feasibility measurement

    Five images sampled across the affected active history, at unchanged dimensions:

    Encoding Total image bytes Reduction
    Original PNG/JPEG 3,039,267 —
    Lossless WebP 2,111,024 30.5%
    WebP quality 85 465,202 84.7%

    The lossless outputs passed decoded-pixel equality checks. The lossy outputs were measured for size, not validated for task-specific readability. These are five-image results, not a benchmark of the entire request or proof that re-encoding fixes the disconnects. Same-dimension re-encoding primarily addresses upload bytes; visual token usage should not be assumed to fall proportionally.

  8. FengYeeLx commented on Sep 9, 2026

    @FengYeeLx

    Related reports with additional current-stable evidence

    For cross-reference, I have two related reports from Codex CLI 0.153.4 / Windows:

    I am linking both here because large retained image history can make full-request retry/fallback paths substantially more expensive, while #43022 demonstrates that WebSocket liveness/retry behavior has an independent failure mode. I am not claiming that the two issues have the same root cause.

  9. jroth1111 commented on Sep 11, 2026

    @jroth1111

    Additional recovery evidence: a deterministic localhost mock reproducer using the real Codex CLI, plus an important compatibility constraint for offline repair of newer rollouts.

    Reproduction and measured recovery

    Tested separately with codex-cli 0.154.0 and 0.152.1 on macOS. Each test used an isolated temporary CODEX_HOME, a custom Responses provider bound only to localhost, no credentials, and entirely generated images. This was not a request to the production OpenAI gateway.

    1. Create a normal persisted thread using a minimal successful mock SSE response, then exit its CLI process.
    2. Append 12 paired synthetic function_call/function_call_output records containing 12 distinct valid 1280×1280 noise PNGs as input_image data URLs. Preserve consecutive ordinals for paginated rollouts. Each encoded image is below 10 MiB.
    3. Resume with a short text-only prompt. The mock rejects JSON request bodies above 67,108,864 bytes with HTTP 413 and Encoded request body exceeds 67108864 bytes.
    4. With the test client stopped, preserve the original rollout, extract older images to hash-verified files, and append a compatible compacted recovery checkpoint. Its replacement_history preserves all active response items and call/output IDs, replacing ten older image objects with input_text archive references while retaining the two most recent images. Preserve associated harness metadata and consecutive ordinals. No model summarization is performed.
    5. Resume the same thread against the same mock.

    Both versions produced these measured outbound JSON sizes:

    Before recovery After recovery
    Request bytes 78,732,011 13,175,160
    Inline images 12 2
    Explicit archived-image references 0 10
    CLI exit code 1 0

    The successful final response came from the mock. This establishes that the real client can replay the bounded checkpoint and complete a follow-up request; it does not establish production-gateway recovery, model reasoning quality, or the production cap's exact accounting.

    A separate affected local rollout had 117,965,114 bytes of reconstructed active response-item JSON and 79 inline image occurrences. That is an offline history estimate, not a captured request. It contained only one compaction checkpoint, so deleting stale checkpoints would not address that particular active-history payload.

    Why repair should be native

    Blindly shrinking existing JSONL records is unsafe for newer paginated storage. On source commit fc948f8c473e5d11e780ffcf1fd7f812a2020932, thread-history materialization resumes from stored byte offsets, while model-context loading reconstructs paginated lineage and replacement checkpoints. The tested approach leaves every original byte/ordinal unchanged and adds a recovery suffix. It does not reduce the on-disk rollout size.

    The local repair implementation passed 19 unit tests covering unchanged history prefixes, metadata/ordinals, backup and extracted-image hashes, recent-image retention, malformed input, concurrent changes, and refusal to edit running clients. It declines unsupported lineage/rollback/advanced-history cases rather than guessing. A file not appearing in lsof is insufficient by itself: an app-server can still hold cached history in memory.

    An official byte-aware request preflight and offline recovery operation would be valuable, covering both inference and compaction, preserving recent visual inputs and retrievable originals, and avoiding repeated submission of the same deterministically oversized body. Related: #30441 and #43015.

    Only synthetic test results, aggregate measurements, and public source references are included here. No real thread IDs, local paths, prompts, original images, raw rollouts, account data, provider credentials, or private logs are attached.


    Update: recurrence during continued work, and a recovery copy that leaves the source untouched

    The affected long-running Goals workflow became unusable again after continued image review. The saved active history had accumulated 64 new inline image occurrences, totaling 118,506,921 bytes of reconstructed active response-item JSON. These are offline history measurements, not a captured wire payload.

    The local workaround's limitation matters: its background repair waited for Codex/IDE clients to exit, so it repeatedly deferred while the workload remained active. Offline repair is recovery, not prevention of an active request-size failure. This limitation belongs to the external workaround; it is not a claim that Codex ships that maintenance behavior. Enabling the already-enabled compression and compaction-image-budget flags did not add an aggregate active-request byte guard.

    The installed CLI had been upgraded to 0.154.0, but that does not upgrade already-running processes or the desktop app's separately bundled core. I am therefore not presenting the recurrence as a clean 0.154.0 regression test.

    A revised recovery path now snapshots the source, checks that it did not change during preparation, and publishes a separate self-contained conversation under a new identity with a bounded replacement checkpoint. It does not edit the loaded source conversation or assume that a closed file descriptor means the app has unloaded it. Existing response items, call/output pairs and metadata are retained; selected older image objects become explicit references to preserved originals.

    For the affected conversation:

    • The new copy's reconstructed active history was 7,611,133 bytes, retaining three inline images.
    • The original rollout's SHA-256 was unchanged after preparation and verification.
    • The recovered copy completed a live verification turn on the original model, returning the requested short marker without tool calls or dataset changes. This used the configured provider path; it was not an independently captured direct-gateway request or a test that the full long-running workflow will stay healthy.
    • The original task's on-disk work remained available. Goals database state was not copied or automatically restarted.

    The separate, entirely synthetic CLI 0.154.0 + localhost 64 MiB mock test also passed for this recovery-copy approach: 78,732,011 bytes / 12 images rejected → 13,175,417 bytes / 2 images accepted, with ten archive references and the source rollout unchanged. The local repair utility passed 24 unit tests, including recovery while other clients exist, refusal to publish a copy that still exceeds its target, metadata/ordinal preservation, unchanged source bytes, and live scanning despite in-place repair being deferred. These utility tests are not upstream Codex integration-suite results.

    Requested native behavior remains:

    1. Enforce a serialized-byte budget before both inference and compaction requests, including aggregate growth from many individually acceptable images.
    2. Provide supported recovery that can unload/repair a thread safely or create a bounded recovery copy, preserving originals, context metadata and pagination semantics.
    3. Surface the byte-size failure and actionable recovery in the UI; do not imply that token headroom or a successful prior cleanup prevents recurrence.

    No private IDs, paths, images, prompts, credentials, account identifiers or raw logs are included in this update.

  10. htbk commented on Sep 14, 2026

    @htbk

    Additional report with an explicit 64 MiB rejection, repeated across three image-heavy desktop tasks, including a clean continuation that failed again. Posting at the account owner's request after preserving the work and inspecting local evidence.

    Environment inspected September 14, 2026

    • macOS 26.6.2, arm64.
    • Desktop bundle com.openai.codex, version 26.908.40834, build 8881 (read from the installed bundle).
    • Bundled executable: codex-cli 0.154.0-alpha.6.2.
    • These are the currently installed versions; I have not established the exact build at every earlier failure or bisected a regression.

    Actual failure

    {"error":"decompressed request body exceeds 67108864 bytes"}

    The task becomes unable to process subsequent short prompts. Continuing from a concise handoff in a fresh task initially works, but the same failure recurs as native image results accumulate. A third image-heavy task on the same installation also failed. For that task, the owner reported that manual compaction failed or was unavailable. A fresh continuation from saved files completed successfully.

    Measurements

    A read-only streaming audit of two preserved JSONL histories found:

    Case Estimated active serialized record bytes Native inline image bytes in active history Total history file bytes
    Original failed task 83,701,423 83,046,675 6,167,904,849
    Clean replacement, later failed 69,257,931 68,206,629 85,733,059

    The native image payload alone exceeds 67,108,864 bytes in each case. The replacement's largest single native tool-result record was 15,907,967 bytes; another was 11,780,403 bytes. This does not depend on a user pasting base64 as message text.

    Method: count active response_item/turn_context serialized records after the latest compaction, including retained replacement history, and count native inline image fields. Exclude duplicate UI event_msg copies. These are local history estimates, not captured HTTP request lengths; the HTTP envelope, system instructions and tool definitions are not included. Total JSONL file size is reported separately and is not asserted to be the outgoing body size. Both histories parsed without malformed records.

    Observed workflow and impact

    1. Use built-in image generation and local image viewing while producing illustrated documents.
    2. Continue reviewing successive images and rendered pages in one task, including across compactions.
    3. Send or continue with an ordinary short instruction.
    4. Receive the exact decompressed-body error above; subsequent attempts in that task fail again.
    5. Moving only the task/goal to a clean continuation does not prevent recurrence when the same image workflow continues.

    Existing files were recoverable. Generated originals and useful work stranded outside the main project needed explicit recovery. This is loss of task usability, not a claim that the service deleted local files. I have not intentionally reproduced it again after installing a workaround.

    Requested safeguards

    • Preflight serialized/decompressed request bytes independently of model token counts, with room for envelope overhead.
    • Bound and content-deduplicate image representations entering active context while retaining full-quality originals outside the request.
    • Make compaction byte-aware, including images retained in replacement history.
    • Provide a supported, non-destructive recovery path when remote compaction would itself exceed the request limit; repeated submission of the same oversized history does not recover it.
    • Surface the byte budget, offending items and actionable recovery state in the UI.

    Our current workaround is a conservative 16 MiB local active-history budget, an 8 MiB reserve before a native image-generation result, and viewing-only JPEG derivatives capped at 384 KiB (plus native crops when needed). Production originals remain unchanged. The guard has already stopped further generation before the failure threshold. This is a workflow workaround, not a fix to Codex.

    No private document content, images, conversation transcripts, credentials or private paths are included in this report.

  11. HankHuang0516 commented on Sep 23, 2026

    @HankHuang0516

    A fresh recurrence is reproducible on the current Codex desktop build.

    Sanitized diagnostics:

    • Codex desktop: 26.917.51856 (build 10492), macOS 26.6.2
    • Bundled Codex CLI: 0.155.0-alpha.16
    • Preserved rollout JSONL size: 166,507,202 bytes across 8,911 records
    • 9 individual records exceed 8 MiB
    • Largest record is 19,692,286 bytes and contains 5 inline data:image/png;base64 URLs
    • Other oversized records are approximately 15.49, 15.17, 14.74, 10.71, 10.05, 9.74, 9.71, and 9.55 MB
    • The recurrence happened after a successful compaction (window 7): additional inline image tool outputs were appended afterward, so token-aware compaction did not prevent the serialized request body from exceeding 67,108,864 bytes
    • Some tool output records also duplicate large content between stdout and aggregated_output
    • Both the older global CLI and the bundled CLI could not archive/unarchive the task while the desktop app held the rollout file open

    This confirms the issue persists in a newer desktop/bundled CLI build. The missing guardrail is still byte-aware request preflight plus bounded/deduplicated image persistence. A supported recovery action that creates a text-only continuation from the latest compaction would also prevent users from having to edit live rollout files.

  12. bickov commented on Sep 26, 2026

    @bickov

    Same blind spot shows up on the Anthropic side too, just from the other end. instead of the token counter missing the bytes, claude code enforces a hard pixel gate before token accounting is even involved. the exact error there is: "An image in the conversation exceeds the dimension limit for many-image requests (2000px). Start a new session with fewer images." so on that stack the failure is a wall, not a silent accumulation, but the underlying issue is the same, image size is decoupled from whatever the client's context management is actually watching.

    One thing about the idea of sending a preview or bounded crop instead of the raw image, further up in the thread. that only helps if the preview is actually cheaper in tokens, and that's not automatic. I measured a downscaled screenshot against a structured text extraction of the same page. on a single screen the extraction runs a bit cheaper, something like 1,000 tokens against 1,600, nothing dramatic. on a tall page it can go the other way, extraction ran to about 11k tokens against under 500 for the image in one test I ran. so whatever preflight check gets built here should probably measure both candidates rather than assume text or a resized crop is automatically the cheap option.

  13. chipfighter commented on Sep 28, 2026

    @chipfighter

    Similar here on the Windows desktop app. I was iterating on image generation with GPT in one long task, and after a dozen or so images the connection kept dropping and the retries every few minutes never went through. Every request was carrying every image from the task's history again, about 44 MB each. The only way out was a new task, which loses the conversation.

    While this is open, I built a workaround and open-sourced it (disclosure: I'm the author): Codex Attachment Manager, a plugin for the Codex desktop app.

    • A tab in the side panel lists every image in the task, turn by turn: the ones you attached and the ones the model viewed through tools. It also estimates the size of the next request in MB rather than tokens, since that's the number that breaks here.
    • Uncheck the images you no longer need. From the next message on, a local proxy replaces them with short placeholders before the request leaves your machine. Same task, same conversation; the rollout on disk isn't modified.
    • The model is told the images were left out on purpose. If it needs one back, it replies need IMG-xxx and you check it again.

    Limits: desktop app only; default OpenAI backend only (it won't install over your own openai_base_url); results of Codex's built-in image generation can't be unchecked. Tested on Windows with Codex 26.924. macOS and Linux only pass CI so far, so reports are welcome.

    Install it from Codex's Plugins page (add the marketplace chipfighter/codex-attachment-manager) or with a one-line command; see the README.

    Every image in the task is unchecked. Asked about v1's chat button, the model replies need IMG-002; the panel points to IMG-002, the user checks it, and the model answers from the image.

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 workingcontextIssues related to context management (including compaction)tool-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