Skip to content

Claim Claude usage-limit resets from the resets popover - #1413

Merged
robinebers merged 12 commits into
mainfrom
feat/claude-claim-reset
Oct 18, 2026
Merged

robinebers merged 12 commits into
mainfrom
feat/claude-claim-reset

Conversation

@robinebers

@robinebers robinebers commented Oct 18, 2026 •

Copy link
Copy Markdown
Owner

TL;DR: Claude's Rate Limit Resets popover can now claim a usage-limit reset with a "Use" button, the same way Codex's does. The first claim was made live from this build and succeeded.

What was happening

What this changes

  • Typed grants status. ClaudeResetGrantStatus parses the cedar_ember block once. The row and the claim both read it, and the hourly grants check caches it.

  • Claiming. ClaudeProvider+ResetClaim.swift sends POST /api/organizations/{org}/reset_rate_limits with {program: "cedar_ember", grant_id: next_grant_id, request_id}. It uses the same headers and User-Agent as the grants read and a 25s timeout.

    • Credential selection reuses the provider's loaders and leads with the login the grants were read with.
    • A token about to expire is refreshed first. On 401/403 the claim moves to the next login with the same request_id, so a retry can't spend twice.
    • The org UUID is cached from the profile request the provider already makes.
    • Grant id, request id and org UUID are format-checked before anything is sent.
    • After a claim, the grants cache is cleared and Claude is force-refreshed, so the spent reset leaves the row right away instead of an hour later.
  • Shared claim seam. A ResetClaimService protocol with resetClaims / resetClaim environment keys replaces the Codex-only keys. Codex conforms unchanged. The post-claim refresh retry loop is shared too.

  • Outcomes. Two new popover results:

    • .unconfirmed (reset_unconfirmed, stamp_indeterminate).
    • .unavailableNow(until:) (cooldown, paused, low_priority_active).

    reset and already_used count as claimed. not_limited reads as nothing to reset. ineligible, no_grant, unknown_grant and not_next_grant read as no longer available.

  • Popover copy. The confirm card names what the grant refills, for example "Refills your session and weekly limits now." "Use" only appears when the account is eligible and next_grant_id names a grant with resets left.

  • Docs. docs/providers/claude.md covers claiming. docs/research/claude-reset-grant-claim.md has the protocol and both live responses.

Closes #1291.

Heads-up

  • Format checks match Claude Code 2.1.302: the strings in its binary list the result and reason codes. Seven public projects that claim the same way (found with GitHits) agree on the request shape. None of them published a real response before this.
  • A grant with a malformed id or resets_left now rejects the whole block, the way Claude Code does. The row then reads "No data" instead of skipping that one grant.
  • ClaudeProvider.swift was already over 500 lines (656 → 668). The claim itself lives in its own extension file.
  • The popover tracks claimed resets by deadline. Two resets sharing one deadline both drop from the timeline after one claim, until the forced refresh repaints the row.

Tests

  • swift build passes. swift test ran 1632 tests: 0 failures, 4 skipped.
  • ClaudeResetClaimTests cover:
    • The request URL, body and headers.
    • The outcome mapping for every known result, reason and HTTP status.
    • request_id reuse across an auth fallback.
    • Clearing the grants cache after a claim.
    • No claim when ineligible.
    • Malformed blocks.
  • Live dry run, opt-in with OPENUSAGE_LIVE_CLAUDE_CLAIM_DRYRUN=1. It sent our real request with a grant id that matches no grant. The server answered 200 {"result":"ineligible","reason":"unknown_grant","reset":false,...} and spent nothing.
  • Live claim from the dev build's popover: 200 {"result":"reset","reset":true,"resets_left":0,"cleared":["five_hour","seven_day","seven_day_overage_included"],...}. The forced refresh completed right after.

Screenshots

Not captured. The only grant was spent in the live test, so the "Use" button and confirm card can't be shown again until a new grant arrives.

🤖 Generated with Claude Code


Note

High Risk
Adds provider API writes against Anthropic with OAuth credentials and idempotency rules; incorrect claim or cache handling could spend resets or show stale meters after a claim.

Overview
Claude Rate Limit Resets can now be spent from the same timeline popover as Codex: eligible accounts get a Use flow that POSTs Anthropic’s cedar_ember claim (next_grant_id + idempotent request_id), scoped to the login that fetched grants, with grant-specific confirm copy and new banners for unconfirmed and cooldown/paused outcomes.

Reset claiming is generalized behind a shared ResetClaimService and resetClaims / resetClaim environment keys (replacing Codex-only wiring). ClaudeResetGrantStatus parses and caches the cedar_ember block for both the row and claimability; post-claim cache invalidation (resetClaimEpoch, unsettled request-id reuse) and the forced refresh retry loop live in shared ResetClaimRefresh. Docs and ClaudeResetClaimTests cover the protocol and flows.

Reviewed by Cursor Bugbot for commit 7021912. Bugbot is set up for automated code reviews on this repo. Configure here.

robinebers and others added 6 commits October 18, 2026 15:46
The Rate Limit Resets row now derives from ClaudeResetGrantStatus, which
the reset claim will also read. A block with a malformed grant is rejected
whole, like Claude Code does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds a shared ResetClaimService seam (Codex conforms unchanged), the Claude
claim POST to /api/organizations/{org}/reset_rate_limits with next_grant_id
and the popover's request id, outcome mapping including unconfirmed and
unavailable-now, and Claude-specific confirm copy. The post-claim forced
refresh loop is shared by both providers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mocked HTTP only. The live dry run (unknown grant id, nothing spent) is
skipped unless OPENUSAGE_LIVE_CLAUDE_CLAIM_DRYRUN=1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

The claim can target a different account after credential fallback, and a successful claim can leave pre-claim meters and grant counts cached during Anthropic's cooldown. Both should be addressed before merging.

Reviewed changes: Reviewed the Claude reset-claim implementation and its integration with the shared resets popover.

  • Grant status: Typed parsing supplies the reset count, claimability, and confirmation text from the hourly grants check.
  • Claude claims: Adds the reset POST, credential selection and refresh, response mapping, cache clearing, and post-claim refresh.
  • Shared integration: Generalizes the Codex claim service/environment wiring and adds unavailable and unconfirmed popover outcomes.
  • Coverage and docs: Adds mocked claim tests, an opt-in live probe, and protocol/provider documentation. Targeted Swift syntax parsing passed; full macOS build, tests, and UI verification were not available on this Linux host.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using gpt-6.1-sol | 𝕏

Comment thread Sources/OpenUsage/Providers/Claude/ClaudeProvider+ResetClaim.swift Outdated
Comment thread Sources/OpenUsage/Providers/Claude/ClaudeProvider+ResetClaim.swift Outdated

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread Sources/OpenUsage/Views/RateLimitResetsDetail.swift
/// type doc). `nil` makes the timeline read-only (no "Use" affordance).
var claim: ((_ expiry: Date, _ redeemRequestID: String) async -> ResetClaimOutcome)?
/// The confirm card's explanation line; `nil` keeps the generic one.
var confirmDetail: String?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deadline-less grants cannot be claimed

Medium Severity

A claimable Claude grant with no ends_at still increments the available count but is omitted from expiriesAt. The popover then takes the unknown-expiries path, which has no Use control. isClaimable can be true, yet the user has no way to confirm the claim. Claude does not need a deadline to spend next_grant_id, so these resets are stuck at read-only.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 843852c. Configure here.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changed: every cedar_ember grant observed so far, including the one claimed live, carries ends_at, and the popover's claim flow is keyed by expiry instant. Adding an undated claim path is a larger popover change; leaving this open for the owner to decide.

🤖 Addressed by Claude Code

Comment thread Sources/OpenUsage/Providers/Claude/ClaudeProvider+ResetGrants.swift
robinebers and others added 3 commits October 18, 2026 16:03
Cache only the grant status and derive the row from it, drop status
fields nothing reads, split the claim into candidate, refresh and
organization steps, and share one cached-profile lookup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A claim now uses only the login whose grants were read, since grant ids are
shared promos another login could also hold. A successful claim clears the
cached usage and any 429 cooldown so the post-claim refresh fetches live
meters instead of serving the pre-claim snapshot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The popover stops hiding nodes by the clicked deadline once the post-claim
refresh has already reconciled the timeline, and fresh expiries replace its
local bookkeeping. A grants read that a claim overtook no longer caches the
pre-claim status.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

One race remains in the new grants-cache invalidation: a failed in-flight read can restore spent grants for an hour after a claim.

Reviewed changes: Reviewed the follow-up credential binding, post-claim cache invalidation, and popover reconciliation changes.

  • Bound claims to one login: Removed fallback to unrelated credentials and added an auth-rejection regression test.
  • Invalidated post-claim usage: Cleared cached usage and the provider cooldown, added a cooldown regression test, and introduced an epoch to reject overtaken grants reads.
  • Reconciled the timeline: Reset local claimed-node tracking when fresh expiries arrive and avoided hiding a second node after reconciliation.
  • Updated behavior documentation: Documented login binding and immediate refresh during cooldown. Targeted Swift syntax parsing passed; full macOS build, tests, and UI verification were unavailable on this Linux host.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using gpt-6.1-sol | 𝕏

Comment thread Sources/OpenUsage/Providers/Claude/ClaudeProvider+ResetGrants.swift Outdated

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread Sources/OpenUsage/Providers/Claude/ClaudeProvider+ResetClaim.swift
Comment thread Sources/OpenUsage/Providers/Claude/ClaudeProvider+ResetGrants.swift Outdated
Read the grant and its login together before any await, look for the
Desktop login the grants check may have used, reuse a claim's request id
until Anthropic answers definitively, and recheck the claim counter after
the failed-lookup await.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes: Reviewed d600ed72..1c20af2b, focusing on claim identity, unsettled retries, and grants-cache races.

  • Captured claim identity: Snapshotted the grant and credential fingerprint together before suspension, and added Desktop-forced credential lookup while retaining exact-login binding.
  • Preserved retry identity: Retained request IDs after transport failures, server errors, and unreadable successful responses; added a regression test for retrying after a 502 with the original ID.
  • Closed the cache race: Moved the failed-read epoch check after credential validation, keeping it contiguous with the cache write.
  • Updated retry documentation: Explained request-ID reuse after reopening the popover. Targeted Swift syntax parsing passed; full macOS build, tests, and UI verification were unavailable on this Linux host.

Pullfrog  | View workflow run | Using gpt-6.1-sol | 𝕏

A refused claim (not limited, cooldown) now leaves the cached usage and
grants alone, and a usage read that a claim overtook no longer caches its
pre-claim meters.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes: Reviewed 1c20af2b..160c3a2f, focusing on outcome-specific cache invalidation and in-flight usage-cache writes.

  • Narrowed cache invalidation: Preserved caches for refused or temporarily unavailable claims, invalidated grants alone for no-credit responses, and retained full usage invalidation for successful or unconfirmed claims.
  • Guarded usage caching: Applied the shared claim epoch to the final successful usage-cache write so an overtaken fetch cannot restore pre-claim meters.
  • Added regression coverage: Added a test verifying that not_limited preserves the grants check. Targeted Swift syntax parsing passed; full macOS build, tests, and UI execution were unavailable on this Linux host.

Pullfrog  | View workflow run | Using gpt-6.1-sol | 𝕏

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

There are 3 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 160c3a2. Configure here.

Comment thread Sources/OpenUsage/Providers/Claude/ClaudeProvider+ResetClaim.swift Outdated
case .noCredit:
forgetGrantsAfterResetClaim()
case .nothingToReset, .unavailableNow, .failed:
break

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gone grants stay cached in cooldown

Medium Severity

.noCredit now calls forgetGrantsAfterResetClaim() only, so lastGoodUsage and rateLimitedUntil stay. The post-claim refresh still hits the 429 cooldown and serves the old Rate Limit Resets line, even though resetGrantsCheck was cleared and the grant is gone.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 160c3a2. Configure here.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changing: this is display-only. Nothing is spent, the popover already says the reset is no longer available, and the row corrects on the next non-throttled fetch. Not worth more cache surgery.

🤖 Addressed by Claude Code

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes: Reviewed 160c3a2f..70219129, focusing on idempotency after an unsettled Claude claim.

  • Preserved unsettled request IDs: Kept the original request ID across intervening HTTP refusals, so a later retry does not lose the identity of an earlier possibly successful claim.
  • Extended regression coverage: Added a 429 between the existing 502 and already_used responses and verified all three attempts use the original request ID. Targeted Swift syntax parsing passed; full macOS build, tests, and UI execution were unavailable on this Linux host.

Pullfrog  | View workflow run | Using gpt-6.1-sol | 𝕏

@robinebers
robinebers merged commit a7953bf into main Oct 18, 2026
6 checks passed
@robinebers
robinebers deleted the feat/claude-claim-reset branch October 18, 2026 12:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Claude: claim usage-limit reset grants from the resets popover

1 participant