Responses WebSocket falls back when compacted replacement_history contains large inline images
What version of Codex CLI is running?
codex-cli 0.133.0.
The original affected session metadata includes codex-cli 0.130.0, but the WebSocket fallback was reproduced from a copied session using the current Codex CLI/App around 0.133.0.
What subscription do you have?
ChatGPT Pro 20x.
Which model were you using?
gpt-5.5 with reasoning_effort=xhigh.
What platform is your computer?
macOS arm64.
What terminal emulator and version are you using (if applicable)?
iTerm2 3.6.10.
The affected thread was resumed through Codex CLI/App flows. The reproduction used copied CODEX_HOME directories and codex resume against the same session JSONL payload. No terminal multiplexer was intentionally used for the reproduction session; separate monitoring/caffeinate processes may have been run in other terminals.
Codex doctor report
Redacted summary from codex doctor --json:
- overallStatus:
ok - codexVersion:
0.133.0 - install method:
brew - runtime platform:
macos-aarch64 - installation:
installation looks consistent - config:
config loaded - model:
gpt-5.5 - model provider:
openai - auth:
auth is configured - auth mode:
chatgpt - provider reachability:
active provider endpoints are reachable over HTTP - WebSocket reachability:
Responses WebSocket handshake succeeded - WebSocket handshake result:
HTTP 101 Switching Protocols - WebSocket wire API:
responses - terminal:
iTerm2 - terminal version:
3.6.10 - terminal size:
211x98 - state paths:
state paths and databases are inspectable - active rollout files:
110 files, 1371653678 total bytes, 12469578 average bytes
I am not pasting the raw doctor JSON because it contains local usernames, paths, project names, and state locations.
What issue are you seeing?
A long-running Codex thread consistently falls back from Responses WebSocket to HTTPS when resumed.
This may share a root cause with #24388, because both involve large input_image payloads retained inside compacted replacement_history. The observed failure mode here is different: instead of remote compaction failing with context_length_exceeded, the Responses WebSocket stream closes before response.completed, retries several times, and then falls back to HTTP.
I was able to reproduce the fallback using a copied CODEX_HOME, and then eliminate the fallback by removing only inline image payloads from compacted replacement_history.
Observed WebSocket failure pattern:
stream disconnected - retrying sampling request (1/5 ...)
stream disconnected - retrying sampling request (2/5 ...)
stream disconnected - retrying sampling request (3/5 ...)
stream disconnected - retrying sampling request (4/5 ...)
stream disconnected - retrying sampling request (5/5 ...)
falling back to HTTP
The HTTP fallback then proceeds, but the affected thread becomes prone to long idle periods.
What steps can reproduce the bug?
I cannot attach the raw session JSONL because it contains local paths, prompts, tool output, and image payloads. The structural reproduction I used was:
- Start from a long-running Codex thread with multiple compactions and historical image inputs.
- Confirm that compacted
replacement_historycontains retainedinput_imagepayloads. - Copy the affected session JSONL into a clean temporary
CODEX_HOME. - Resume the same thread from the copied session.
- Observe that WebSocket warmup succeeds, but the actual resumed turn payload fails over WebSocket, retries 5 times, and falls back to HTTP.
- Create another copy of the same session JSONL.
- Replace only
input_imageitems insidetype: "compacted"records with small text placeholders. - Resume the same thread from that sanitized copy.
- Observe that
responses_websocketcompletes normally, with no HTTP fallback and nostream disconnectedretries.
Control reproduction with copied session JSONL:
- WebSocket warmup succeeded.
- The actual resumed turn payload failed over WebSocket.
stream disconnectedoccurred 5 times.- Codex fell back to HTTP.
This suggests the issue is reproducible from the session/history payload itself, not from the original live ~/.codex state.
Compaction-only sanitized reproduction:
- I replaced only
input_imageitems insidetype: "compacted"records with text placeholders. - Non-compacted history was left mostly unchanged.
Before sanitization:
- Session JSONL size: ~703 MB
- Largest compacted JSONL line: ~17.4 MB
- Total
input_imageentries: 1789 input_imageentries inside compacted records: 1277input_imageentries outside compacted records: 512- Latest compacted
replacement_history: ~17.39 MB - Inline image data in that replacement history: ~17.17 MB
- Image data share of replacement history: ~98.75%
After compaction-only sanitization:
- Session JSONL size: ~195 MB
- Largest compacted JSONL line: ~241 KB
input_imageentries inside compacted records: 0input_imageentries outside compacted records: 509
Result:
responses_websocketcompleted normally.responses_http: 0stream disconnected: 0- fallback: 0
- turn completed successfully.
This suggests the failure is specifically tied to inline images retained in compacted replacement_history, not simply to any historical input_image item anywhere in the session JSONL.
I also tested a full-image-sanitized copy, where all input_image items were replaced with text placeholders. That also completed normally over WebSocket.
What is the expected behavior?
Either:
- compacted
replacement_historyshould not retain large raw inline image payloads unnecessarily, - the Responses WebSocket transport should handle this payload shape reliably,
- Codex should surface a more specific close reason, such as payload size / unsupported multimodal replay / request shape issue, or
- Codex should automatically recover from this history shape without requiring manual session JSONL surgery.
A session should not repeatedly fail Responses WebSocket and silently fall back to HTTP because compacted history retained large inline image payloads.
Additional information
The thread did not always fail WebSocket. It worked over WebSocket for several days before the first hard fallback.
The first hard fallback appeared after a compaction where replacement_history had grown to ~17.38 MB and contained 42 inline images.
In the latest compaction:
- Messages in replacement history: 382
- Messages containing images: 18
input_imagecount in replacement history: 42- Image bytes inside image-containing messages: ~99.88% of those messages
The images appear to be retained as historical user message content inside compacted replacement history.
Privacy note: I am not attaching raw rollout/session files because they contain private prompts, tool output, local paths, and inline image data. I can provide further redacted structural data if useful.
13 Comments
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
This lines up with what I see in the repo as a plausible failure boundary:
CompactedItemwithreplacement_history: Some(new_history.clone())replacement_historyas the live replacement historyA useful narrow test case might be: construct a compacted rollout item whose
replacement_historycontains one or moreinput_imagecontent items with large data URLs, resume/reconstruct it, and assert either that compacted history no longer carries raw image payloads into the next sampling request or that it is converted into a bounded placeholder before the WebSocket request is built.The key distinction in the report seems important: images outside compacted records did not trigger the fallback after sanitizing only compacted
replacement_history, so I would keep the first fix/test scoped to compacted history rather than all historical multimodal content.Adding a related Windows Desktop datapoint. Same underlying data shape as this issue (
compacted.payload.replacement_historyretaining inline images), but the observed symptom on my machine was Desktop session/rollout bloat and UI hangs rather than only Responses WebSocket fallback.Environment:
OpenAI.Codex_26.527.3686.0_x64__2p2nqsd0c76g0Sanitized structural stats from the affected rollout:
Largest relevant record:
Why I think this belongs here:
response_itemandevent_msgforms.payload.replacement_history, creating another large durable record.%USERPROFILE%\.codex\sessionsrestored Desktop responsiveness.I also added the broader Windows Desktop hang/session-list datapoint in #24676, but this comment is specifically for the compacted
replacement_historyamplification aspect.Privacy note: I am not uploading the raw rollout, screenshots, sqlite databases, or unsanitized logs because they contain private conversation/image data.
Windows confirmation: retained compacted images caused WebSocket fallback
I reproduced this on:
codex-cli 0.144.4gpt-5.6-solreasoning_effort=maxThe affected long-running session repeatedly produced:
``
text
``Reconnecting... 4/5
Stream disconnected before completion: failed to send websocket request:
IO error: An established connection was aborted by the software in your
host machine. (os error 10053)
It then fell back to HTTPS and remained on HTTPS after stopping and resuming the session. Other Codex sessions on the same machine continued working normally over WebSockets, suggesting this was specific to the affected session payload rather than general network or WebSocket availability.
Structural measurements from the affected rollout before repair:
input_image/data-URL occurrencesreplacement_history: 82 items and 13 inline imagesI closed the session, created a byte-identical backup, and performed a schema-aware cleanup:
input_imageobjects with smallinput_textplaceholdersencrypted_contentand all other fields untouchedAfter cleanup:
input_imageobjectsThis provides a Windows confirmation that the retained compacted image payload was causal. It appears to be the same retention mechanism described in #33493 and #23257.
Uploaded affected thread: 019f6455-ea0e-7481-ba54-5c665a6ea33f
Uploaded investigation and recovery thread: 019f6964-aa8c-7901-9dbb-90dd8249fb05
Note: Both threads were uploaded through Codex feedback.
The affected thread contains the original WebSocket failure and HTTPS fallback. The investigation thread contains the structural rollout analysis, schema-aware image cleanup, validation results, and confirmation that the same affected session resumed successfully over WebSockets afterward.
I traced the generic disconnect message to the established Responses WebSocket close-frame path on current
main(79479cd).The pump correctly forwards
Message::Close(frame), butrun_websocket_response_streamcurrently matchesMessage::Close(_)and discards both the close code and reason before returning:websocket closed by server before response.completedThat means a server close such as
1009 / Message Too Bigis indistinguishable from an ordinary transport disconnect in the CLI. This explains why an oversized image-heavyreplacement_historycan produce five generic reconnect attempts without surfacing the actionable server reason.I prepared a minimal diagnostic-only patch that:
code=1009 reason=Message Too BigPatch commit: https://github.com/i69699/codex/commit/b1a2aaf
Validation on Windows:
cargo +stable nextest run --no-fail-fast -p codex-api: 159/159 passedcargo +stable clippy --fix --tests --allow-dirty -p codex-api: passed with no warningsThe branch is ready if a maintainer would like to invite a PR.
macOS reproduction on the current Desktop build
I added a detailed macOS corroboration to #36232. The retained inline-image/history shape also matches the root cause tracked here.
Environment
26.803.616010.147.0-alpha.6.526.5, Apple Silicon (arm64)openaiprovidergpt-5.6-sol, xhigh effortTwo independent affected tasks
In both tasks, a new turn records the user message but no subsequent model event before:
stream disconnected before completion: error sending request for url (https://chatgpt.com/backend-api/codex/responses)Other tasks in the same Desktop process, on the same network and built-in provider, remained healthy. Task A's last successful model input was only 78,018 / 258,400 tokens, so serialized history byte size appears to be a pressure signal independent of the displayed token usage.
The visible failure in these two cases reaches the HTTPS Responses endpoint rather than only surfacing a WebSocket fallback, but the causal payload signature is consistent: compaction leaves large inline image/tool payloads in the active history, and retrying the affected task resends them.
The useful boundary in your reproduction is very specific: a ~17.4 MB compacted record dominated by retained inline images, reproducible WebSocket failure from a copied session, and normal completion after removing only those compacted image payloads.
Codex Rescue cannot repair Codex’s WebSocket/HTTP path or automatically rewrite
replacement_history. I’m testing whether its local oversized-record diagnosis catches this safely and remains bounded on a ~703 MB rollout. It leaves the source untouched and does not replay unknown side effects.If you still have the pre-sanitization copy, could you run:
pipx install codex-rescue==0.1.0a3codex-rescue sessionscodex-rescue doctor --latestSanitized output only is enough—no raw rollout, images, databases, prompts, paths, or secrets.
https://github.com/shleder/codex-rescue
I ran a separate local-only structural scanner against the pre-sanitization copy rather than installing another recovery tool. It does not access the network, SQLite databases, or the Codex app; it never emits source paths, session IDs, prompts, tool arguments, image contents, or source hashes. The source file's size and modification time were unchanged before and after scanning.
Public-safe result:
replacement_history: 49replacement_history: 405 messages, 39 messages with images, 44input_image/data-URL entries, 16,591,360 Base64 payload bytes (estimated decoded image bytes: 12,443,474)replacement_history: 16,763,158 bytes, 410 messages, 39 messages with images, the same 44 image entries and 16,591,360 Base64 payload bytesThis independently confirms that the active compaction payload is dominated by retained inline image bytes. The attachments are the local read-only scanner and its public-safe JSON result.
codex-issue-24550-public-evidence.zip
Thanks — this is still useful evidence.
I won’t count it as a Codex Rescue validation because you intentionally used a separate local scanner, but the result is valuable independently: the source stayed unchanged, the rollout was valid JSONL, and the active compacted history still carried 44 inline images / ~16.6 MB of Base64 payload.
That is enough for me to keep Rescue’s scope conservative here: detect/report the oversized persisted history and require review, but not claim that it repairs the compaction/image-retention bug itself.
No need to install another tool unless you specifically want a side-by-side comparison. Thanks for keeping the output privacy-safe.
I implemented a candidate fix for the retained inline-image payload issue:
The change externalizes inline Base64 images returned in compacted replacement history into a content-addressed, per-thread store. Compacted history retains only bounded
codex-image://references, andview_imageresolves a referenced image on demand with size, path, and SHA-256 validation. Remote image URLs remain unchanged.Targeted compaction, storage, integrity, path-safety, follow-up request, and
view_imagetests pass, as doesjust fix -p codex-core.I attempted to open a PR, but the compare page currently says that pull requests are limited to repository collaborators, and the API returns
Resource not accessible by integration. Could a maintainer review/cherry-pick this commit, open a PR from the branch, or advise on obtaining collaborator access? I am happy to adjust the implementation based on review feedback.Additional Windows Desktop control: the regression appears to be repeated retry/fallback scope, not image retention alone
I prepared a separate report before finding this issue. The root cause matches this issue closely, so I am adding only the new comparative evidence here instead of opening a duplicate.
Environment
26.818.3698.00.149.0-alpha.40.148.0-alpha.21gpt-5.6-sol, ChatGPT authentication127.0.0.1:7890Affected thread
input_imageentries containing 22.938 MiB of Base64 image datafalling back to HTTPHistorical control
An older long thread created under
0.147.0-alpha.6.5was substantially larger:This suggests that retaining large inline images predates
0.148. The severe latency regression on this machine is the cost of retrying the complete WebSocket upload five times and not consistently preserving the HTTP fallback across later turns/internal sessions.Confirmed workaround
Using the same ChatGPT authentication and endpoint through a custom provider with:
completed both plain-text and image-input tests normally on
0.149.0-alpha.4.This seems consistent with the fallback-scope problem addressed for delegated sessions in #38681. Could maintainers check whether durable
turn_inputdispatch, Guardian/delegated sessions, or Desktop reconstruction creates a new Responses client that loses the parent/thread fallback state? It would also help to expose a supported HTTP-only switch for the built-in ChatGPT provider, or to fall back after the first failed oversized WebSocket upload instead of retransmitting it five times.Privacy note: no raw rollouts, prompts, image contents, account identifiers, or authentication data are included.
Thanks — this control makes the boundary between the two issues much clearer.
We already have separate candidate transport fixes alongside the compaction/image change:
The important remaining gap is that the first fix only applies when the peer provides a standard 1009 close frame. Windows socket errors 10053/10054 do not necessarily expose that close frame, so they can still enter the generic retry budget. PR #38681 also preserves fallback only for delegated sessions; your data suggests that later turns, Desktop reconstruction, or another Responses client recreation path may still lose the fallback state.
I therefore think these should remain two independently reviewable fixes:
Your historical control is especially useful because it shows that payload size is the trigger, while the repeated retry/fallback scope is the latency regression.
Thanks for the control experiment above - separating transport fallback loops from image retention clarifies both bugs. For anyone auditing local damage from either path,
vetto rescue diagnoseclassifies retained oversized payloads in a rollout read-only; useful when you need to know whether a given thread is safe to resume before candidate fixes land.