use crate::time;
use serde_json::Value;
use std::cmp::Ordering;
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct Window {
pub kind: String,
pub scope: Option<String>,
pub percent: f64,
pub resets_at: Option<i64>,
pub is_active: bool,
#[serde(default)]
pub severity: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub length_seconds: Option<i64>,
}
pub fn anthropic_window_length(kind: &str) -> Option<i64> {
match kind {
"session" | "five_hour" => Some(5 * 3600),
"seven_day" => Some(7 * 86_400),
weekly if weekly.starts_with("weekly_") => Some(7 * 86_400),
_ => None,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Source {
Live,
ClaudeCodeCache,
Remembered,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct Snapshot {
pub windows: Vec<Window>,
pub observed_at: Option<i64>,
pub account_uuid: Option<String>,
pub source: Source,
}
impl Window {
pub(crate) fn used(&self, now: i64) -> f64 {
if self.resets_at.is_some_and(|at| at <= now) {
0.0
} else {
self.percent
}
}
pub(crate) fn same_limit(&self, other: &Window) -> bool {
limit(&self.kind) == limit(&other.kind) && self.scope == other.scope
}
pub(crate) fn same_window(&self, other: &Window) -> bool {
self.same_limit(other)
&& matches!(
(self.resets_at, other.resets_at),
(Some(x), Some(y)) if x.abs_diff(y) < SAME_RESET
)
}
}
fn limit(kind: &str) -> &str {
match kind {
"five_hour" => "session",
"seven_day" => "weekly_all",
other => other,
}
}
const SAME_RESET: u64 = 60;
pub(crate) fn recency(a: &Window, b: &Window, now: i64) -> Ordering {
match (a.resets_at, b.resets_at) {
(Some(x), Some(y)) if x.abs_diff(y) >= SAME_RESET => x.cmp(&y),
_ => a.used(now).total_cmp(&b.used(now)),
}
}
pub(crate) fn merge(
known: Option<&Snapshot>,
offered: Option<&Snapshot>,
now: i64,
) -> Option<Snapshot> {
let Some(offered) = offered else {
return known.cloned();
};
let Some(known) = known else {
let mut first = offered.clone();
first.observed_at = first.observed_at.or(Some(now));
return Some(first);
};
let answered = offered.source == Source::Live && offered.observed_at.is_some();
let taken_since = offered.observed_at > known.observed_at;
let (mut advanced, mut confirmed) = (false, false);
let mut windows = Vec::new();
for had in &known.windows {
match offered.windows.iter().find(|w| w.same_limit(had)) {
Some(given) => match recency(given, had, now) {
Ordering::Less if taken_since => {
advanced = true;
windows.push(given.clone());
}
Ordering::Less => windows.push(had.clone()),
Ordering::Equal if had.resets_at.is_some_and(|at| at <= now) => {
confirmed = true;
windows.push(if given.resets_at.is_none_or(|at| at > now) {
given.clone()
} else {
Window {
percent: 0.0,
resets_at: None,
..had.clone()
}
});
}
Ordering::Equal => {
confirmed = true;
windows.push(had.clone());
}
Ordering::Greater => {
advanced = true;
windows.push(given.clone());
}
},
None if answered && had.resets_at.is_some_and(|at| at <= now) => {}
None => windows.push(had.clone()),
}
}
for given in &offered.windows {
if !known.windows.iter().any(|w| w.same_limit(given)) {
advanced = true;
windows.push(given.clone());
}
}
let vouched = advanced || (confirmed && offered.observed_at.is_some());
Some(Snapshot {
windows,
observed_at: if advanced {
known
.observed_at
.max(Some(offered.observed_at.unwrap_or(now)))
} else if confirmed {
known.observed_at.max(offered.observed_at)
} else {
known.observed_at
},
account_uuid: known
.account_uuid
.clone()
.or_else(|| offered.account_uuid.clone()),
source: if vouched {
offered.source
} else {
known.source
},
})
}
fn percent(v: &Value) -> Option<f64> {
let p = v.as_f64()?;
(p.is_finite() && p >= 0.0).then_some(p)
}
fn window_from_limit(l: &Value) -> Option<Window> {
let named = |what: &str| {
l.get("scope")
.and_then(|s| s.get(what))
.and_then(|m| m.get("display_name"))
.and_then(Value::as_str)
.map(str::to_owned)
};
Some(Window {
kind: l.get("kind")?.as_str()?.to_string(),
scope: named("model").or_else(|| named("surface")),
severity: l.get("severity").and_then(Value::as_str).map(str::to_owned),
percent: percent(l.get("percent")?)?,
resets_at: l
.get("resets_at")
.and_then(Value::as_str)
.and_then(time::parse),
is_active: l.get("is_active").and_then(Value::as_bool).unwrap_or(false),
length_seconds: l
.get("kind")
.and_then(Value::as_str)
.and_then(anthropic_window_length),
})
}
fn window_from_named(kind: &str, v: &Value) -> Option<Window> {
Some(Window {
kind: kind.to_string(),
scope: None,
severity: None,
percent: percent(v.get("utilization")?)?,
resets_at: v
.get("resets_at")
.and_then(Value::as_str)
.and_then(time::parse),
is_active: false,
length_seconds: anthropic_window_length(kind),
})
}
fn windows_of(u: &Value) -> Vec<Window> {
let mut windows: Vec<Window> = u
.get("limits")
.and_then(Value::as_array)
.map(|ls| ls.iter().filter_map(window_from_limit).collect())
.unwrap_or_default();
if windows.is_empty() {
for kind in ["five_hour", "seven_day"] {
if let Some(w) = u.get(kind).and_then(|v| window_from_named(kind, v)) {
windows.push(w);
}
}
}
windows
}
pub fn from_usage_object(u: &Value, observed_at: i64) -> Snapshot {
Snapshot {
windows: windows_of(u),
observed_at: Some(observed_at),
account_uuid: None,
source: Source::Live,
}
}
pub fn from_config_cache(config: &Value) -> Option<Snapshot> {
let c = config.get("cachedUsageUtilization")?;
Some(Snapshot {
windows: windows_of(c.get("utilization")?),
observed_at: c
.get("fetchedAtMs")
.and_then(Value::as_i64)
.map(|ms| ms / 1000),
account_uuid: c
.get("accountUuid")
.and_then(Value::as_str)
.map(str::to_owned),
source: Source::ClaudeCodeCache,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn real_config() -> Value {
serde_json::json!({"cachedUsageUtilization": {
"fetchedAtMs": 1789933772292i64,
"accountUuid": "1f0e2d3c-4b5a-4968-8776-a5b4c3d2e1f0",
"utilization": {
"five_hour": {"utilization": 62, "resets_at": "2026-09-20T22:20:00.095287+00:00"},
"seven_day": {"utilization": 48, "resets_at": "2026-09-27T02:00:00.095306+00:00"},
"limits": [
{"kind": "session", "group": "session", "percent": 62,
"resets_at": "2026-09-20T22:20:00.095287+00:00", "scope": null, "is_active": true},
{"kind": "weekly_all", "group": "weekly", "percent": 48,
"resets_at": "2026-09-27T02:00:00.095306+00:00", "scope": null, "is_active": false},
{"kind": "weekly_scoped", "group": "weekly", "percent": 0,
"resets_at": "2026-09-27T02:00:00+00:00",
"scope": {"model": {"id": null, "display_name": "Fable"}}, "is_active": false}
]}}})
}
#[test]
fn reads_the_real_cache_shape() {
let s = from_config_cache(&real_config()).expect("should parse");
assert_eq!(s.windows.len(), 3);
assert_eq!(
s.account_uuid.as_deref(),
Some("1f0e2d3c-4b5a-4968-8776-a5b4c3d2e1f0")
);
assert_eq!(s.observed_at, Some(1789933772));
let scoped = s
.windows
.iter()
.find(|w| w.kind == "weekly_scoped")
.unwrap();
assert_eq!(scoped.scope.as_deref(), Some("Fable"));
}
#[test]
fn falls_back_to_the_named_windows_when_limits_is_missing() {
let mut c = real_config();
c["cachedUsageUtilization"]["utilization"]
.as_object_mut()
.unwrap()
.remove("limits");
let s = from_config_cache(&c).unwrap();
assert_eq!(s.windows.len(), 2);
assert_eq!(s.windows[0].kind, "five_hour");
assert_eq!(s.windows[0].percent, 62.0);
}
#[test]
fn a_nonsense_percentage_is_dropped_rather_than_drawn() {
for nonsense in [serde_json::json!(-5), serde_json::json!("75")] {
let mut c = real_config();
c["cachedUsageUtilization"]["utilization"]["limits"][0]["percent"] = nonsense;
assert_eq!(from_config_cache(&c).unwrap().windows.len(), 2);
}
}
#[test]
fn an_exceeded_limit_is_kept_not_dropped() {
let mut c = real_config();
c["cachedUsageUtilization"]["utilization"]["limits"][0]["percent"] = serde_json::json!(104);
let s = from_config_cache(&c).unwrap();
assert_eq!(s.windows.len(), 3);
assert_eq!(s.windows[0].percent, 104.0);
}
#[test]
fn missing_cache_is_not_an_error() {
assert!(from_config_cache(&serde_json::json!({})).is_none());
}
const NOW: i64 = 1_789_935_000;
const HOUR: i64 = 3_600;
fn measured(kind: &str, percent: f64, resets_at: Option<i64>) -> Window {
Window {
kind: kind.into(),
scope: None,
percent,
resets_at,
is_active: true,
severity: None,
length_seconds: anthropic_window_length(kind),
}
}
fn reading(windows: Vec<Window>, observed_at: Option<i64>) -> Snapshot {
Snapshot {
windows,
observed_at,
account_uuid: None,
source: Source::Live,
}
}
fn shares(reading: &Snapshot) -> Vec<(&str, f64)> {
reading
.windows
.iter()
.map(|w| (w.kind.as_str(), w.percent))
.collect()
}
#[test]
fn a_later_reset_is_a_newer_window_whatever_its_share() {
let full = measured("session", 90.0, Some(NOW + HOUR));
let next = measured("session", 2.0, Some(NOW + 6 * HOUR));
assert_eq!(recency(&next, &full, NOW), Ordering::Greater);
assert_eq!(recency(&full, &next, NOW), Ordering::Less);
}
#[test]
fn within_one_window_the_higher_share_is_the_newer() {
let earlier = measured("session", 20.0, Some(NOW + HOUR));
let later = measured("session", 22.0, Some(NOW + HOUR));
assert_eq!(recency(&later, &earlier, NOW), Ordering::Greater);
assert_eq!(recency(&earlier, &later, NOW), Ordering::Less);
}
#[test]
fn resets_a_second_apart_are_one_window() {
let answered = measured("session", 22.0, Some(NOW + HOUR));
let passed = measured("five_hour", 20.0, Some(NOW + HOUR + 1));
assert_eq!(recency(&passed, &answered, NOW), Ordering::Less);
assert_eq!(recency(&answered, &passed, NOW), Ordering::Greater);
}
#[test]
fn a_window_past_its_reset_counts_as_reset() {
let over = measured("session", 90.0, Some(NOW - 1));
let begun = measured("session", 5.0, None);
assert_eq!(recency(&begun, &over, NOW), Ordering::Greater);
assert_eq!(recency(&over, &begun, NOW), Ordering::Less);
}
#[test]
fn a_reading_that_says_no_time_never_moves_a_limit_backwards() {
let known = reading(vec![measured("session", 22.0, Some(NOW + HOUR))], Some(NOW));
for behind in [
measured("five_hour", 20.0, Some(NOW + HOUR)),
measured("five_hour", 95.0, Some(NOW - 4 * HOUR)),
] {
let merged = merge(Some(&known), Some(&reading(vec![behind], None)), NOW).unwrap();
assert_eq!(shares(&merged), [("session", 22.0)]);
}
let ahead = reading(vec![measured("five_hour", 25.0, Some(NOW + HOUR))], None);
let merged = merge(Some(&known), Some(&ahead), NOW).unwrap();
assert_eq!(
shares(&merged),
[("five_hour", 25.0)],
"under the name it came with"
);
}
#[test]
fn an_answer_taken_after_everything_known_is_what_the_limits_are_now() {
let known = reading(
vec![
measured("session", 0.0, None),
measured("weekly_all", 100.0, Some(NOW + 33 * HOUR)),
],
Some(NOW - HOUR),
);
let answered = reading(
vec![
measured("session", 5.0, Some(NOW + 5 * HOUR)),
measured("weekly_all", 14.0, Some(NOW + 33 * HOUR)),
],
Some(NOW),
);
let mut cached = reading(answered.windows.clone(), Some(NOW - 5));
cached.source = Source::ClaudeCodeCache;
for offered in [answered, cached] {
let merged = merge(Some(&known), Some(&offered), NOW).unwrap();
assert_eq!(
shares(&merged),
[("session", 5.0), ("weekly_all", 14.0)],
"{:?}",
offered.source
);
assert_eq!(merged.observed_at, offered.observed_at);
assert_eq!(merged.source, offered.source);
}
let unused = reading(vec![measured("weekly_all", 0.0, None)], Some(NOW));
let merged = merge(Some(&known), Some(&unused), NOW).unwrap();
assert_eq!(
shares(&merged),
[("session", 0.0), ("weekly_all", 0.0)],
"and one that finds nothing used and no window running"
);
}
#[test]
fn an_answer_taken_no_later_than_what_is_known_never_lowers_a_share() {
let known = reading(
vec![measured("weekly_all", 100.0, Some(NOW + 33 * HOUR))],
Some(NOW - 60),
);
for taken in [NOW - 60, NOW - HOUR] {
let answered = reading(
vec![measured("weekly_all", 14.0, Some(NOW + 33 * HOUR))],
Some(taken),
);
let merged = merge(Some(&known), Some(&answered), NOW).unwrap();
assert_eq!(shares(&merged), [("weekly_all", 100.0)], "taken at {taken}");
}
}
#[test]
fn a_limit_only_one_reading_measured_is_kept() {
let scoped = Window {
scope: Some("Fable".into()),
..measured("weekly_scoped", 5.0, Some(NOW + 50 * HOUR))
};
let known = reading(
vec![measured("session", 22.0, Some(NOW + HOUR)), scoped],
Some(NOW - 60),
);
let offered = reading(
vec![
measured("five_hour", 25.0, Some(NOW + HOUR)),
measured("seven_day", 40.0, Some(NOW + 50 * HOUR)),
],
None,
);
let merged = merge(Some(&known), Some(&offered), NOW).unwrap();
assert_eq!(
shares(&merged),
[
("five_hour", 25.0),
("weekly_scoped", 5.0),
("seven_day", 40.0)
]
);
}
#[test]
fn a_limit_an_answer_leaves_out_goes_once_its_reset_has_passed() {
let known = reading(
vec![
measured("session", 22.0, Some(NOW + HOUR)),
measured("weekly_scoped", 5.0, Some(NOW - 1)),
],
Some(NOW - 60),
);
let answered = reading(vec![measured("session", 25.0, Some(NOW + HOUR))], Some(NOW));
let merged = merge(Some(&known), Some(&answered), NOW).unwrap();
assert_eq!(shares(&merged), [("session", 25.0)]);
}
#[test]
fn a_reading_that_is_not_an_answer_just_now_never_takes_a_limit_away() {
let known = reading(
vec![
measured("session", 40.0, Some(NOW - 10)),
measured("weekly_all", 30.0, Some(NOW + 50 * HOUR)),
],
Some(NOW - HOUR),
);
let passed = reading(
vec![measured("seven_day", 31.0, Some(NOW + 50 * HOUR))],
None,
);
let mut cached = reading(passed.windows.clone(), Some(NOW - 5));
cached.source = Source::ClaudeCodeCache;
for offered in [passed, cached] {
let merged = merge(Some(&known), Some(&offered), NOW).unwrap();
assert_eq!(
shares(&merged),
[("session", 40.0), ("seven_day", 31.0)],
"{:?}",
offered.source
);
assert_eq!(merged.windows[0].used(NOW), 0.0);
}
}
#[test]
fn with_one_side_absent_the_other_stands() {
let known = reading(
vec![measured("session", 22.0, Some(NOW + HOUR))],
Some(NOW - 60),
);
assert_eq!(merge(Some(&known), None, NOW), Some(known.clone()));
assert_eq!(merge(None, None, NOW), None);
let first = merge(None, Some(&reading(known.windows.clone(), None)), NOW).unwrap();
assert_eq!(shares(&first), [("session", 22.0)]);
assert_eq!(first.observed_at, Some(NOW), "stamped when it arrived");
}
#[test]
fn a_reading_that_says_no_time_is_stamped_only_when_it_moves_something() {
let mut known = reading(
vec![measured("session", 22.0, Some(NOW + HOUR))],
Some(NOW - 600),
);
known.source = Source::Remembered;
let repeated = reading(vec![measured("five_hour", 22.0, Some(NOW + HOUR))], None);
let merged = merge(Some(&known), Some(&repeated), NOW).unwrap();
assert_eq!(
merged.windows, known.windows,
"a repeat changes nothing, name and all"
);
assert_eq!(merged.observed_at, Some(NOW - 600));
assert_eq!(
merged.source,
Source::Remembered,
"and it is still what was known"
);
let moved = reading(vec![measured("five_hour", 23.0, Some(NOW + HOUR))], None);
assert_eq!(
merge(Some(&known), Some(&moved), NOW).unwrap().observed_at,
Some(NOW)
);
let mut confirmed = reading(known.windows.clone(), Some(NOW - 5));
confirmed.source = Source::ClaudeCodeCache;
let merged = merge(Some(&known), Some(&confirmed), NOW).unwrap();
assert_eq!(
merged.observed_at,
Some(NOW - 5),
"a measured repeat confirms it"
);
assert_eq!(merged.source, Source::ClaudeCodeCache);
}
#[test]
fn a_window_past_its_reset_is_reset_by_a_reading_that_finds_nothing_used() {
let known = reading(
vec![measured("session", 100.0, Some(NOW - HOUR))],
Some(NOW - 2 * HOUR),
);
for (said, source) in [
(measured("session", 0.0, None), Source::Live),
(measured("session", 0.0, Some(NOW - HOUR)), Source::Live),
(measured("five_hour", 0.0, None), Source::ClaudeCodeCache),
] {
let mut offered = reading(vec![said], Some(NOW - 5));
offered.source = source;
let merged = merge(Some(&known), Some(&offered), NOW).unwrap();
assert_eq!(merged.windows[0].percent, 0.0, "{source:?}");
assert_eq!(merged.windows[0].resets_at, None, "{source:?}");
assert_eq!(merged.source, source, "and it is what said so");
assert_eq!(
merge(Some(&merged), Some(&offered), NOW).as_ref(),
Some(&merged),
"a repeat changes nothing"
);
}
let untimed = reading(vec![measured("five_hour", 0.0, None)], None);
let merged = merge(Some(&known), Some(&untimed), NOW).unwrap();
assert_eq!(merged.windows[0].percent, 0.0);
assert_eq!(
merged.observed_at,
Some(NOW - 2 * HOUR),
"a reading that says no time moved nothing forward"
);
}
}