From ce595faadcafe14376df80969956b979befa1692 Mon Sep 17 00:00:00 2001 From: lurunzi Date: Wed, 28 Oct 2026 08:51:52 +0000 Subject: [PATCH] fix(rate-limits): keep Claude OAuth credits through live statusline updates (#29759) The statusline payload carries only the 5-hour and 7-day windows, so each live post rebuilt Claude's usage without extraUsage and cleared the credit balance until the next OAuth poll. Retain the last OAuth extraUsage alongside fableWeekly. Automated Claude polls on main are gated only by Retry-After, so credits keep refreshing on cadence. Fixes #29755 Co-authored-by: Neil --- .../service-live-claude-usage.test.ts | 24 +++++++++++++++---- .../service/service-fetch-policy.ts | 3 ++- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/main/rate-limits/service-live-claude-usage.test.ts b/src/main/rate-limits/service-live-claude-usage.test.ts index 63ada73d33b..5dc3f168ec6 100644 --- a/src/main/rate-limits/service-live-claude-usage.test.ts +++ b/src/main/rate-limits/service-live-claude-usage.test.ts @@ -130,16 +130,27 @@ describe('RateLimitService', () => { } ) - it('keeps polling on cadence for an account with a Fable window the live feed cannot carry', async () => { + it('keeps OAuth-only Fable and credit fields through live posts and polls them on cadence', async () => { vi.useFakeTimers() try { - const withFable = (usedPercent: number): ProviderRateLimits => ({ + const withOAuthUsage = (usedPercent: number): ProviderRateLimits => ({ ...okProvider('claude', 40), - fableWeekly: { usedPercent, windowMinutes: 10080, resetsAt: null, resetDescription: null } + fableWeekly: { usedPercent, windowMinutes: 10080, resetsAt: null, resetDescription: null }, + extraUsage: { + balance: usedPercent, + unit: 'currency', + currencyCode: 'USD', + enabled: true, + disabledReason: null, + resetsAt: null, + spent: null, + spendLimit: null, + spentPercent: null + } }) vi.mocked(fetchClaudeRateLimits) - .mockImplementationOnce(async () => withFable(18)) - .mockImplementation(async () => withFable(39)) + .mockImplementationOnce(async () => withOAuthUsage(18)) + .mockImplementation(async () => withOAuthUsage(39)) mockFreshBackgroundProviderFetches() const service = new RateLimitService() @@ -155,11 +166,14 @@ describe('RateLimitService', () => { fiveHour: { used_percentage: 50 + minute }, sevenDay: { used_percentage: 30 } }) + expect(service.getState().claude?.fableWeekly?.usedPercent).toBe(18) + expect(service.getState().claude?.extraUsage?.balance).toBe(18) await vi.advanceTimersByTimeAsync(60 * 1000) } expect(fetchClaudeRateLimits).toHaveBeenCalledTimes(2) expect(service.getState().claude?.fableWeekly?.usedPercent).toBe(39) + expect(service.getState().claude?.extraUsage?.balance).toBe(39) service.stop() } finally { diff --git a/src/main/rate-limits/service/service-fetch-policy.ts b/src/main/rate-limits/service/service-fetch-policy.ts index a090be0171c..a7ac6f6ff7f 100644 --- a/src/main/rate-limits/service/service-fetch-policy.ts +++ b/src/main/rate-limits/service/service-fetch-policy.ts @@ -172,8 +172,9 @@ export abstract class RateLimitServiceFetchPolicy extends RateLimitServiceFetchT provider: 'claude', session, weekly, - // Statusline lacks Fable scope; retain the latest OAuth value. + // Statusline lacks Fable scope and credits; retain the latest OAuth values. fableWeekly: inherited?.fableWeekly ?? null, + extraUsage: inherited?.extraUsage ?? null, updatedAt: Date.now(), error: null, status: 'ok',