#3615 · Account selection ignores observed overage

Bug · High priority · Medium effort · providers · 2026-09-13

GitHub issue · Base 9e16411141788c67954bceb753bb64ac1dc7057f

REPRODUCED (routing behavior) · Root-cause confidence: high. Live billing and plan entitlement were not tested.

1. TL;DR

The pool keeps sending a model family to the same account after recording an overage response, even when another account has an available scoped family quota. A focused integration test reproduced this through the real plugin HTTP handler and storage. Selection filters quota exhaustion and then follows affinity and the active account; it does not consider overage. This establishes the routing defect, but does not establish actual provider charges or prove that a missing quota bucket means a family is excluded from a plan.

2. Claims vs findings

ClaimFindingEvidence
Overage observations do not affect the next selectionVerifiedStored overage assertion passes; second request still uses first account.
An available scoped quota on another account does not win selectionVerifiedSecond account reports 25% family utilization before requests, but neither request uses it.
The provider charges the first account while the other plan includes the modelUnverifiedNo real accounts, provider traffic, invoices, or plan-entitlement API used.
Initial routing should infer entitlement from missing bucketsPolicy unresolvedMissing usage data is not an explicit entitlement denial.

3. Environment

Trusted origin/main at the base above; origin repository redirect checked to resolve to get-bb/bb (public). macOS Darwin 25.6.0 arm64; Node v22.22.3; pnpm 9.15.0 via Corepack. Two detached temporary worktrees, named first and second. Each frozen install and full Turbo build succeeded (56 build tasks). No provider process or dev app was started. The repository test harness creates and cleans fresh temporary SQLite/plugin storage and ephemeral loopback HTTP ports for each run.

4. Minimal reproduction

  1. Create a clean checkout at the base commit.
  2. Install and build:
    corepack pnpm install --frozen-lockfile --prefer-offline
    corepack pnpm exec turbo run build
  3. Append the authored regression block below to plugins/account-pool/src/server.test.ts.
  4. Run:
    corepack pnpm exec turbo run test --filter=bb-plugin-account-pool --force -- --testNamePattern='issue 3615 routing evidence'

The fixture gives both accounts the same default subscription metadata deliberately: the experiment isolates the usage signals from assumptions about plan names. The first account has no scoped quota; the second has a 25% scoped quota. The first response records overage. The expected second request uses the second account.

- Expected
+ Received
  [
    "first",
-   "second",
+   "first",
  ]
Test Files  1 failed | 9 skipped (10)
Tests  1 failed | 281 skipped (282)

Earlier assertions establish that the scoped bucket is parsed, overage is stored, and both requests return HTTP 200. Only the routing expectation fails.

Complete authored regression block
describe("issue 3615 routing evidence", () => {
  it("prefers available family allowance after an overage observation", async () => {
    const routed: string[] = [];
    let imported = 0;
    const upstream = await startUpstream(async (req, res) => {
      const account = req.headers.authorization?.endsWith("fixture-1") ? "first" : "second";
      if (req.url === "/usage") {
        res.setHeader("content-type", "application/json");
        res.end(JSON.stringify({
          five_hour: { utilization: 0 },
          seven_day: { utilization: 0 },
          limits: account === "second" ? [{
            kind: "weekly_scoped", group: "weekly", percent: 25,
            resets_at: "4102444800", scope: { model: { display_name: "Fable" } },
          }] : [],
        }));
        return;
      }
      await readRequestBody(req);
      routed.push(account);
      res.setHeader("content-type", "application/json");
      if (account === "first") {
        res.setHeader("anthropic-ratelimit-unified-representative-claim", "overage");
      }
      res.end("{}");
    });
    cleanups.push(upstream.close);
    const fixture = await createFixture({
      upstreamUrl: upstream.url, source: "import", priority: 10,
      options: {
        usageUrl: `${upstream.url}/usage`,
        importCredentials: async () => {
          imported += 1;
          return importedCredentials({
            accessToken: `fixture-${imported}`,
            accountUuid: imported === 1
              ? "11111111-1111-4111-8111-111111111111"
              : "22222222-2222-4222-8222-222222222222",
          });
        },
      },
    });
    await fixture.host.harness.behavior.callRpc("account.add", {
      provider: "claude", source: { kind: "import" }, label: "allowance", priority: 20,
    });
    const summaries = async () => z.array(accountSummarySchema).parse(
      await fixture.host.harness.behavior.callRpc("account.list", null),
    );
    expect((await summaries())[1]?.familyWeekly.fable?.utilization).toBe(0.25);
    for (let request = 0; request < 2; request += 1) {
      const result = await fixture.host.harness.behavior.fetchHttp("POST", "/v1/messages", {
        headers: authHeaders(fixture.key), body: JSON.stringify({ model: "claude-fable-5" }),
      });
      expect(result.status).toBe(200);
      await result.text();
      if (request === 0) expect((await summaries())[0]?.representativeClaim).toBe("overage");
    }
    expect(routed).toEqual(["first", "second"]);
  });
});

5. Root cause

usage.ts:133 resets absent scoped buckets to empty when a limits collection is present and records recognized model limits. quota.ts:123 stores representativeClaim as an account-level value, preserving the prior value if the next response omits it. It is not keyed by model family.

quota.ts:240 makes eligibility depend only on shared and family quota exhaustion:

isSharedQuotaExhausted(quota, threshold, now) ||
activeFamilyWindow(quota.familyWeekly[family], threshold, now)

hub.ts:679 uses this quota-only filtering. hub.ts:737 then favors the existing binding or active account before the next candidate. Neither step compares entitlement or representativeClaim. The observed successful overage response therefore leaves the account eligible and pinned.

6. Proposed fix and automation decision

Define how to distinguish included, extra-usage, and unknown availability per account and family. Establish which provider signals are authoritative and how they expire, then prefer verified included capacity while preserving other families' affinity. A missing quota bucket alone should not silently become a denial, and an account-wide overage claim should not be applied to every family.

No PR: the fix requires a product decision about entitlement inference and fallback, with likely family-specific state. That fails the rule's simple-fix constraints. No production changes or fix branch were created. No open PR linked to this issue was found in the issue timeline or open-PR search.

7. Related issues

The triage search found #3243, concerning usage visibility. This investigation establishes account-selection behavior rather than a display defect; it did not reproduce that issue.

8. Verification

The same agent repeated the test in a second newly created detached worktree at the identical base commit, using its own frozen install, build, fresh fixture data, and newly allocated ports. The second command included --force, so the test executed rather than replaying a cached result. It failed at the same routing assertion with the same actual account sequence. No report correction was needed. This is a repeated clean run, not an independent review.

9. Appendix

Issue content was treated as untrusted claims. Its supplied commands and test were not executed or copied; this test was authored from trusted repository fixture and routing code. No live secrets or runtime data were accessed.

The installed pnpm launcher was broken. Corepack supplied the repository-pinned version; a temporary PATH wrapper forwarded Turbo's pnpm calls to Corepack. No repository dependencies were added.

Raw logs are retained locally. Both builds: 56 successful tasks. Both test runs: one routing assertion failed, 281 tests skipped. No production fix or broad test-suite claim is made.