5252month_ts = (local_midnight - timedelta (days = 30 )).timestamp () * 1000
5353
5454WINDOW_HOURS = [5 , 5 , 5 , 5 , 4 ]
55+ WINDOW_DURATION_MS = 5 * 3600 * 1000
5556window_boundaries = []
5657cum_h = 0
5758for wh in WINDOW_HOURS :
6667 current_window_idx = i
6768 break
6869
70+ # Fallback (fixed grid) — overridden below once we've seen real activity.
6971cur_win_start = window_boundaries [current_window_idx ][0 ]
7072cur_win_end = window_boundaries [current_window_idx ][1 ]
7173
74+ # Buffer for rolling-window detection: Claude's real 5h rate-limit window
75+ # starts from the first message after a ≥5h gap, not from a fixed clock grid.
76+ # We keep ~6h of events so we can reconstruct the current window start from data.
77+ _WINDOW_BUFFER_MS = WINDOW_DURATION_MS + 3600 * 1000
78+ window_event_cutoff_ts = now_ms - _WINDOW_BUFFER_MS
79+ window_events = [] # [(ts_ms, inp, out, cr, cc)]
80+
7281# --- Credentials & tier ---
7382sub_type = "unknown"
7483tier = "unknown"
@@ -174,9 +183,10 @@ def add_tokens_by_ts(ts_ms, inp, out, cr, cc):
174183 tok_today ["cache_read" ] += cr ; tok_today ["cache_create" ] += cc
175184 tok_total ["input" ] += inp ; tok_total ["output" ] += out
176185 tok_total ["cache_read" ] += cr ; tok_total ["cache_create" ] += cc
177- # Session window
178- if cur_win_start <= ts_ms < cur_win_end :
179- win_input += total_in ; win_output += out
186+ # Session window: buffer events; computed after full scan so we can align
187+ # to the real rolling 5h window (first-message-after-gap), not a fixed grid.
188+ if ts_ms >= window_event_cutoff_ts :
189+ window_events .append ((ts_ms , inp , out , cr , cc ))
180190 # Rate tracking — keep components separate for per-type rates
181191 if ts_ms >= rate_cutoff_ts :
182192 recent_events .append ((ts_ms , inp , out , cr , cc ))
@@ -561,6 +571,36 @@ def calc_rate(window_ms, extract_fn):
561571 rate_output_5m = rate_output_30m = 0.0
562572 rate_all_5m = rate_all_30m = 0.0
563573
574+ # --- Rolling session window detection ---
575+ # Walk events ascending; a new window starts whenever an event arrives ≥5h
576+ # after the previous window start. The last computed start is the current one
577+ # if it hasn't expired yet; otherwise fall back to the fixed clock grid.
578+ window_source = "fixed"
579+ if window_events :
580+ window_events .sort (key = lambda e : e [0 ])
581+ ws_roll = window_events [0 ][0 ]
582+ for ev in window_events :
583+ if ev [0 ] >= ws_roll + WINDOW_DURATION_MS :
584+ ws_roll = ev [0 ]
585+ we_roll = ws_roll + WINDOW_DURATION_MS
586+ if we_roll > now_ms :
587+ cur_win_start = ws_roll
588+ cur_win_end = we_roll
589+ window_source = "rolling"
590+ # Realign window slot index to whichever fixed slot contains the
591+ # detected start (keeps the "N/5" indicator meaningful).
592+ for i , (ws_fb , we_fb ) in enumerate (window_boundaries ):
593+ if ws_fb <= ws_roll < we_fb :
594+ current_window_idx = i
595+ break
596+
597+ win_input = 0
598+ win_output = 0
599+ for ts , inp , out , cr , cc in window_events :
600+ if cur_win_start <= ts < cur_win_end :
601+ win_input += inp + cr + cc
602+ win_output += out
603+
564604print (json .dumps ({
565605 "subscription" : {"type" : sub_type , "tier" : tier },
566606 "limits" : limits ,
@@ -570,7 +610,9 @@ def calc_rate(window_ms, extract_fn):
570610 "est_cost" : {"today" : round (cost_today , 4 ), "week" : round (cost_week , 4 ), "total" : round (cost_total , 4 )},
571611 "session_window" : {
572612 "number" : current_window_idx + 1 , "total" : len (WINDOW_HOURS ),
613+ "start_ts" : int (cur_win_start ),
573614 "end_ts" : int (cur_win_end ),
615+ "source" : window_source ,
574616 "input_limit" : daily_in // 5 , "output_limit" : daily_out // 5 ,
575617 "input_used" : win_input , "output_used" : win_output ,
576618 },
0 commit comments