Skip to content

Recover Codex websocket requests after upstream resets#3904

Open
dale0525 wants to merge 68 commits into
router-for-me:devfrom
dale0525:codex-ws-1009-http-fallback
Open

Recover Codex websocket requests after upstream resets#3904
dale0525 wants to merge 68 commits into
router-for-me:devfrom
dale0525:codex-ws-1009-http-fallback

Conversation

@dale0525

@dale0525 dale0525 commented Jun 18, 2026

Copy link
Copy Markdown

Summary

  • recover fresh Codex websocket requests by falling back to the HTTP Codex executor when upstream websocket setup/read fails before any downstream payload is emitted, including close code 1009 (message too big) and repeated pre-payload websocket: close sent failures
  • sanitize websocket-only envelope fields before HTTP fallback, and keep fallback scoped to requests that can be safely replayed without upstream websocket context
  • preserve and replay local websocket transcript state when Codex upstream websocket state is lost, including reconnects keyed by the downstream Codex turn metadata
  • intercept upstream previous_response_not_found websocket errors and retry the same downstream request once with transcript replay when local transcript state is available
  • guard append-only and incomplete incremental websocket requests from unsafe fresh reconnects or HTTP fallback, so missing upstream context is surfaced as a session disconnect instead of losing conversation state
  • add regression coverage for 1009 fallback, stale/closed websocket retry behavior, transcript replay after state loss, and previous_response_not_found replay recovery

Testing

  • go test ./sdk/api/handlers/openai -run 'TestResponsesWebsocket' -count=1
  • go test ./internal/runtime/executor -run 'TestCodexWebsockets|TestCodexExecutor' -count=1
  • go test ./sdk/api/handlers -run 'TestRequestExecutionMetadata' -count=1
  • CC=/usr/bin/clang go build -o test-output ./cmd/server && rm test-output

@github-actions
github-actions Bot changed the base branch from main to dev June 18, 2026 15:06
@github-actions

Copy link
Copy Markdown

This pull request targeted main.

The base branch has been automatically changed to dev.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements an HTTP fallback mechanism for the Codex Websockets Executor when a "message too big" error (WebSocket close code 1009) is encountered during execution or streaming. If no payload has been sent yet, the executor falls back to standard HTTP execution or streaming. A test case has been added to verify this fallback behavior. The reviewer identified a critical race condition where the downstream websocket connection could still be closed prematurely due to the upstream disconnect notification being triggered immediately on a 1009 error. A code suggestion was provided to dynamically determine the notification flag based on the error type to prevent this premature closure.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines 1569 to +1571
func (e *CodexWebsocketsExecutor) invalidateUpstreamConn(sess *codexWebsocketSession, conn *websocket.Conn, reason string, err error) {
e.clearUpstreamConn(sess, conn, reason, err, true)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

There is a subtle race condition where the downstream websocket connection can still be closed prematurely despite the fallback mechanism.

When readUpstreamLoop encounters a 1009 (CloseMessageTooBig) error, it immediately calls invalidateUpstreamConn, which always invokes clearUpstreamConn with notify = true. Because readUpstreamLoop runs concurrently and triggers this notification immediately after sending the error to readCh, it will almost always execute before ExecuteStream or Execute can receive the error and call clearUpstreamConn(..., false). This triggers sess.notifyUpstreamDisconnect(err), closing sess.upstreamDisconnectCh and signaling the downstream handler to close the downstream websocket before the HTTP fallback stream can be completed.

To resolve this, we should dynamically determine the notify flag in invalidateUpstreamConn based on whether the error is a 1009 message too big error.

Suggested change
func (e *CodexWebsocketsExecutor) invalidateUpstreamConn(sess *codexWebsocketSession, conn *websocket.Conn, reason string, err error) {
e.clearUpstreamConn(sess, conn, reason, err, true)
}
func (e *CodexWebsocketsExecutor) invalidateUpstreamConn(sess *codexWebsocketSession, conn *websocket.Conn, reason string, err error) {
notify := !isCodexWebsocketMessageTooBigError(err)
e.clearUpstreamConn(sess, conn, reason, err, notify)
}

@dale0525
dale0525 force-pushed the codex-ws-1009-http-fallback branch from 02f5664 to b279372 Compare June 18, 2026 15:09

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 02f566421f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

terminateReason = "message_too_big_http_fallback"
helps.RecordAPIWebsocketError(ctx, e.cfg, "read_message_too_big_http_fallback", errRead)
if sess != nil {
e.clearUpstreamConn(sess, conn, terminateReason, errRead, false)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Suppress session disconnect before falling back

When a downstream Responses websocket uses an execution session, readUpstreamLoop is the goroutine that receives the close-1009, sends it into readCh, and then immediately calls invalidateUpstreamConn, which notifies UpstreamDisconnectChan; the websocket handler's subscriber closes the client connection before this goroutine reaches this clearUpstreamConn(..., false) fallback path. In that session/passthrough case the HTTP fallback stream is written to a closed downstream websocket, so 1009 still drops the client instead of falling back.

Useful? React with 👍 / 👎.

if sess != nil {
e.clearUpstreamConn(sess, conn, terminateReason, errRead, false)
}
fallbackResult, errFallback := e.CodexExecutor.ExecuteStream(ctx, auth, req, opts)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve previous_response_id for fallback

For downstream websocket passthrough on later turns, req.Payload can be only an incremental response.create/response.append with previous_response_id, not the full transcript. Passing that payload directly to CodexExecutor.ExecuteStream loses the session state because the HTTP Codex executor deletes previous_response_id before POSTing, so a close-1009 on a follow-up turn sends the fallback request without conversation context; the fallback needs to replay/expand the transcript or avoid HTTP fallback for incremental websocket requests.

Useful? React with 👍 / 👎.

@dale0525
dale0525 force-pushed the codex-ws-1009-http-fallback branch from b279372 to 4832fe6 Compare June 18, 2026 15:28
@dale0525

Copy link
Copy Markdown
Author

Addressed the current automated review feedback in 4832fe6:

  • Suppressed upstream-disconnect notifications for websocket close 1009 so the downstream websocket is not closed before HTTP fallback can complete.
  • Extended the fallback regression test to run with an execution session and assert the disconnect channel is not signaled.
  • Gated HTTP fallback to requests without previous_response_id, so incremental websocket follow-up requests do not fall back to HTTP with incomplete context.
  • Added a regression test for the previous_response_id case.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4832fe6f96

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +827 to +829
func canFallbackCodexWebsocketRequestToHTTP(body []byte) bool {
return strings.TrimSpace(gjson.GetBytes(body, "previous_response_id").String()) == ""
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Disable HTTP fallback for append-only turns

When a downstream Responses websocket is in passthrough mode, later turns can be response.append messages that carry only the incremental input and no previous_response_id (the handler accepts that shape in normalizeResponsesWebsocketPassthroughRequest). This helper therefore allows the new 1009 HTTP fallback for those append-only turns, but CodexExecutor.ExecuteStream has no access to the reused websocket session and posts only this incremental payload over HTTP, so the fallback response is generated without the prior conversation context. Please also reject type == "response.append" here unless the fallback path can replay the full transcript.

Useful? React with 👍 / 👎.

@dale0525
dale0525 force-pushed the codex-ws-1009-http-fallback branch from 4832fe6 to d3aa355 Compare June 18, 2026 15:34
@dale0525

Copy link
Copy Markdown
Author

Addressed the latest append-only review feedback in d3aa355:

  • HTTP fallback is now disabled when the websocket request body is type == "response.append".
  • The incremental-request regression test now covers both previous_response_id and response.append payloads to ensure neither falls back to HTTP without full transcript context.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d3aa355613

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

}

func (e *CodexWebsocketsExecutor) invalidateUpstreamConn(sess *codexWebsocketSession, conn *websocket.Conn, reason string, err error) {
notify := !isCodexWebsocketMessageTooBigError(err)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Notify 1009 disconnects when fallback is skipped

When a session request receives close-1009 but the new HTTP fallback is not taken (for example previous_response_id/response.append requests, or after sentPayload is already true), ExecuteStream falls through to the normal read-error path. Because this line suppresses UpstreamDisconnectChan solely based on the close code, the Responses websocket subscriber in sdk/api/handlers/openai/openai_responses_websocket.go never closes the downstream client even though the upstream session was cleared; keep the suppression tied to the actual fallback path rather than every 1009 disconnect.

Useful? React with 👍 / 👎.

@dale0525
dale0525 force-pushed the codex-ws-1009-http-fallback branch from d3aa355 to c9bcede Compare June 18, 2026 15:44
@dale0525

Copy link
Copy Markdown
Author

Addressed the latest 1009-disconnect review feedback in c9bcede:

  • readUpstreamLoop no longer invalidates immediately when it successfully delivers an upstream read error to the active request.
  • The active request now decides whether to suppress disconnect notification only on the actual HTTP fallback path.
  • Non-fallback 1009 paths, including previous_response_id, response.append, and post-payload errors, invalidate the upstream session and notify UpstreamDisconnectChan.
  • The incremental-request regression test now runs with an execution session and asserts the disconnect notification is delivered when fallback is skipped.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c9bcedea0e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

if sess != nil {
e.clearUpstreamConn(sess, conn, terminateReason, errRead, false)
}
fallbackResult, errFallback := e.CodexExecutor.ExecuteStream(ctx, auth, req, opts)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Strip websocket envelope before HTTP fallback

When a downstream Responses websocket passthrough request hits close-1009 before any payload, this falls back by passing the original websocket event payload to CodexExecutor.ExecuteStream. That payload still contains the transport envelope (type: "response.create", preserved by normalizeResponsesWebsocketPassthroughRequest), while the HTTP Responses POST should receive only the create body; the HTTP executor does not remove the top-level type, so the new 1009 fallback can be rejected for actual websocket clients even though the direct test payload lacks this field. Build a sanitized HTTP request (or strip websocket-only fields) before invoking the HTTP executor.

Useful? React with 👍 / 👎.

@dale0525
dale0525 force-pushed the codex-ws-1009-http-fallback branch from c9bcede to baf3ad6 Compare June 18, 2026 15:55
@dale0525

Copy link
Copy Markdown
Author

Addressed the websocket-envelope HTTP fallback feedback in baf3ad6:

  • HTTP fallback now builds a sanitized request before calling the HTTP Codex executor.
  • The sanitized fallback request removes websocket-only type: "response.create" and generate fields from both req.Payload and opts.OriginalRequest.
  • The 1009 fallback regression test now uses a realistic websocket envelope and asserts the HTTP fallback body does not contain type or generate.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: baf3ad6823

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +147 to +148
case ch <- ev:
delivered = true

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Don't treat buffered delivery as handled after terminal events

When the upstream sends a terminal event and then closes before the request goroutine runs its deferred clearActive, this buffered send can enqueue the close error into readCh even though the request has already returned and no one will read it. Because delivered becomes true, readUpstreamLoop skips invalidating the session, leaving sess.conn pointing at a closed socket; the next turn then hits the send-error path, notifies UpstreamDisconnectChan before retrying, and closes the downstream websocket despite the retry potentially succeeding.

Useful? React with 👍 / 👎.

@dale0525
dale0525 force-pushed the codex-ws-1009-http-fallback branch from baf3ad6 to 597b123 Compare June 18, 2026 16:14
@dale0525

Copy link
Copy Markdown
Author

Addressed the latest terminal-close review feedback in 597b123:

  • Terminal websocket events now stop accepting buffered active-read delivery, so a close frame immediately after response.completed is not treated as consumed by a request that already returned.
  • readUpstreamLoop clears the closed upstream connection without notifying UpstreamDisconnectChan when the close follows a terminal event for the same completed request.
  • Session send-error retry now suppresses the disconnect notification until the retry actually fails, so a stale closed socket does not close the downstream websocket before a successful reconnect.
  • Added a regression test that sends response.completed followed by an immediate close, verifies the session connection is cleared without downstream disconnect, and confirms the next turn reconnects successfully.

Testing:

  • go test ./internal/runtime/executor -run 'TestCodexWebsockets'
  • go test ./sdk/api/handlers/openai -run 'TestResponsesWebsocket'
  • CC=/usr/bin/clang go build -o test-output ./cmd/server && rm test-output

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 597b123236

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +1611 to +1613
if terminalDeliveredCh != nil && sess.activeChannelUnchangedOrCleared(terminalDeliveredCh) {
e.clearUpstreamConn(sess, conn, "upstream_disconnected_after_terminal", errRead, false)
return

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Suppress terminal closes before the next turn attaches

When the upstream sends response.completed and then its close frame is processed only after the stream goroutine has cleared the old active channel and the downstream immediately starts another turn, activeCh now points at the new request, so this check is false and the old terminal-close is delivered to that new request at line 1615. That makes the next turn fail and notifies UpstreamDisconnectChan, closing the downstream websocket instead of silently reconnecting; the added test waits for sess.conn == nil before sending the second request, so it misses this fast-follow case.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 597b123236

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +1611 to +1613
if terminalDeliveredCh != nil && sess.activeChannelUnchangedOrCleared(terminalDeliveredCh) {
e.clearUpstreamConn(sess, conn, "upstream_disconnected_after_terminal", errRead, false)
return

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Suppress terminal closes before the next turn attaches

When the upstream sends response.completed and then its close frame is processed only after the stream goroutine has cleared the old active channel and the downstream immediately starts another turn, activeCh now points at the new request, so this check is false and the old terminal-close is delivered to that new request at line 1615. That makes the next turn fail and notifies UpstreamDisconnectChan, closing the downstream websocket instead of silently reconnecting; the added test waits for sess.conn == nil before sending the second request, so it misses this fast-follow case.

Useful? React with 👍 / 👎.

@dale0525
dale0525 force-pushed the codex-ws-1009-http-fallback branch from 597b123 to 549a4ad Compare June 18, 2026 16:30
@dale0525

Copy link
Copy Markdown
Author

Addressed the latest fast-follow terminal-close feedback in 549a4ad:

  • Terminal-close errors after a completed response now clear the stale upstream connection without notifying UpstreamDisconnectChan.
  • If the downstream has already attached the next turn to that stale connection, the reader delivers an internal stale-terminal-close signal instead of the raw close error.
  • The current turn handles that signal by opening a fresh websocket and resending the request before any downstream payload is emitted.
  • The regression test now fast-follows the second turn before the old terminal close is processed, verifies the old websocket receives that request, and then verifies the retry succeeds on a fresh websocket without downstream disconnect.

Testing:

  • go test ./internal/runtime/executor -run 'TestCodexWebsockets'
  • go test ./sdk/api/handlers/openai -run 'TestResponsesWebsocket'
  • CC=/usr/bin/clang go build -o test-output ./cmd/server && rm test-output

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 549a4ad449

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines 620 to 623
e.clearUpstreamConn(sess, conn, "send_error", errSend, false)

// Retry once with a new websocket connection for the same execution session.
connRetry, respHSRetry, errDialRetry := e.ensureUpstreamConn(ctx, auth, sess, authID, wsURL, wsHeaders)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Stop old reader before retrying a stale send

When a session write fails because the reused websocket is stale, this clears and closes the old connection but leaves the current readCh accepting events while the retry is being set up. The old readUpstreamLoop is still running; closing conn can make it deliver that old-connection error into the active channel and close it, so after the retry succeeds the stream skips the mismatched event (ev.conn != connRetry) and then fails with session read channel closed instead of reading the retry response. Stop delivery for the old active channel or replace it before closing the stale connection.

Useful? React with 👍 / 👎.

@dale0525
dale0525 force-pushed the codex-ws-1009-http-fallback branch from 549a4ad to a9868d0 Compare June 18, 2026 16:42
@dale0525

Copy link
Copy Markdown
Author

Addressed the latest stale-send retry feedback in a9868d0:

  • Session send-error retry now marks the old websocket connection read error as suppressed before closing the stale connection.
  • The active read channel is stopped and replaced before retrying, so the old reader cannot close the channel that the retried request will read from.
  • readUpstreamLoop now ignores suppressed or no-longer-current connection read errors without notifying UpstreamDisconnectChan.

Testing:

  • go test ./internal/runtime/executor -run 'TestCodexWebsockets'
  • go test ./sdk/api/handlers/openai -run 'TestResponsesWebsocket'
  • CC=/usr/bin/clang go build -o test-output ./cmd/server && rm test-output

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a9868d0cfb

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +381 to +382
readCh = make(chan codexWebsocketRead, 4096)
sess.setActive(readCh)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clear the replacement active channel

When a non-streaming Execute call uses an execution session and this retry path is taken, the earlier defer sess.clearActive(readCh) has already captured the original channel, so this reassignment leaves the replacement channel registered after a successful retry. In that scenario a later upstream close can be delivered into the abandoned buffered channel and skipped, leaving the session pointing at a closed websocket until the next turn hits the stale-send path; use a deferred closure or explicitly clear the replacement channel before returning.

Useful? React with 👍 / 👎.

@dale0525
dale0525 force-pushed the codex-ws-1009-http-fallback branch from a9868d0 to 4ff42dc Compare June 18, 2026 16:51
@dale0525

Copy link
Copy Markdown
Author

Addressed the latest replacement active-channel feedback in 4ff42dc:

  • Non-streaming Execute now clears the current active read channel through a deferred closure, so a replacement channel created during send retry is not left registered after a successful retry.

Testing:

  • go test ./internal/runtime/executor -run 'TestCodexWebsockets'
  • go test ./sdk/api/handlers/openai -run 'TestResponsesWebsocket'
  • CC=/usr/bin/clang go build -o test-output ./cmd/server && rm test-output

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4ff42dc9f9

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +794 to +798
if !sentPayload && isCodexWebsocketMessageTooBigError(errRead) && canFallbackCodexWebsocketRequestToHTTP(upstreamBody) {
terminateReason = "message_too_big_http_fallback"
helps.RecordAPIWebsocketError(ctx, e.cfg, "read_message_too_big_http_fallback", errRead)
if sess != nil {
e.clearUpstreamConn(sess, conn, terminateReason, errRead, false)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve websocket context after HTTP fallback

When a downstream Responses websocket passthrough first turn hits 1009 here, the HTTP fallback can successfully return a response while clearUpstreamConn leaves no upstream websocket with conversation state. The passthrough handler accepts later response.append messages without tracking/replaying lastResponseID, so the next append-only turn on the same downstream socket is sent to a fresh upstream websocket with only incremental input and loses the prior turn. Please close the downstream websocket after this fallback or force transcript/previous_response_id replay before accepting later append-only turns.

Useful? React with 👍 / 👎.

}
terminateReason = "read_error"
terminateErr = errRead
if sess != nil && !sentPayload && isCodexWebsocketStaleTerminalCloseError(errRead) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Disable stale-close retry for append-only turns

When a passthrough downstream sends a later response.append without previous_response_id, its context exists only on the reused upstream websocket. If a stale terminal close from the previous turn arrives before any bytes from this new turn, this retry path reconnects and resends the request; buildCodexWebsocketRequestBody rewrites the append as response.create without replaying the transcript, so the follow-up is answered as a fresh conversation. Please skip this reconnect retry for append-only requests or expand them before retrying.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f02ac3dac1

ℹ️ 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +2127 to +2130
errType := strings.TrimSpace(gjson.GetBytes(payload, "error.type").String())
if errType == "" {
errType = strings.TrimSpace(gjson.GetBytes(payload, "body.error.type").String())
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Read top-level error_type for statusless errors

When Codex sends a statusless top-level error such as {"type":"error","error_type":"invalid_request_error","message":"bad request"}, this status inference never copies error_type into errType even though the payload builder below preserves that field. The error is therefore surfaced as a 500 instead of the intended 400, which makes clients and retry/auth-release logic treat a client-side request error as an upstream server failure.

Useful? React with 👍 / 👎.

Comment on lines +2006 to +2015
errType := strings.TrimSpace(gjson.GetBytes(payload, "error.type").String())
if errType == "" {
errType = strings.TrimSpace(gjson.GetBytes(payload, "body.error.type").String())
}
if errType == "" {
topLevelType := strings.TrimSpace(gjson.GetBytes(payload, "type").String())
if !strings.EqualFold(topLevelType, wsEventTypeError) {
errType = topLevelType
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Classify top-level error_type in downstream errors

For downstream websocket passthrough, a statusless error event with top-level error_type (for example invalid_request_error) reaches this parser directly; because type is just error, errType remains empty and the status defaults to 500. That makes recoverable/client-side Codex errors sent in this supported top-level shape look like server failures to the websocket client.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d016a1f060

ℹ️ 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

if status <= 0 {
status = int(gjson.GetBytes(payload, "status_code").Int())
}
errCode := strings.TrimSpace(gjson.GetBytes(payload, "error.code").String())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Classify string-valued websocket errors

When Codex sends the statusless form with a bare error string, e.g. {"type":"error","error":"websocket_connection_limit_reached"}, the executor-side detector already treats that error value as a connection-limit code, but this downstream parser only copies error.code/body.error.code/top-level code. The status therefore falls through to 500, so websocket clients see a server error and shouldReleaseResponsesWebsocketPinnedAuth will not release a saturated pinned auth instead of handling it as a 429; read a string-valued error as a code before inferring status.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 88ed5c043f

ℹ️ 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

current := s.conn
suppressed := s.suppressReadErrorConn == conn
s.connMu.Unlock()
return current != conn || suppressed

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Deliver close errors for externally closed Codex sockets

When a Codex auth is removed (sdk/cliproxy/service.go:753) or a session is otherwise closed, closeCodexWebsocketSession clears sess.conn before closing the socket. With this condition, the reader sees current != conn and suppresses the resulting read error instead of delivering it to the active request channel, so an in-flight request waiting in readCodexWebsocketMessage can hang until the client context or idle timeout rather than failing promptly. Suppress only explicitly marked/stale reader errors, or still notify activeCh for externally closed sockets.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4b1c72c4e1

ℹ️ 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

continue
}

if wsErr, ok := codexWebsocketStatuslessErrorEvent(payload); ok {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Handle statusless errors before downstream passthrough

For downstream websocket passthrough, statusless upstream error events hit the DownstreamWebsocket branch and return on eventType == "error" before this statusless-error handler runs. In cases like {"type":"error","code":"websocket_connection_limit_reached"} the handler may release/retry auth, but the executor leaves the old upstream conn cached; the next turn can then reuse the same failed socket/auth via ensureUpstreamConn instead of clearing it as the statusful error path does.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3751163081

ℹ️ 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +565 to +570
if forwardErrMsg != nil {
lastRequest = previousLastRequest
lastResponseOutput = previousLastResponseOutput
lastResponseID = previousLastResponseID
lastResponsePendingToolCallIDs = previousLastResponsePendingToolCallIDs
passthroughModelName = previousPassthroughModelName

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restore pinned auth when rolling back failed turns

When a non-passthrough turn switches to another websocket-capable auth and then returns a normal upstream error that shouldReleaseResponsesWebsocketPinnedAuth does not catch (for example a generic 400 validation error), the selected-auth callback may already have set pinnedAuthID/lastAttemptedAuthID for that failed turn. This rollback restores the previous transcript and passthrough model but leaves those auth fields pointing at the failed model's auth, so the next model-less follow-up derives the previous model from lastRequest while executing with WithPinnedAuthID for the failed model, which can route to the wrong auth or fail auth selection; save and restore or clear the pinned auth state with the rest of the rolled-back turn state.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c437e9c9e9

ℹ️ 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +992 to +995
if !canRetryCodexWebsocketRequestAfterStaleTerminalClose(sess, upstreamBody) {
terminateReason = "stale_terminal_close_append_without_retry"
helps.RecordAPIWebsocketError(ctx, e.cfg, "read_stale_terminal_close_append_without_retry", errRead)
sess.notifyUpstreamDisconnect(errRead)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat stale terminal closes as replayable state loss

When the upstream closes just after a previous terminal event and the next follow-up/create turn has already installed a new active reader, readUpstreamLoop marks the session lost and delivers errCodexWebsocketStaleTerminalClose. For live-context requests this branch sends that error and notifies UpstreamDisconnectChan, but the Responses websocket retry logic only intercepts request requires existing websocket session or previous_response_not_found, so the downstream socket is closed instead of replaying the cached transcript. Return an interceptable state-loss error (and avoid the disconnect notification) for this no-fresh-retry case.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ea1d805596

ℹ️ 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

if codexWebsocketRequestUsesPreviousResponseID(body) || codexWebsocketRequestIsAppendOnly(body) {
return false
}
return codexWebsocketInputLooksFullTranscript(gjson.GetBytes(body, "input"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reset type-less user-only replays before resending

Fresh evidence after the earlier reset fix: this return still requires an assistant/tool/compaction marker. When the Responses handler sends a normalized replay body with no type and an input containing only user/developer messages after a prior upstream turn completed with no output, this returns false, so resetUpstreamConnForFreshContext is skipped; buildCodexWebsocketRequestBody then sends that full transcript as response.create on top of the old Codex websocket state, duplicating prior context instead of starting from the replayed transcript.

Useful? React with 👍 / 👎.

Comment on lines +2032 to +2033
} else if strings.EqualFold(errCode, "previous_response_not_found") || strings.EqualFold(errType, "invalid_request_error") {
status = http.StatusBadRequest

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat message-only previous-response errors as replayable

This status inference still only checks errCode/errType before defaulting statusless errors to 500. If a downstream passthrough error arrives as a statusless message-only envelope such as {"type":"error","message":"previous_response_id ... not found"}, responsesWebsocketPreviousResponseNotFoundError rejects it because the status is not 400, so the cached transcript replay path is skipped even though the executor-side status inference handles message-only previous-response misses.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 498703c1cc

ℹ️ 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +610 to +611
if callerScope == "" {
return sessionKey

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Bind anonymous reconnect cache entries to a caller

Fresh evidence: AuthMiddleware explicitly allows requests without setting userApiKey when no auth providers are configured (internal/api/server.go:1990-1992), so this branch stores/restores transcript state under only the client-supplied session header in that supported mode. In an unauthenticated deployment, any client that reuses or guesses another client's X-Client-Request-Id/Session-Id can inherit cached lastRequest and response output for up to 30 minutes; skip reconnect caching without a caller scope or add another per-client binding.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ead92185bd

ℹ️ 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

isTerminalEvent := eventType == "response.completed" || eventType == "response.done" || eventType == "error"
isTerminalEvent := isCodexWebsocketSuccessfulTerminalEvent(eventType) || eventType == "response.failed" || eventType == "error"
if eventType == "response.failed" {
wsErr, _, _ := codexTerminalFailureErr(payload)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Map response.failed state-loss errors to replayable 400

When Codex reports upstream state loss as a terminal response.failed event whose response.error only carries code: previous_response_not_found or the matching message without type: invalid_request_error, this branch converts it through codexTerminalFailureErr, which defaults that body to 502. The Responses websocket retry guard only replays cached transcript for 400 previous-response errors, so this recoverable state-loss terminal failure is forwarded/release-handled instead of being replayed; preserve/map previous_response_not_found to 400 before returning the stream error.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1faa455a8f

ℹ️ 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

default:
}
s.clearActive(ch)
close(ch)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid closing active read channels cross-goroutine

When an auth/session close calls closeCodexWebsocketSession while readUpstreamLoop has already captured the same active channel for a text frame, this closes ch from deliverActiveRead; the reader goroutine can then execute its ch <- codexWebsocketRead{...} send on a closed channel and panic the process. Cancel or mark the channel inactive without closing it, or serialize all sends with the close.

Useful? React with 👍 / 👎.

Comment on lines +2046 to +2047
} else {
status = http.StatusInternalServerError

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Classify statusless rate-limit errors as 429

When downstream passthrough receives a statusless Codex error with error.type: rate_limit_error or code: rate_limit_exceeded, this inference falls through to 500 because only websocket connection limits and previous-response/invalid-request errors are recognized. shouldReleaseResponsesWebsocketPinnedAuth does not release pinned auths on 500, so a rate-limited credential can stay pinned instead of failing over; map those statusless rate-limit shapes to 429.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: db741f4475

ℹ️ 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

strings.Contains(message, "previous_response_id") && strings.Contains(message, "not found") {
return http.StatusBadRequest
}
return http.StatusInternalServerError

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Map statusless Codex rate limits to 429

In the executor path, a statusless Codex websocket error such as {"type":"error","error":{"type":"rate_limit_error","code":"rate_limit_exceeded"}} (or usage_limit_reached) is not recognized by the cases above and falls through to a 500 here. Non-passthrough Codex websocket callers then see a server failure instead of a retryable 429, and quota errors also lose the existing retry-after handling used by the terminal-error path; add the same rate/quota classification before defaulting to 500.

Useful? React with 👍 / 👎.

dale0525 added 3 commits July 16, 2026 06:03
…allback

# Conflicts:
#	internal/runtime/executor/codex_websockets_executor.go
#	internal/runtime/executor/codex_websockets_executor_test.go
#	sdk/api/handlers/openai/openai_responses_websocket.go
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