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