Found while comparing graff's WS→SSE ladder against openai/codex's (deepwiki: their WebsocketStreamOutcome::FallbackToHttp fast-paths an HTTP 426 Upgrade Required straight to sticky HTTP fallback, no retry burn).
graff's ws.zig already distinguishes this case — a non-101 handshake status returns error.UpgradeRequired for 426 specifically, and the error's doc comment says "the caller should fall back to SSE" (ws.zig:52-53, :255-257). But no caller consumes it: UpgradeRequired appears nowhere outside ws.zig, so agent_ws.postLive treats it as a generic handshake failure. Against a server/proxy that has declared it won't upgrade, graff burns one full rebuild + fresh WS dial (which 426s again) before the two-failure latch (ws_failures_before_fallback = 2) finally sets ws_off.
Fix: in the postLive connect-failure handling, catch error.UpgradeRequired and latch ws_off immediately — the server answered authoritatively; a retry can't change its mind. One extra else if on an existing error switch plus a test (the mock in agent_ws_mock.zig can answer 426).
Note: agent_ws.zig sits at 598/600 — this edit must budget a shave (per the slice-1b PR note).
Found while comparing graff's WS→SSE ladder against openai/codex's (deepwiki: their
WebsocketStreamOutcome::FallbackToHttpfast-paths an HTTP426 Upgrade Requiredstraight to sticky HTTP fallback, no retry burn).graff's
ws.zigalready distinguishes this case — a non-101 handshake status returnserror.UpgradeRequiredfor426specifically, and the error's doc comment says "the caller should fall back to SSE" (ws.zig:52-53,:255-257). But no caller consumes it:UpgradeRequiredappears nowhere outsidews.zig, soagent_ws.postLivetreats it as a generic handshake failure. Against a server/proxy that has declared it won't upgrade, graff burns one full rebuild + fresh WS dial (which 426s again) before the two-failure latch (ws_failures_before_fallback = 2) finally setsws_off.Fix: in the
postLiveconnect-failure handling, catcherror.UpgradeRequiredand latchws_offimmediately — the server answered authoritatively; a retry can't change its mind. One extraelse ifon an existing error switch plus a test (the mock inagent_ws_mock.zigcan answer 426).Note:
agent_ws.zigsits at 598/600 — this edit must budget a shave (per the slice-1b PR note).