Skip to content

fix(ws): a 426 latches SSE immediately — the server answered authoritatively (#427) - #443

Merged
justrach merged 1 commit into
mainfrom
fix/427-ws-426-fastpath
Aug 6, 2026
Merged

fix(ws): a 426 latches SSE immediately — the server answered authoritatively (#427)#443
justrach merged 1 commit into
mainfrom
fix/427-ws-426-fastpath

Conversation

@justrach

@justrach justrach commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Fixes #427, the codex-parity gap found in the deepwiki comparison: ws.zig has always raised error.UpgradeRequired distinctly on a 426 handshake, but no caller consumed it — so a server that declared it won't upgrade cost one full rebuild + redial before the two-failure latch.

The fix

One term folded into the ladder's existing decision: declined or wsShouldFallback(...). A 426 latches ws_off and serves this attempt over the persistent SSE path — no CodexWsReanchor, no rebuild, no second dial. The structurally-unreachable delta-body case still routes through the existing guard so a previous_response_id can never reach SSE. Trace notes the decision in the file's own ternary idiom; transport_aborted deliberately untouched (a routing decision is not a stream abort). Cap paid honestly: one syntactic fold, agent_ws.zig at 599/600.

Evidence

  • agent_ws_mock.refuseUpgrade serves the whole ladder on one loopback port (N refused handshakes with a chosen status, then the SSE POST graff falls back to). New agent_ws_fallback_test.zig drives the real postLive end-to-end: (a) one 426 → 1 dial, ws_off, SSE completion, correct trace note; (b) a 500 handshake keeps today's retry-then-latch exactly; (c) zero transport_aborted events in either case.
  • Revert-proof: removing the declined term → 995 pass / 1 fail, exactly test (a).
  • Tests 994 → 996 (+2 exactly); all 9 golden eval files PASS byte-identical; fmt + line-guard clean; 14 consecutive green suite runs.
  • Harness lesson embedded in the tests: a blocked accept isn't cancellable, so failed assertions used to hang the mock — releaseAccept (throwaway dial, the http: a send-failed request re-pools its dead connection — one WriteFailed poisons every later non-streaming request (compaction, [title], subagents) #177 trick) + LIFO-ordered guards make reverts fail clean instead of parking forever. Worth copying for future multi-connection mocks.

…ry (#427)

ws.zig has always returned error.UpgradeRequired for a non-101 handshake
whose status is 426, with a doc comment saying the caller should fall back
to SSE — but no caller consumed it. postLive treated it as a generic
transport failure, so the server's authoritative "I will not upgrade this
endpoint" still cost one full rebuild plus a fresh dial (which 426s again)
before ws_failures_before_fallback finally set ws_off.

postLive now folds `declined` into the ladder's fallback term: a 426 latches
ws_off immediately and THIS attempt falls through to the SSE path, with no
error.CodexWsReanchor asking request() to rebuild first. openai/codex does
the same thing (WebsocketStreamOutcome::FallbackToHttp). The trace note
names the reason instead of claiming a second failure that never happened.
Everything else is unchanged: a non-426 handshake failure keeps its free
retry and only latches on the second, and the delta-body guard still routes
through CodexWsReanchor so a previous_response_id is never replayed on SSE.

Tests: agent_ws_mock grows refuseUpgrade, which serves the whole ladder over
one loopback port — `refusals` refused handshakes with a caller-chosen status
line, then the SSE POST graff falls back to — plus releaseAccept, so a failed
assertion reports rather than parking fut.await on a blocked accept. The two
new tests in agent_ws_fallback_test.zig drive postLive end to end: a 426 with
exactly one dial and the turn served over SSE, and a 500 that still spends the
free retry first. Both assert the sink saw no transport_aborted — choosing a
transport is a routing decision, not a cut stream. 994 -> 996 tests.

agent_ws.zig was at 598/600, so the read loop's `budget` if/else is folded to
one line to pay for the new branch; no behaviour rides on it. 599 lines.

Co-Authored-By: Codegraff <blackfloofie@codegraff.com>
@justrach
justrach merged commit d908dfd into main Aug 6, 2026
6 checks passed
@justrach
justrach deleted the fix/427-ws-426-fastpath branch August 6, 2026 09:27
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.

WS: honor error.UpgradeRequired — a 426 should latch SSE immediately, not burn a retry (codex parity)

1 participant