Repository navigation
[Bug]: pool affinity splits a conversation tree that upstream keys as one prompt-cache cohort #4780
Description
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) 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()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.- Preserve parent cache affinity for ephemeral forks openai/codex#44862 ("Preserve parent cache affinity for ephemeral forks") goes further, making an ephemeral fork inherit its parent's session id as
prompt_cache_keyspecifically so fork routing follows the parent's cache.
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 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
- 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-idand differ inthread-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_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 same prompt_cache_key should 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
- I searched existing issues and documentation.
- I removed secrets, tokens, account details, request credentials, and personal data.
Activity
lidge-jun commentedon Sep 16, 2026
리뷰 · 우선순위 77 / 80
이 이슈는 Codex 풀의 어피니티 단위가 클라이언트 프롬프트 캐시 코호트와 어긋난다는 설계 구멍이다. 현재
devHEADb3035fe292(패키지 2.57.0, #4720 계정/카탈로그 L2 이후)에서도 그대로다.src/codex/auth-context.ts의codexPoolAffinityKey(약 93–118행)는 ‘Every thread keys as ITSELF’라고 분명히 적고, 실제 키는src/codex/lineage.tscodexConversationIdentity(약 183행대)가 만든다. 루트는 HMAC(session, thread), 자식은 HMAC(session ?? parent, thread)라서 thread-id가 키에 들어간다.업스트림은 그 트리를 하나의 캐시 코호트로 본다. 같은
session-id(루트 thread id)와 같은prompt_cache_key를 쓰는데, 프록시는 스레드마다 다른 풀 바인딩을 허용한다. 그 결과 한 트리의 멤버가 서로 다른 계정에 묶여도 모두 같은 warm prefix를 주장하고, 갈라진 쪽 계정에서는 그 키가 결정적으로 cold가 된다. #4546이 말하는 토큰 낭비의 또 다른 문이다. 에러는 안 나고 캐시 미스·토큰만 늘어나서 잡기 어렵다.완화는 이미 있다.
src/codex/routing.tspickLineageServingAccount(약 378–405행)와lineageServingAccountId(약 338행대)가 첫 배치에서 부모·형제의 현재 serving 계정을 힌트로 준다. 주석도 ‘hint for turn one, not a root-wide pin’이라고 적는다. 부모를 나중에 옮기면 자식은 따라가지 않는다. 이슈가 말하는 ‘힌트이지 핀이 아니다’가 tip 코드와 정확히 맞다.이건 한 줄 버그픽스가 아니다. 바인딩 단위를 session 코호트로 바꾸면 per-thread 독립성이 줄고, 보내기 예산·배치·#4546 레인과 맞물린다. 반대로 per-thread를 유지하면 코호트 분리를 placement가 능동적으로 막아야 한다. 어느 쪽이든 ‘같은 prompt_cache_key를 든 두 요청이 다른 계정으로 나가면 안 된다’는 불변식을 문서·테스트로 고정할 가치가 크다.
types/config 스플릿과 직접 충돌하지 않는다. #4546 에픽이 아직 OPEN이므로 이 이슈는 그 비용 레인에 붙는 설계 티켓으로 두는 게 맞다. 미리보기 배포는 계획에 없다.
경로
src/codex/auth-context.tscodexPoolAffinityKey(93–118) - 스레드마다 자기 키. 트리 단위 코호트와 어긋난다.경로
src/codex/lineage.tscodexConversationIdentity(167–209) - HMAC에 thread-id가 들어가 자식이 독립 바인딩을 갖는다.경로
src/codex/routing.tspickLineageServingAccount(378–405) - 첫 배치 힌트만 있고 이후 핀이 없다.심볼 불변식 - 같은 prompt_cache_key를 든 요청이 서로 다른 pool 계정으로 나갈 수 있다. #4546 토큰 낭비의 다른 입구.
메인테이너의 판단이 필요한 지점
너의 추천
받아라. #4546 비용 레인에 설계 티켓으로 붙이고, 먼저 ‘같은 prompt_cache_key → 같은 계정’ 불변식과 회귀 테스트를 쓴 뒤 단위 변경 vs placement 강화 중 하나를 고르라. 지금 당장 per-thread 키만 지우는 핫픽스는 보내지 마라.
이 댓글은 grok-bot이 작성했습니다
fix(codex): key pool affinity on the conversation cohort, not the thr…
lidge-jun commentedon Sep 17, 2026
Landed via #4935 at 282df53
이 댓글은 grok-bot이 작성했습니다