Refresh button is a no-op while a provider is in backoff #5
Description
Problem
The Refresh button in the KDE popup cannot recover a provider that is in backoff, because it goes through the same guard the poll timer does. Once a provider has failed enough times to reach the 15-minute backoff cap, clicking Refresh does nothing at all — no request is made, no error is shown, and the card keeps displaying stale numbers.
shared/core/scheduler.js:
function queueProviderRequest(name, provider) {
const state = providerStates.get(name);
if (!state || !provider || typeof provider.getUsage !== 'function')
return Promise.resolve();
if (backoffManager.shouldBackoff(name))
return Promise.resolve(); // <-- no way past this
...
}
async function refresh() {
const runs = providers.map(({name, provider}) => queueProviderRequest(name, provider));
await Promise.all(runs);
return getSummary();
}refresh() is the only entry point, and kde/plasma6/contents/ui/main.qml calls it from both places:
- line 82 —
Timer { interval: App.DEFAULT_POLL_INTERVAL_MS; onTriggered: root.pollNow() } - line 162 —
Button { text: i18n("Refresh"); onClicked: root.pollNow() }
So a manual refresh is indistinguishable from an automatic tick. There is no force path.
Why this is worse than it sounds: the UI keeps looking live
Two labels in the popup are computed locally, so they keep moving while the data behind them is frozen:
- "Next update in 3m" counts down off the poll timer, which keeps firing — the requests are just dropped.
- "Resets in 4h 24m" is derived from the stored
resets_atISO string, so it keeps ticking against the wall clock.
The card therefore looks like it is updating. There is no visual indication that a provider is in backoff.
Reproduction
- Put the Claude provider into repeated failure. The easiest natural trigger is an expired OAuth access token:
~/.claude/.credentials.jsonhas anexpiresAtin the past and the refresh call also fails (offline, or a transient 5xx). Each failure callsbackoffManager.scheduleBackoff, doubling from 30s toward the 15-minute cap. - Let the failures continue for a few minutes so
attemptclimbs. - Restore connectivity / a valid token.
- Open the popup and click Refresh.
Expected: an explicit user action issues a request immediately, regardless of backoff state.
Actual: queueProviderRequest returns Promise.resolve() before touching the network. The panel and the card keep the previous values, and "Next update in 3m" keeps counting down as if a fetch were pending.
Observed gap
Panel and popup versus a direct call to the same endpoint (api.anthropic.com/api/oauth/usage), read within the same second:
| widget | endpoint | |
|---|---|---|
| Claude session | 90% left | 45% left |
| Claude weekly | 29% left | 25% left |
A 45-point gap on the session window. The 5-hour window had rolled over a few minutes before the widget's last successful fetch, so it captured a nearly-empty window and then froze there while usage climbed. Restarting plasmashell — which clears the in-memory backoff state — brought the panel to 24% against an endpoint reading of 25%, i.e. current.
Suggested fix
Add an opt-in bypass rather than weakening the automatic backoff, which is doing its job:
function queueProviderRequest(name, provider, {force = false} = {}) {
...
if (!force && backoffManager.shouldBackoff(name))
return Promise.resolve();
...
}
async function refresh({force = false} = {}) {
const runs = providers.map(({name, provider}) => queueProviderRequest(name, provider, {force}));
await Promise.all(runs);
return getSummary();
}and have the Refresh button pass {force: true} while the timer keeps calling refresh() unchanged. A manual click is a strong signal that the user wants a request now; the automatic cadence should still be protected.
I have a patch for this and will open a PR.
Environment
- brainusage 1.0.1 (
kde/plasma6) - KDE Plasma 6 on Wayland, Arch-based distribution
- Claude provider only; a second provider on the same install is unauthenticated and fails independently, which does not affect the Claude card since provider states are keyed by name
Activity