tokenmaxxing 0.1.2__tar.gz → 0.1.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tokenmaxxing
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: Menu bar app showing your live Claude Code session and weekly usage as a colored progress bar.
5
5
  Project-URL: Homepage, https://github.com/alvations/tokenmaxxing
6
6
  Project-URL: Repository, https://github.com/alvations/tokenmaxxing
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "tokenmaxxing"
7
- version = "0.1.2"
7
+ version = "0.1.3"
8
8
  description = "Menu bar app showing your live Claude Code session and weekly usage as a colored progress bar."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -1,6 +1,6 @@
1
1
  """tokenmaxxing — menu bar app for Claude Code session and weekly usage."""
2
2
 
3
- __version__ = "0.1.2"
3
+ __version__ = "0.1.3"
4
4
 
5
5
  from tokenmaxxing.app import main
6
6
 
@@ -604,42 +604,58 @@ class ClaudeMonitorApp(rumps.App):
604
604
  # ----- worker / dispatch -----
605
605
 
606
606
  def _worker_loop(self):
607
+ """Background poll loop. Must NEVER die — any uncaught exception
608
+ leaves the UI frozen on the last state forever, with no further
609
+ polls to recover."""
607
610
  consecutive_failures = 0
608
611
  while True:
609
- now = time.time()
610
- if now < self._backoff_until:
611
- self._wake.wait(timeout=min(REFRESH_SECONDS, self._backoff_until - now))
612
+ try:
613
+ now = time.time()
614
+ if now < self._backoff_until:
615
+ self._wake.wait(timeout=min(REFRESH_SECONDS, self._backoff_until - now))
616
+ self._wake.clear()
617
+ continue
618
+
619
+ oauth_data = _get_oauth_data()
620
+ payload, err, is_rate_limited = fetch_usage(oauth_data)
621
+ # Drop any Refresh-now clicks that arrived during the poll — the
622
+ # in-flight poll already satisfies them, and re-polling immediately
623
+ # can trip the OAuth endpoint's rate limit.
612
624
  self._wake.clear()
613
- continue
614
-
615
- oauth_data = _get_oauth_data()
616
- payload, err, is_rate_limited = fetch_usage(oauth_data)
617
- # Drop any Refresh-now clicks that arrived during the poll — the
618
- # in-flight poll already satisfies them, and re-polling immediately
619
- # can trip the OAuth endpoint's rate limit.
620
- self._wake.clear()
621
- with self._pending_lock:
622
- self._pending = (payload, err, is_rate_limited, oauth_data)
623
-
624
- if payload is not None:
625
- consecutive_failures = 0
626
- self._backoff_until = 0
627
- self._record_snapshot(payload)
628
- sleep_for = REFRESH_SECONDS
629
- elif is_rate_limited:
630
- consecutive_failures += 1
631
- backoff = _calc_backoff(consecutive_failures)
632
- self._backoff_until = time.time() + backoff
633
- # Retry as soon as the backoff window allows, instead of waiting
634
- # the full REFRESH_SECONDS — otherwise the menu keeps showing
635
- # "[rate limited]" for ~5 min after a single transient 429 even
636
- # when the API recovered within seconds.
637
- sleep_for = backoff
638
- else:
639
- sleep_for = REFRESH_SECONDS
640
-
641
- self._wake.wait(timeout=sleep_for)
642
- self._wake.clear()
625
+ with self._pending_lock:
626
+ self._pending = (payload, err, is_rate_limited, oauth_data)
627
+
628
+ if payload is not None:
629
+ consecutive_failures = 0
630
+ self._backoff_until = 0
631
+ self._record_snapshot(payload)
632
+ sleep_for = REFRESH_SECONDS
633
+ elif is_rate_limited:
634
+ consecutive_failures += 1
635
+ backoff = _calc_backoff(consecutive_failures)
636
+ self._backoff_until = time.time() + backoff
637
+ # Retry as soon as the backoff window allows, instead of waiting
638
+ # the full REFRESH_SECONDS — otherwise the menu keeps showing
639
+ # "[rate limited]" for ~5 min after a single transient 429 even
640
+ # when the API recovered within seconds.
641
+ sleep_for = backoff
642
+ else:
643
+ sleep_for = REFRESH_SECONDS
644
+
645
+ self._wake.wait(timeout=sleep_for)
646
+ self._wake.clear()
647
+ except Exception as e: # noqa: BLE001
648
+ # Surface the failure to the UI as a transient error and keep
649
+ # looping. Without this, a single bad poll kills polling forever.
650
+ print(f"tokenmaxxing worker: {type(e).__name__}: {e}", file=sys.stderr)
651
+ with self._pending_lock:
652
+ self._pending = (None, f"worker error: {type(e).__name__}", False, None)
653
+ # Avoid a tight loop if the exception happens immediately.
654
+ try:
655
+ self._wake.wait(timeout=REFRESH_SECONDS)
656
+ self._wake.clear()
657
+ except Exception: # noqa: BLE001
658
+ time.sleep(REFRESH_SECONDS)
643
659
 
644
660
  def _apply_pending(self, _sender):
645
661
  with self._pending_lock:
@@ -665,6 +681,11 @@ class ClaudeMonitorApp(rumps.App):
665
681
  self._render()
666
682
 
667
683
  def _manual_refresh(self, _sender):
684
+ # Force-clear any pending backoff so the click actually polls.
685
+ # Without this, when the worker is in backoff (e.g. after sustained
686
+ # 429s), the wake event fires but the loop top-checks _backoff_until
687
+ # and goes right back to waiting — the click is swallowed.
688
+ self._backoff_until = 0
668
689
  self._wake.set()
669
690
 
670
691
  def _record_snapshot(self, payload):
File without changes
File without changes
File without changes