Responses WebSocket falls back when compacted replacement_history contains large inline images

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

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:

  1. Start from a long-running Codex thread with multiple compactions and historical image inputs.
  2. Confirm that compacted replacement_history contains retained input_image payloads.
  3. Copy the affected session JSONL into a clean temporary CODEX_HOME.
  4. Resume the same thread from the copied session.
  5. Observe that WebSocket warmup succeeds, but the actual resumed turn payload fails over WebSocket, retries 5 times, and falls back to HTTP.
  6. Create another copy of the same session JSONL.
  7. Replace only input_image items inside type: "compacted" records with small text placeholders.
  8. Resume the same thread from that sanitized copy.
  9. Observe that responses_websocket completes normally, with no HTTP fallback and no stream disconnected retries.

Control reproduction with copied session JSONL:

  • WebSocket warmup succeeded.
  • The actual resumed turn payload failed over WebSocket.
  • stream disconnected occurred 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_image items inside type: "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_image entries: 1789
  • input_image entries inside compacted records: 1277
  • input_image entries 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_image entries inside compacted records: 0
  • input_image entries outside compacted records: 509

Result:

  • responses_websocket completed normally.
  • responses_http: 0
  • stream 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_history should 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_image count 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.

View original on GitHub ↗

13 Comments

github-actions[bot] contributor · 3 months ago

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

  • #24388

Powered by Codex Action

SylvainWinning · 3 months ago

This lines up with what I see in the repo as a plausible failure boundary:

  • compaction writes a CompactedItem with replacement_history: Some(new_history.clone())
  • rollout reconstruction later treats replacement_history as the live replacement history
  • WebSocket fallback is currently transport-level, so this specific payload-shape problem can look like a generic stream disconnect

A useful narrow test case might be: construct a compacted rollout item whose replacement_history contains one or more input_image content 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.

VVittgenstein · 2 months ago

Adding a related Windows Desktop datapoint. Same underlying data shape as this issue (compacted.payload.replacement_history retaining inline images), but the observed symptom on my machine was Desktop session/rollout bloat and UI hangs rather than only Responses WebSocket fallback.

Environment:

  • Codex Desktop Store package: OpenAI.Codex_26.527.3686.0_x64__2p2nqsd0c76g0
  • Platform: Windows x64
  • Date observed: 2026-06-01 local time

Sanitized structural stats from the affected rollout:

File size:                       102.44 MB
JSONL lines:                     982
Lines containing data:image:     31
Bytes in image-containing lines: 100.37 MB
data:image URI occurrences:      51
Unique image data URIs:          34
Total data:image URI bytes:      100.26 MB
compacted records:               2
bytes in compacted records:      24.74 MB
largest single record:           24.207 MB compacted record with 16 image refs
response_item user image lines:  14
event_msg user_message images:   14

Largest relevant record:

Line 757: type=compacted, ~24.207 MB, 16 data:image refs

Why I think this belongs here:

  • The rollout was not large because of ordinary text; almost all bytes were inline image payloads.
  • The same user-provided screenshots appear to be persisted in both response_item and event_msg forms.
  • Compaction then retained prior image-heavy history inside payload.replacement_history, creating another large durable record.
  • Quarantining the image-heavy rollout out of %USERPROFILE%\.codex\sessions restored Desktop responsiveness.

I also added the broader Windows Desktop hang/session-list datapoint in #24676, but this comment is specifically for the compacted replacement_history amplification aspect.

Privacy note: I am not uploading the raw rollout, screenshots, sqlite databases, or unsanitized logs because they contain private conversation/image data.

FiredPhoenixIR · 1 month ago

Windows confirmation: retained compacted images caused WebSocket fallback

I reproduced this on:

  • codex-cli 0.144.4
  • Microsoft Windows NT 10.0.26200.0 x64
  • gpt-5.6-sol
  • reasoning_effort=max
  • OpenAI model provider

The 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:

  • 7,241 JSONL records
  • 14 compacted records
  • 90.1 MiB rollout size
  • 52 input_image/data-URL occurrences
  • 13 original images plus 39 copies retained in compacted replacement histories
  • Latest compacted replacement_history: 82 items and 13 inline images
  • Approximately 24.9 million image data-URL characters in the latest active replacement history

I closed the session, created a byte-identical backup, and performed a schema-aware cleanup:

  • Replaced only typed input_image objects with small input_text placeholders
  • Left encrypted_content and all other fields untouched
  • Preserved the session ID, message ordering, summaries, and all 7,241 JSONL records
  • Validated every JSONL record afterward

After cleanup:

  • 0 input_image objects
  • 0 embedded image data URLs
  • Rollout size reduced from 90.1 MiB to 18.8 MiB
  • The same session resumed normally
  • Responses completed over WebSockets with no retries or HTTPS fallback

This 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.

i69699 · 25 days ago

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), but run_websocket_response_stream currently matches Message::Close(_) and discards both the close code and reason before returning:

websocket closed by server before response.completed

That means a server close such as 1009 / Message Too Big is indistinguishable from an ordinary transport disconnect in the CLI. This explains why an oversized image-heavy replacement_history can produce five generic reconnect attempts without surfacing the actionable server reason.

I prepared a minimal diagnostic-only patch that:

  • preserves and reports the close code and reason, for example code=1009 reason=Message Too Big
  • keeps the existing generic message when the peer supplies no close frame
  • does not change retry or HTTP fallback behavior

Patch commit: https://github.com/i69699/codex/commit/b1a2aaf

Validation on Windows:

  • cargo +stable nextest run --no-fail-fast -p codex-api: 159/159 passed
  • cargo +stable clippy --fix --tests --allow-dirty -p codex-api: passed with no warnings

The branch is ready if a maintainer would like to invite a PR.

lanchuanshuang · 15 days ago

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

  • Codex Desktop / ChatGPT app 26.803.61601
  • Bundled Codex CLI 0.147.0-alpha.6.5
  • macOS 26.5, Apple Silicon (arm64)
  • ChatGPT authentication with the built-in openai provider
  • gpt-5.6-sol, xhigh effort

Two independent affected tasks

  • Task A: 109,259,104-byte rollout; latest active post-compaction history 48,758,514 bytes; 6 retained generated images contributed 21,413,208 bytes of inline base64.
  • Task B: 139,210,895-byte rollout; latest active post-compaction history 30,237,919 bytes; 7 retained generated images contributed 9,866,588 bytes of inline base64, plus several browser/screenshot records of approximately 2.4–4.7 MB each.

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.

shleder · 14 days ago

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.0a3
codex-rescue sessions
codex-rescue doctor --latest

Sanitized output only is enough—no raw rollout, images, databases, prompts, paths, or secrets.

https://github.com/shleder/codex-rescue

i69699 · 14 days ago

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:

  • Source rollout size: 496,800,226 bytes
  • JSONL records: 31,119; malformed records: 0
  • Compaction records: 98; records with replacement_history: 49
  • Largest compacted record: 16,776,971 bytes
  • Largest compacted record's replacement_history: 405 messages, 39 messages with images, 44 input_image/data-URL entries, 16,591,360 Base64 payload bytes (estimated decoded image bytes: 12,443,474)
  • Latest compacted record that still contains replacement_history: 16,763,158 bytes, 410 messages, 39 messages with images, the same 44 image entries and 16,591,360 Base64 payload bytes
  • Across retained replacement histories: 1,284 inline-image entries and 389,049,480 Base64 payload bytes
  • No record exceeded the scanner's 64 MiB bounded JSON parsing limit

This 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

shleder · 13 days ago

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.

i69699 · 12 days ago

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, and view_image resolves 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_image tests pass, as does just 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.

love39miku831 · 7 days ago

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

  • Windows 10 Home 22H2 (19045), x64
  • Codex Desktop package 26.818.3698.0
  • Current bundled CLI 0.149.0-alpha.4
  • Affected thread created under 0.148.0-alpha.21
  • gpt-5.6-sol, ChatGPT authentication
  • Clash/Mihomo HTTP proxy at 127.0.0.1:7890

Affected thread

  • 15 PNG inputs, about 17.2 MiB on disk
  • One compacted history record retained 15 input_image entries containing 22.938 MiB of Base64 image data
  • 40 recorded model requests:
  • 23.07–23.87 MiB before zstd
  • 17.24–17.57 MiB after zstd
  • Retained logs contained at least:
  • 9 × Windows socket error 10053
  • 10 × Windows socket error 10054
  • 5 × falling back to HTTP
  • A typical failure batch uploaded/retried the same large WebSocket request five times. Each attempt took about 37 seconds, so HTTP fallback occurred only after roughly 185–188 seconds.
  • Later turns or internal/delegated sessions could repeat the five-upload sequence.

Historical control

An older long thread created under 0.147.0-alpha.6.5 was substantially larger:

  • final compacted history: 54 images / 55.545 MiB of Base64 image data
  • 25 recorded requests, median 56.921 MiB before compression / 41.865 MiB after compression
  • only one retained 10054 and one HTTP fallback
  • after that fallback, 25 subsequent requests in the same session stayed on HTTP without repeating WebSocket failures

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:

supports_websockets = false
wire_api = "responses"

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_input dispatch, 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.

i69699 · 7 days ago

Thanks — this control makes the boundary between the two issues much clearer.

We already have separate candidate transport fixes alongside the compaction/image change:

  • b11bc1b preserves the WebSocket close code/reason and maps an explicit 1009 / Message Too Big close to a non-retryable error, so that case does not retransmit the same request five times.
  • 8705dc2 prevents reuse of a WebSocket whose pump task has already stopped.
  • 844b237 addresses the payload source by externalizing compacted inline images into bounded content-addressed references with on-demand loading.

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:

  1. bound/externalize retained inline images during compaction; and
  2. make oversized-request fallback durable across the task and reconstructed/derived clients, while avoiding five full retransmissions after the first deterministic oversized WebSocket failure.

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.

shleder · 1 day ago

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 diagnose classifies 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.