From 61ec5accdb65f412bcc762a8bda05e961208f5fc Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Fri, 12 Jun 2026 12:25:51 -0500 Subject: [PATCH] fix(aibridge): recognize hyphenated session-id header from newer Codex releases (#26346) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex prompts were showing up as one session per request in the AI Sessions list instead of being grouped into a conversation. ## Root Cause AI Gateway extracts the Codex session key from the `session_id` request header: https://github.com/coder/coder/blob/main/aibridge/session.go#L57-L58 Newer Codex releases renamed the header to `session-id` (hyphen) in [`codex-rs/codex-api/src/requests/headers.rs`](https://github.com/openai/codex/blob/main/codex-rs/codex-api/src/requests/headers.rs): ```rust insert_header(&mut headers, "session-id", &id); ``` `Header.Get` is case-insensitive but not underscore/hyphen-insensitive, so no session key is extracted and every request falls back to its own session. Reproduced with Codex CLI 0.139.0. ## Changes - Check `session-id` first, fall back to the legacy `session_id` for older Codex versions - Added test cases for the hyphenated header and precedence ## Before/After The same three-prompt Codex conversation ("Write a haiku about Pittsburgh" → "Now make it about Coder" → "Translate it to Spanish", via `codex exec` + `codex exec resume --last`) against a local build. **Before**: each prompt of the conversation lands as its own session, Threads: 1 ![before](https://raw.githubusercontent.com/coder/coder/recordings/recordings/codex-session-grouping/before.jpg) **After**: the conversation is a single session with Threads: 3 ![after](https://raw.githubusercontent.com/coder/coder/recordings/recordings/codex-session-grouping/after.jpg) Clicking into the session shows all three threads on the session timeline: ![after session detail](https://raw.githubusercontent.com/coder/coder/recordings/recordings/codex-session-grouping/after-session-detail.jpg) Linear: [AIGOV-437](https://linear.app/codercom/issue/AIGOV-437) 🤖 Generated with Coder Agents on behalf of @bpmct --- aibridge/session.go | 6 ++++++ aibridge/session_test.go | 12 ++++++++++++ 2 files changed, 18 insertions(+) diff --git a/aibridge/session.go b/aibridge/session.go index 9882029ea3..dcd60ed85a 100644 --- a/aibridge/session.go +++ b/aibridge/session.go @@ -55,6 +55,12 @@ func GuessSessionID(client Client, r *http.Request) *string { } return cleanRef(matches[1]) case ClientCodex: + // Codex renamed the header from "session_id" to "session-id" in + // newer releases. Check the current name first, then fall back to + // the legacy name for older Codex versions. + if sid := cleanRef(r.Header.Get("session-id")); sid != nil { + return sid + } return cleanRef(r.Header.Get("session_id")) case ClientMux: return cleanRef(r.Header.Get("X-Mux-Workspace-Id")) diff --git a/aibridge/session_test.go b/aibridge/session_test.go index 24d72d57a3..222f00a068 100644 --- a/aibridge/session_test.go +++ b/aibridge/session_test.go @@ -105,6 +105,18 @@ func TestGuessSessionID(t *testing.T) { headers: map[string]string{"session_id": "codex-session-123"}, sessionID: utils.PtrTo("codex-session-123"), }, + { + name: "codex_with_hyphenated_session_header", + client: aibridge.ClientCodex, + headers: map[string]string{"session-id": "codex-session-456"}, + sessionID: utils.PtrTo("codex-session-456"), + }, + { + name: "codex_hyphenated_header_takes_precedence", + client: aibridge.ClientCodex, + headers: map[string]string{"session-id": "codex-session-new", "session_id": "codex-session-old"}, + sessionID: utils.PtrTo("codex-session-new"), + }, { name: "codex_with_whitespace_in_header", client: aibridge.ClientCodex,