@@ -22,10 +22,22 @@ are not:
2222 file in the round with four open PRs, which is also why the wp4b call-site
2323 wiring could not follow #4277 immediately.
2424
25- This cycle therefore takes the kernel plus the generic consumer and leaves the
26- Codex and Anthropic import swaps to a later layer. That ordering is not a
27- concession: a kernel that nothing imports yet is still verifiable through the
28- generic kind, and it keeps the contended files out of this PR entirely.
25+ This cycle takes the kernel, the Anthropic import swap and the generic consumer.
26+ Only the CODEX import swap is deferred, and it is deferred for free: once
27+ ` pool-rotation.ts ` re-exports the kernel, ` src/codex/ ` keeps its existing import
28+ path and needs no edit at all. So the contended files stay out of this PR without
29+ the kernel being an orphan.
30+
31+ Two kinds of change are moving here and they carry different risk, which is why
32+ only one of them is behind the flag:
33+
34+ - ** Relocation** is behaviour-preserving. Moving the state and primitives into
35+ ` pool-kernel.ts ` and re-exporting them changes no selection outcome, so it is
36+ not flagged. ` git ` history and a green existing suite are its proof.
37+ - ** Behaviour** is flagged. The generic kind consuming ` strategy ` and
38+ ` autoSwitchThreshold ` , and the DTO reporting ` inert: false ` , only happen when
39+ ` pool.kernel ` is on. Flag off restores today's outcomes exactly, because the
40+ pre-kernel path is the same code reached through the shim.
2941
3042Anchors confirmed present on ` origin/dev ` : ` selectPriorityTier ` :86,
3143` pickRoundRobinAccount ` :189, ` notePoolRotationSuccess ` :213,
@@ -69,29 +81,73 @@ and stamps `inert: true` on the generic DTO (`:40-54`, `:57-67`).
6981## Change surface
7082
7183NEW ` src/oauth/pool-kernel.ts `
72- - move ` SelectionState ` , ` pickRoundRobinAccount ` , ` peekRoundRobinAccount ` ,
73- ` seedPoolRotationAccount ` , ` notePoolRotationSuccess ` , ` notePoolRotationFailure ` ,
74- ` selectPriorityTier ` , and the strategy/sticky normalizers
84+ - move the WHOLE private ` selectionState ` map together with
85+ ` pickRoundRobinAccount ` , ` peekRoundRobinAccount ` , ` seedPoolRotationAccount ` ,
86+ ` notePoolRotationSuccess ` , ` notePoolRotationFailure ` , ` clearPoolRotationState ` ,
87+ ` selectPriorityTier ` , the priority parsers, ` POOL_KEY_* ` and the strategy and
88+ sticky normalizers. Moving a function subset while leaving the map behind would
89+ split one piece of state across two modules.
90+ - the move is safe: ` pool-rotation.ts ` imports only two TYPES,
91+ ` OcxAccountPoolRotationStrategy ` from ` ../types ` and ` GenerationContext ` from
92+ ` ../lib/state-store-sweeper ` . Neither creates a cycle into ` src/oauth ` .
7593- add ` genericPoolKey(provider) => \ ` generic:\$ {provider}\` `
76- - lift fill-first to ` pickFillFirst(ids, afterId, hasHeadroom) ` so both existing
77- copies call one implementation
78- - extend the reconcile sweep to ` generic:* ` keys, which ` :270-276 ` currently skips
94+ - add a fill-first helper with the signature
95+ ` pickFillFirst(ids, afterId, hasHeadroom, stableAll) ` . The earlier three-argument
96+ shape was rejected by the audit: both existing copies walk a STABLE FULL roster
97+ and not the eligible subset, so dropping ` stableAll ` changes the wrap order
98+ whenever an ineligible id sits between two eligible ones.
99+ - extend the reconcile sweep to ` generic:* ` . ` buildGenerationContext ` already fills
100+ ` oauthAccountKeys ` from ` listLiveOAuthAccountKeys ` as ` provider\0id ` for every
101+ live OAuth provider, so the sweep needs no new field and no Codex dependency;
102+ today those keys are simply skipped as ` valid === null ` .
103+
104+ NOT moved, deliberately: the Codex fill-first copy in ` src/codex/routing.ts ` stays
105+ where it is. Deleting it is the only thing that would force an edit to a file lane
106+ L3 owns, and the audit flagged that as a blocker against this unit's own freeze.
107+ Only ` anthropic-routing.ts ` and the generic kind switch to the kernel helper, and
108+ the Anthropic caller keeps its weekly ` exhausted5h ` pre-filter rather than pushing
109+ that rule into the shared helper.
79110
80111MODIFY ` src/codex/pool-rotation.ts ` — re-export the kernel so existing importers
81112and ` tests/codex-integration/codex-pool-rotation.test.ts ` keep working unchanged.
82113
83- MODIFY ` src/oauth/generic-account-failover.ts ` — route selection through the kernel
84- by strategy: ` quota ` keeps ` rankAccountsByHeadroom ` , ` round-robin ` calls
85- ` pickRoundRobinAccount(genericPoolKey(name), ...) ` , ` fill-first ` calls the lifted
86- helper; seed on manual selection; note success and failure. Keep the presence
114+ MODIFY ` src/oauth/generic-account-failover.ts ` — branch BOTH paths on strategy, not
115+ just the proactive one. ` preferredInitialAccount ` currently no-ops when the active
116+ account is healthy and requires ` hasHeadroomEvidence ` , and the 429 path always ends
117+ in ` rankAccountsByHeadroom ` ; leaving either unbranched keeps the strategy inert in
118+ practice even after the DTO says otherwise. ` quota ` keeps
119+ ` rankAccountsByHeadroom ` , ` round-robin ` calls
120+ ` pickRoundRobinAccount(genericPoolKey(name), ...) ` , and ` fill-first ` uses the
121+ kernel helper with ` autoSwitchThreshold ` as its headroom test. Keep the presence
87122quorum, the ` EXCLUDED_PROVIDERS ` guard and the per-provider ` health ` cooldown.
88123
89- MODIFY ` src/oauth/pool-settings-capability.ts ` — drop ` inert: true ` , add
90- ` stickyLimit ` . MODIFY ` src/types/provider.ts:512-518 ` comments and
91- ` oauth-account-routes.ts:395 ` to accept ` stickyLimit ` .
92-
93- MODIFY ` src/codex/routing.ts ` and ` src/oauth/anthropic-routing.ts ` — import from
94- the kernel instead of holding their own copies.
124+ MODIFY ` src/server/management/oauth-account-routes.ts ` — a manual account selection
125+ must seed the cursor, or the operator's pick immediately loses to sticky
126+ round-robin. Today that PUT calls only ` forgetGenericFailoverRoster ` , which clears
127+ the presence cache and not the rotation state. Add
128+ ` seedPoolRotationAccount(genericPoolKey(provider), accountId) ` beside it, mirroring
129+ what ` resetAnthropicRoutingForManualSelection ` already does for Anthropic.
130+ ` clearGenericFailoverHealth ` is the wrong map and ` clearPoolRotationState ` wipes
131+ where seeding is wanted.
132+
133+ MODIFY ` src/oauth/pool-settings-capability.ts ` — report ` inert ` from the flag rather
134+ than as a type literal. While ` pool.kernel ` is off the generic DTO must keep saying
135+ ` inert: true ` , because nothing consumes the strategy yet and the reversibility rule
136+ below requires the old behaviour to be exactly restorable. The literal becomes a
137+ computed field and only turns false once the kernel is on.
138+
139+ Known readers of that field, all of which move in the same PR:
140+ ` src/cli/account-extended.ts ` (forces generic auto-switch inactive),
141+ ` tests/server/account-pool-management-api.test.ts ` and
142+ ` tests/cli/cli-account-pool-verbs.test.ts ` . The GUI does not read it.
143+ Also lift the ` stickyLimit ` rejection at ` oauth-account-routes.ts:395 ` and update
144+ ` src/types/provider.ts:512-518 ` comments.
145+
146+ MODIFY ` src/oauth/anthropic-routing.ts ` — import from the kernel. ` src/codex/ `
147+ keeps importing ` ./pool-rotation ` , which is now a re-export, so this layer needs
148+ no edit inside lane L3's files at all. The audit confirmed the shim is sufficient:
149+ ` routing.ts ` , ` auth-api.ts ` , ` account-priority.ts ` and
150+ ` state-store-registrations.ts ` all keep their existing import path.
95151
96152## Reversibility (audit blocker, mandatory)
97153
@@ -109,6 +165,13 @@ the kernel instead of holding their own copies.
109165
110166## Tests
111167
168+ Audit record: the A-phase reviewer returned PASS-WITH-FINDINGS with two blockers,
169+ both folded above. The first was that lifting fill-first out of its Codex copy
170+ would have forced an edit inside lane L3's freeze. The second was that dropping
171+ ` inert: true ` unconditionally contradicts this document's own reversibility rule,
172+ which requires ` pool.kernel ` to default off and the old behaviour to be exactly
173+ restorable.
174+
112175- ` tests/codex-integration/codex-pool-rotation.test.ts ` — unchanged behaviour
113176 through the re-export (` pickRoundRobinAccount ` ` :270 ` , ` selectPriorityTier ` ` :111 ` )
114177- ` tests/oauth/generic-oauth-failover.test.ts ` — a configured strategy changes the
0 commit comments