v2 Phase 4: decouple Session (local usage) from the rate-limit API

The Session screen's tokens/cost are computed locally from transcripts and need
no network, but they were gated behind a successful rate-limit API call — so an
expired OAuth token or API outage froze the whole watch.

Daemon: always compute and send local usage every cycle; rate-limit utilization
is now best-effort and merged on top when available, with ok=true only when it's
fresh (ok=false on expired token / API down). poll_api returns just the
rate-limit fields; connect_and_run owns the payload + ok flag.

Firmware: ui_update always refreshes the Session screen; the rate-limit bars and
the freshness clock update only when data->ok, so the usage view falls back to
its existing idle "Zzz" state instead of showing stale or zeroed percentages.

Verified: with the token expired (HTTP 401) the daemon still sends
tk/tc/tn/to + ok:false and the Session screen keeps updating.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wenil
2026-06-20 18:49:34 +03:00
co-authored by Claude Opus 4.8
parent 4c1b63e645
commit b188349262
2 changed files with 65 additions and 46 deletions
+42 -32
View File
@@ -253,18 +253,16 @@ async def poll_api(token: str) -> dict | None:
except ValueError:
return 0
payload = {
# Rate-limit utilization only. The local token usage and the "ok" flag are
# merged by the caller (connect_and_run) so the Session screen keeps updating
# even when this call fails (expired token / API down) — see decoupling there.
return {
"s": pct(hdr("anthropic-ratelimit-unified-5h-utilization")),
"sr": reset_minutes(hdr("anthropic-ratelimit-unified-5h-reset")),
"w": pct(hdr("anthropic-ratelimit-unified-7d-utilization")),
"wr": reset_minutes(hdr("anthropic-ratelimit-unified-7d-reset")),
"st": hdr("anthropic-ratelimit-unified-5h-status", "unknown"),
"ok": True,
}
# Piggyback today's local token usage/cost onto the same 60s payload. Local
# file read (~tens of ms once per minute) — fine to do inline in the poller.
payload.update(compute_today_usage())
return payload
async def scan_for_device():
@@ -496,39 +494,51 @@ async def connect_and_run(device, stop_event: asyncio.Event, tray_state=None) ->
elapsed = now - last_poll
if session.refresh_requested.is_set() or elapsed >= POLL_INTERVAL:
session.refresh_requested.clear()
# Local token usage (Session screen) needs no network — compute it
# every cycle so the watch keeps updating even with no/expired token.
payload = compute_today_usage()
# Rate-limit utilization is best-effort. A genuine 401/403 flags the
# token; a transient failure (network/DNS/5xx) leaves the tray state
# alone (SC#5: a DNS blip must not read as "token expired").
rl = None
auth_problem = False
token = read_token() # D-09: fresh each cycle
if not token:
log("No token; skipping poll")
if tray_state:
tray_state.set_error("token expired — run claude login")
auth_problem = True
log("No token; sending local usage only")
else:
try:
payload = await poll_api(token)
rl = await poll_api(token)
except AuthError:
# Real 401/403 — token genuinely needs a refresh.
auth_problem = True
if rl is not None:
payload.update(rl)
payload["ok"] = True # rate-limit data is fresh
else:
payload["ok"] = False # rate-limit unknown -> watch usage view goes idle
if await session.write_payload(payload):
last_poll = time.time()
used_successfully = True
consecutive_failures = 0 # D-03: reset on success
if rl is not None:
if tray_state:
tray_state.set_connected(time.time())
elif auth_problem:
if tray_state:
tray_state.set_error("token expired — run claude login")
payload = None
if payload is not None:
if await session.write_payload(payload):
last_poll = time.time()
used_successfully = True
consecutive_failures = 0 # D-03: reset on success
if tray_state:
tray_state.set_connected(time.time())
else:
consecutive_failures += 1
if consecutive_failures >= ZOMBIE_BREAK_LIMIT:
log(
f"Zombie link detected ({consecutive_failures} consecutive"
f" write failures); abandoning connection"
)
break
# else: payload is None from a TRANSIENT failure (network/DNS,
# timeout, rate-limit, 5xx). poll_api already logged it; do NOT
# toast "token expired" — that mislabeled a boot-time DNS blip
# as an auth problem (SC#5). Leave tray state unchanged; the next
# tick retries and set_connected() recovers it.
# transient rate-limit failure: leave tray state unchanged
else:
consecutive_failures += 1
if consecutive_failures >= ZOMBIE_BREAK_LIMIT:
log(
f"Zombie link detected ({consecutive_failures} consecutive"
f" write failures); abandoning connection"
)
break
# Wake on a refresh request OR a stop, whichever comes first. Waking
# promptly on stop_event is what lets the finally below run