Skip to content

Commit 6441e83

Browse files
committed
fix(gateway): prefer Codex window continuity
1 parent 74eaec1 commit 6441e83

15 files changed

Lines changed: 69 additions & 9 deletions

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -864,9 +864,9 @@ limit checks, accounting, and account selection path. `/v1/realtime` and OpenAI
864864
Realtime SDK websocket or session routes are not supported.
865865

866866
Continuity headers are local routing inputs. Codex Pooler chooses them in this
867-
order: `x-codex-session-id` > `session-id` > `x-session-affinity` >
868-
`session_id` > `x-codex-conversation-id`. `session-id` and
869-
`x-session-affinity` are not forwarded upstream. Local timing regressions showed
867+
order: `x-codex-window-id` > `x-codex-session-id` > `session-id` >
868+
`x-session-affinity` > `session_id` > `x-codex-conversation-id`. `session-id` and
869+
`x-session-affinity` are not forwarded upstream. The raw `x-codex-window-id` value is hashed before it becomes a local persisted session key. Local timing regressions showed
870870
`/v1/responses` HTTP streaming and Responses websocket paths stay inside the
871871
observed client budgets with the existing stream timeout settings, so no new
872872
route-specific timeout defaults are required.

lib/codex_pooler/gateway/contracts.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ defmodule CodexPooler.Gateway.Contracts do
77
@continuation_anchor_headers [
88
"x-codex-previous-response-id",
99
"x-codex-turn-state",
10+
"x-codex-window-id",
1011
"x-codex-session-id",
1112
"session-id",
1213
"x-session-affinity",

lib/codex_pooler/gateway/payloads/request_options.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ defmodule CodexPooler.Gateway.Payloads.RequestOptions do
5656

5757
@websocket_responses_endpoint "/backend-api/codex/responses"
5858

59-
@session_header_sources ~w(x-codex-session-id session-id x-session-affinity session_id x-codex-conversation-id)
59+
@session_header_sources ~w(x-codex-window-id x-codex-session-id session-id x-session-affinity session_id x-codex-conversation-id)
6060

6161
@prompt_cache_key_routes [
6262
"/v1/responses",

lib/codex_pooler/gateway/persistence/session_continuity.ex

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -905,11 +905,29 @@ defmodule CodexPooler.Gateway.Persistence.SessionContinuity do
905905
defp session_key(%RequestOptions{} = request_options) do
906906
request_options.continuity.accepted_turn_state
907907
|> blank_to_nil()
908-
|> Kernel.||(request_options.continuity.session_header |> blank_to_nil())
908+
|> Kernel.||(session_header_session_key(request_options))
909909
|> Kernel.||(request_options.continuity.session_key |> blank_to_nil())
910910
|> Kernel.||(Ecto.UUID.generate())
911911
end
912912

913+
defp session_header_session_key(%RequestOptions{
914+
continuity: %{session_header_source: "x-codex-window-id", session_header: session_header}
915+
}) do
916+
case blank_to_nil(session_header) do
917+
nil -> nil
918+
value -> "x-codex-window-id:" <> safe_hash(value)
919+
end
920+
end
921+
922+
defp session_header_session_key(%RequestOptions{continuity: %{session_header: session_header}}) do
923+
blank_to_nil(session_header)
924+
end
925+
926+
defp safe_hash(value) when is_binary(value) do
927+
:crypto.hash(:sha256, value)
928+
|> Base.encode16(case: :lower)
929+
end
930+
913931
defp conversation_key(%RequestOptions{} = request_options) do
914932
request_options.continuity.conversation_key |> blank_to_nil()
915933
end

lib/codex_pooler_web/controllers/runtime/gateway_controller_helpers.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ defmodule CodexPoolerWeb.Runtime.GatewayControllerHelpers do
207207

208208
defp session_header(conn) do
209209
[
210+
"x-codex-window-id",
210211
"x-codex-session-id",
211212
"session-id",
212213
"x-session-affinity",

test/codex_pooler/gateway/contracts_test.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ defmodule CodexPooler.Gateway.ContractsTest do
66
@anchor_headers [
77
"x-codex-previous-response-id",
88
"x-codex-turn-state",
9+
"x-codex-window-id",
910
"x-codex-session-id",
1011
"session-id",
1112
"x-session-affinity",

test/codex_pooler/gateway/payloads/request_options_test.exs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,13 @@ defmodule CodexPooler.Gateway.Payloads.RequestOptionsTest do
451451
end
452452

453453
test "normalizes session header provenance to the compatibility allowlist" do
454+
assert %{continuity: %{session_header_source: "x-codex-window-id"}} =
455+
RequestOptions.build(
456+
%{session_header: "window-session", session_header_source: "X-Codex-Window-ID"},
457+
"/backend-api/codex/responses",
458+
%{"model" => "example-model"}
459+
)
460+
454461
assert %{continuity: %{session_header_source: "session-id"}} =
455462
RequestOptions.build(
456463
%{session_header: "local-session", session_header_source: "Session-ID"},

test/codex_pooler/gateway/routing/session_continuity_test.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ defmodule CodexPooler.Gateway.Routing.SessionContinuityTest do
165165
end
166166

167167
for session_header_source <- [
168+
"x-codex-window-id",
168169
"x-codex-session-id",
169170
"session-id",
170171
"x-session-affinity",

test/codex_pooler_web/codex_responses_socket_test.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ defmodule CodexPoolerWeb.CodexResponsesSocketTest do
3232
assert recovery["anchor_removal"]["headers"] == [
3333
"x-codex-previous-response-id",
3434
"x-codex-turn-state",
35+
"x-codex-window-id",
3536
"x-codex-session-id",
3637
"session-id",
3738
"x-session-affinity",

test/codex_pooler_web/controllers/runtime/backend_codex_controller_test.exs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1775,7 +1775,7 @@ defmodule CodexPoolerWeb.Runtime.BackendCodexControllerTest do
17751775
end
17761776
end
17771777

1778-
test "POST /backend-api/codex/responses prefers x-codex-session-id over opencode continuity headers",
1778+
test "POST /backend-api/codex/responses prefers x-codex-window-id over broader continuity headers",
17791779
%{conn: conn} do
17801780
upstream =
17811781
start_upstream(
@@ -1788,10 +1788,14 @@ defmodule CodexPoolerWeb.Runtime.BackendCodexControllerTest do
17881788

17891789
setup = gateway_setup(upstream)
17901790

1791+
raw_window_id = "window-session-wins-fixture"
1792+
expected_session_key = hashed_window_session_key(raw_window_id)
1793+
17911794
conn =
17921795
conn
17931796
|> auth(setup)
1794-
|> put_req_header("x-codex-session-id", "codex-session-wins-fixture")
1797+
|> put_req_header("x-codex-window-id", raw_window_id)
1798+
|> put_req_header("x-codex-session-id", "codex-session-lower-priority-fixture")
17951799
|> put_req_header("session-id", "session-id-lower-priority-fixture")
17961800
|> put_req_header("x-session-affinity", "session-affinity-lower-priority-fixture")
17971801
|> put_req_header("session_id", "session-underscore-lower-priority-fixture")
@@ -1805,16 +1809,17 @@ defmodule CodexPoolerWeb.Runtime.BackendCodexControllerTest do
18051809

18061810
assert %CodexSession{} =
18071811
session =
1808-
Repo.get_by(CodexSession, session_key: "codex-session-wins-fixture")
1812+
Repo.get_by(CodexSession, session_key: expected_session_key)
18091813

1814+
refute Repo.get_by(CodexSession, session_key: "codex-session-lower-priority-fixture")
18101815
refute Repo.get_by(CodexSession, session_key: "session-id-lower-priority-fixture")
18111816
refute Repo.get_by(CodexSession, session_key: "session-affinity-lower-priority-fixture")
18121817
refute Repo.get_by(CodexSession, session_key: "session-underscore-lower-priority-fixture")
18131818
refute Repo.get_by(CodexSession, session_key: "conversation-lower-priority-fixture")
18141819

18151820
assert [request] = Repo.all(from(r in Request, where: r.pool_id == ^setup.pool.id))
18161821
assert request.request_metadata["codex_session_id"] == session.id
1817-
assert request.request_metadata["codex_session_key"] == "codex-session-wins-fixture"
1822+
assert request.request_metadata["codex_session_key"] == expected_session_key
18181823

18191824
assert [captured] = FakeUpstream.requests(upstream)
18201825
captured_headers = Map.new(captured.headers)
@@ -8812,6 +8817,14 @@ defmodule CodexPoolerWeb.Runtime.BackendCodexControllerTest do
88128817
refute inspect(captured.headers) =~ "lineage-unrelated"
88138818
end
88148819

8820+
defp hashed_window_session_key(raw_window_id) do
8821+
digest =
8822+
:crypto.hash(:sha256, raw_window_id)
8823+
|> Base.encode16(case: :lower)
8824+
8825+
"x-codex-window-id:" <> digest
8826+
end
8827+
88158828
defp assert_lineage_metadata_not_persisted!(setup, metadata) do
88168829
requests = Repo.all(from(r in Request, where: r.pool_id == ^setup.pool.id))
88178830

@@ -8958,6 +8971,7 @@ defmodule CodexPoolerWeb.Runtime.BackendCodexControllerTest do
89588971
assert recovery["anchor_removal"]["headers"] == [
89598972
"x-codex-previous-response-id",
89608973
"x-codex-turn-state",
8974+
"x-codex-window-id",
89618975
"x-codex-session-id",
89628976
"session-id",
89638977
"x-session-affinity",

0 commit comments

Comments
 (0)