Skip to content

fix(responses): classify Codex WS failures instead of restating them - #4232

Merged
lidge-jun merged 2 commits into
devfrom
codex/260911-l6-streaming-tools
Sep 11, 2026
Merged

fix(responses): classify Codex WS failures instead of restating them#4232
lidge-jun merged 2 commits into
devfrom
codex/260911-l6-streaming-tools

Conversation

@lidge-jun

@lidge-jun lidge-jun commented Sep 10, 2026

Copy link
Copy Markdown
Owner

Summary

A long Codex thread that dies on the WebSocket transport reports one of two sentences, and neither distinguishes the cases it is true of:

codex websocket closed before a Responses terminal event (close 1006 Connection ended)
codex websocket response prelude timed out

A socket that was never answered, a socket that answered in 40 ms with a quota frame and then produced nothing for 90 seconds, and a socket that died after the response was already flowing all reach the user as the same text. The reporter of #4191 established the little that was knowable by toggling the proxy by hand and observing that the same thread recovers immediately when OpenCodex is bypassed. Making that failure legible is what this change does.

The exchange now keeps a content-free stage record: the response.create frame's byte count, whether the send completed, how many upstream frames arrived, how many of those the metadata channel claimed as quota or response metadata, how many Responses events were relayed downstream, and the durations from send to the first upstream frame and to the failure. classifyCodexWsFailure reduces that to one of before-send, no-upstream-frame, no-response-event, after-response-started, and the detail is appended to the existing message:

codex websocket closed before a Responses terminal event (close 1006 Connection ended) [cause=no-response-event request=15982104B sent=yes frames=3 control=3 relayed=0 first-frame=41ms elapsed=1204ms]

The detail is a suffix, after the close-code tail, so (close 1006 Connection ended) remains one contiguous substring for every existing reader and assertion. The frame is measured only while a failure message is being built, so the happy path never pays for sizing a multi-megabyte string. Three paths carry it: the prelude timeout, the close-before-terminal message, and the transport error. The size- and queue-limit failures already name their own precise cause and are untouched.

Every field is a size, a count, or a duration. No request body, header, close reason beyond what was already surfaced, or account identifier can reach a message built from this record.

A finding worth recording separately. Reading the exchange for this issue shows where the 90-second prelude budget actually goes: preludeTimer is armed once after a successful send and is cleared only by commitResponse(), which runs solely on a frame the metadata channel did not claim. So the budget measures send to the first Responses event, and upstream liveness on the control channel does not extend it. A socket that keeps sending quota updates while the backend works through a large replayed thread still dies at exactly 90 s, and before this change it reported the same sentence as a socket that was never answered. cause=no-response-event with control=3 first-frame=41ms is what that now looks like.

Deliberately not implemented, per the lane packet, which scopes this issue to honest classification and returns the rest as a report:

  • Automatic HTTP/SSE fallback after an open socket dies. failStream treats a completed send as possibly executing upstream. The maintainer comment on the issue is explicit that responseCommitted === false and zero downstream bytes are not proof the upstream did not accept or execute the frame, so a resend gated on either can duplicate a turn. The new classification must not be read as a fallback-eligibility signal; the type comment says so at the definition, because no-upstream-frame is exactly the value a future reader would be tempted to misuse.
  • A configurable or longer prelude budget (Make the Codex WebSocket response prelude timeout configurable #3976). 90 s is already three times the original 30 s, and the finding above suggests the real question is different: whether control-channel liveness should extend the budget at all.
  • A size preflight below the current ceiling. codexWsCreateFrameExceedsLimit routes at 16 MiB − 64 KiB; the band just under it still dials the socket, and a long full-replay thread sits there. Narrowing eligibility by predicted size or expected time-to-first-token changes transport selection for every user, not only failing ones. The byte count now in the failure message is what would supply the measurements for that decision.

Design notes and the reproduction reasoning are in devlog/_plan/260911_l6_streaming_tools/010_4191_ws_failure_classification.md.

Refs #4191 — the report's remaining asks are answered above rather than patched, so the issue stays open.

Verification

  • bun run test, bun run test:changed, bun run typecheck and bun run build:gui: NOT RUN, by operator instruction for this dispatch round. Hosted CI on the exact pushed head is the only product evidence this change offers.
  • New focused regression test: tests/responses/ws-failure-stage.test.ts, in the responses domain beside ws-upstream.test.ts, registered in scripts/test-layout/layout.json and tests/fixtures/test-layout-expected.json. It covers the classifier's four stages, the renderer's exact output including the n/a durations, the contiguity of the close-code tail that existing assertions depend on, and four end-to-end cases through a fake socket: an unanswered close, a quota-only close, a close after relayed events, and the prelude timeout under fake timers.
  • Compatibility with the four existing assertions in tests/responses/ws-upstream.test.ts (lines 1061, 1281, 1542-1543, 1556) was checked by reading each one: all four match on substrings or unanchored regexes that end at or before the close-code tail, and the detail is appended after it.
  • Reviewed by a read-only subagent against the staged diff before push.

Checklist

  • Scope stays focused and avoids unrelated cleanup.
  • Docs or release notes were updated when needed.
  • Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults.

Summary by CodeRabbit

  • Bug Fixes

    • Improved WebSocket failure reporting when a response ends unexpectedly or times out.
    • Failure messages now identify whether the issue occurred before sending, before receiving data, before a response event, or after streaming began.
    • Added diagnostic details such as request size, frame counts, relayed events, and timing to help explain incomplete responses.
    • Enhanced close and oversized-frame errors with clearer, more actionable context.
  • Tests

    • Added comprehensive coverage for WebSocket failure classification, timeout handling, close messages, and streaming scenarios.

A long Codex thread that dies on the WebSocket transport reports one of two
sentences, and neither distinguishes the cases it is true of: a socket that was
never answered, a socket that carried only quota control frames, and a socket
that died after the response was already flowing all produce the same text. The
reporter of #4191 had to establish that much by toggling the proxy by hand.

Add a content-free stage record to the exchange: create-frame byte count,
whether the send completed, upstream frame count, how many of those the metadata
channel claimed, Responses events relayed downstream, and the durations from
send to first frame and to the failure. classifyCodexWsFailure reduces it to
before-send, no-upstream-frame, no-response-event, or after-response-started,
and the detail is appended after the existing message so the close-code tail
stays one contiguous substring for every reader that matches on it.

The frame is measured only when a failure message is being built, so the happy
path never pays for sizing a multi-megabyte string.

The classification is not a fallback-eligibility signal, and the no-replay-after-
send contract is unchanged: a completed send may be executing upstream whatever
the counters say.

Refs #4191
@lidge-jun
lidge-jun requested a review from Ingwannu as a code owner September 10, 2026 22:50
@github-actions

Copy link
Copy Markdown
Contributor

Deterministic PR hygiene checks passed.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 10, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-10T22:53:56.870756Z 030316c PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions github-actions Bot added the bug Something isn't working label Sep 10, 2026
@coderabbitai

coderabbitai Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: b5ed224f-621a-4420-a7df-decbb68fd516

📥 Commits

Reviewing files that changed from the base of the PR and between ed839a3 and 030316c.

📒 Files selected for processing (7)
  • devlog/_plan/260911_l6_streaming_tools/000_packet.md
  • devlog/_plan/260911_l6_streaming_tools/010_4191_ws_failure_classification.md
  • scripts/test-layout/layout.json
  • src/server/responses/codex-ws-exchange.ts
  • src/server/responses/codex-ws-wire.ts
  • tests/fixtures/test-layout-expected.json
  • tests/responses/ws-failure-stage.test.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 7 remain after this review.


📝 Walkthrough

Walkthrough

The change adds Codex WebSocket failure-stage classification and diagnostic details. The exchange records frame, control, relay, and timing data. New tests cover classification, formatting, close handling, timeouts, and streamed-response failures.

Changes

Codex WebSocket diagnostics

Layer / File(s) Summary
Diagnostic plan and verification scope
devlog/_plan/260911_l6_streaming_tools/*
The planning notes define failure stages, diagnostic fields, excluded fallback and budget changes, ownership rules, and verification cases.
Failure-stage contract and message rendering
src/server/responses/codex-ws-wire.ts
The wire module adds CodexWsFailureStage, four failure causes, diagnostic rendering, and optional details for close messages.
Exchange instrumentation and regression coverage
src/server/responses/codex-ws-exchange.ts, tests/responses/ws-failure-stage.test.ts, scripts/test-layout/layout.json, tests/fixtures/test-layout-expected.json
The exchange records WebSocket activity and timing, appends diagnostics to failure paths, and adds unit and integration coverage with test-layout registration.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~25 minutes

Severity of issue fixed: Medium

Sequence Diagram(s)

sequenceDiagram
  participant Codex WebSocket
  participant codex-ws-exchange
  participant codex-ws-wire
  participant SSE stream
  Codex WebSocket->>codex-ws-exchange: Send frames and emit WebSocket events
  codex-ws-exchange->>codex-ws-exchange: Record counters and timing
  codex-ws-exchange->>codex-ws-wire: Create failure-stage snapshot
  codex-ws-wire-->>codex-ws-exchange: Return cause and diagnostic suffix
  codex-ws-exchange->>SSE stream: Report the classified failure
Loading

Suggested reviewers: invalid-email-address

Merge Risk: ⚪ Minimal · up to 03031

This PR adds diagnostic detail to Codex WebSocket failure messages without changing behavior, fallback logic, or size/queue-limit handling. Review found no unresolved correctness, security, or data-exposure issues in the classification or rendering logic, and the new test coverage exercises the classifier stages and message formatting paths. The change is low risk and safe to merge from a code-correctness standpoint, with hosted CI serving as the remaining runtime validation since local checks were not run.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 9 functions across 3 files. (4 skipped: 4… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: classifying Codex WebSocket failures instead of reporting indistinguishable messages. It matches the implementation and PR objectives.
Full details: Docstring Coverage

Explanation

Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 9 functions across 3 files. (4 skipped: 4 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/260911-l6-streaming-tools

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@lidge-jun

Copy link
Copy Markdown
Owner Author

리뷰 · 우선순위 72 / 80

이 PR은 Codex WebSocket이 중간에 죽을 때 사용자에게 보이던 두 문장(codex websocket closed before a Responses terminal event, codex websocket response prelude timed out)을 그대로 두지 않고, 실제로 어느 단계까지 갔는지 붙입니다. 지금 dev(HEAD ed839a3ee, 패키지 2.51.0, 방금 #4226 L1·#4227 L5가 올라간 상태)에서 src/server/responses/codex-ws-exchange.ts의 prelude 타이머는 보내기 성공 직후 한 번만 켜지고, commitResponse()는 메타데이터 채널이 가져가지 않은(Responses) 프레임에서만 꺼집니다. 그래서 쿼터 프레임만 오고 응답 본문이 안 오면 90초 뒤 같은 문장으로 죽고, 소켓이 아예 안 답해도 같은 문장입니다. 이슈 #4191 제보자는 프록시를 껐다 켜서 우회할 때만 된다는 사실밖에 못 남겼습니다. 이 변경이 그 구멍을 메웁니다.

구현은 codex-ws-wire.ts에 내용 없는 단계 기록 CodexWsFailureStage(요청 바이트, 전송 여부, upstream/control/relayed 카운트, first-frame·elapsed ms)와 classifyCodexWsFailure(before-send / no-upstream-frame / no-response-event / after-response-started), codexWsFailureDetail 접미사를 두고, exchange 쪽 세 실패 경로(prelude timeout, close-before-terminal, transport error)에만 붙입니다. 접미사는 close-code 꼬리 (close 1006 Connection ended) 뒤에만 붙어서, 기존 ws-upstream.test.ts 부분 문자열·정규식 매칭을 깨지 않습니다. 프레임 바이트 측정은 실패 메시지를 만들 때만 해서, 정상 경로에서 메가바이트급 create 프레임을 매번 재지 않습니다. 크기·큐 한도 실패는 이미 원인이 분명해서 손대지 않았습니다.

레인 패킷이 정한 범위도 지켰습니다. 자동 HTTP/SSE 재전송, prelude 예산 늘리기(#3976), 16 MiB−64 KiB 아래 대역의 더 낮은 size preflight는 일부러 안 넣었고, 분류 결과를 fallback 자격으로 읽지 말라는 주석까지 타입 정의에 박아 두었습니다. 보내기 끝난 뒤에는 upstream이 이미 돌고 있을 수 있어서 responseCommitted === false나 relayed=0만으로 재전송하면 턴이 중복될 수 있습니다. 이슈는 Closes가 아니라 Refs #4191로 열어 둔 판단이 맞습니다. 남은 질문은 “쿼터 프레임이 오는 동안 prelude를 연장할지”, “그 아래 크기 대역을 언제 SSE로 내릴지”이고, 그건 별도 정책·측정이 필요합니다.

테스트는 tests/responses/ws-failure-stage.test.ts로 분리했고, 분류 4단계·렌더·close-code 연속성·가짜 소켓 end-to-end(무응답 close, 쿼터만, 응답 시작 후 drop, fake timer prelude)를 잡습니다. layout.json / test-layout-expected.json에도 등록했습니다. 로컬 스위트·typecheck·GUI 빌드는 라운드 지시대로 NOT RUN이고, 증거는 푸시 헤드 030316c5d의 호스트 CI입니다. 지금 api usage·gates·test 1/4·4/4 등은 초록이고 test 2/4·3/4·macos 샤드는 아직 pending입니다. devlog 패킷·유닛 260911_l6_streaming_tools/도 같이 와서, 현재 dev 방향(일곱 레인 실행, L1/L5 다음 L6)과 맞습니다. L6 패킷의 두 번째 항목 #4190(qoder 스캐폴딩 누수)은 이 PR 범위 밖입니다.

경로 src/server/responses/codex-ws-wire.ts · classifyCodexWsFailure / codexWsFailureDetail - 원인 네 칸과 접미사 렌더. close-code 꼬리 뒤에만 붙여 기존 리더를 지키려는 선택이 맞습니다. fallback 자격으로 읽지 말라는 주석도 유지하세요.

경로 src/server/responses/codex-ws-exchange.ts · failureStage() - 카운터와 지연 측정. 빈 문자열·비문자열 프레임도 upstreamFrames를 올린 뒤 early return 합니다. 드물지만 빈 프레임만 오면 no-upstream-frame 대신 no-response-event로 보일 수 있습니다. 진단용이라 치명적이진 않지만, 빈 프레임은 카운트에서 빼는 편이 더 정직합니다.

경로 tests/responses/ws-failure-stage.test.ts - 회귀 고정. 기존 ws-upstream.test.ts FakeWebSocket 패턴을 재사용한 점은 좋습니다. 호스트 CI의 responses 샤드가 이 파일을 실제로 돌리는지만 확인하면 됩니다.

경로 devlog/_plan/260911_l6_streaming_tools/ - 패킷은 #4191을 “정직한 분류만”, #4190은 qoder 어댑터로 분리했습니다. 이 PR은 1번만 처리했고, 미구현 세 가지를 보고로 남긴 구성이 레인 지시와 일치합니다.

메인테이너의 판단이 필요한 지점

  • #4191을 이 PR만으로 부분 완료로 둘지, 아니면 “원인 이름 붙이기”를 닫고 prelude/제어채널·하위 size preflight를 후속 이슈로 쪼갤지.
  • 쿼터(control) 프레임이 오는 동안 prelude 90초를 연장할지, 아니면 지금처럼 Responses 첫 이벤트 기준으로 두고 원인만 더 잘 보이게 할지.
  • 16 MiB−64 KiB 바로 아래 대역을 언제 SSE로 강제할지. 이제 실패 메시지에 request=…B가 들어가니, 측정값을 모은 뒤 별도 이슈로 여는 편이 안전합니다.
  • L6 두 번째 작업 #4190을 이 브랜치에 스택할지, dev에 #4232를 먼저 랜딩한 뒤 새 헤드에서 열지.

너의 추천
남은 CI(특히 test 2/4·3/4와 responses 관련 샤드)가 초록이면 #4232를 머지하세요. #4191에는 이 PR로 “실패 단계가 메시지에 붙는다”는 반쪽 완료 코멘트를 남기고 이슈는 열어 두세요. 자동 SSE 재전송·예산 연장·하위 size preflight는 여기에 끼워 넣지 마세요. 빈 프레임이 upstreamFrames를 올리는 점은 머지 전 한 줄로 고치거나, 후속 청소로 남겨도 됩니다. 다음에 L6 #4190을 같은 레인에서 이어서 열면 됩니다.

이 댓글은 grok-bot이 작성했습니다

@lidge-jun
lidge-jun force-pushed the codex/260911-l6-streaming-tools branch 2 times, most recently from 82b41d5 to 030316c Compare September 10, 2026 22:59
@lidge-jun
lidge-jun merged commit 9341de0 into dev Sep 11, 2026
54 of 57 checks passed
@lidge-jun
lidge-jun deleted the codex/260911-l6-streaming-tools branch September 11, 2026 00:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant