You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
499 client_closed_request errors during web-search tool usage #521
When using the web-search tool (or when the model invokes web search during a conversation), the request fails with a 499 status code and client_closed_request error. The upstream error message is "client closed request during web-search".
Reproduction
Use a model that triggers web search (e.g. qwen3.8-max-preview via alibaba-token-plan-intl provider)
Upstream error: client closed request during web-search
Duration: ~304s before failure
Provider: alibaba-token-plan-intl
Model: qwen3.8-max-preview
The request appears to be cancelled by the client while the upstream provider is still processing the web search, resulting in a 499 rather than a successful response.
Thanks for the detailed logs — the 499 / client_closed_request / client closed request during web-search shape matches how we intentionally classify this path today (see #158), so this does not look like a simple mislabel.
We checked current dev: this is not already fixed there, and #416's ~60s sidecar deadline would not explain a ~304s turn death. That duration lines up more with a ~300s wall-clock cancel (client abort or similar) than with the sidecar timeout or a routed 200s 504.
Before we dig into a deeper fix, could you share:
ocx version / git SHA
Client (Codex CLI / App / Cursor / other?)
Whether the web-search UI appeared, and roughly how many searches ran
The last SSE events before failure (especially response.incomplete vs response.failed)
Any custom stallTimeoutSec / webSearchSidecar config, plus ChatGPT forward health at the time
Happy to keep investigating once we have those details — leaving this open.
Thanks — I checked the local OCX data and the Codex App logs for the failing turn.
OCX version:2.7.41 (installed package and running GUI). This npm install does not expose a gitHead.
Client: Codex App / Codex Desktop (originator: Codex Desktop, Codex App build reported 0.146.0-alpha.3.1 in the local session metadata).
Web-search UI: it did not appear. I therefore cannot give a UI-visible search count.
Last events / actual sequence: I do not see either response.incomplete or response.failed arriving from the SSE stream. Each request first got 200 OK with content-type: text/event-stream, then produced no further SSE activity until the Codex client logged: stream disconnected before completion: idle timeout waiting for SSE
The same turn was retried automatically. I found four consecutive OCX request records with the same failure shape:
ocx-ms2bn77h-xs: 499 after 303,985 ms
ocx-ms2btpxv-yc: 499 after 303,914 ms
ocx-ms2c08ru-yy: 499 after 304,721 ms
ocx-ms2c6sko-10n: 499 after 304,120 ms
Corresponding Codex log timestamps show retry 1 at 21:45:10, retry 2 at 21:50:14, retry 3 at 21:55:19, and retry 4 at 22:00:24, each specifically because of the SSE idle timeout.
Configuration / health: no custom stallTimeoutSec is present. The web-search sidecar is openai/gpt-5.6-luna (backend openai). During the incident window, the usage log contains many successful OpenAI requests, including multiple gpt-5.6-luna requests returning 200 in roughly 1.5–12.5s, so the ChatGPT forwarding path was generally healthy at that time.
This looks less like a single ~300s App wall-clock cancellation and more like the App's SSE idle timeout firing repeatedly while the routed web-search request never emits another event.
Thanks — that reply is exactly what we needed, and it usefully revises the earlier ~300s wall-clock cancel read.
Updated read
The four back-to-back ~304s failures with Codex’s stream disconnected before completion: idle timeout waiting for SSE, noresponse.incomplete / response.failed, and no web-search UI appearing points less at a generic App cancel and more at:
Codex Desktop’s SSE idle timer firing while the turn emitted no further events after the initial 200 OK.
That is the failure mode OpenCodex’s response.heartbeat keep-alives are meant to prevent on the Responses bridge. So the open questions are now:
whether heartbeats are actually leaving the proxy on this web-search turn, and/or
whether Codex Desktop 0.146.0-alpha.3.1 re-arms its idle timer on response.heartbeat the same way the CLI path does.
The OCX 499 / client_closed_request / client closed request during web-search classification still looks correct as the logged shape once the client drops the stream (#158) — it does not by itself explain why the stream went silent for ~5 minutes.
One more artifact, if you can
For a single failing turn, can you confirm whether any of these frames appeared after 200 OK?
response.created
response.heartbeat
response.output_item.* / text deltas
anything else before the idle disconnect
A short Desktop/SSE log snippet or a note that zero events arrived after headers is enough. That splits “proxy never heartbeat’d / never started the bridge loop” from “Desktop ignored heartbeats.”
Leaving this open under needs-info until we have that.
I checked the raw Codex Desktop log DB and the rollout record for the specific failing request ocx-ms2c6sko-10n.
What is observable:
The request started at 21:55:20.
Desktop logged the HTTP response at 21:55:24 as 200 OK, content-type: text/event-stream.
From that point until 22:00:24, there are no decoded response frames/items recorded for that request: no response.created, no response.heartbeat, no response.output_item.*, no text delta, and no terminal event.
The next request-scoped record is the idle-timeout/retry exactly five minutes later: stream disconnected before completion: idle timeout waiting for SSE
The rollout JSONL likewise contains no response item in that interval; its next entry is only the token_count emitted at the timeout/retry boundary.
One important limitation: these are Desktop's decoded-event/application logs, not a raw SSE byte capture. The retained OCX diagnostics for this old failed request contain only the final usage/error record, not an outbound-frame trace. So I can confirm zero events were surfaced/decoded by Desktop after the headers, but I cannot prove from the retained artifacts whether OCX sent no response.heartbeat frames or Desktop received them and did not recognize/log/re-arm on them.
A fresh reproduction with OCX outbound SSE-frame logging or a loopback packet capture would be needed to split those two cases definitively.
Thanks — that is enough to move this off pure guessing.
What we can take as established
For ocx-ms2c6sko-10n, Desktop saw 200 OK / text/event-stream, then no decoded frames for ~5 minutes (response.created, response.heartbeat, deltas, terminal events all absent) until the SSE idle timeout. Your caveat is noted: this is decoded/application evidence, not a raw byte capture, so it still cannot prove “OCX sent nothing” versus “Desktop received frames and did not log/re-arm.”
Given that response.created is also missing, the stronger working hypothesis is now:
the Responses bridge never started writing frames on this web-search turn (or stalled before the first event), rather than “Desktop ignores heartbeats.” Heartbeat-ignore would more often still show response.created first.
The logged OCX 499 / client_closed_request / client closed request during web-search shape remains the correct downstream classification once Desktop drops the idle stream (#158); it is not the root cause of the five-minute silence.
Next step on our side
We will treat this as actionable and investigate the web-search / Alibaba + sidecar path for “SSE headers, then zero outbound frames until client idle cancel.”
A fresh repro with temporary OCX outbound SSE-frame logging (or a loopback capture) would still be useful if you hit it again — that is the only way to close the last send-vs-decode split — but it is no longer required to keep the issue blocked.
Clearing the needs-info wait on our side and leaving this open for investigation.
Investigation landed a fix in #555: wire response.heartbeat keepalives no longer get suppressed by invisible adapter heartbeats during web-search buffering / raw-byte progress. That should stop Codex Desktop's ~5 min SSE idle timeout from firing while upstream is still alive.
Fixed by PR #555, merged into dev at df57bd9. The bridge now separates upstream liveness from client-visible wire activity, so invisible adapter heartbeats during web-search buffering no longer suppress parser-ignored response.heartbeat frames. Verified on rebased head d2bef13 with local bridge test/typecheck/privacy/full pre-push gate, independent Sol audit, CodeRabbit, and hosted cross-platform CI; the initial macOS timeout was rerun and passed.
Activity
github-actions commentedon Jul 26, 2026
Wibias commentedon Jul 26, 2026
Thanks for the detailed logs — the
499/client_closed_request/client closed request during web-searchshape matches how we intentionally classify this path today (see #158), so this does not look like a simple mislabel.We checked current
dev: this is not already fixed there, and #416's ~60s sidecar deadline would not explain a ~304s turn death. That duration lines up more with a ~300s wall-clock cancel (client abort or similar) than with the sidecar timeout or a routed 200s 504.Before we dig into a deeper fix, could you share:
response.incompletevsresponse.failed)stallTimeoutSec/ webSearchSidecar config, plus ChatGPT forward health at the timeHappy to keep investigating once we have those details — leaving this open.
fix(ci): close freeform issue-quality template bypass
fede-oss commentedon Jul 27, 2026
Thanks — I checked the local OCX data and the Codex App logs for the failing turn.
OCX version:
2.7.41(installed package and running GUI). This npm install does not expose agitHead.Client: Codex App / Codex Desktop (
originator: Codex Desktop, Codex App build reported0.146.0-alpha.3.1in the local session metadata).Web-search UI: it did not appear. I therefore cannot give a UI-visible search count.
Last events / actual sequence: I do not see either
response.incompleteorresponse.failedarriving from the SSE stream. Each request first got200 OKwithcontent-type: text/event-stream, then produced no further SSE activity until the Codex client logged:stream disconnected before completion: idle timeout waiting for SSEThe same turn was retried automatically. I found four consecutive OCX request records with the same failure shape:
ocx-ms2bn77h-xs: 499 after 303,985 msocx-ms2btpxv-yc: 499 after 303,914 msocx-ms2c08ru-yy: 499 after 304,721 msocx-ms2c6sko-10n: 499 after 304,120 msCorresponding Codex log timestamps show retry 1 at 21:45:10, retry 2 at 21:50:14, retry 3 at 21:55:19, and retry 4 at 22:00:24, each specifically because of the SSE idle timeout.
Configuration / health: no custom
stallTimeoutSecis present. The web-search sidecar isopenai/gpt-5.6-luna(backendopenai). During the incident window, the usage log contains many successful OpenAI requests, including multiplegpt-5.6-lunarequests returning 200 in roughly 1.5–12.5s, so the ChatGPT forwarding path was generally healthy at that time.This looks less like a single ~300s App wall-clock cancellation and more like the App's SSE idle timeout firing repeatedly while the routed web-search request never emits another event.
Wibias commentedon Jul 27, 2026
Thanks — that reply is exactly what we needed, and it usefully revises the earlier ~300s wall-clock cancel read.
Updated read
The four back-to-back ~304s failures with Codex’s
stream disconnected before completion: idle timeout waiting for SSE, noresponse.incomplete/response.failed, and no web-search UI appearing points less at a generic App cancel and more at:Codex Desktop’s SSE idle timer firing while the turn emitted no further events after the initial
200 OK.That is the failure mode OpenCodex’s
response.heartbeatkeep-alives are meant to prevent on the Responses bridge. So the open questions are now:0.146.0-alpha.3.1re-arms its idle timer onresponse.heartbeatthe same way the CLI path does.The OCX
499/client_closed_request/client closed request during web-searchclassification still looks correct as the logged shape once the client drops the stream (#158) — it does not by itself explain why the stream went silent for ~5 minutes.One more artifact, if you can
For a single failing turn, can you confirm whether any of these frames appeared after
200 OK?response.createdresponse.heartbeatresponse.output_item.*/ text deltasA short Desktop/SSE log snippet or a note that zero events arrived after headers is enough. That splits “proxy never heartbeat’d / never started the bridge loop” from “Desktop ignored heartbeats.”
Leaving this open under
needs-infountil we have that.fede-oss commentedon Jul 27, 2026
I checked the raw Codex Desktop log DB and the rollout record for the specific failing request
ocx-ms2c6sko-10n.What is observable:
200 OK,content-type: text/event-stream.response.created, noresponse.heartbeat, noresponse.output_item.*, no text delta, and no terminal event.stream disconnected before completion: idle timeout waiting for SSEtoken_countemitted at the timeout/retry boundary.One important limitation: these are Desktop's decoded-event/application logs, not a raw SSE byte capture. The retained OCX diagnostics for this old failed request contain only the final usage/error record, not an outbound-frame trace. So I can confirm zero events were surfaced/decoded by Desktop after the headers, but I cannot prove from the retained artifacts whether OCX sent no
response.heartbeatframes or Desktop received them and did not recognize/log/re-arm on them.A fresh reproduction with OCX outbound SSE-frame logging or a loopback packet capture would be needed to split those two cases definitively.
Wibias commentedon Jul 27, 2026
Thanks — that is enough to move this off pure guessing.
What we can take as established
For
ocx-ms2c6sko-10n, Desktop saw200 OK/text/event-stream, then no decoded frames for ~5 minutes (response.created,response.heartbeat, deltas, terminal events all absent) until the SSE idle timeout. Your caveat is noted: this is decoded/application evidence, not a raw byte capture, so it still cannot prove “OCX sent nothing” versus “Desktop received frames and did not log/re-arm.”Given that
response.createdis also missing, the stronger working hypothesis is now:the Responses bridge never started writing frames on this web-search turn (or stalled before the first event), rather than “Desktop ignores heartbeats.” Heartbeat-ignore would more often still show
response.createdfirst.The logged OCX
499/client_closed_request/client closed request during web-searchshape remains the correct downstream classification once Desktop drops the idle stream (#158); it is not the root cause of the five-minute silence.Next step on our side
We will treat this as actionable and investigate the web-search / Alibaba + sidecar path for “SSE headers, then zero outbound frames until client idle cancel.”
A fresh repro with temporary OCX outbound SSE-frame logging (or a loopback capture) would still be useful if you hit it again — that is the only way to close the last send-vs-decode split — but it is no longer required to keep the issue blocked.
Clearing the
needs-infowait on our side and leaving this open for investigation.Wibias commentedon Jul 27, 2026
Investigation landed a fix in #555: wire
response.heartbeatkeepalives no longer get suppressed by invisible adapter heartbeats during web-search buffering / raw-byte progress. That should stop Codex Desktop's ~5 min SSE idle timeout from firing while upstream is still alive.fix(bridge): keep wire heartbeats during adapter-only progress
lidge-jun commentedon Jul 27, 2026
Fixed by PR #555, merged into dev at df57bd9. The bridge now separates upstream liveness from client-visible wire activity, so invisible adapter heartbeats during web-search buffering no longer suppress parser-ignored response.heartbeat frames. Verified on rebased head d2bef13 with local bridge test/typecheck/privacy/full pre-push gate, independent Sol audit, CodeRabbit, and hosted cross-platform CI; the initial macOS timeout was rerun and passed.
fix(ci): close freeform issue-quality template bypass
fix(bridge): keep wire heartbeats during adapter-only progress