From eadc4238597a0cc82882a73d39627f90a6166239 Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Tue, 8 Sep 2026 20:45:07 +0000 Subject: [PATCH 1/4] =?UTF-8?q?=F0=9F=A4=96=20fix:=20preserve=20Codex=20OA?= =?UTF-8?q?uth=20prompt=20cache=20routing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Forward the existing stable prompt cache scope as session-id on Codex OAuth Responses requests, preserving explicit headers and keyless callers. --- _Generated with [`mux`](https://github.com/coder/mux) • Model: `openai:gpt-6-astra` • Thinking: `off` • Cost: `$36.74`_ --- .../services/providerModelFactory.test.ts | 23 +++++++++++++++++++ src/node/services/providerModelFactory.ts | 12 ++++++++++ 2 files changed, 35 insertions(+) diff --git a/src/node/services/providerModelFactory.test.ts b/src/node/services/providerModelFactory.test.ts index d88422fba1..e6bde7808a 100644 --- a/src/node/services/providerModelFactory.test.ts +++ b/src/node/services/providerModelFactory.test.ts @@ -1187,6 +1187,29 @@ describe("ProviderModelFactory GitHub Copilot", () => { expect(headers.get("authorization")).toBe("Bearer test-access-token"); expect(headers.get("chatgpt-account-id")).toBe("test-account-id"); expect(headers.get("content-type")).toBe("application/json"); + expect(headers.get("session-id")).toBeNull(); + + for (const sessionId of [undefined, "explicit-session"]) { + const promptCacheKey = "mux-v1-project-scope"; + const cacheHeaders = new Headers(request.headers); + if (sessionId) cacheHeaders.set("session-id", sessionId); + for (let turn = 0; turn < 2; turn++) { + await capturedFetch(request.url, { + method: "POST", + headers: cacheHeaders, + body: JSON.stringify({ + model: "gpt-5.3-codex", + input: [{ role: "user", content: `Turn ${turn}` }], + prompt_cache_key: promptCacheKey, + }), + }); + const outgoing = requests.at(-1); + expect(outgoing?.input).toBe(CODEX_ENDPOINT); + expect(new Headers(outgoing?.init?.headers).get("session-id")).toBe( + sessionId ?? promptCacheKey + ); + } + } } finally { PROVIDER_REGISTRY.openai = originalOpenAIRegistry; } diff --git a/src/node/services/providerModelFactory.ts b/src/node/services/providerModelFactory.ts index f5d7fb31bc..0f81c2d161 100644 --- a/src/node/services/providerModelFactory.ts +++ b/src/node/services/providerModelFactory.ts @@ -1728,6 +1728,18 @@ export class ProviderModelFactory { try { const headers = new Headers(init?.headers); headers.delete("content-length"); + // Codex derives its cache routing key from session-id, not the body key. + // Reuse Xum's stable scope so successive OAuth turns reach the same cache. + const { prompt_cache_key: promptCacheKey } = JSON.parse(body) as { + prompt_cache_key?: unknown; + }; + if ( + !headers.has("session-id") && + typeof promptCacheKey === "string" && + promptCacheKey.length > 0 + ) { + headers.set("session-id", promptCacheKey); + } nextInit = { ...init, headers, From e6bf739db9a7d2df4ab611b5a135396912e82405 Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Tue, 8 Sep 2026 20:53:30 +0000 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=A4=96=20fix:=20encode=20Codex=20cach?= =?UTF-8?q?e=20scopes=20for=20HTTP=20headers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Project scopes include names that may contain Unicode or control characters. Encode the stable scope rather than letting Headers reject it and skip normalization. --- _Generated with [`mux`](https://github.com/coder/mux) • Model: `openai:gpt-6-astra` • Thinking: `off` • Cost: `$48.99`_ --- src/node/services/providerModelFactory.test.ts | 12 +++++++----- src/node/services/providerModelFactory.ts | 3 ++- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/node/services/providerModelFactory.test.ts b/src/node/services/providerModelFactory.test.ts index e6bde7808a..11b4269a5a 100644 --- a/src/node/services/providerModelFactory.test.ts +++ b/src/node/services/providerModelFactory.test.ts @@ -1189,8 +1189,12 @@ describe("ProviderModelFactory GitHub Copilot", () => { expect(headers.get("content-type")).toBe("application/json"); expect(headers.get("session-id")).toBeNull(); - for (const sessionId of [undefined, "explicit-session"]) { - const promptCacheKey = "mux-v1-project-scope"; + for (const [promptCacheKey, sessionId, expectedSessionId] of [ + ["mux-v1-project-scope", undefined, "mux-v1-project-scope"], + ["mux-v1-日本語-scope", undefined, "mux-v1-%E6%97%A5%E6%9C%AC%E8%AA%9E-scope"], + ["mux-v1-project\nscope", undefined, "mux-v1-project%0Ascope"], + ["mux-v1-project-scope", "explicit-session", "explicit-session"], + ] as const) { const cacheHeaders = new Headers(request.headers); if (sessionId) cacheHeaders.set("session-id", sessionId); for (let turn = 0; turn < 2; turn++) { @@ -1205,9 +1209,7 @@ describe("ProviderModelFactory GitHub Copilot", () => { }); const outgoing = requests.at(-1); expect(outgoing?.input).toBe(CODEX_ENDPOINT); - expect(new Headers(outgoing?.init?.headers).get("session-id")).toBe( - sessionId ?? promptCacheKey - ); + expect(new Headers(outgoing?.init?.headers).get("session-id")).toBe(expectedSessionId); } } } finally { diff --git a/src/node/services/providerModelFactory.ts b/src/node/services/providerModelFactory.ts index 0f81c2d161..2f18e16257 100644 --- a/src/node/services/providerModelFactory.ts +++ b/src/node/services/providerModelFactory.ts @@ -1738,7 +1738,8 @@ export class ProviderModelFactory { typeof promptCacheKey === "string" && promptCacheKey.length > 0 ) { - headers.set("session-id", promptCacheKey); + // Project names may contain Unicode or control characters invalid in headers. + headers.set("session-id", encodeURIComponent(promptCacheKey)); } nextInit = { ...init, From 2fb324663e5363995e96699b547abb713903aedd Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Tue, 8 Sep 2026 21:16:04 +0000 Subject: [PATCH 3/4] =?UTF-8?q?=F0=9F=A4=96=20fix:=20keep=20Codex=20normal?= =?UTF-8?q?ization=20safe=20for=20malformed=20project=20names?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace lone UTF-16 surrogates via UTF-8 before encoding the stable session scope. Assert that normalization still strips truncation and disables storage. --- _Generated with [`mux`](https://github.com/coder/mux) • Model: `openai:gpt-6-astra` • Thinking: `off` • Cost: `$61.26`_ --- src/node/services/providerModelFactory.test.ts | 5 +++++ src/node/services/providerModelFactory.ts | 7 +++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/node/services/providerModelFactory.test.ts b/src/node/services/providerModelFactory.test.ts index 11b4269a5a..7ebbedae99 100644 --- a/src/node/services/providerModelFactory.test.ts +++ b/src/node/services/providerModelFactory.test.ts @@ -1193,6 +1193,7 @@ describe("ProviderModelFactory GitHub Copilot", () => { ["mux-v1-project-scope", undefined, "mux-v1-project-scope"], ["mux-v1-日本語-scope", undefined, "mux-v1-%E6%97%A5%E6%9C%AC%E8%AA%9E-scope"], ["mux-v1-project\nscope", undefined, "mux-v1-project%0Ascope"], + ["mux-v1-\ud800-scope", undefined, "mux-v1-%EF%BF%BD-scope"], ["mux-v1-project-scope", "explicit-session", "explicit-session"], ] as const) { const cacheHeaders = new Headers(request.headers); @@ -1205,10 +1206,14 @@ describe("ProviderModelFactory GitHub Copilot", () => { model: "gpt-5.3-codex", input: [{ role: "user", content: `Turn ${turn}` }], prompt_cache_key: promptCacheKey, + store: true, + truncation: "auto", }), }); const outgoing = requests.at(-1); expect(outgoing?.input).toBe(CODEX_ENDPOINT); + expect(JSON.parse(outgoing?.init?.body as string)).toMatchObject({ store: false }); + expect(JSON.parse(outgoing?.init?.body as string)).not.toHaveProperty("truncation"); expect(new Headers(outgoing?.init?.headers).get("session-id")).toBe(expectedSessionId); } } diff --git a/src/node/services/providerModelFactory.ts b/src/node/services/providerModelFactory.ts index 2f18e16257..f5ddca9e4b 100644 --- a/src/node/services/providerModelFactory.ts +++ b/src/node/services/providerModelFactory.ts @@ -1738,8 +1738,11 @@ export class ProviderModelFactory { typeof promptCacheKey === "string" && promptCacheKey.length > 0 ) { - // Project names may contain Unicode or control characters invalid in headers. - headers.set("session-id", encodeURIComponent(promptCacheKey)); + // UTF-8 replaces lone surrogates before encoding project names for HTTP headers. + headers.set( + "session-id", + encodeURIComponent(Buffer.from(promptCacheKey).toString()) + ); } nextInit = { ...init, From 0b8540c9f2a04f5e38cef4e22bb2b3970ca25e5d Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Wed, 9 Sep 2026 06:25:06 +0000 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=A4=96=20fix:=20recognize=20clean=20t?= =?UTF-8?q?itled=20Codex=20security=20reviews?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Accept the plain and auto-triggered security-review headings without exempting findings, appended findings, or repeated/unknown envelopes. --- _Generated with [`mux`](https://github.com/coder/mux) • Model: `openai:gpt-6-astra` • Thinking: `high` • Cost: `$108.19`_ --- scripts/check_codex_comments_test.py | 20 ++++++++++++-------- scripts/lib/codex_comments.jq | 2 +- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/scripts/check_codex_comments_test.py b/scripts/check_codex_comments_test.py index 0a6aac6112..8e47649302 100644 --- a/scripts/check_codex_comments_test.py +++ b/scripts/check_codex_comments_test.py @@ -141,16 +141,20 @@ def test_observed_comments_only_exempt_completed_informational_reviews(self): cache=data if cached else None, ) - def test_auto_triggered_security_heading_does_not_hide_findings(self): - header = "### 🛡️ Codex Security Review · _Automatically triggered_\n\n" + def test_security_headings_do_not_hide_findings(self): clean = FIXTURES["security_no_findings"]["body"] - for body, expected in ( - (clean, 0), - (clean + "\n\n[P1] A security issue still needs fixing.", 1), - ("Security review completed. Found a P1 credential disclosure.", 1), + for header in ( + "### 🛡️ Codex Security Review\n\n", + "### 🛡️ Codex Security Review · _Automatically triggered_\n\n", ): - with self.subTest(body=body): - self.assert_gate(expected, snapshot([comment(header + body)])) + for body, expected in ( + (clean, 0), + (clean + "\n\n[P1] A security issue still needs fixing.", 1), + ("Security review completed. Found a P1 credential disclosure.", 1), + (header + clean, 1), + ): + with self.subTest(header=header, body=body): + self.assert_gate(expected, snapshot([comment(header + body)])) def test_summary_completion_requires_metadata_and_every_review_row(self): completed = FIXTURES["summary"]["body"] diff --git a/scripts/lib/codex_comments.jq b/scripts/lib/codex_comments.jq index f917e5a48d..9163095a4b 100644 --- a/scripts/lib/codex_comments.jq +++ b/scripts/lib/codex_comments.jq @@ -4,7 +4,7 @@ # Strip only the observed help text: a heading alone must not hide a finding # added inside the details section, or a second section appended after it. def codex_without_help: - ltrimstr("### 🛡️ Codex Security Review · _Automatically triggered_\n\n") + sub("^### 🛡️ Codex Security Review( · _Automatically triggered_)?\n\n"; "") | rtrimstr("\n\n
ℹ️ About Codex in GitHub\n
\n\n" + "[Your team has set up Codex to review pull requests in this repo](https://chatgpt.com/codex/cloud/settings/general). Reviews are triggered when you\n" + "- Open a pull request for review\n- Mark a draft as ready\n- Comment \"@codex review\" or \"@codex security review\".\n\n"