Repository navigation
fix(codex): honest affinity labels and opt-in request-owned conversation affinity (#8080) #8096
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
345828c
fix(codex): honest affinity labels and opt-in request-owned conversat…
lidge-jun c07a28b
fix(codex): scope conversation-state issuers per bearer and fix revie…
lidge-jun 4ae03a6
test(codex): drive real preview callbacks; document issuer retention …
lidge-jun 87d6eeb
fix(codex): release a caller-owned main binding after its bearer is r…
lidge-jun ca1551c
fix(codex): fence caller-main releases to selection-time bindings (#8…
lidge-jun File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| # 261102 — Request-owned bearer conversation affinity (#8080) | ||
|
|
||
| **Reader summary.** When Codex CLI signs in with ChatGPT and talks to a Pool-mode proxy, its own | ||
| bearer makes `main` one more round-robin candidate (#5019), but the request had no affinity key, so | ||
| every turn re-ran the strategy and a conversation hopped between main and the stored accounts. The | ||
| request log labelled each of those picks `new_bind/healthy` although nothing was stored. This unit | ||
| makes every affinity label describe what selection actually stored, and adds the opt-in | ||
| `codexPool.requestOwnedAffinity` (default off) that keeps one account per conversation for a | ||
| request-owned bearer. Research: `010_research.md`. Contract: `structure/providers/codex-request-owned-affinity.md`. | ||
|
|
||
| ## Scope | ||
|
|
||
| - In: `src/codex/request-owned-affinity.ts` (key, label check, issuer helpers), | ||
| `src/codex/request-owned-main-pin.ts` (byte-for-byte move), wiring in `src/codex/auth-context.ts`, | ||
| request previews (`src/server/responses/request-prepare.ts`), issuer and retry/compact fallbacks, | ||
| request log and usage normalization, config field, manual-selection issuer retention. | ||
| - Out: affinity persistence (#8091), round-robin eligibility (#8092), Claude identity lanes. | ||
|
|
||
| ## Decisions | ||
|
|
||
| 1. Labels are checked when selection returns: `new_bind`/`rebound` stay only if the key's entry names | ||
| the selected account; otherwise `unbound` (`request_owned`, `no_conversation`, or the original | ||
| reason) or `cleared`. `unbound` is not a spend move reason. | ||
| 2. The opt-in key is an HMAC under a process-random secret over the exact Authorization value and the | ||
| conversation key. Account-id claims are not used as principal. | ||
| 3. Final auth and both previews (first and recovery, per candidate model) share one derivation; fixed | ||
| selectors and Reserve get no key; request-owned traffic never records lineage. | ||
| 4. A separate bearer-scoped conversation-state key (never passed to routing) carries issuer records | ||
| for request-owned Pool and caller-owned main contexts, with or without the opt-in. | ||
| 5. Manual account selection keeps issuer records while clearing routing bindings. | ||
| 6. A refusal on a caller-owned main binding releases that binding without touching shared main health. | ||
|
|
||
| ## Acceptance | ||
|
|
||
| Regression tests in `tests/codex-integration/codex-request-owned-affinity.test.ts`, | ||
| `tests/codex-integration/reserve-auth-context.test.ts`, `tests/responses/compact-transient-alternate.test.ts`, | ||
| `tests/responses/responses-alternate-main-cancellation.test.ts` and | ||
| `tests/responses/responses-preview-main-read-fence.test.ts` fail on the pre-change code and pass after it. | ||
|
|
||
| ## Outcome | ||
|
|
||
| PR #8096. Plan audited over five rounds; implementation reviewed by a dedicated security review | ||
| and an independent code review before publication. |
20 changes: 20 additions & 0 deletions
20
devlog/_plan/261102_request_owned_affinity/010_research.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| # 010 — Research (#8080) | ||
|
|
||
| Source: Terra research task, 2026-11-02, read-only, at 6f1d3e65c. | ||
|
|
||
| - Ownership flag: `codexRouteCredentialOwnership` (src/server/responses/core-auth.ts) marks a forwardable | ||
| caller bearer request-scoped. | ||
| - `poolStateEligible` (src/codex/auth-context.ts) rejects request-scoped credentials, so `affinityKey` and | ||
| lineage are undefined and `resolveCodexAccountForThreadDetailed` receives a null key. | ||
| - Since #5019/#5024 an unpinned caller bearer makes main an ordinary round-robin candidate; a stored | ||
| account selection materializes that stored credential. Intentional and documented in | ||
| structure/providers/openai-tiers.md. | ||
| - Round-robin commits its pick with a null key but binds only when `commitAffinity && threadId` | ||
| (src/codex/routing/selection.ts). With main + one stored account and sticky limit 1 the ring | ||
| alternates; with two stored accounts it cycles main, A, B. | ||
| - Every unbound success returns `affinityAfterRelease(null, undefined)` = `new_bind/healthy`. | ||
| - Log path: `noteCodexSelectionAffinity` (pool/main-pool only) → request log → `persistedAffinityFields` | ||
| → usage.jsonl via `KNOWN_AFFINITY_MOVES/REASONS` (src/usage/log.ts). No exhaustive Record over the | ||
| unions; GUI does not render these values. | ||
| - Options weighed: opt-in process-local namespace (chosen); always serve main (contradicts #5019); | ||
| raise sticky limit (shared counter, not affinity); omit label only (honest but uninformative). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.