diff --git a/src/features/quota/quotaTimelineModel.ts b/src/features/quota/quotaTimelineModel.ts index 58b8d3ba..c9012a76 100644 --- a/src/features/quota/quotaTimelineModel.ts +++ b/src/features/quota/quotaTimelineModel.ts @@ -268,6 +268,7 @@ export function laneHasWindow(lane: TimelineLane): boolean { /** Shape the lane builder reads. Deliberately structural — see the note below. */ interface WindowLike { + id?: string; label?: string; usedPercent?: number | null; resetAtMs?: number | null; @@ -352,7 +353,25 @@ export function buildTimelineLane(input: TimelineLaneInput): TimelineLane { const windows = ((quota as { windows?: WindowLike[] }).windows ?? []).filter( (window) => typeof window.resetAtMs === 'number' ); - const chosen = pickLaneWindow(windows, maxPeriodHours); + const preferredCodexId = + maxPeriodHours !== undefined && maxPeriodHours <= SESSION_PERIOD_HOURS + ? 'five-hour' + : 'weekly'; + // Codex can report model-scoped windows with the same period as the account + // window (for example GPT-5.3-Codex-Spark weekly). A reset-time tie-break + // would make the lane silently switch to that model's quota. Keep the lane + // anchored to the standard account window whenever it fits this view. + const preferredCodexWindow = + provider === 'codex' + ? windows.find( + (window) => + window.id === preferredCodexId && + typeof window.periodHours === 'number' && + window.periodHours > 0 && + (maxPeriodHours === undefined || window.periodHours <= maxPeriodHours) + ) + : undefined; + const chosen = preferredCodexWindow ?? pickLaneWindow(windows, maxPeriodHours); if (!chosen) return empty; const resetCredits = diff --git a/tests/quotaTimeline.test.ts b/tests/quotaTimeline.test.ts index 8ee6d29e..a8433542 100644 --- a/tests/quotaTimeline.test.ts +++ b/tests/quotaTimeline.test.ts @@ -281,6 +281,39 @@ describe('buildTimelineLane', () => { ]); }); + test('codex: keeps the weekly lane on the account quota instead of Spark quota', () => { + const accountReset = at(2026, 7, 1, 20); + const sparkReset = at(2026, 6, 29, 20); + const lane = buildTimelineLane({ + ...base, + provider: 'codex', + quota: { + status: 'success', + windows: [ + { + id: 'weekly', + label: 'Weekly limit', + usedPercent: 70, + resetAtMs: accountReset, + periodHours: 168, + }, + { + id: 'gpt-5-3-codex-spark-weekly-0', + label: 'GPT-5.3-Codex-Spark weekly limit', + usedPercent: 2, + resetAtMs: sparkReset, + periodHours: 168, + }, + ], + }, + maxPeriodHours: 14 * 24, + }); + + expect(lane.anchorMs).toBe(accountReset); + expect(lane.periodHours).toBe(168); + expect(lane.remaining).toBe(30); + }); + test('codex: includes available reset credits with parseable expiry dates', () => { const expiresAt = '2026-08-02T12:00:00Z'; const lane = buildTimelineLane({