Skip to content

feat(codex): give V2 threads real lineage and place a child on its parent's serving account (#4546) - #4640

Merged
lidge-jun merged 6 commits into
devfrom
codex/4546-wpd-v2-lineage-placement
Sep 14, 2026
Merged

lidge-jun merged 6 commits into
devfrom
codex/4546-wpd-v2-lineage-placement

Conversation

@lidge-jun

@lidge-jun lidge-jun commented Sep 14, 2026

Copy link
Copy Markdown
Owner

Summary

Codex V2 threads now carry a real parent/child/grandchild relation, and a newly spawned child starts on the account that is actually serving its parent instead of cold.

codexPoolAffinityKey preferred x-codex-parent-thread-id, so every child of one parent bound under the raw parent id: one shared entry, unrelated to the root's own app:HMAC(session, thread) binding. No child could hold a binding of its own, a grandchild keyed on something nobody had ever bound, and a newly spawned child started with a cold prefix while its parent was being served warm somewhere else.

A request carrying thread-id now keys as app:HMAC(session-id ?? parent, thread-id). The set of requests that bind at all is deliberately unchanged — a bare thread-id with no session and no parent still has no family anchor and stays unbound — and only the value moves, for requests naming a parent.

src/codex/lineage.ts records, per authenticated scope, each thread's conversation key, its immediate parent and its transitive root, so a grandchild resolves to its root rather than to its parent. It is bounded in both dimensions, by idle TTL plus an LRU cap on records per scope and on scopes, and is never keyed across authentication scopes.

Placement, and only placement, changes: a thread that has never bound starts on the account currently serving its parent — a live transient detour included, rather than the parent's stale home — then on a compatible sibling's, then on ordinary cold placement. The child holds an ordinary binding of its own, and the hint does not move the shared active-account cursor. The asymmetry is pinned: a parent bound to A but served by B places a new child on B, and when the parent later moves to C, the child already progressing on B is untouched while the next new child starts on C.

Four review findings are fixed on top of that. A parent-only turn now resolves through the existing scoped parent record instead of re-deriving HMAC(parent, parent), which equals the root key only when session-id happens to equal thread-id. A lineage miss reads the legacy raw-parent key once, adopts its binding and retires the entry, so a conversation live across an in-process code swap is not silently cold-rebound — the exact defect this unit exists to prevent. Preview derives lineage from the same transformed auth headers and eligibility predicate resolveCodexAuthContext uses, so it cannot follow a family binding that final auth deliberately refuses to create. And "currently serving" now inspects the compatible model-detour entry before the ordinary binding.

Stacked on #4639.

Verification

Not run, by explicit instruction: the local suite, bun run typecheck, bun install, and any build. The only proof for this unit is hosted CI at the exact final head SHA; this push used --no-verify.

New coverage in tests/codex-integration/codex-lineage-placement.test.ts: twelve tests covering the unchanged unbound set, transitive root resolution inside one auth scope, bounds in both dimensions, a child starting on the parent's account under its own key, following the account actually serving the parent, the parent-move asymmetry, sibling placement, cold fallback, a parent-only turn continuing the parent's conversation with or without a session id, adoption of a binding left under the old raw-parent key, following the parent's model detour, and preview reading the family only for a request that may own Pool state. tests/codex-integration/codex-auth-context.test.ts has its one raw-value assertion rewritten to the new contract.

Known open: the lineage-backed worker/interactive answer is exported but not consumed — admission still classifies from headers alone. The cost-attribution root lookup is exported and nothing spends against it yet.

Checklist

  • Scope stays focused and avoids unrelated cleanup.
  • Docs or release notes were updated when needed.
  • Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults.

Summary by CodeRabbit

  • New Features

    • Codex threads now maintain independent account affinity instead of sharing a parent thread’s lane.
    • New conversation lineage tracking helps newly created child threads start on the account serving their parent or a compatible sibling.
    • Parent-only requests continue using the parent conversation lane.
    • Routing previews and final credential selection now use consistent thread lineage information.
    • Lineage state is bounded and expires when inactive.
  • Documentation

    • Updated OpenAI tier documentation to describe thread affinity and lineage-based placement.

@lidge-jun
lidge-jun requested a review from Ingwannu as a code owner September 14, 2026 15:23
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 14, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-14T15:29:14.712439Z 50dba9c PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

Copy link
Copy Markdown
Contributor

Deterministic PR hygiene checks passed.

@github-actions github-actions Bot added the enhancement New feature or request label Sep 14, 2026
@coderabbitai

coderabbitai Bot commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 6004fd38-44f0-4a5a-8d66-8bb7c6018554

📥 Commits

Reviewing files that changed from the base of the PR and between c3106e3 and cc70912.

📒 Files selected for processing (12)
  • scripts/test-layout/layout.json
  • src/codex/auth-context.ts
  • src/codex/lineage.ts
  • src/codex/routing.ts
  • src/server/request-log-conversation.ts
  • src/server/responses/core.ts
  • structure/providers/openai-tiers.md
  • tests/codex-integration/codex-auth-context.test.ts
  • tests/codex-integration/codex-lineage-placement.test.ts
  • tests/fixtures/test-layout-expected.json
  • tests/responses/responses-pool-401-refresh.test.ts
  • tests/routing/subagent-fallback-handle-responses.test.ts

📝 Walkthrough

Walkthrough

Codex V2 affinity now uses opaque per-thread conversation keys. Authenticated lineage is stored with TTL and LRU bounds. Initial child placement can follow a parent or sibling account, while response previews use the same credential scope as final resolution.

Changes

Codex lineage-aware pool placement

Layer / File(s) Summary
Lineage identity and bounded storage
src/codex/lineage.ts
Adds per-thread conversation keys, authenticated lineage records, transitive root lookup, bounded TTL/LRU storage, cost-attribution lookup, and workflow-lane classification.
Pool-state eligibility and authentication wiring
src/codex/auth-context.ts
Replaces parent-based affinity derivation with per-thread identity. Pool-state eligibility now controls lineage preview and recording. Recorded lineage is passed into account resolution.
Lineage-aware account routing
src/codex/routing.ts
Routing can adopt live legacy bindings and use a parent’s or compatible sibling’s serving account for first placement. Child bindings remain independent.
Credential-domain preview integration and validation
src/server/responses/core.ts, tests/codex-integration/*, tests/responses/*, tests/routing/*, structure/providers/openai-tiers.md, scripts/test-layout/layout.json, tests/fixtures/test-layout-expected.json, src/server/request-log-conversation.ts
Response previews use credential headers and ownership decisions that match final authentication. Tests and provider documentation cover the new affinity and lineage behavior.

Estimated code review effort: 4 (Complex) | ~60 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant Responses
  participant AuthContext
  participant Lineage
  participant Routing
  Responses->>AuthContext: derive credential-domain headers and Pool eligibility
  AuthContext->>Lineage: preview or record thread lineage
  AuthContext->>Routing: resolve account with lineage
  Routing->>Lineage: inspect parent and sibling lineage
  Lineage-->>Routing: return serving-account hint
  Routing-->>Responses: return preview or selected account
Loading
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/4546-wpd-v2-lineage-placement

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 50dba9cba6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +4098 to +4102
const previewAuthHeaders = codexRouteCredentialDomainHeaders(
req,
route,
options,
credentialDomainWasRewritten,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Derive the preview key from transformed headers

When a parent-only request enters a policy or other credential-domain rewrite that strips Authorization, previewAuthHeaders selects the local lineage scope, but poolAffinityKey was already derived from req.headers. Because codexConversationIdentity consults the scoped parent record to resolve parent-only turns, the preview can query HMAC(parent,parent) while final auth reuses the recorded HMAC(session,parent) binding, causing subagent fallback to evaluate a cold account instead of the account that will actually serve the request. Derive the preview affinity key from previewAuthHeaders as well.

Useful? React with 👍 / 👎.

Comment on lines +4104 to +4107
const poolLineage = previewCodexPoolLineage(previewAuthHeaders, options.codexAuthPolicy ?? config, {
accountId: route.codexAccountId,
modelId: route.modelId,
admission: options.admission,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Re-evaluate lineage after fallback route changes

When a thread spawn starts on an account-qualified route and fallback rewrites it to an unqualified Pool route, this initial-route calculation returns no lineage because route.codexAccountId is fixed, and the same poolLineage is then reused for every fallback candidate. Final auth resolves against the rewritten unqualified route and does record/use lineage, so fallback can judge model health or quota against a cold account while the request is ultimately placed on its family's account. Compute eligibility and lineage for each candidate route, including the post-recovery fallback path, rather than freezing the initial route's result.

Useful? React with 👍 / 👎.

Comment on lines +78 to +79
`x-codex-parent-thread-id` rides that parent's own lane as `app:HMAC(parent, parent)`: one parent
id still maps to exactly one lane, and no caller-supplied identifier reaches Pool state. Which

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Document the recorded key for parent-only turns

When the parent has already been recorded and its session-id differs from its thread-id, codexConversationIdentity deliberately returns the recorded HMAC(session,parent) key for a parent-only request; it falls back to HMAC(parent,parent) only when no scoped record exists. This authoritative contract currently states that every such request uses the fallback, contradicting both the implementation and the added regression test, so update it to describe the recorded-key precedence.

AGENTS.md reference: structure/AGENTS.md:L9-L10

Useful? React with 👍 / 👎.

Comment thread src/codex/lineage.ts
@@ -0,0 +1,458 @@
/**

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Update every mapped structure owner

This adds a new src/codex/ module and changes src/server/, but the commit updates only structure/providers/openai-tiers.md; structure/INDEX.md maps src/codex/ to eight documents and src/server/ to fourteen, leaving the other mapped owners untouched. Update every mapped document in this change, or correct the manifest ownership if those documents do not actually describe these areas.

AGENTS.md reference: structure/AGENTS.md:L49-L50

Useful? React with 👍 / 👎.

@lidge-jun

Copy link
Copy Markdown
Owner Author

리뷰 · 우선순위 69 / 80

이 PR은 스택 문서의 wpd(V2 lineage placement) 입니다. 문서 표에서는 wpf 다음·wpa 앞(4번)이었는데, 실제 브랜치는 codex/4546-wph-pool-refresh-join 위에 쌓여 순서가 표와 어긋납니다. 지금 dev(627274b)에는 아직 V2 parent/child 바인딩 키 붕괴가 남아 있고, 그게 프롬프트 캐시를 깨는 #4546 갈래 중 하나입니다.

문제는 plain합니다. codexPoolAffinityKeyx-codex-parent-thread-id를 선호하면서, 같은 부모의 자식들이 raw parent id 하나로 묶이고, 루트의 app:HMAC(session, thread) 바인딩과 무관해졌습니다. 손자는 아무도 바인딩한 적 없는 키로 가고, 새 자식은 부모가 따뜻한 계정에 있는데도 차가운 prefix로 시작합니다.

src/codex/lineage.ts가 호출자 스코프(HMAC)·TTL·엔트리/스코프/형제 상한을 둔 프로세스 로컬 혈통을 만들고, 자식 첫 배치를 부모 serving account에 올리며, 레거시 raw-parent 키를 새 키로 한 번 이주합니다. 재시작 비내구·예산 미구현·워커 분류는 노출만 하고 미배선이라는 주석이 정직합니다. auth-context·routing·request-log·core·구조 문서·통합 테스트가 같이 옵니다.

라인 src/codex/lineage.ts (프로세스 로컬·HMAC 키) - 재시작 후 혈통이 사라지는 것은 의도입니다. 다만 롤링 배포 중 두 프로세스가 같은 대화를 나눠 보면 배치 힌트가 갈라집니다. 단일 인스턴스 전제인지 운영 노트에 남기세요.

라인 스택 순서 - 문서는 wpd가 wpa/wpb와 독립(계약 모듈)이라고 했지만, 실제 base는 wph입니다. 독립이 맞다면 dev에 가깝게 리베이스해 표 순서(wpd→wpa→…)를 복구할 여지가 있습니다. 의존이 생겼다면 문서 표를 갱신해야 합니다.

경로 CI - test 1/4 실패·다수 pending. 앞 스택 빨간불과 겹칩니다. wpd 고유 실패인지 분리해 보세요.

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

  • 표를 코드 스택에 맞출지, 브랜치를 표에 맞게 다시 쌓을지
  • 워커 분류(codexLineageWorkflowLane) 배선을 이 PR에 넣을지 후속으로 둘지
  • types/config 분할 무관

너의 추천
내용 우선순위는 높습니다. 머지 전에 (1) 문서 순서 vs 실제 base를 맞추고, (2) 호스티드 CI 최종 SHA 녹색을 확인하세요. 독립이 유지되면 wpd를 더 아래 스택에서 빼 dev에 가깝게 올리는 편이 리뷰·되돌리기에 유리합니다.

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

@lidge-jun
lidge-jun force-pushed the codex/4546-wph-pool-refresh-join branch from e7dc6f9 to 4ecb95f Compare September 14, 2026 15:32
@lidge-jun
lidge-jun force-pushed the codex/4546-wpd-v2-lineage-placement branch from 50dba9c to 7f7c4fc Compare September 14, 2026 15:32
@lidge-jun
lidge-jun force-pushed the codex/4546-wph-pool-refresh-join branch from 4ecb95f to 67c4115 Compare September 14, 2026 15:49
@lidge-jun
lidge-jun force-pushed the codex/4546-wpd-v2-lineage-placement branch from 7f7c4fc to b85b0bd Compare September 14, 2026 15:49
@lidge-jun
lidge-jun force-pushed the codex/4546-wph-pool-refresh-join branch from 67c4115 to a0918cd Compare September 14, 2026 16:08
@lidge-jun
lidge-jun force-pushed the codex/4546-wpd-v2-lineage-placement branch from b85b0bd to bd2a4d4 Compare September 14, 2026 16:08
@lidge-jun
lidge-jun force-pushed the codex/4546-wph-pool-refresh-join branch from a0918cd to 34614c7 Compare September 14, 2026 16:14
@lidge-jun
lidge-jun force-pushed the codex/4546-wpd-v2-lineage-placement branch from bd2a4d4 to 3711e49 Compare September 14, 2026 16:14
@lidge-jun
lidge-jun force-pushed the codex/4546-wpd-v2-lineage-placement branch from b467d4f to f1a140c Compare September 14, 2026 16:30
@lidge-jun
lidge-jun force-pushed the codex/4546-wph-pool-refresh-join branch from 34614c7 to fe8917d Compare September 14, 2026 16:30
@lidge-jun
lidge-jun force-pushed the codex/4546-wpd-v2-lineage-placement branch 2 times, most recently from 975d177 to 635f631 Compare September 14, 2026 16:48
@lidge-jun
lidge-jun force-pushed the codex/4546-wph-pool-refresh-join branch from 29c9aa3 to 6966470 Compare September 14, 2026 17:46
Base automatically changed from codex/4546-wph-pool-refresh-join to dev September 14, 2026 17:46
…rent's serving account (#4546)

Part of the stacked delivery closing the remaining OCX-4546 cost-guard scope.

Verification posture: local suite, typecheck, install and build NOT run by explicit instruction. Hosted CI at the exact final head is the only proof. Pushed with --no-verify.
…legacy affinity key (#4546)

Review findings on the V2 lineage layer: a parent-only request keyed HMAC(parent,parent), which equals the root key only when session-id equals thread-id, so a real root followed by a parent-only turn started cold; a binding made under the old raw-parent key was never probed, so a live conversation was silently cold-rebound across an in-process code swap; preview derived lineage from raw headers before final auth decided whether Pool state was permitted; and current-serving-account ignored model-detour affinity.

Verification posture: local suite, typecheck, install and build NOT run by explicit instruction. Hosted CI at the exact final head is the only proof. Pushed with --no-verify.
…ent relatively (#4546)

Hosted CI failed three lineage rows. The real defect: a parent-only turn resolves its key through the recorded lineage, which is TTL-bounded, but codexPoolAffinityKey read Date.now() internally - so any caller on a fixed clock saw a live record as expired and fell back to HMAC(parent,parent), the key the parent never bound under. The function now takes the clock like everything else on this path.

The other two rows asserted exact account names derived from quota-strategy ordering the author reasoned through but could not observe. They now assert what this layer actually promises: the child binds to whatever account is serving its parent at placement time, an already-bound child is untouched when the parent later moves, and a new child reads the parent's current account instead of its sibling's.

Verification posture: local suite, typecheck, install and build NOT run by explicit instruction. Hosted CI at the exact final head is the only proof.
… a fixture account (#4546)

Two more rows encoded quota-fixture outcomes as invariants. Where the parent lands after its own quota refusal is the strategy's decision and may legitimately be the account the child already holds, so nothing is asserted about that destination. The orphan row now asserts that the child follows its SIBLING's actual placement, which is the reachable half of the family when the parent is ineligible, instead of naming an account the fixture happened to produce.

Verification posture: local suite, typecheck, install and build NOT run by explicit instruction. Hosted CI at the exact final head is the only proof.
…ity key (#4546)

The suite asserted the binding under the RAW parent thread id, which is the keying this layer deliberately removes: a thread now keys as itself through an opaque HMAC and the parent header is a first-placement hint. The read-back uses the derived key, so the assertion still proves the replayed account stays selectable on the next request without pinning the old raw-parent key.

Verification posture: local suite, typecheck, install and build NOT run by explicit instruction. Hosted CI at the exact final head is the only proof.
The #2509 oracle asserts both fallback preview sites forward the model-eligible account set, but its regex required modelId to be the final argument. This layer appends the resolved pool lineage so preview and final resolution agree on a child's first turn, which is a new trailing argument rather than a dropped eligible set. The pattern now allows anything after modelId and keeps the guarantee it exists for.

Verification posture: local suite, typecheck, install and build NOT run by explicit instruction. Hosted CI at the exact final head is the only proof.
@lidge-jun
lidge-jun force-pushed the codex/4546-wpd-v2-lineage-placement branch from 38df147 to cc70912 Compare September 14, 2026 17:47
@lidge-jun
lidge-jun merged commit 38a2d9f into dev Sep 14, 2026
4 of 19 checks passed
@lidge-jun
lidge-jun deleted the codex/4546-wpd-v2-lineage-placement branch September 14, 2026 17:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant