Skip to content

fix: harden stream transport reliability#25

Merged
rexdotsh merged 15 commits into
mainfrom
fix/stream-transport-reliability
Jul 11, 2026
Merged

fix: harden stream transport reliability#25
rexdotsh merged 15 commits into
mainfrom
fix/stream-transport-reliability

Conversation

@rexdotsh

@rexdotsh rexdotsh commented Jul 11, 2026

Copy link
Copy Markdown
Owner

Summary

Hardens long-stream reliability across the proxy transports, aligning the Codex WebSocket transport with OpenCode's implementation (ws client, hard termination, session-scoped fallback).

Downstream SSE delivery

  • Codex WebSocket streams now emit SSE keepalive comments so idle proxies/load balancers don't kill long turns
  • Strips stale content-length/content-encoding, adds Cache-Control: no-cache, no-transform and X-Accel-Buffering: no
  • Explicit upstream content types (e.g. JSON error bodies on streaming requests) are preserved instead of being relabeled as text/event-stream, and keepalive comments are never injected into non-SSE bodies

Upstream lifecycle

  • Every upstream fetch receives the downstream request's abort signal, so client disconnects cancel upstream work (and are not recorded as 500 proxy failures)
  • Claude/OpenAI SSE transforms are pull-driven, preserving downstream backpressure instead of buffering unboundedly

Codex WebSocket transport (aligned with OpenCode)

  • Migrated from Bun's native WebSocket to ws@8.21.0 (same client OpenCode uses): terminate() for hard cleanup of invalid/aborted/timed-out sockets, send-completion callbacks, graceful close only for healthy reuse
  • Connection-limit retries are race-safe: events from a replaced socket are ignored via a socket epoch, and a retry that loses to a concurrent failure terminates its socket instead of leaking it
  • Compaction turns (X-Codex-Turn-Metadata: request_kind=compaction) deterministically route over HTTP, avoiding 1009 message too big closes
  • Fallback to SSE remains strictly session-scoped with a 5 minute TTL: a mid-stream 1006 moves only that session to HTTP; other sessions keep using WebSocket
  • Failure tracking maps are bounded (1000 sessions, FIFO eviction)

Diagnostics

  • Expected client cancellations no longer log warnings
  • Real stream failures include requestBytes, payloadCount, upstreamIdleMs, downstreamIdleMs, and WebSocket close codes

Test plan

  • bun test (83 tests, includes new coverage for abort termination, retry/close races, session fallback isolation, backpressure, content-type preservation, keepalive suppression)
  • bun typecheck
  • bun lint
  • bun run build
  • Two independent agent review passes of the full diff (all findings fixed)

@rexdotsh

Copy link
Copy Markdown
Owner Author

Follow-up hardening from a second deep review pass, four more commits:

  • c68b91f — stale socket events during connection-limit retries (HIGH): when the upstream sent the limit payload and then closed the socket, the queued close event could fail the retried stream, spuriously pin the session to HTTP fallback, and leak the freshly opened retry socket. Events now carry a socket epoch and are ignored once their socket is replaced; the retry also re-checks settled after connecting and terminates the new socket if the stream already failed. onError is serialized through the message chain so it can't clobber a terminal payload still being processed.
  • 634af47 — fallback state lifecycle (MEDIUM): restores main's 5-minute TTL on session fallback marks (the Set conversion had made them permanent until restart) and bounds streamFailureCounts at 1000 sessions with FIFO eviction.
  • 2b3b2e2 — analytics (LOW): client-aborted upstream fetches are no longer recorded as 500 proxy failures.
  • 7d9fd0f — keepalive corruption (LOW): SSE keepalive comments are no longer attached to explicitly non-SSE bodies (e.g. slow-draining JSON error responses).

New regression tests cover the retry/close race (including no-fallback afterwards) and keepalive suppression/presence. 84 tests, typecheck, lint, build all pass.

@rexdotsh

Copy link
Copy Markdown
Owner Author

Dropped the egress proxy support (proxy-from-env, https-proxy-agent) in 75a3941 — deployment target is a plain VPS behind a Cloudflare Tunnel (ingress only), so outbound connections dial directly and the proxy path was dead code. Only runtime dependency added by this PR is now ws.

@rexdotsh
rexdotsh merged commit ea7a796 into main Jul 11, 2026
3 checks passed
@rexdotsh
rexdotsh deleted the fix/stream-transport-reliability branch July 11, 2026 12:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant