@@ -1264,24 +1264,58 @@ fn do_poll(send_hwnd: SendHwnd) {
12641264 let _ = PostMessageW ( hwnd, WM_APP_USAGE_UPDATED , WPARAM ( 0 ) , LPARAM ( 0 ) ) ;
12651265 }
12661266 }
1267- Err ( _e) => {
1268- // Show refresh indicator — retry will recover silently
1269- let mut state = lock_state ( ) ;
1270- if let Some ( s) = state. as_mut ( ) {
1271- s. session_text = "..." . to_string ( ) ;
1272- s. weekly_text = "..." . to_string ( ) ;
1273- s. last_poll_ok = false ;
1274-
1275- // Exponential backoff retry: 30s, 60s, 120s, ... up to poll_interval
1276- s. retry_count = s. retry_count . saturating_add ( 1 ) ;
1277- let backoff = RETRY_BASE_MS
1278- . saturating_mul ( 1u32 . checked_shl ( s. retry_count - 1 ) . unwrap_or ( u32:: MAX ) ) ;
1279- let retry_ms = backoff. min ( s. poll_interval_ms ) ;
1267+ Err ( e) => {
1268+ // Distinguish token expiry (needs user action) from transient errors (retry helps).
1269+ let notify_expired = {
1270+ let mut state = lock_state ( ) ;
1271+ let mut should_notify = false ;
1272+ if let Some ( s) = state. as_mut ( ) {
1273+ s. last_poll_ok = false ;
1274+ match e {
1275+ poller:: PollError :: TokenExpired => {
1276+ // Only show the balloon on the first failure so it doesn't spam.
1277+ if s. retry_count == 0 {
1278+ should_notify = true ;
1279+ }
1280+ s. session_text = "auth?" . to_string ( ) ;
1281+ s. weekly_text = "re-login" . to_string ( ) ;
1282+ s. retry_count = s. retry_count . saturating_add ( 1 ) ;
1283+ // Retry every 5 minutes — polling more often won't help until
1284+ // the user re-authenticates via 'claude logout && claude login'.
1285+ unsafe {
1286+ let _ = KillTimer ( hwnd, TIMER_RESET_POLL ) ;
1287+ SetTimer ( hwnd, TIMER_POLL , 5 * 60 * 1000 , None ) ;
1288+ }
1289+ }
1290+ _ => {
1291+ // Transient network / credential-missing errors: exponential backoff.
1292+ s. session_text = "..." . to_string ( ) ;
1293+ s. weekly_text = "..." . to_string ( ) ;
1294+ s. retry_count = s. retry_count . saturating_add ( 1 ) ;
1295+ let backoff = RETRY_BASE_MS
1296+ . saturating_mul ( 1u32 . checked_shl ( s. retry_count - 1 ) . unwrap_or ( u32:: MAX ) ) ;
1297+ let retry_ms = backoff. min ( s. poll_interval_ms ) ;
1298+ unsafe {
1299+ let _ = KillTimer ( hwnd, TIMER_RESET_POLL ) ;
1300+ SetTimer ( hwnd, TIMER_POLL , retry_ms, None ) ;
1301+ }
1302+ }
1303+ }
1304+ }
1305+ should_notify
1306+ } ;
12801307
1281- unsafe {
1282- // Kill the 5-second reset poll so it doesn't bypass backoff
1283- let _ = KillTimer ( hwnd, TIMER_RESET_POLL ) ;
1284- SetTimer ( hwnd, TIMER_POLL , retry_ms, None ) ;
1308+ if notify_expired {
1309+ let strings = {
1310+ let state = lock_state ( ) ;
1311+ state. as_ref ( ) . map ( |s| s. language . strings ( ) )
1312+ } ;
1313+ if let Some ( strings) = strings {
1314+ tray_icon:: notify_balloon (
1315+ hwnd,
1316+ strings. token_expired_title ,
1317+ strings. token_expired_body ,
1318+ ) ;
12851319 }
12861320 }
12871321
0 commit comments