Repository navigation
Desktop threads can be poisoned by inline base64 tool images, leading to {"detail":"Bad Request"} on resume and likely inflated token usage #18629
Description
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:
{"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:{"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:
{"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.
Activity
github-actions commentedon Apr 20, 2026
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
cnjimmyshao commentedon Apr 26, 2026
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.neokry commentedon May 5, 2026
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.
Astro-Han commentedon May 8, 2026
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.
cnjimmyshao commentedon May 9, 2026
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.
dfranklin-at-bg commentedon May 11, 2026
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 commentedon May 15, 2026
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:
Necmttn commentedon Jun 21, 2026
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.
Electro-Dig commentedon Jul 15, 2026
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.
penguinspecz commentedon Jul 20, 2026
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.
jkammerland commentedon Jul 21, 2026
Hit same bug on codex-cli 0.144.6. Viewing four 1024×1024 PNGs created one 22.4 MB inline tool result. WebSocket disconnected,
HTTPS fallback started, then every prompt in that thread returned {"detail":"Bad Request"}. Session JSONL remains valid; only
thread is poisoned. Looks related to #24550 and #28316.
waveblink commentedon Aug 16, 2026
I am seeing a closely related failure on Windows in Codex Desktop.
Observed behavior:
{"detail":"Bad Request"}.stream disconnected before completion/WebSocket closed by server before response.completedreconnect failures.Sanitized local evidence:
This seems consistent with the possibility that image/tool output or a related streaming/compaction path can destabilize a task. It would be especially useful if the app could surface a more specific rejection reason and safely recover the task rather than leaving the user with only a generic Bad Request.
yyysheng commentedon Aug 25, 2026
Additional sanitized Windows Desktop reproduction, with a compaction-specific failure mode.
Environment:
Observed sequence:
full_context_window_limit_reached=falseandtoken_limit_reached=falsewere reported.This suggests a recovery deadlock: the thread becomes large enough that it needs compaction, but the remote compaction request itself is rejected before it can shrink the history. In this case the limiting factor appears to be serialized request size / replayed tool payloads rather than model context-window exhaustion.
This looks closely related to #11478 / #11440, except the current Desktop failure surfaces as generic HTTP 400 /
{"detail":"Bad Request"}rather than a visible 413.Expected behavior:
Privacy note: I intentionally omitted local filesystem paths, OS username, project/workspace names, prompts, image contents, thread/session IDs, request IDs, and other identifying content. Only rounded aggregate size/token measurements are included.
chipfighter commentedon Sep 28, 2026
A workaround for desktop users while this is open. My case had the same shape on the Windows desktop app: I was iterating on image generation with GPT in one long task, and after a dozen or so images every request carried all of them again, about 44 MB each. The connection kept dropping, the retries every few minutes never went through, and the only way out was a new task, which loses the conversation.
So I built an open-source plugin for the Codex desktop app (disclosure: I'm the author): Codex Attachment Manager.
need IMG-xxxand 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.m1r01848 commentedon Oct 14, 2026
Additional macOS Desktop reproduction: one 33.19 MB
view_imagetool result permanently poisons a threadI can reproduce this on a current macOS Codex Desktop installation. This case provides a fairly sharp boundary: a single custom tool result containing three inline images is immediately followed by permanent
{"detail":"Bad Request"}failures.Environment
26.1007.71326(build20398, production channel)0.162.0-alpha.17.2gpt-5.6-sol, reasoning effortxhighPrivacy-safe measurements
128,200,371bytes,7,309linesresponse_item/custom_tool_call_output,33,185,463serialized bytesexectool call that invokedview_imagefor three local1920x1080PNG filesinput_imagedata URLsimage_urlstring:11,061,594bytes (Base64/ASCII)72,813input tokens against a258,400token context windowstream disconnected before completion: websocket closed by server before response.completedretries, followed by an HTTP fallbackPOST /backend-api/codex/responses400, 24-byte body{"detail":"Bad Request"}The installed
codex doctorreports healthy state databases, successful Desktop app-server initialization, reachable provider endpoints, and a successful Responses WebSocket handshake. It also reports472active rollout files consuming about2.45 GB, which is consistent with substantial persisted session data but not a general connectivity failure.Inference
The timing and measurements point to a serialized request/tool-output byte limit rather than ordinary token-window exhaustion: the last successful request was well below the reported model context window, then a single 33.19 MB inline image result entered durable history, and no subsequent model usage record was created before the 400.
This is also an aggregate-output problem, not only a per-image problem: each individual image data URL was about 11.06 MB, while the single combined tool result was about 33.19 MB.
Expected behavior
I can provide the affected thread ID, failed turn IDs, request ID, timestamps, and the full local rollout through a private support channel. I am intentionally not attaching the 128 MB transcript or any raw image/prompt data publicly.