Desktop threads can be poisoned by inline base64 tool images, leading to `{"detail":"Bad Request"}` on resume and likely inflated token usage

Open 💬 10 comments Opened Apr 20, 2026 by mikezio
💡 Likely answer: A maintainer (github-actions[bot], contributor) responded on this thread — see the highlighted reply below.

Summary

Codex desktop appears to persist image-producing tool output inline into replayable session history, especially as function_call_output items containing input_image payloads with data:image/...;base64,... content.

After enough of these accumulate, the thread can become unstable and start failing on later turns with:

{"detail":"Bad Request"}

This also appears likely to inflate model input usage for the affected thread because the replay payload becomes extremely large.

User-visible symptoms

  • A thread works normally for a while, then later starts failing with:

``json
{"detail":"Bad Request"}
``

  • The agent stops mid-session or fails on later continue attempts.
  • Once a thread reaches this state, it may keep failing repeatedly.
  • The affected thread/session becomes extremely large.

What I found locally

I inspected saved session JSONL data under ~/.codex/sessions/... and found many oversized function_call_output entries containing embedded inline image payloads such as:

  • type: "input_image"
  • image_url: "data:image/png;base64,..."

These image payloads are being persisted directly into replayable session history rather than stored out-of-line.

In the affected thread I observed:

  • repeated event_msg errors with:

``json
{"detail":"Bad Request"}
``

  • many very large function_call_output records containing base64 images
  • very large cumulative input-token counters in the same thread
  • app-state telemetry showing very large delta-byte growth around the same period

Likely root cause

The desktop app seems to serialize tool screenshots inline into session state.

A strong lead is in the bundled browser-use plugin code shipped with the app bundle. In the installed app I found code in both:

  • /Applications/Codex.app/Contents/Resources/plugins/openai-bundled/plugins/browser-use/scripts/browser-client.mjs
  • /Applications/Codex.app/Contents/Resources/plugins/openai-bundled/plugins/chrome/scripts/browser-client.mjs

that converts screenshot buffers into inline data URLs before emitting them:

processImage:t=>{
  if(!Buffer.isBuffer(t)) throw new Error(...);
  return `data:image/jpeg;base64,${t.toString("base64")}`
}

That means screenshot-heavy tool output can enter replayable thread state as huge inline strings.

Why this matters

This is more than a display bug:

  • it can permanently poison a thread
  • it can trigger replay/resume failures with 400 / {"detail":"Bad Request"}
  • it likely wastes context/token budget if these payloads are replayed or summarized into later requests
  • it probably affects any user doing longer sessions with image-heavy tools (Computer Use / browser-use / screenshot-producing tools)

Repro idea

  1. Start a long Codex desktop thread.
  2. Use Computer Use / browser-use or another screenshot-producing tool repeatedly.
  3. Continue the same thread many times.
  4. Inspect the saved session JSONL and observe large input_image / data:image/...;base64,... payloads inside function_call_output.
  5. Eventually the thread starts failing with:

``json
{"detail":"Bad Request"}
``

Expected behavior

Tool images should not be embedded as large base64 strings in replayable session history.

Instead, Codex should:

  • store image blobs out-of-line
  • persist only a file/blob reference or lightweight handle
  • cap / summarize oversized tool outputs before they enter replayable thread state
  • reject or sanitize replay-hostile payloads during persistence and/or resume

Actual behavior

Large inline base64 screenshots are persisted directly in function_call_output, and the thread later fails with:

{"detail":"Bad Request"}

Suggested fix

  • externalize tool images to blob/file storage before persistence
  • persist only lightweight references in session history
  • add hard caps/truncation for replayed tool output
  • prevent megabyte-scale inline payloads from being stored in replayable thread context
  • consider a repair/sanitization path for already-poisoned sessions

Notes

I also suspect this bug may contribute to inflated subscription/token usage for affected threads because the stored payload becomes massive and may be replayed to the model on later turns.

If helpful, I can provide a sanitized example session structure showing the oversized function_call_output / input_image pattern without exposing private data.

View original on GitHub ↗

10 Comments

github-actions[bot] contributor · 3 months ago

Potential duplicates detected. Please review them and close your issue if it is a duplicate.

  • #17880
  • #18450
  • #17392
  • #18572

Powered by Codex Action

cnjimmyshao · 2 months ago

I hit the same class of issue on macOS with Codex Desktop, with an additional failure mode that blocks startup / verification.

Environment:

  • Codex.app: 26.422.30944, CFBundleVersion 2080
  • Bundled app-server reported: 0.125.0-alpha.3
  • Electron: 41.2.0
  • Platform: macOS arm64

Observed symptoms:

  • Codex Desktop became stuck during verification/startup.
  • Main-process dialog:

RangeError: Invalid string length
at Mu.write / Socket.onStdoutData
in workspace-root-drop-handler-B7KjFQ8M.js.

Local evidence:

  • ~/.codex/sessions had grown to about 2.8 GB.
  • Three rollout JSONL files were abnormal: about 1.4 GB, 1.3 GB, and 60 MB.
  • Sampling the large JSONL lines showed image_generation_call / function_call_output records containing inline data:image/png;base64,... payloads.
  • A compacted history entry also embedded prior image payloads into replacement_history.
  • Desktop log showed:

Failed to parse MCP message
with lineBytes=537005464
for a thread/read response.

  • Moving those oversized rollout files out of ~/.codex/sessions reduced the active session directory to about 12 MB and allowed Codex Desktop to start normally again.

Expected:

  • Generated/tool images should be stored out-of-line or referenced by blob/file handles.
  • thread/read should not emit hundreds of MB in a single stdout JSON line.
  • Desktop should cap/skip/repair oversized sessions instead of hanging during verification or throwing a main-process RangeError.

Workaround confirmed:

  • Quarantine the oversized rollout JSONL files outside ~/.codex/sessions, then restart Codex Desktop.
neokry · 2 months ago

I saw a closely matching failure in Codex Desktop on macOS while testing a local Vite app.

Flow:

  • Spawned a subagent to playtest http://127.0.0.1:5123/
  • It loaded the page with Playwright and interacted successfully through visible UI/roles
  • It opened the in-game "Lab Manual" modal successfully
  • Then it called page.screenshot() and emitted it with nodeRepl.emitImage({ bytes, mimeType: 'image/png' })
  • The session log then stored a very large function_call_output containing an input_image with an inline data:image/png;base64,...
  • The agent stopped making progress after that image output and had to be shut down after ~3 minutes

This happened in a fresh subagent thread, so the stall was not caused by a long existing chat history. The symptom strongly suggests a single inline screenshot payload can be enough to destabilize or heavily inflate the active thread, not only repeated screenshots over a long session.

Astro-Han · 2 months ago

I hit the same underlying issue with Codex App's built-in imagegen, not browser-use or computer-use screenshots.

Environment:

  • Codex App: 26.506.21252 (2575)
  • Platform: Darwin 25.4.0 arm64 arm
  • Plan: omitted

Observed failure mode:

  • After restarting Codex App, it restored an old image-heavy session and became CPU-heavy / stuck.
  • Moving the oversized rollout JSONL out of ~/.codex/sessions and restarting Codex App stopped the high CPU.
  • While the app was still running, app-server still held an open file descriptor to the moved rollout file, so the full restart was needed to release it.

Sanitized local evidence:

  • Affected rollout JSONL size: 235 MB
  • JSONL lines: 4,256
  • Largest single JSONL line: 9,455,323 bytes
  • Event/type counts:
  • function_call_output: 745
  • function_call: 745
  • image_generation_call: 29
  • event_msg: 1,877
  • turn_context: 86
  • compacted: 7
  • The largest sampled records were response_item / function_call_output entries containing inline image payloads. The output payload length was about 9,455,166 chars.
  • Built-in imagegen payloads also contributed heavily:
  • image_generation_call records: 29
  • total image_generation_call.result chars: 90,454,556
  • max single image_generation_call.result chars: 3,924,372
  • The same imagegen payloads also appeared in image_generation_end event messages:
  • image_generation_end records: 29
  • total image_generation_end.result chars: 90,454,556
  • max single image_generation_end.result chars: 3,924,372

So this appears to affect Codex App's built-in imagegen path as well, not only screenshot-producing tools. It also overlaps with #21211 because restoring/opening the large image-heavy session caused high CPU during history hydration.

Expected:

  • Large image/tool outputs should not be stored inline in replayable session JSONL.
  • Store image blobs out-of-line as artifacts and keep only metadata/references in JSONL.
  • Avoid duplicating large image payloads across event_msg and response_item surfaces.
  • Session restore should lazy-load heavy image/tool payloads or skip/repair oversized sessions instead of eagerly parsing/rendering them.

I am intentionally omitting local paths, session id, prompts, image data, workspace names, and external service content for privacy.

cnjimmyshao · 2 months ago

I took a quick look at the current main code path and I think this issue has a fairly crisp persistence-layer shape.

The recurring pattern seems to be:

  • UserInput::Image / UserInput::LocalImage become ContentItem::InputImage { image_url: "data:image/...;base64,..." } in codex-rs/protocol/src/models.rs.
  • RolloutRecorder::record_items keeps persisted ResponseItems mostly as-is.
  • JsonlWriter::write_rollout_item serializes the RolloutItem directly to JSONL, so any inline image data URL in:
  • ResponseItem::Message
  • FunctionCallOutput / CustomToolCallOutput content items
  • ImageGenerationCall.result
  • CompactedItem.replacement_history

can become a very large JSONL line.

  • EventMsg::ImageGenerationEnd is also persisted in limited mode, so generated image payloads can be duplicated across response and event surfaces.

I think the durable fix should happen before rollout serialization, not only in Desktop thread rendering. Otherwise existing UI pagination or lazy rendering can reduce symptoms while new rollouts still accumulate replay-hostile inline blobs.

A focused first PR could be:

  1. Add a rollout attachment store under CODEX_HOME, probably content-addressed by sha256 and scoped by thread/session id.
  2. Before writing rollout JSONL, rewrite inline data:image/*;base64,... payloads into small internal attachment references.
  3. Keep old inline JSONL readable for backward compatibility.
  4. Resolve attachment references only at the boundary that actually needs model-ready image bytes, rather than during thread list/read hydration.
  5. Add regression tests showing that image-heavy response items persist as small JSONL records while resume/model replay still works.

I am deliberately framing this as a narrow storage fix rather than a broader Desktop performance PR. It should compose with thread pagination/lazy UI hydration work, but it addresses a different invariant: replayable rollout history should not store large image blobs inline.

Would this direction align with the maintainers’ intended design? If so, I would be happy to put together a small invited PR limited to the rollout persistence/read compatibility path.

dfranklin-at-bg · 2 months ago

Another potential occurrence using the Box plugin. A bug in Box plugin caused the session to be poisoned. Codex could not even answer a basic question like "what time is it". (I have not yet logged a bug against the plugin). Output from Codex looked like this:
{
"type": "error",
"error": {
"type": "invalid_request_error",
"code": "invalid_value",
"message": "Invalid 'input[111].output[1].image_url'. Expected a base64-encoded data URL with an image MIME type (e.g. 'data:image/
png;base64,aW1nIGJ5dGVzIGhlcmU='), but got empty base64-encoded bytes.",
"param": "input[111].output[1].image_url"
},
"status": 400
}

Here's what the Box plugin did:

box_get_preview_page({"fileId":"2212855813609","page":1}) returned a successful tool result with an empty PNG:

{"type":"image","data":"","mimeType":"image/png"}

Codex then serialized that into the resumed transcript as:

{"type":"input_image","image_url":"data:image/png;base64,","detail":"high"}

That entry is invalid because the base64 payload is empty. On every later turn in that same resumed session, Codex replayed the transcript, hit the same bad image_url, and the API rejected the whole request before the model could answer anything.

The practical workaround is: don't resume that session. Start a fresh Codex session, or resume from an earlier/unaffected session.

Although the Box plugin errantly returned an empty image as a successful result, Codex should not allow the session to be poisoned. Codex should sanitize or reject empty image tool outputs before saving/replaying them.

DawgZter · 2 months ago

Adding another concrete repro/repair datapoint for Codex Desktop.

Feedback ID from in-app flow: 019e20c6-fe64-7e92-8e5f-9260d5827531

Affected session:

  • Session ID: 019e17c4-3951-78f2-a6b4-ce9886b5eee7
  • Thread name: command analysis
  • App context: long-running Codex Desktop session using screenshot/image-producing workflows

What happened:

  • The session repeatedly became unusable after inline image outputs were written into the saved JSONL transcript.
  • Removing one bad image was not enough; after continuing the thread, the problem came back when more image blocks were produced.
  • I eventually made the session text-only as a local workaround.

Local repair evidence:

  • Multiple bad structured image blocks had to be removed over time.
  • One image decoded from base64 but failed image validation with a truncated-image read.
  • Final workaround stripped all structured image blocks from the affected session transcript:
  • removed 380 structured image/tool-result image blocks
  • changed 342 JSONL lines
  • verified 0 remaining input_image, image, data:image, or image_url markers
  • verified the repaired JSONL parsed cleanly: 85,065 lines, 0 JSON parse errors
  • I also moved repair backups out of ~/.codex/sessions because leaving backup .jsonl... files near live sessions looked risky if the app scans/globs that directory.

User-visible impact:

  • A single corrupt or replay-hostile image payload can make a valuable long-running thread effectively unusable.
  • The user has to manually edit local session JSONL to recover.
  • The same session can re-break after repair if the agent/tooling emits another image block.
  • The current practical workaround is to tell the agent not to use image(...), view_image, browser screenshots, Markdown image syntax, or data:image/... URLs, which is fragile.

Requested product behavior:

  • Validate/sanitize image payloads before persisting them to replayable session history.
  • On load/resume, skip corrupt/truncated/oversized image payloads and replace them with a placeholder instead of breaking the thread.
  • Store images out-of-line as blobs/files/handles rather than inline base64 inside JSONL.
  • Add a built-in "repair session / strip broken media" option.
  • Add a real text-only/no-images mode that screenshot-capable tools and agents respect.
  • Ensure non-live backup files in the sessions tree are not accidentally treated as active sessions.
Necmttn · 29 days ago

Image/tool binary payloads should be externalized before they enter replayable history.

Store a content-addressed blob once, then put a lightweight content_ref in the transcript with media type, byte count, hash, producer tool id, redaction status, and retention policy. Replay can decide whether to hydrate the blob, summarize it, or omit it.

That also makes token-cost attribution auditable.

---

_Generated with ax._

Electro-Dig · 5 days ago

Adding a Windows reproduction with stronger evidence that repeated compaction amplifies the inline-image storage problem, rather than merely preserving each image once.

Environment:

  • Codex Desktop: 26.707.9981.0 (Microsoft Store)
  • Codex CLI: 0.144.1
  • Windows 11 x64, build 26200

Sanitized measurements from the largest affected rollout:

rollout size:                         2,408,117,275 bytes (~2.24 GiB)
JSONL records:                        17,591
compacted records:                    47
bytes in compacted records:           ~2,118.5 MiB
response_item records / bytes:        11,357 / ~137.7 MiB
event_msg records / bytes:            5,943 / ~39.9 MiB
records containing image_url:         134
records containing base64:            148
largest compacted JSONL line:         ~53.7 MiB

I parsed one of the largest compacted records structurally. It contained 52 fields under paths like:

payload.replacement_history[*].content[*].image_url

All were data:image/png;base64,... values. Those 52 image URL fields totaled 56,180,605 characters in that single compacted record; the largest individual values were about 3.16M, 2.03M, and 2.01M characters.

Two other recent rollouts were ~1.61 GiB and ~1.56 GiB and showed the same shape: most bytes were in compacted, with historical inline image URLs copied into replacement_history. The active sessions directory gained roughly 8.36 GiB in seven days during screenshot/image-heavy work.

This looks like multiplicative persistence amplification:

  1. screenshot/image output enters replayable history as an inline data URL;
  2. later compaction serializes a replacement history that still contains those data URLs;
  3. repeated compactions append new large copies to the rollout;
  4. the rollout therefore grows by tens of MiB per compaction even when the underlying images are old.

Current main appears consistent with this behavior in codex-rs/core/src/compact_remote_v2.rs:

  • build_v2_compacted_history retains user/developer/system messages and clones them into replacement history;
  • message_text_token_count charges ContentItem::InputImage zero tokens (the enclosing message is only charged the later .max(1));
  • truncate_message_text_to_token_budget pushes every InputImage unchanged;
  • regression tests explicitly assert that retained-history truncation preserves images.

This means one retained message containing dozens of multi-megabyte data URLs can cost roughly one unit of the text budget while adding tens of MiB to every persisted compaction.

A durable fix likely needs both:

  • externalizing/deduplicating image blobs before rollout persistence, with backward-compatible reads; and
  • ensuring CompactedItem.replacement_history stores lightweight references or sanitized model context rather than cloning historical base64 payloads.

A smaller defensive change could add an image count/byte budget when building compacted history, retain only the newest bounded images, and add a regression test asserting that repeated compaction of image-bearing history does not grow rollout bytes linearly with the full image payload.

I have archived old rollouts as a local workaround. I am not attaching raw JSONL because it contains private conversation and image data.

penguinspecz · 1 day ago

Additional macOS A/B evidence showing both history amplification and downstream impact:

Measured rollout-family amplification

I classified every local rollout by its first session_meta record and followed source.subagent.thread_spawn.parent_thread_id to its root.

  • Root family 019f5db5…: 198 files / 29,480,944,591 bytes (27.456 GiB)
  • Root family 019f48a5…: 248 files / 5,445,233,918 bytes (5.071 GiB)
  • Combined: 446 files / 34,926,178,509 bytes (32.527 GiB)
  • Largest root rollout: 825,815,139 bytes, 70,181 JSONL records
  • That root included 217 data:image records, 216 input_image records, and a largest single JSONL record of 9,958,819 bytes
  • Many archived children were 380–645 MB. Their metadata identified them as depth-1/depth-2 subagents of the same root, and they contained replayed parent history before their own turns.

This means one long-lived image/tool-heavy task does not merely create one large audit file: subagent lineage can multiply the persisted parent payload across hundreds of files.

Downstream controlled test

OpenUsage 0.7.6 scans both ~/.codex/sessions and archived_sessions to estimate Codex usage. With these histories present:

  • OpenUsage showed 20.03 GB in the macOS process list
  • measured peak physical footprint was 21.8 GB
  • its initial Codex refresh took 66.265 seconds

I then quit OpenUsage and atomically moved only the two affected archived families outside the scanned directories. All 446 files retained the same device, inode, and byte size at the quarantine destination.

Afterward:

  • ~/.codex fell from about 35 GB to 2.6 GB
  • OpenUsage relaunched and remained at a 234 MB physical footprint during a 25-second initial-scan sample

I am filing the downstream whole-file parsing problem separately with OpenUsage, but the A/B result demonstrates that the Codex rollout shape is hazardous to downstream consumers even when they only need token_count records.

Quota observation

During the same period, a weekly Codex allowance was consumed in roughly 28 hours (Sol 5.6 High). While Sol is known to consume rate limits quite fast, this still makes context amplification a plausible contributor, but local JSONL size alone does not prove that every persisted byte was submitted to or billed by the model. Server-side request accounting would be needed to confirm causality.

Requested persistence/consumer contract

In addition to externalizing image blobs, please consider:

  1. Child rollouts should reference parent lineage/history by ID and immutable cursor, not copy parent records into every child file.
  2. Durable audit history, model replay context, UI transcript, and binary artifact storage should be separate bounded representations.
  3. Provide a stable, compact, versioned local usage-event stream so accounting tools do not need to parse full-fidelity replay/audit logs.
  4. Add a supported repair/cold-storage path for already-amplified task families.
  5. Publish whether compaction/replay excludes persisted image/tool payloads and expose request-side context accounting by category.

Screenshot and a sanitized downstream report are available; no prompt text, source code, auth data, or raw image payloads need to be shared.