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
Codex App repeatedly shows Reconnecting …/5 after an upstream safety block interrupts a response. Expected: surface the block as a terminal error rather than repeatedly reconnecting.
Send a request from Codex App through OpenCodex using the configuration above. Intermittently, output begins, then stops with repeated reconnect attempts. No deterministic minimal prompt identified.
Error and evidence
stream disconnected before completion: This request was blocked by our safety systems. Reason: Potentially unintended activity.
In a 400-request log sample, 44 requests had this error; 27 recorded output before failure. All 44 were logged as HTTP 502, errorCode: upstream_server_error.
The upstream block's cause is unknown; direct-client behavior has not been compared. Could the error mapping cause a terminal rejection to be treated as retryable?
Checks
Searched existing issues and documentation.
Removed credentials, account identifiers, prompts, and personal data.
Closed by #5200, squash-merged into dev as 3fd293d841.
The mapping hypothesis in this report was right, and it is worth saying exactly how it was confirmed rather than just asserting it.
Codex classifies a response.failed by error.code alone. Its fatal set is seven codes — context_length_exceeded, insufficient_quota, usage_not_included, cyber_policy, misalignment_policy_violation, invalid_prompt, bio_policy — and everything outside that set is retried. CodexErr::is_retryable confirms the same split from the other side. Codex also has no handler for a bare SSE error event at all, which makes the terminal this proxy synthesizes the only channel a refusal can travel through.
That channel was discarding the verdict. upstreamErrorTailFrame in src/server/relay.ts stamped upstream_server_error on every synthesized terminal unconditionally. The upstream's message survived, so the client displayed the refusal text while treating the turn as a retryable disconnect — which is exactly the "shows the block, then reconnects" behaviour reported here, and why all 44 requests in the sample logged as HTTP 502 with upstream_server_error.
The fix is classification, not circumvention. A structured code decides on its own when the upstream sends one; a recognized refusal phrase is read only when there is no code at all. Code and message are read from the same candidate priority, so the envelope that supplied the message also supplies the verdict. The refusal set was narrowed to the four content refusals; insufficient_quota and context_length_exceeded stay on their existing quota and context paths. Without a refusal code the terminal is byte-identical to before, so a genuine transport failure remains retryable — there is a regression asserting precisely that.
Four rounds of parallel adversarial review all returned FAIL on the first pass and found five real defects independently: a non-refusal code being overridden by message matching, a code-only refusal with no message falling through to adapter_eof, a refusal nested under response.error invisible to the code scan, a read failure after a refusal being overwritten with upstream_reset and retried, and a low-priority nested refusal beating a high-priority transient code. All five are fixed with regressions.
One thing was deliberately left out of scope: the request-log accounting that records these as 502. Changing it surfaced a separate defect where an earlier bare error latches a verdict a later genuine terminal should set, and that could not be verified without running locally, which this lane is prohibited from doing.
Evidence is exact-head hosted CI at 614fbd76829ad9b4c1d04b030a1c92d637549554: test 1/4 through 4/4 and gates all successful, with the lane's regressions running on those Linux shards. No local suite was run.
Activity
fix(responses): deliver an upstream refusal as a terminal error
test(responses): pin how an upstream refusal reaches Codex as retryable
fix(responses): deliver an upstream refusal as a terminal error
Deliver an upstream refusal as a terminal error instead of a retryabl…
lidge-jun commentedon Sep 19, 2026
Closed by #5200, squash-merged into
devas3fd293d841.The mapping hypothesis in this report was right, and it is worth saying exactly how it was confirmed rather than just asserting it.
Codex classifies a
response.failedbyerror.codealone. Its fatal set is seven codes —context_length_exceeded,insufficient_quota,usage_not_included,cyber_policy,misalignment_policy_violation,invalid_prompt,bio_policy— and everything outside that set is retried.CodexErr::is_retryableconfirms the same split from the other side. Codex also has no handler for a bare SSEerrorevent at all, which makes the terminal this proxy synthesizes the only channel a refusal can travel through.That channel was discarding the verdict.
upstreamErrorTailFrameinsrc/server/relay.tsstampedupstream_server_erroron every synthesized terminal unconditionally. The upstream's message survived, so the client displayed the refusal text while treating the turn as a retryable disconnect — which is exactly the "shows the block, then reconnects" behaviour reported here, and why all 44 requests in the sample logged as HTTP 502 withupstream_server_error.The fix is classification, not circumvention. A structured code decides on its own when the upstream sends one; a recognized refusal phrase is read only when there is no code at all. Code and message are read from the same candidate priority, so the envelope that supplied the message also supplies the verdict. The refusal set was narrowed to the four content refusals;
insufficient_quotaandcontext_length_exceededstay on their existing quota and context paths. Without a refusal code the terminal is byte-identical to before, so a genuine transport failure remains retryable — there is a regression asserting precisely that.Four rounds of parallel adversarial review all returned FAIL on the first pass and found five real defects independently: a non-refusal code being overridden by message matching, a code-only refusal with no message falling through to
adapter_eof, a refusal nested underresponse.errorinvisible to the code scan, a read failure after a refusal being overwritten withupstream_resetand retried, and a low-priority nested refusal beating a high-priority transient code. All five are fixed with regressions.One thing was deliberately left out of scope: the request-log accounting that records these as 502. Changing it surfaced a separate defect where an earlier bare error latches a verdict a later genuine terminal should set, and that could not be verified without running locally, which this lane is prohibited from doing.
Evidence is exact-head hosted CI at
614fbd76829ad9b4c1d04b030a1c92d637549554:test 1/4through4/4andgatesall successful, with the lane's regressions running on those Linux shards. No local suite was run.