Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
f9bf31c
docs(devlog): plan the API-key pool call-site wiring
lidge-jun Sep 11, 2026
1c93a6f
docs(devlog): fold the wiring plan blockers
lidge-jun Sep 11, 2026
085fa6f
feat(providers): use the warm API key on the first attempt
lidge-jun Sep 11, 2026
78d30c7
test(server): cover the pre-dispatch API key pick end to end
lidge-jun Sep 11, 2026
9b067c2
docs(devlog): plan quota-aware API key selection
lidge-jun Sep 11, 2026
f5f87e8
docs(devlog): fold the quota-selection plan blockers
lidge-jun Sep 11, 2026
8c1edce
feat(providers): rank API keys by quota headroom
lidge-jun Sep 11, 2026
9fbff86
test(providers): cover quota-ranked API key selection
lidge-jun Sep 11, 2026
ba21644
docs: document apiKeyPoolStrategy, including the new quota value
lidge-jun Sep 11, 2026
526eaf0
Merge branch 'codex/generic-pool-kernel' into codex/key-pool-wiring
lidge-jun Sep 11, 2026
e5ff46c
docs(devlog): plan the pool-settings contract consolidation
lidge-jun Sep 11, 2026
5a86180
docs(devlog): fold the wp5 audit, which resized the unit
lidge-jun Sep 11, 2026
4fd8377
docs(devlog): narrow the wp5 cycle to the guard and the validator col…
lidge-jun Sep 11, 2026
7a1e30b
docs(devlog): fold the re-audit residuals and name the guard targets
lidge-jun Sep 11, 2026
3213e8b
test(server): pin the three legacy pool contracts exactly
lidge-jun Sep 11, 2026
ee59544
refactor(oauth): one validator for pool strategy and sticky limit
lidge-jun Sep 11, 2026
999c1ba
docs(devlog): scope wp3 to the ordering criterion c-4 actually states
lidge-jun Sep 11, 2026
cd5d070
docs(devlog): fold the wp3 blockers, including a bar that was the thr…
lidge-jun Sep 11, 2026
3f0e79d
docs(devlog): mark the pre-audit wp3 change surface superseded
lidge-jun Sep 11, 2026
270c175
feat(codex): keep a bound thread on its account until that account is…
lidge-jun Sep 11, 2026
68e7074
test(codex): prove cache affinity outranks quota but not exhaustion
lidge-jun Sep 11, 2026
035bd54
docs(devlog): plan the compact and images key-pick seams
lidge-jun Sep 11, 2026
2628213
docs(devlog): fold the wp4c audit, including the images snapshot trap
lidge-jun Sep 11, 2026
2ad973e
feat(server): pick a warm API key on the compact and images paths
lidge-jun Sep 11, 2026
400b111
test(server): cover the keyed image first-send key pick
lidge-jun Sep 11, 2026
a37e794
test(adapters): cover the native compact first-send key pick
lidge-jun Sep 11, 2026
ce98d2b
docs(devlog): plan the unified pool route and the enabled defect
lidge-jun Sep 11, 2026
2c9e545
docs(devlog): fold the wp5c audit; the new field stays off the legacy…
lidge-jun Sep 11, 2026
f306e4f
feat(management): one pool-settings contract for all three kinds
lidge-jun Sep 11, 2026
5a0a8f9
test(server): cover the unified pool contract and the inherited enabled
lidge-jun Sep 11, 2026
1053138
docs: describe the unified pool route and correct a stale claim
lidge-jun Sep 11, 2026
a594f7e
docs(devlog): plan the GUI pool client merge
lidge-jun Sep 11, 2026
a054ba4
docs(devlog): fold the wp5b audit; the silent-no-op trap a third time
lidge-jun Sep 11, 2026
e0ecab1
docs(devlog): amend the wp5b spec instead of only recording the audit
lidge-jun Sep 11, 2026
f85f6ac
feat(gui): one pool client for every kind
lidge-jun Sep 11, 2026
610dad6
refactor(gui): move the Codex strategy write beside the client it uses
lidge-jun Sep 11, 2026
3dc69c4
docs(devlog): capture the pool settings surface for wp5b
lidge-jun Sep 11, 2026
fb3df8e
Merge remote-tracking branch 'origin/dev' into codex/key-pool-wiring
lidge-jun Sep 11, 2026
f567714
test(cli): cover the unified pool route in the headless parity sweep
lidge-jun Sep 11, 2026
d2ca1ab
fix(providers): rebuild the route when the pre-dispatch key pick fires
lidge-jun Sep 11, 2026
86b3613
test(providers): pin the rebuild contract and the Responses core path
lidge-jun Sep 11, 2026
2559502
fix(providers): expire a cached key quota and reset cursors on a batc…
lidge-jun Sep 11, 2026
f593498
docs: document cache affinity and the pre-dispatch key pick
lidge-jun Sep 11, 2026
6097a51
test(providers): stub the destination check in the cursor-reset case
lidge-jun Sep 11, 2026
5282c40
Merge remote-tracking branch 'origin/dev' into codex/key-pool-wiring
lidge-jun Sep 11, 2026
6318374
chore(skills): regenerate the management surface map after the dev merge
lidge-jun Sep 11, 2026
c67e596
fix: stop masking a failed pool read and a dead pool credential
lidge-jun Sep 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,139 @@ The Anthropic and generic halves are not frozen, so a narrower first slice exist
unify the affinity key for those two kinds only, leaving the Codex thread-affinity
map on its current key until the freeze lifts. That slice still needs assumption 1
answered, which is why this phase stays closed rather than being re-scoped now.

## wp3 plan — what criterion c-4 actually requires

This phase was recorded as blocked on three product decisions: the shared affinity key shape,
the shared-cohort `prompt_cache_key` fallback, and a minimum-token cache gate. Re-reading the
criterion against the code shows none of the three is on the path to it.

> c-4: Account selection consults cache affinity before quota for subscription pools, proven by
> a test where the cache-affine account is chosen over a higher-headroom one.

That is a statement about **ordering**, not about key shape. The phase title pairs ordering with
"a unified affinity key", but only the ordering half is an acceptance criterion, and the two are
separable: reordering uses each kind's EXISTING affinity binding and introduces no new key.
Assumption 1 gates the unified key, not this. Assumption 2 is a property of the Anthropic
session-key derivation, which the ordering change does not touch. Assumption 3 is explicitly
optional in the original text ("decide whether to implement") and is not required by c-4.

So the unified key stays open and stays out of this cycle. The ordering ships now.

## Only one kind actually breaks cache affinity

Verified on the branch head rather than assumed:

- **Anthropic already honours affinity unconditionally.** `src/oauth/anthropic-routing.ts`:604-610
returns `{ reason: "affinity" }` whenever the affined account is present, not reauth-flagged,
not cooled and credential-usable. `autoSwitchThreshold` governs NEW-session picks
(`anthropicAutoSwitchThreshold`, :111) and never rebinds a live session.
- **Codex does not.** `reevaluateAffinityQuota` (`src/codex/routing.ts`:2031) rebinds a live
thread whenever the quota strategy is active and usage crosses `autoSwitchThreshold` (:2047),
which throws away a warm prompt cache on a hint rather than on evidence.
- The generic OAuth kind has no affinity at all, so it has nothing to reorder.

That makes this a one-function change, and it makes the criterion's "pools" plural satisfiable:
after it, both subscription pools keep a bound conversation on its account until that account
genuinely cannot serve.

## Change surface

`src/codex/routing.ts`, `reevaluateAffinityQuota` only. Under `pool.kernel`, the rebind bar
stops being "crossed the threshold" and becomes the same **drained** test the pin-release path
already uses (`releaseDrainedCodexAccountPin`, :1866):

```
!isCodexAccountUsable(config, entry.accountId, selectionOptions)
|| !hasCodexQuotaHeadroom(config, entry.accountId, selectionOptions, now)
```

Reusing that predicate rather than inventing a second notion of "spent" is deliberate: two
definitions of exhausted in one file is how they drift. The reeval-interval short circuit keeps
its current shape so a bound thread is still not re-scored more than once a minute.

Flag off restores today's behaviour exactly, which is what makes shipping this without the three
open decisions safe rather than presumptuous.

## Acceptance

- A bound thread on an account at 90% usage with `autoSwitchThreshold: 80` and a sibling at 10%
KEEPS its account while the flag is on — the cache-affine account chosen over the
higher-headroom one, which is c-4 verbatim.
- The same fixture with the flag off still moves, so the old behaviour is provably intact.
- A bound thread whose account is genuinely drained still moves with the flag on, so the change
is a reordering and not a pin.
- Red control: with the flag branch removed, the first case must fail.

### wp3 plan audit — FAIL, folded

**Blocker 1 — the "drained" bar I proposed IS the threshold.** `releaseDrainedCodexAccountPin`
reads `!isCodexAccountUsable || !hasCodexQuotaHeadroom`, and `hasCodexQuotaHeadroom`
(`src/codex/routing.ts`:1387-1395) is `usage < (autoSwitchThreshold ?? 80)`. Reusing it inside
`reevaluateAffinityQuota` would have preserved today's 80% rebind exactly, so the plan's own
acceptance case — a bound thread at 90% with threshold 80 KEEPING its account — could not have
passed. The argument for reuse ("don't invent a second notion of spent") was right in spirit and
wrong in fact: the pin path deliberately releases at the auto-switch crossing, which is a
different question from whether the account can still serve.

The bar this phase needs is genuine exhaustion, and it is not expressible as the existing
predicate. Definition used instead, local to the reeval and stated once:

```
spent = !isCodexAccountUsable(config, id, selectionOptions) // reauth, excluded, cooled
|| (!isUnknownUsage(usage) && usage >= 100) // allowance actually gone
```

Per minor 7 the usable half is already guaranteed by the caller, which requires
`isCodexAccountSelectable`, so in practice the test reduces to the usage half — kept explicit
anyway so the predicate reads correctly on its own.

**Major 3 — `previewReusableAffinityAccount` duplicates the same threshold move.**
`src/codex/routing.ts`:1984 carries its own copy for the preview path. Changing only the
mutating site would make `previewCodexAccountForRequest` disagree with
`resolveCodexAccountForThreadDetailed` — and the suite already contains cases asserting those
two agree. Both move together.

**Major 4 — the reeval interval must stop keying off the old bar.** The short circuit stamps
`lastReevalAt` only when `overThreshold`, so leaving it as-is while the rebind bar changes
re-scores a thread on every request through the whole 80-99% band. The short circuit follows the
new bar, keeping the once-a-minute ceiling intact.

**Minor 6, taken — the flag is wrong.** `pool.kernel` is the generic-OAuth strategy-consume
flag introduced in wp2b; reusing it for a Codex affinity rule would overload one switch with two
unrelated meanings and make either one impossible to turn on alone. This uses its own
`pool.cacheAffinity`, defaulting off.

**Minor 5 recorded.** `tests/codex-integration/codex-routing.test.ts` contains cases that require
the immediate over-threshold switch. They stay green because the flag defaults off, and that is
the check that proves flag-off is byte-identical rather than merely claimed.

### Major 2 — rebutted, with its limit stated

The audit is right that today's stickiness is keyed on thread and session identity rather than
on a cache key, and that a thread-keep test therefore proves "identity stickiness outranks
quota", not "a measured cache is consulted". That distinction is real and is exactly what the
deferred unified key would close.

It does not block c-4. In this codebase the thread/session binding IS the mechanism by which a
warm prompt cache stays reachable: the cache lives on the account that served the conversation,
so keeping the conversation there is what preserves it. c-4 asks that the affine account win
over a higher-headroom one, and after this change it does. What remains open — and is recorded
as open rather than quietly satisfied — is making the binding explicitly cache-derived instead
of identity-derived. The criterion's plural "pools" is likewise honest only because Anthropic
already holds its live sessions; this change brings Codex to the behaviour Anthropic has, rather
than adding a second implementation.

### The "## Change surface" block above is SUPERSEDED

It still names `pool.kernel`, `hasCodexQuotaHeadroom` and `reevaluateAffinityQuota` alone.
Implementing it as written fails three of the folded findings and cannot pass the 90% keep case.
The fold is the spec. Concretely, the build is:

- `src/types/config.ts` and `src/config.ts` — `pool.cacheAffinity?: boolean`, default off.
- `src/codex/routing.ts` `reevaluateAffinityQuota` AND `previewReusableAffinityAccount` — both
copies swap the rebind bar to `!isCodexAccountUsable || (!isUnknownUsage(usage) && usage >= 100)`
when the flag is on, and the `lastReevalAt` short circuit keys off that same bar.

The pre-audit block stays as the record of what was planned before the audit rather than being
rewritten to look correct.
Loading
Loading