Skip to content

[BUG] Windowless quota snapshots are never seeded into the fallback cache, causing redundant quota API calls #152

Description

@iceteaSA

normalizeQuota deliberately preserves a top-level checkedAt so a windowless snapshot — one with no five_hour/seven_day windows, e.g. { scoped: [], checkedAt: T } — can still carry its freshness. The shared helper honours that:

function quotaSnapshotCheckedAt(quota: OAuthQuotaSnapshot | undefined) {
  return Math.max(
    quota?.five_hour?.checkedAt ?? 0,
    quota?.seven_day?.checkedAt ?? 0,
    ...(quota?.scoped?.map((window) => window.checkedAt) ?? []),
    quota?.checkedAt ?? 0,          // <-- top-level
  )
}

But neither seed path uses it. Both recompute freshness from only the two standard windows and bail out at zero:

packages/core/src/accounts.ts (seedFallbackQuota)

const checkedAt = Math.max(
  account.quota.five_hour?.checkedAt ?? 0,
  account.quota.seven_day?.checkedAt ?? 0,
)
if (checkedAt <= 0) return

packages/core/src/quota-manager.ts (seedFallbacksFromAccounts)

const checkedAt = Math.max(
  account.quota.five_hour?.checkedAt ?? 0,
  account.quota.seven_day?.checkedAt ?? 0,
)
if (checkedAt <= 0) continue

So for an account whose persisted snapshot is windowless, checkedAt computes to 0 and the snapshot is dropped on the floor instead of being seeded into QuotaManager. The manager then treats the account as having no cached quota and hits the usage API again — on every reload and every background pass — even though a perfectly fresh snapshot was sitting in the state file.

This is the exact snapshot shape the normalizer went out of its way to preserve, which is what makes the omission look unintentional rather than a decision.

Fix direction: call the shared quotaSnapshotCheckedAt(account.quota) in both seed paths rather than recomputing the max inline, and use the same value when deriving refreshAfter. That also stops the two sites drifting from the helper again later.

Found during a read-through of b1d8f8c; not something I've reproduced against the live API, so the trigger condition (Anthropic returning a windowless snapshot) is inferred from the normalizer's own handling rather than observed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions