Explain an expired Claude sign-in instead of hiding the limits (#6795)

Only the Claude Code CLI can refresh the OAuth token it saves; the
collector just reads it. A machine left alone long enough finds the
token lapsed, and that branch returned an empty limits list with no
status text at all, so the panel hid its whole limits section and
explained nothing.

Say what is wrong, and fall back to the cached limits already on disk
rather than discarding them. Cached windows are kept only until they
reset: a percentage from a window that has rolled over describes a
period that is over, and pinning a stale 78% on an allowance that is
now untouched would be worse than showing nothing. The probe-failure
path gets the same filtering for the same reason.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
David Heinemeier Hansson
2026-08-13 11:13:07 +02:00
committed by GitHub
co-authored by Claude Opus 5
parent ecd57bceee
commit c8fb5be42e
2 changed files with 134 additions and 10 deletions
+51 -10
View File
@@ -765,24 +765,65 @@ def probe_limits(access_token: str) -> dict[str, Any]:
return {"ok": True, "limits": limits}
# A cached percentage outlives the probe that measured it, but only until its
# window rolls over: once a window has reset, the figure describes a period
# that is over, and a stale 78% would misreport an allowance that is now
# untouched. A window with no reset time, or one that will not parse, is kept
# — an unreadable timestamp is no reason to throw away a real number.
def limit_window_open(entry: dict[str, Any], now: dt.datetime) -> bool:
raw = str(entry.get("resetsAt") or "")
if raw == "":
return True
try:
resets_at = dt.datetime.fromisoformat(raw.replace("Z", "+00:00"))
except Exception:
return True
if resets_at.tzinfo is None:
resets_at = resets_at.replace(tzinfo=dt.timezone.utc)
return resets_at > now
def usable_cached_limits(cached: dict[str, Any]) -> list[dict[str, Any]]:
entries = cached.get("limits")
if not isinstance(entries, list):
return []
now = dt.datetime.now(dt.timezone.utc)
return [entry for entry in entries if isinstance(entry, dict) and limit_window_open(entry, now)]
def collect_limits(access_token: str, expires_at_ms: int, force: bool) -> dict[str, Any]:
result = {"limits": [], "usageStatusText": "", "authHelpText": AUTH_HELP}
if access_token == "":
result["usageStatusText"] = "Waiting for auth"
return result
if expires_at_ms > 0 and expires_at_ms <= time.time() * 1000:
return result
# A panel that is opened and shut repeatedly must not turn into a request
# per flick, so recent probe results are reused for a short window — and
# kept as the answer of record when a later probe fails.
probe_cache = cache_root() / "claude-limits.json"
cached = read_fresh_json(probe_cache, float("inf")) or {}
fallback = usable_cached_limits(cached)
# Probing needs a live token and only the Claude Code CLI can mint one: it
# refreshes the credential file when it runs, so a machine left alone long
# enough finds the saved token lapsed. Say so — an empty limits list with
# nothing else set hides the whole section and explains nothing — and keep
# showing the last numbers whose window has not since reset.
if access_token == "":
result["limits"] = fallback
result["usageStatusText"] = "Waiting for auth"
return result
if expires_at_ms > 0 and expires_at_ms <= time.time() * 1000:
result["limits"] = fallback
result["usageStatusText"] = "Sign-in expired"
result["authHelpText"] = (
"Claude Code's saved sign-in expired"
+ (" — showing the last known limits." if fallback else ".")
+ " Start Claude Code, or run `claude auth login`, to refresh it."
)
return result
fetched_at = number(cached.get("fetchedAtMs")) / 1000
min_interval = 0 if force else PROBE_MIN_INTERVAL_SECONDS
if cached.get("limits") and time.time() - fetched_at < max(min_interval, PROBE_MIN_INTERVAL_SECONDS):
result["limits"] = cached["limits"]
if fallback and time.time() - fetched_at < max(min_interval, PROBE_MIN_INTERVAL_SECONDS):
result["limits"] = fallback
return result
probe = probe_limits(access_token)
@@ -795,8 +836,8 @@ def collect_limits(access_token: str, expires_at_ms: int, force: bool) -> dict[s
# route. Ask the shell to try again sooner than its regular interval.
if probe.get("transport"):
result["retryAdvised"] = True
if cached.get("limits"):
result["limits"] = cached["limits"]
if fallback:
result["limits"] = fallback
else:
result["usageStatusText"] = "Claude limits unavailable"
result["authHelpText"] = probe["helpText"]