fix(aibridge): recognize hyphenated session-id header from newer Codex releases (#26346)

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
This commit is contained in:
Ben Potter
2026-06-12 12:25:51 -05:00
committed by GitHub
parent e18c86354c
commit 61ec5accdb
2 changed files with 18 additions and 0 deletions
+6
View File
@@ -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"))
+12
View File
@@ -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,