From e78cbfba9bd308bfcbcd17e1524d33e6734ab8ef Mon Sep 17 00:00:00 2001 From: David Cabrera Date: Fri, 30 Oct 2026 18:16:23 -0500 Subject: [PATCH] fix(rate-limits): map Cursor pools for team-limited accounts (#36394) Team-limited (enterprise) Cursor accounts report the member's included allowance as `individualUsage.overall` instead of an enabled `plan` pool, so the monthly meter showed nothing. Fall back to `overall` when `plan` is absent or disabled. Team-wide `teamUsage.onDemand` stays unmapped because it is not this member's allowance. Co-authored-by: Neil --- .../rate-limits/cursor-usage-mapping.test.ts | 36 +++++++++++++++++++ src/main/rate-limits/cursor-usage-mapping.ts | 18 ++++++++-- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/src/main/rate-limits/cursor-usage-mapping.test.ts b/src/main/rate-limits/cursor-usage-mapping.test.ts index 1e7da2188fc..52ecee8ca26 100644 --- a/src/main/rate-limits/cursor-usage-mapping.test.ts +++ b/src/main/rate-limits/cursor-usage-mapping.test.ts @@ -114,6 +114,42 @@ describe('mapCursorUsageSummary', () => { expect(mapped.monthly).toBeNull() }) + it('maps a team-limited member allowance without publishing team-wide on-demand', () => { + const mapped = mapCursorUsageSummary( + parseCursorUsageSummary({ + ...CYCLE, + membershipType: 'enterprise', + limitType: 'team', + isUnlimited: false, + individualUsage: { + overall: { enabled: true, used: 75_000, limit: 500_000, remaining: 425_000 } + }, + teamUsage: { + onDemand: { enabled: true, used: 500_000, limit: 2_500_000, remaining: 2_000_000 } + } + }) + ) + expect(mapped.planType).toBe('enterprise') + expect(mapped.monthly?.usedPercent).toBe(15) + expect(mapped.buckets).toEqual([]) + }) + + it('uses the overall allowance only when the plan pool is absent or disabled', () => { + const overall = { enabled: true, used: 90, limit: 100 } + expect( + mapCursorUsageSummary({ + ...CYCLE, + individualUsage: { plan: { enabled: true, totalPercentUsed: 30 }, overall } + }).monthly?.usedPercent + ).toBe(30) + expect( + mapCursorUsageSummary({ + ...CYCLE, + individualUsage: { plan: { enabled: false, totalPercentUsed: 0 }, overall } + }).monthly?.usedPercent + ).toBe(90) + }) + it('reports an unlimited plan without inventing a percentage', () => { const mapped = mapCursorUsageSummary({ ...CYCLE, isUnlimited: true, membershipType: 'ultra' }) expect(mapped.isUnlimited).toBe(true) diff --git a/src/main/rate-limits/cursor-usage-mapping.ts b/src/main/rate-limits/cursor-usage-mapping.ts index f2051807d83..e8fc476ccb2 100644 --- a/src/main/rate-limits/cursor-usage-mapping.ts +++ b/src/main/rate-limits/cursor-usage-mapping.ts @@ -27,7 +27,12 @@ const cursorUsageSummarySchema = z isUnlimited: z.unknown(), // The dashboard sends null for absent pools; optional-only schemas would reject the entire valid response. individualUsage: z - .object({ plan: cursorPoolSchema.nullish(), onDemand: cursorPoolSchema.nullish() }) + .object({ + plan: cursorPoolSchema.nullish(), + // Team-limited accounts report the member's included allowance as `overall`. + overall: cursorPoolSchema.nullish(), + onDemand: cursorPoolSchema.nullish() + }) .partial() .nullish() }) @@ -104,6 +109,14 @@ function windowMinutesFor(startMs: number | null, endMs: number | null): number return Math.max(1, Math.round((endMs - startMs) / 60_000)) } +// A disabled primary pool must not hide the fallback pool Cursor actually meters. +function preferEnabledPool( + primary: CursorPool | null | undefined, + fallback: CursorPool | null | undefined +): CursorPool | null | undefined { + return primary?.enabled === false && fallback?.enabled === true ? fallback : (primary ?? fallback) +} + // On-demand's raw allowance avoids rounding in its reported percentage. function poolPercent( pool: CursorPool | null | undefined, @@ -132,7 +145,7 @@ export function mapCursorUsageSummary(summary: CursorUsageSummary): CursorUsageM resetDescription: description }) - const plan = summary.individualUsage?.plan + const plan = preferEnabledPool(summary.individualUsage?.plan, summary.individualUsage?.overall) // Team-owned zero pools must not imply healthy quota or suppress legacy fallback. const planEnabled = plan?.enabled !== false const buckets: RateLimitBucket[] = [] @@ -145,6 +158,7 @@ export function mapCursorUsageSummary(summary: CursorUsageSummary): CursorUsageM buckets.push({ name: CURSOR_OTHER_MODELS_BUCKET_NAME, ...toWindow(otherModels) }) } + // teamUsage.onDemand is team-wide spend, not this member's allowance, so it stays unmapped. const onDemand = summary.individualUsage?.onDemand const onDemandPercent = onDemand?.enabled === true ? poolPercent(onDemand, 'totalPercentUsed') : null