Client or integration
ZCode / dsh / Codex CLI / Codex App (any client of the Responses native passthrough or native chat SSE lanes)
Area
Proxy and routing — upstream body relay (mid-stream transport failure)
Summary
A mid-stream socket reset after HTTP 200 + SSE headers, but before any body byte is relayed to the client, currently kills the turn with a terminal response.failed / upstream_reset (retryable: false). The client (ZCode/dsh/Codex) has no pi-style turn-level retry, so the turn stops with Turn execution failed ... The socket connection was closed unexpectedly.
This is a real, recurring shape on this setup: opencodex → Clash proxy → opencode.ai/zen (Cloudflare terminates idle keep-alive connections server-side; Bun's fetch pool reuses the half-closed socket), with long reasoning streams (muse-spark) making the mid-stream window large.
Root cause
fetchWithTransientRetry / fetchWithResetRetry (src/lib/upstream-retry.ts) only cover pre-stream failures — fetch() rejecting before response headers. Once headers arrive and the relay starts reading the body, a reset surfaces as a ReadableStream.read() rejection, which is outside every pre-stream retry wrapper. The eager relay (relay-eager.ts) and the tee/pull relay (relay.ts) catch it and synthesize response.failed / upstream_reset — deliberately NOT a resend (relay.ts:251-257 comment), because replaying after partial output would duplicate tool-call side effects.
Adjacent work (not equivalent)
Suggested fix (mirrors pi, more conservative)
Wrap the passthrough body before tee/eager relay with a zero-output gate:
- Only when zero bytes were consumed and no protocol terminal was seen (nothing relayed to the client), and the error matches
isConnectionResetError (socket connection was closed unexpectedly / ECONNRESET / EPIPE), and the client signal is alive:
- Refetch once with
connection-reset recovery init (Connection: close + keepalive: false) so Bun never reuses the half-closed pooled socket, and continue relaying the fresh body.
- Any partial-output failure keeps the existing fail-closed tail (replaying after emitted tool calls would duplicate side effects).
I have a working local implementation of exactly this (a wrapWithZeroOutputRefetch body wrapper + refetchOnZeroOutputReset helper in src/lib/upstream-retry.ts, wired into both the Responses passthrough and the native chat SSE lane, verified byte-identical re-application after upgrades). Happy to turn it into a PR following the repo's conventions if maintainers are open to it.
Environment
- opencodex 2.40.0 / 2.41.0, Bun 1.4.0, Windows
- Provider: opencode-zen/muse-spark-1.3-contributor-free via local gateway → proxy 127.0.0.1:7897 → opencode.ai/zen
Client or integration
ZCode / dsh / Codex CLI / Codex App (any client of the Responses native passthrough or native chat SSE lanes)
Area
Proxy and routing — upstream body relay (mid-stream transport failure)
Summary
A mid-stream socket reset after HTTP 200 + SSE headers, but before any body byte is relayed to the client, currently kills the turn with a terminal
response.failed / upstream_reset(retryable: false). The client (ZCode/dsh/Codex) has no pi-style turn-level retry, so the turn stops withTurn execution failed ... The socket connection was closed unexpectedly.This is a real, recurring shape on this setup: opencodex → Clash proxy →
opencode.ai/zen(Cloudflare terminates idle keep-alive connections server-side; Bun's fetch pool reuses the half-closed socket), with long reasoning streams (muse-spark) making the mid-stream window large.Root cause
fetchWithTransientRetry/fetchWithResetRetry(src/lib/upstream-retry.ts) only cover pre-stream failures —fetch()rejecting before response headers. Once headers arrive and the relay starts reading the body, a reset surfaces as aReadableStream.read()rejection, which is outside every pre-stream retry wrapper. The eager relay (relay-eager.ts) and the tee/pull relay (relay.ts) catch it and synthesizeresponse.failed / upstream_reset— deliberately NOT a resend (relay.ts:251-257 comment), because replaying after partial output would duplicate tool-call side effects.Adjacent work (not equivalent)
retryable: trueso the client can retry. The generic Responses passthrough and chat SSE lanes have no equivalent gate.Suggested fix (mirrors pi, more conservative)
Wrap the passthrough body before tee/eager relay with a zero-output gate:
isConnectionResetError(socket connection was closed unexpectedly/ECONNRESET/EPIPE), and the client signal is alive:connection-resetrecovery init (Connection: close+keepalive: false) so Bun never reuses the half-closed pooled socket, and continue relaying the fresh body.I have a working local implementation of exactly this (a
wrapWithZeroOutputRefetchbody wrapper +refetchOnZeroOutputResethelper insrc/lib/upstream-retry.ts, wired into both the Responses passthrough and the native chat SSE lane, verified byte-identical re-application after upgrades). Happy to turn it into a PR following the repo's conventions if maintainers are open to it.Environment