Skip to content

[Bug]: pool affinity splits a conversation tree that upstream keys as one prompt-cache cohort #4780

Closed

Description

@lidge-jun

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() 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_id is 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.
  • 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_key specifically 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

  1. Run a Codex pool with two or more accounts and an App client that spawns subagents.
  2. Start a conversation and let it spawn at least one child thread. Root and child share session-id and differ in thread-id.
  3. Observe that the proxy derives two distinct affinity keys and can bind the two threads to different accounts.
  4. 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.

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

  1. lidge-jun commented on Sep 16, 2026

    @lidge-jun
    OwnerAuthor

    리뷰 · 우선순위 77 / 80

    이 이슈는 Codex 풀의 어피니티 단위가 클라이언트 프롬프트 캐시 코호트와 어긋난다는 설계 구멍이다. 현재 dev HEAD b3035fe292(패키지 2.57.0, #4720 계정/카탈로그 L2 이후)에서도 그대로다. src/codex/auth-context.ts의 codexPoolAffinityKey(약 93–118행)는 ‘Every thread keys as ITSELF’라고 분명히 적고, 실제 키는 src/codex/lineage.ts codexConversationIdentity(약 183행대)가 만든다. 루트는 HMAC(session, thread), 자식은 HMAC(session ?? parent, thread)라서 thread-id가 키에 들어간다.

    업스트림은 그 트리를 하나의 캐시 코호트로 본다. 같은 session-id(루트 thread id)와 같은 prompt_cache_key를 쓰는데, 프록시는 스레드마다 다른 풀 바인딩을 허용한다. 그 결과 한 트리의 멤버가 서로 다른 계정에 묶여도 모두 같은 warm prefix를 주장하고, 갈라진 쪽 계정에서는 그 키가 결정적으로 cold가 된다. #4546이 말하는 토큰 낭비의 또 다른 문이다. 에러는 안 나고 캐시 미스·토큰만 늘어나서 잡기 어렵다.

    완화는 이미 있다. src/codex/routing.ts pickLineageServingAccount(약 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.ts codexPoolAffinityKey (93–118) - 스레드마다 자기 키. 트리 단위 코호트와 어긋난다.
    경로 src/codex/lineage.ts codexConversationIdentity (167–209) - HMAC에 thread-id가 들어가 자식이 독립 바인딩을 갖는다.
    경로 src/codex/routing.ts pickLineageServingAccount (378–405) - 첫 배치 힌트만 있고 이후 핀이 없다.
    심볼 불변식 - 같은 prompt_cache_key를 든 요청이 서로 다른 pool 계정으로 나갈 수 있다. #4546 토큰 낭비의 다른 입구.

    메인테이너의 판단이 필요한 지점

    너의 추천
    받아라. #4546 비용 레인에 설계 티켓으로 붙이고, 먼저 ‘같은 prompt_cache_key → 같은 계정’ 불변식과 회귀 테스트를 쓴 뒤 단위 변경 vs placement 강화 중 하나를 고르라. 지금 당장 per-thread 키만 지우는 핫픽스는 보내지 마라.

    이 댓글은 grok-bot이 작성했습니다

  2. added
    account-poolOAuth, credentials, Codex pool, quota, failover, plans
    proxyHTTP proxy, routing, reverse-proxy / management auth
    on Sep 16, 2026
  3. lidge-jun commented on Sep 17, 2026

    @lidge-jun
    OwnerAuthor

    Landed via #4935 at 282df53

    이 댓글은 grok-bot이 작성했습니다

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    account-poolOAuth, credentials, Codex pool, quota, failover, plansbugSomething isn't workingproxyHTTP proxy, routing, reverse-proxy / management auth

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions