Desktop threads can be poisoned by inline base64 tool images, leading to `{"detail":"Bad Request"}` on resume and likely inflated token usage
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
continueattempts. - 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_msgerrors with:
``json``
{"detail":"Bad Request"}
- many very large
function_call_outputrecords 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
- Start a long Codex desktop thread.
- Use Computer Use / browser-use or another screenshot-producing tool repeatedly.
- Continue the same thread many times.
- Inspect the saved session JSONL and observe large
input_image/data:image/...;base64,...payloads insidefunction_call_output. - 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.
10 Comments
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
I hit the same class of issue on macOS with Codex Desktop, with an additional failure mode that blocks startup / verification.
Environment:
Observed symptoms:
RangeError: Invalid string lengthat
Mu.write/Socket.onStdoutDatain
workspace-root-drop-handler-B7KjFQ8M.js.Local evidence:
~/.codex/sessionshad grown to about 2.8 GB.image_generation_call/function_call_outputrecords containing inlinedata:image/png;base64,...payloads.replacement_history.Failed to parse MCP messagewith
lineBytes=537005464for a
thread/readresponse.~/.codex/sessionsreduced the active session directory to about 12 MB and allowed Codex Desktop to start normally again.Expected:
thread/readshould not emit hundreds of MB in a single stdout JSON line.RangeError.Workaround confirmed:
~/.codex/sessions, then restart Codex Desktop.I saw a closely matching failure in Codex Desktop on macOS while testing a local Vite app.
Flow:
http://127.0.0.1:5123/page.screenshot()and emitted it withnodeRepl.emitImage({ bytes, mimeType: 'image/png' })function_call_outputcontaining aninput_imagewith an inlinedata:image/png;base64,...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.
I hit the same underlying issue with Codex App's built-in imagegen, not browser-use or computer-use screenshots.
Environment:
Observed failure mode:
~/.codex/sessionsand restarting Codex App stopped the high CPU.Sanitized local evidence:
function_call_output: 745function_call: 745image_generation_call: 29event_msg: 1,877turn_context: 86compacted: 7response_item/function_call_outputentries containing inline image payloads. The output payload length was about 9,455,166 chars.image_generation_callrecords: 29image_generation_call.resultchars: 90,454,556image_generation_call.resultchars: 3,924,372image_generation_endevent messages:image_generation_endrecords: 29image_generation_end.resultchars: 90,454,556image_generation_end.resultchars: 3,924,372So 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:
event_msgandresponse_itemsurfaces.I am intentionally omitting local paths, session id, prompts, image data, workspace names, and external service content for privacy.
I took a quick look at the current
maincode path and I think this issue has a fairly crisp persistence-layer shape.The recurring pattern seems to be:
UserInput::Image/UserInput::LocalImagebecomeContentItem::InputImage { image_url: "data:image/...;base64,..." }incodex-rs/protocol/src/models.rs.RolloutRecorder::record_itemskeeps persistedResponseItems mostly as-is.JsonlWriter::write_rollout_itemserializes theRolloutItemdirectly to JSONL, so any inline image data URL in:ResponseItem::MessageFunctionCallOutput/CustomToolCallOutputcontent itemsImageGenerationCall.resultCompactedItem.replacement_historycan become a very large JSONL line.
EventMsg::ImageGenerationEndis 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:
CODEX_HOME, probably content-addressed by sha256 and scoped by thread/session id.data:image/*;base64,...payloads into small internal attachment references.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.
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.
Adding another concrete repro/repair datapoint for Codex Desktop.
Feedback ID from in-app flow:
019e20c6-fe64-7e92-8e5f-9260d5827531Affected session:
019e17c4-3951-78f2-a6b4-ce9886b5eee7command analysisWhat happened:
Local repair evidence:
380structured image/tool-result image blocks342JSONL lines0remaininginput_image,image,data:image, orimage_urlmarkers85,065lines,0JSON parse errors~/.codex/sessionsbecause leaving backup.jsonl...files near live sessions looked risky if the app scans/globs that directory.User-visible impact:
image(...),view_image, browser screenshots, Markdown image syntax, ordata:image/...URLs, which is fragile.Requested product behavior:
Image/tool binary payloads should be externalized before they enter replayable history.
Store a content-addressed blob once, then put a lightweight
content_refin 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._
Adding a Windows reproduction with stronger evidence that repeated compaction amplifies the inline-image storage problem, rather than merely preserving each image once.
Environment:
26.707.9981.0(Microsoft Store)0.144.126200Sanitized measurements from the largest affected rollout:
I parsed one of the largest
compactedrecords structurally. It contained 52 fields under paths like:All were
data:image/png;base64,...values. Those 52 image URL fields totaled56,180,605characters 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 intoreplacement_history. The active sessions directory gained roughly 8.36 GiB in seven days during screenshot/image-heavy work.This looks like multiplicative persistence amplification:
Current
mainappears consistent with this behavior incodex-rs/core/src/compact_remote_v2.rs:build_v2_compacted_historyretains user/developer/system messages and clones them into replacement history;message_text_token_countchargesContentItem::InputImagezero tokens (the enclosing message is only charged the later.max(1));truncate_message_text_to_token_budgetpushes everyInputImageunchanged;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:
CompactedItem.replacement_historystores 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.
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_metarecord and followedsource.subagent.thread_spawn.parent_thread_idto its root.019f5db5…: 198 files / 29,480,944,591 bytes (27.456 GiB)019f48a5…: 248 files / 5,445,233,918 bytes (5.071 GiB)data:imagerecords, 216input_imagerecords, and a largest single JSONL record of 9,958,819 bytesThis 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/sessionsandarchived_sessionsto estimate Codex usage. With these histories present: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:
~/.codexfell from about 35 GB to 2.6 GBI 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_countrecords.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:
Screenshot and a sanitized downstream report are available; no prompt text, source code, auth data, or raw image payloads need to be shared.