Skip to content

Enhancement: transparently refetch once on zero-output mid-stream socket reset (generic passthrough lanes) #3384

Description

@Yum-wu

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:

  1. 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:
  2. 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.
  3. 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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    streamingSSE, WebSocket, terminal stream frames

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions