You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
opencodex derives its Codex pool affinity key as app:HMAC(session-id ?? x-codex-parent-thread-id, thread-id) in src/codex/auth-context.ts. Because thread-id is in the key, every thread in one conversation tree gets its own pool binding.
Upstream treats that tree as one cache cohort. Verified in the pinned clone at /Users/jun/Developer/codex/121_openai-codex:
codex-rs/core/src/client.rs:504-516 -- prompt_cache_key() returns responses_metadata.session_id unless overridden, and for an internal session returns format!("{source}:{parent_thread_id}"). Both are keyed on something the tree shares, never on the thread's own id.
codex-rs/core/src/agent/control.rs:116-137 -- one AgentControl is shared by the root and all descendants and its session_idis the root thread id, so the session-id header is constant across the tree while thread-id is not.
codex-rs/core/tests/suite/prompt_cache_key.rs:126-155 -- asserts differentThreadIds: true while root and child both send promptCacheKey == expected_session_id.
So the proxy splits a cohort the client deliberately merges. src/codex/lineage.ts and the first-placement hook mitigate turn one by placing a new child on the account currently serving its parent, but that is a hint, not a pin -- src/codex/auth-context.ts states it directly: a later move of the parent does not drag the child.
The consequence: members of one tree can end up bound to different pool accounts while every one of them keeps sending the sameprompt_cache_key. The split-off member's key asserts a warm prefix that is, on its account, deterministically cold. The prompt is replayed in full and the cache can never hit, which is the token burn #4546 describes, arriving through a different door.
This is a design decision, not a one-line fix, which is why it is filed rather than patched: changing the affinity unit from (session, thread) to the session cohort trades per-thread binding independence for cohort cache locality, and it interacts with the send-budget and placement work in flight.
Reproduction
Run a Codex pool with two or more accounts and an App client that spawns subagents.
Start a conversation and let it spawn at least one child thread. Root and child share session-id and differ in thread-id.
Observe that the proxy derives two distinct affinity keys and can bind the two threads to different accounts.
Both threads continue to send an identical prompt_cache_key upstream while being served by different credentials.
Suggested direction
Decide explicitly what the binding unit is. Either key pool affinity on the cohort the client already declares -- session-id, which is the root thread id -- or keep per-thread bindings and treat a cohort split as a cost that placement must actively avoid rather than only hint at. Either way the invariant worth writing down is that two requests carrying the same prompt_cache_key should not be served by different accounts.
Client or integration
Codex App
Area
Proxy and routing
Summary
opencodex derives its Codex pool affinity key as
app:HMAC(session-id ?? x-codex-parent-thread-id, thread-id)insrc/codex/auth-context.ts. Becausethread-idis in the key, every thread in one conversation tree gets its own pool binding.Upstream treats that tree as one cache cohort. Verified in the pinned clone at
/Users/jun/Developer/codex/121_openai-codex:codex-rs/core/src/client.rs:504-516--prompt_cache_key()returnsresponses_metadata.session_idunless overridden, and for an internal session returnsformat!("{source}:{parent_thread_id}"). Both are keyed on something the tree shares, never on the thread's own id.codex-rs/core/src/agent/control.rs:116-137-- oneAgentControlis shared by the root and all descendants and itssession_idis the root thread id, so thesession-idheader is constant across the tree whilethread-idis not.codex-rs/core/tests/suite/prompt_cache_key.rs:126-155-- assertsdifferentThreadIds: truewhile root and child both sendpromptCacheKey == expected_session_id.prompt_cache_keyspecifically so fork routing follows the parent's cache.So the proxy splits a cohort the client deliberately merges.
src/codex/lineage.tsand the first-placement hook mitigate turn one by placing a new child on the account currently serving its parent, but that is a hint, not a pin --src/codex/auth-context.tsstates it directly: a later move of the parent does not drag the child.The consequence: members of one tree can end up bound to different pool accounts while every one of them keeps sending the same
prompt_cache_key. The split-off member's key asserts a warm prefix that is, on its account, deterministically cold. The prompt is replayed in full and the cache can never hit, which is the token burn #4546 describes, arriving through a different door.This is a design decision, not a one-line fix, which is why it is filed rather than patched: changing the affinity unit from
(session, thread)to the session cohort trades per-thread binding independence for cohort cache locality, and it interacts with the send-budget and placement work in flight.Reproduction
session-idand differ inthread-id.prompt_cache_keyupstream while being served by different credentials.Suggested direction
Decide explicitly what the binding unit is. Either key pool affinity on the cohort the client already declares --
session-id, which is the root thread id -- or keep per-thread bindings and treat a cohort split as a cost that placement must actively avoid rather than only hint at. Either way the invariant worth writing down is that two requests carrying the sameprompt_cache_keyshould not be served by different accounts.Version
2.57.0 (dev @ 3070d64)
Operating system
macOS 15.5
Logs or error output
(no error is raised; the symptom is a cache miss rate and token spend, not a failure)Checks