From 38546740c7f111f31677ab315cc547246184a384 Mon Sep 17 00:00:00 2001 From: Johannes Klein Date: Wed, 4 Nov 2026 22:15:25 +0100 Subject: [PATCH 1/4] fix(server): streamed usage limits survive a failed probe When the usage probe fails but a turn has streamed limit windows, the windows were hidden behind "Could not read limits." and the next failed probe replaced them with an empty list. Keep and show them; a clean read still wins over a probe-failed one when merging duplicate accounts. Co-Authored-By: Claude Opus 5.5 --- packages/provider-core/src/server/usageLimits.test.ts | 9 +++++++++ packages/provider-core/src/server/usageLimits.ts | 7 ++++++- packages/shared/src/usageLimits.test.ts | 3 +++ packages/shared/src/usageLimits.ts | 10 +++++++--- 4 files changed, 25 insertions(+), 4 deletions(-) diff --git a/packages/provider-core/src/server/usageLimits.test.ts b/packages/provider-core/src/server/usageLimits.test.ts index 4b9c1b39c862b..7d2a132dea3b5 100644 --- a/packages/provider-core/src/server/usageLimits.test.ts +++ b/packages/provider-core/src/server/usageLimits.test.ts @@ -118,5 +118,14 @@ describe("resolveUsageLimitsAfterProbe", () => { expect(resolveUsageLimitsAfterProbe({ published, probed: failed })).toBe(published); expect(resolveUsageLimitsAfterProbe({ published, probed: unsupported })).toBe(unsupported); expect(resolveUsageLimitsAfterProbe({ published: undefined, probed: failed })).toBe(failed); + const streamed = { + checkedAt, + windows: [weekly], + unavailable: { reason: "probeFailed" as const }, + }; + expect(resolveUsageLimitsAfterProbe({ published: streamed, probed: failed })).toBe(streamed); + expect( + resolveUsageLimitsAfterProbe({ published: { ...streamed, windows: [] }, probed: failed }), + ).toBe(failed); }); }); diff --git a/packages/provider-core/src/server/usageLimits.ts b/packages/provider-core/src/server/usageLimits.ts index 1e4bcd0090854..5215824b1692d 100644 --- a/packages/provider-core/src/server/usageLimits.ts +++ b/packages/provider-core/src/server/usageLimits.ts @@ -104,7 +104,12 @@ export function resolveUsageLimitsAfterProbe(input: { readonly probed: ServerProviderUsageLimits | undefined; }): ServerProviderUsageLimits | undefined { const { published, probed } = input; - if (probed?.unavailable?.reason === "probeFailed" && published && !published.unavailable) { + // Streamed windows outlive a failed probe; some setups never get a successful probe. + if ( + probed?.unavailable?.reason === "probeFailed" && + published && + (!published.unavailable || published.windows.length > 0) + ) { return published; } if ( diff --git a/packages/shared/src/usageLimits.test.ts b/packages/shared/src/usageLimits.test.ts index 72a17323386c8..5b86124383700 100644 --- a/packages/shared/src/usageLimits.test.ts +++ b/packages/shared/src/usageLimits.test.ts @@ -23,6 +23,7 @@ import { paceRemainingPercent, usesChatGptSharing, resolveNewThreadModelSelection, + limitsNotice, } from "./usageLimits.ts"; const now = Date.parse("2026-09-03T12:00:00.000Z"); @@ -1346,6 +1347,8 @@ describe("/usage-limits", () => { ), ).toEqual(["hub:duplicate", "hub:oss"]); const failed = provider({ usageLimits: { ...limits, unavailable: { reason: "probeFailed" } } }); + expect(limitsNotice(failed.usageLimits!)).toBeNull(); + expect(limitsNotice({ ...failed.usageLimits!, windows: [] })).toBe("Could not read limits."); expect( collectProviderUsageLimits(selected.instanceId, [failed], sources, now)?.accounts.map( (account) => account.id, diff --git a/packages/shared/src/usageLimits.ts b/packages/shared/src/usageLimits.ts index 869f676acae69..57ca8baccead0 100644 --- a/packages/shared/src/usageLimits.ts +++ b/packages/shared/src/usageLimits.ts @@ -618,12 +618,16 @@ export function limitsNotice(limits: ServerProviderUsageLimits): string | null { if (limits.unavailable?.reason === "unsupported") { return limits.unavailable.message ?? "This account has no subscription limits."; } - if (limits.unavailable?.reason === "probeFailed") { + if (limits.unavailable?.reason === "probeFailed" && limits.windows.length === 0) { return limits.unavailable.message ?? "Could not read limits."; } return limits.windows.length === 0 ? "No limits reported." : null; } +function isCleanRead(limits: ServerProviderUsageLimits): boolean { + return !limits.unavailable && limitsNotice(limits) === null; +} + export function remainingPercent(window: ServerProviderUsageWindow): number { return Math.round(100 - Math.max(0, Math.min(100, window.usedPercent))); } @@ -772,8 +776,8 @@ export function collectProviderUsageLimits( const snapshot = { provider, limits: provider.usageLimits }; const key = resolveKey(nativeReport(provider)) ?? `instance:${provider.instanceId}`; const previous = nativeByAccount.get(key); - const previousUsable = previous ? limitsNotice(previous.usage.limits) === null : false; - const usable = limitsNotice(snapshot.limits) === null; + const previousUsable = previous ? isCleanRead(previous.usage.limits) : false; + const usable = isCleanRead(snapshot.limits); const providerWins = !previous || (usable && !previousUsable) || From 54a3af37580d43ae248d3b05a4247c3561b8b3d8 Mon Sep 17 00:00:00 2001 From: Johannes Klein Date: Wed, 4 Nov 2026 22:25:52 +0100 Subject: [PATCH 2/4] test(server): sparse usage survives a failed probe, still marked unavailable Co-Authored-By: Claude Opus 5.5 --- .../src/server/managedProvider.test.ts | 116 +++++++++--------- 1 file changed, 59 insertions(+), 57 deletions(-) diff --git a/packages/provider-core/src/server/managedProvider.test.ts b/packages/provider-core/src/server/managedProvider.test.ts index 9df85130d6017..0c6c85f3c5b28 100644 --- a/packages/provider-core/src/server/managedProvider.test.ts +++ b/packages/provider-core/src/server/managedProvider.test.ts @@ -712,62 +712,64 @@ describe("makeManagedServerProvider", () => { ).pipe(Effect.provide(layerAlwaysRunTest)), ); - it.effect("keeps sparse usage unavailable after probe failure until a full probe recovers", () => - Effect.scoped( - Effect.gen(function* () { - const failedLimits = { - checkedAt: "2026-04-10T00:00:01.000Z", - windows: [], - unavailable: { reason: "probeFailed", message: "Usage probe timed out." }, - } as const; - const probeSnapshot = yield* Ref.make({ - ...refreshedSnapshot, - usageLimits: failedLimits, - }); - const provider = yield* makeManagedServerProvider({ - resolveMaintenance: () => Effect.succeed(maintenanceCapabilities), - getSettings: Effect.succeed({ enabled: true }), - streamSettings: Stream.empty, - haveSettingsChanged: (previous, next) => previous.enabled !== next.enabled, - initialSnapshot: () => Effect.succeed(initialSnapshot), - checkProvider: Ref.get(probeSnapshot), - refreshOnInterval: false, - }); - yield* Stream.take(provider.streamChanges, 1).pipe(Stream.runDrain); - - const weekly = { - id: "seven_day", - kind: "weekly", - label: "Weekly", - usedPercent: 20, - } as const; - yield* provider.applyUsageLimits({ - checkedAt: "2026-04-10T00:00:02.000Z", - windows: [weekly], - }); - const partial = yield* provider.getSnapshot; - assert.deepStrictEqual(partial.usageLimits, { - checkedAt: "2026-04-10T00:00:02.000Z", - windows: [weekly], - unavailable: failedLimits.unavailable, - }); - yield* provider.applyUsageLimits({ - checkedAt: "2026-04-10T00:00:03.000Z", - windows: [{ ...weekly }], - }); - assert.strictEqual(yield* provider.getSnapshot, partial); - assert.deepStrictEqual((yield* provider.refresh).usageLimits, failedLimits); - - const completeLimits = { - checkedAt: "2026-04-10T00:00:04.000Z", - windows: [ - { id: "five_hour", kind: "session", label: "Session", usedPercent: 10 }, - { ...weekly, usedPercent: 25 }, - ], - } as const; - yield* Ref.set(probeSnapshot, { ...refreshedSnapshot, usageLimits: completeLimits }); - assert.deepStrictEqual((yield* provider.refresh).usageLimits, completeLimits); - }), - ).pipe(Effect.provide(layerAlwaysRunTest)), + it.effect( + "keeps sparse usage marked unavailable through probe failures until a full probe recovers", + () => + Effect.scoped( + Effect.gen(function* () { + const failedLimits = { + checkedAt: "2026-04-10T00:00:01.000Z", + windows: [], + unavailable: { reason: "probeFailed", message: "Usage probe timed out." }, + } as const; + const probeSnapshot = yield* Ref.make({ + ...refreshedSnapshot, + usageLimits: failedLimits, + }); + const provider = yield* makeManagedServerProvider({ + resolveMaintenance: () => Effect.succeed(maintenanceCapabilities), + getSettings: Effect.succeed({ enabled: true }), + streamSettings: Stream.empty, + haveSettingsChanged: (previous, next) => previous.enabled !== next.enabled, + initialSnapshot: () => Effect.succeed(initialSnapshot), + checkProvider: Ref.get(probeSnapshot), + refreshOnInterval: false, + }); + yield* Stream.take(provider.streamChanges, 1).pipe(Stream.runDrain); + + const weekly = { + id: "seven_day", + kind: "weekly", + label: "Weekly", + usedPercent: 20, + } as const; + yield* provider.applyUsageLimits({ + checkedAt: "2026-04-10T00:00:02.000Z", + windows: [weekly], + }); + const partial = yield* provider.getSnapshot; + assert.deepStrictEqual(partial.usageLimits, { + checkedAt: "2026-04-10T00:00:02.000Z", + windows: [weekly], + unavailable: failedLimits.unavailable, + }); + yield* provider.applyUsageLimits({ + checkedAt: "2026-04-10T00:00:03.000Z", + windows: [{ ...weekly }], + }); + assert.strictEqual(yield* provider.getSnapshot, partial); + assert.deepStrictEqual((yield* provider.refresh).usageLimits, partial.usageLimits); + + const completeLimits = { + checkedAt: "2026-04-10T00:00:04.000Z", + windows: [ + { id: "five_hour", kind: "session", label: "Session", usedPercent: 10 }, + { ...weekly, usedPercent: 25 }, + ], + } as const; + yield* Ref.set(probeSnapshot, { ...refreshedSnapshot, usageLimits: completeLimits }); + assert.deepStrictEqual((yield* provider.refresh).usageLimits, completeLimits); + }), + ).pipe(Effect.provide(layerAlwaysRunTest)), ); }); From 7b6f791efb40641023795c9004e9ef82ad3c5b1d Mon Sep 17 00:00:00 2001 From: Johannes Klein Date: Wed, 4 Nov 2026 22:35:22 +0100 Subject: [PATCH 3/4] fix(shared): rank clean reads over failed reads with windows when merging accounts Co-Authored-By: Claude Opus 5.5 --- packages/shared/src/usageLimits.test.ts | 84 +++++++++++++++---------- packages/shared/src/usageLimits.ts | 24 ++++--- 2 files changed, 68 insertions(+), 40 deletions(-) diff --git a/packages/shared/src/usageLimits.test.ts b/packages/shared/src/usageLimits.test.ts index 5b86124383700..a666f5883f098 100644 --- a/packages/shared/src/usageLimits.test.ts +++ b/packages/shared/src/usageLimits.test.ts @@ -189,41 +189,38 @@ describe("pools", () => { auth: { status: "authenticated", email: "Same@example.com" }, usageLimits: { checkedAt, windows: [{ ...window, usedPercent: 40 }] }, }); - const input = new Map([ - [EnvironmentId.make("env-a"), { ...laptop, serverConfig: { providers: [native] } }], - [ - EnvironmentId.make("env-b"), - { - entry: { target: { label: "Desktop" } }, - serverConfig: { - providers: [ - { - ...native, - usageLimits: { - checkedAt: "2026-09-03T11:30:00.000Z", - windows: [{ ...window, usedPercent: 55 }], + const newer: NonNullable = { + checkedAt: "2026-09-03T11:30:00.000Z", + windows: [{ ...window, usedPercent: 55 }], + }; + const inputWith = (newerLimits: NonNullable) => + new Map([ + [EnvironmentId.make("env-a"), { ...laptop, serverConfig: { providers: [native] } }], + [ + EnvironmentId.make("env-b"), + { + entry: { target: { label: "Desktop" } }, + serverConfig: { + providers: [{ ...native, usageLimits: newerLimits }], + usageLimitSources: [ + { + ...source, + accounts: [ + { + id: "claude-same@example.com.json", + driver: claude, + email: "same@example.com", + plan: "Claude Subscription", + usageLimits: { checkedAt, windows: [{ ...window, usedPercent: 10 }] }, + }, + ], }, - }, - ], - usageLimitSources: [ - { - ...source, - accounts: [ - { - id: "claude-same@example.com.json", - driver: claude, - email: "same@example.com", - plan: "Claude Subscription", - usageLimits: { checkedAt, windows: [{ ...window, usedPercent: 10 }] }, - }, - ], - }, - ], + ], + }, }, - }, - ], - ]); - const accounts = collectLimitAccounts(input); + ], + ]); + const accounts = collectLimitAccounts(inputWith(newer)); expect(accounts).toHaveLength(1); expect(accounts[0]).toMatchObject({ key: "env-a:claude", @@ -235,6 +232,11 @@ describe("pools", () => { ], }); expect(accounts[0]?.limits.windows[0]?.usedPercent).toBe(55); + const [clean] = collectLimitAccounts( + inputWith({ ...newer, unavailable: { reason: "probeFailed" } }), + ); + expect(clean?.limits).toMatchObject({ windows: [{ usedPercent: 40 }] }); + expect(clean?.limits.unavailable).toBeUndefined(); }); it("uses the freshest reported plan even when the newest quota has no plan", () => { @@ -1349,6 +1351,22 @@ describe("/usage-limits", () => { const failed = provider({ usageLimits: { ...limits, unavailable: { reason: "probeFailed" } } }); expect(limitsNotice(failed.usageLimits!)).toBeNull(); expect(limitsNotice({ ...failed.usageLimits!, windows: [] })).toBe("Could not read limits."); + const newerEmpty = provider({ + usageLimits: { + checkedAt: "2026-09-03T11:59:00.000Z", + windows: [], + unavailable: { reason: "probeFailed" }, + }, + }); + for (const duplicates of [ + [failed, newerEmpty], + [newerEmpty, failed], + ]) { + expect( + collectProviderUsageLimits(selected.instanceId, duplicates, [], now)?.accounts[0]?.limits + .windows, + ).toEqual(limits.windows); + } expect( collectProviderUsageLimits(selected.instanceId, [failed], sources, now)?.accounts.map( (account) => account.id, diff --git a/packages/shared/src/usageLimits.ts b/packages/shared/src/usageLimits.ts index 57ca8baccead0..5918d229bf471 100644 --- a/packages/shared/src/usageLimits.ts +++ b/packages/shared/src/usageLimits.ts @@ -298,7 +298,16 @@ export function collectLimitAccounts(presentations: LimitPresentations): readonl !previous.environments.some((seen) => seen.environmentId === candidate.environmentId), ), ]; - const winner = fresher ? next : previous; + const nextRank = readRank(next.limits); + const previousRank = readRank(previous.limits); + const winner = + nextRank === previousRank + ? fresher + ? next + : previous + : nextRank > previousRank + ? next + : previous; const creditSource = creditSources.get(key); // A hub e-mail names the account better than the driver's default instance name. const hubEmail = [previous, next].some( @@ -624,8 +633,10 @@ export function limitsNotice(limits: ServerProviderUsageLimits): string | null { return limits.windows.length === 0 ? "No limits reported." : null; } -function isCleanRead(limits: ServerProviderUsageLimits): boolean { - return !limits.unavailable && limitsNotice(limits) === null; +/** Clean read (2) beats a failed probe that still holds streamed windows (1) beats no windows (0). */ +function readRank(limits: ServerProviderUsageLimits): number { + if (limitsNotice(limits) !== null) return 0; + return limits.unavailable ? 1 : 2; } export function remainingPercent(window: ServerProviderUsageWindow): number { @@ -776,12 +787,11 @@ export function collectProviderUsageLimits( const snapshot = { provider, limits: provider.usageLimits }; const key = resolveKey(nativeReport(provider)) ?? `instance:${provider.instanceId}`; const previous = nativeByAccount.get(key); - const previousUsable = previous ? isCleanRead(previous.usage.limits) : false; - const usable = isCleanRead(snapshot.limits); + const rank = readRank(snapshot.limits); const providerWins = !previous || - (usable && !previousUsable) || - (usable === previousUsable && + rank > readRank(previous.usage.limits) || + (rank === readRank(previous.usage.limits) && Date.parse(snapshot.limits.checkedAt) > Date.parse(previous.usage.limits.checkedAt)); const previousCredit = previous?.credits; const credits = From 750d95fabb56a79c617f7c7de2ff47722b4c8fa8 Mon Sep 17 00:00:00 2001 From: Johannes Klein Date: Thu, 5 Nov 2026 12:00:27 +0100 Subject: [PATCH 4/4] refactor(shared): simplify account rank comparison, sharpen probe comment Co-Authored-By: Claude Opus 5.5 --- packages/provider-core/src/server/usageLimits.ts | 2 +- packages/shared/src/usageLimits.ts | 13 ++----------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/packages/provider-core/src/server/usageLimits.ts b/packages/provider-core/src/server/usageLimits.ts index 5215824b1692d..bffc6391aafeb 100644 --- a/packages/provider-core/src/server/usageLimits.ts +++ b/packages/provider-core/src/server/usageLimits.ts @@ -104,7 +104,7 @@ export function resolveUsageLimitsAfterProbe(input: { readonly probed: ServerProviderUsageLimits | undefined; }): ServerProviderUsageLimits | undefined { const { published, probed } = input; - // Streamed windows outlive a failed probe; some setups never get a successful probe. + // Probes can fail permanently (org-disabled nonessential traffic); keep streamed windows. if ( probed?.unavailable?.reason === "probeFailed" && published && diff --git a/packages/shared/src/usageLimits.ts b/packages/shared/src/usageLimits.ts index 5918d229bf471..e42ed2a4d5b74 100644 --- a/packages/shared/src/usageLimits.ts +++ b/packages/shared/src/usageLimits.ts @@ -298,16 +298,8 @@ export function collectLimitAccounts(presentations: LimitPresentations): readonl !previous.environments.some((seen) => seen.environmentId === candidate.environmentId), ), ]; - const nextRank = readRank(next.limits); - const previousRank = readRank(previous.limits); - const winner = - nextRank === previousRank - ? fresher - ? next - : previous - : nextRank > previousRank - ? next - : previous; + const byRank = readRank(next.limits) - readRank(previous.limits); + const winner = byRank > 0 || (byRank === 0 && fresher) ? next : previous; const creditSource = creditSources.get(key); // A hub e-mail names the account better than the driver's default instance name. const hubEmail = [previous, next].some( @@ -633,7 +625,6 @@ export function limitsNotice(limits: ServerProviderUsageLimits): string | null { return limits.windows.length === 0 ? "No limits reported." : null; } -/** Clean read (2) beats a failed probe that still holds streamed windows (1) beats no windows (0). */ function readRank(limits: ServerProviderUsageLimits): number { if (limitsNotice(limits) !== null) return 0; return limits.unavailable ? 1 : 2;