Skip to content

add simulated 5h/7d usage limits via [rate_limits] config - #125

Merged
tmck-code merged 5 commits into
mainfrom
feat/simulated-usage-limits
Aug 30, 2026
Merged

add simulated 5h/7d usage limits via [rate_limits] config#125
tmck-code merged 5 commits into
mainfrom
feat/simulated-usage-limits

Conversation

@tmck-code

@tmck-code tmck-code commented Aug 28, 2026

Copy link
Copy Markdown
Owner

Context

Claude Code only supplies rate_limits in its stdin JSON on some setups. Without it, YAS rendered
the 5h bucket as and omitted the 7d bucket entirely, so users on those setups had no way to see
or reason about their usage windows.

This adds an opt-in simulator so a setup with no real limit data can model the 5h/7d windows
locally, and makes the two buckets behave consistently when idle.

Ships in v0.9.0.

Changes

New [rate_limits] config table

  • Inline five_hour / seven_day tables with budget, window, anchor and epoch:

    [rate_limits]
    five_hour = { budget = 44_000_000, window = "5h", anchor = "rolling" }
    seven_day = { budget = 440_000_000, window = "7d", anchor = "fixed", epoch = "0 0 * * 0" }
  • Defining a key synthesises that bucket and overrides any real payload value; omitting it
    leaves existing behaviour untouched.

  • used_percentage is derived from trailing-window TickRecord history against budget, clamped
    to 0-100.

  • resets_at is a genuine future timestamp, so the burndown trend isn't suppressed.

  • anchor = "rolling" runs now-window .. now; anchor = "fixed" aligns to the cron epoch.

Cron subset (claude/yas/cron.py)

  • Stdlib-only 5-field parser supporting *, integers, comma lists and */n steps.
  • Anything outside that subset is a load-time error rather than a silent mis-anchor.
  • Load-time errors also for epoch with anchor="rolling", and anchor="fixed" without epoch.
  • One bad bucket doesn't block the other.

Minute-granularity updates

  • Derived values are quantized to the minute: stable within a minute, recomputed on rollover.
  • A named regression guard pins the interval at 60s so it can't drift onto the shared 300s
    CACHE_TTL_SECONDS.
  • An in-process bucket cache additionally dedupes usage_since scans within mon's polling loop.
    (It cannot span statusline renders, since each render is a fresh process — the cross-render
    stability comes from the quantization, not the cache.)

Consistency fix

  • An idle 7d bucket now renders like 5h, instead of being omitted. This is a change to default
    behaviour, independent of the new config.

Checklist

  • Tests added or updated for new/changed behaviour
  • (bug fixes) a regression test now covers the situation so it can't come back
  • N/A — no behaviour change, because:

Screenshots / recording

before / after
kitchen-sink kitchen-sink
idle-7d idle-7d
rate-limits-config-synthesized rate-limits-config-synthesized

kitchen-sink is the default demo fixture; idle-7d is a hand-crafted variant (no existing demo
scenario has an idle seven_day bucket — all 50 set a nonzero seven_day_pct) confirming that
main omits the idle 7d bucket while this branch renders . The new cell picks up a matching
/ divider pair and the right box edge stays aligned; the full demo gate over all 50 scenarios
showed border/elbow glyphs byte-identical (the only diffs were pre-existing ±1-cell sparkline
jitter that reproduces on main).

rate-limits-config-synthesized demonstrates this PR's headline feature end-to-end: stdin supplies
no rate_limits payload at all, but a [rate_limits] config table is set. Before (main) has
no config support and no stdin data, so it shows the exact bug described in Context (5h , 7d
omitted). After (this branch), the config-driven simulator synthesizes real usage from local
history — 5h ≈ 53.3%, 7d ≈ 36.9% — instead of /omitted.

  • N/A — no visible change

Benchmark

Command Mean [ms] Min [ms] Max [ms] Relative
main 45.4 ± 1.7 42.3 50.6 1.00
PR 46.3 ± 2.9 43.1 63.5 1.02 ± 0.07

Within noise — the error bars overlap.

  • N/A — no performance-relevant change

System info

Key Value
OS Linux debian-work 6.12.105+deb13-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.12.105-1 (2026-08-24) x86_64 GNU/Linux
Claude Code 2.1.250 (Claude Code)
Terminal TERM=tmux-256color TERM_PROGRAM=tmux SHELL=/bin/bash COLORTERM=truecolor
Locale LANG=en_AU.UTF-8 LC_ALL=
Python Python 3.12.7
uv uv 0.9.17

🤖 Generated with Claude Code

tmck-code and others added 4 commits August 28, 2026 13:55
Setups where Claude Code does not supply rate_limits in its stdin JSON
previously rendered the 5h bucket as infinity and omitted 7d entirely.
This adds an opt-in simulator so those setups can model the limits.

New [rate_limits] table takes inline five_hour / seven_day tables:

  [rate_limits]
  five_hour = { budget = 44_000_000, window = "5h", anchor = "rolling" }
  seven_day = { budget = 440_000_000, window = "7d", anchor = "fixed", epoch = "0 0 * * 0" }

Defining a key synthesises that bucket and overrides any real value;
omitting it leaves existing behaviour untouched. used_percentage is
derived from trailing-window TickRecord history against budget, clamped
to 0-100, with a genuine resets_at so the burndown trend is not
suppressed. Rolling windows run now-window..now; fixed windows align to
a cron epoch.

cron.py implements a narrow stdlib-only 5-field subset ('*', integers,
comma lists, '*/n'); anything else is a load-time error, as are epoch
with anchor="rolling" and anchor="fixed" without epoch.

Derived values are quantized to the minute, so they are stable within a
minute and advance on rollover. A named regression guard pins the
interval at 60s so it cannot drift onto the shared 300s CACHE_TTL.

Also makes an idle 7d bucket render as infinity to match 5h, rather
than being omitted, for consistency.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Extends the demo harness to model a Claude Code setup with no
rate_limits payload at all, and a present-but-idle 7d bucket -
closing the coverage gap PR #125 flagged as a follow-up (it needed
a hand-crafted fixture since no scenario exercised the idle-7d path).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Demonstrates the PR's headline feature directly: stdin supplies no
rate_limits payload, but a [rate_limits] config table is set, so YAS
synthesizes real 5h/7d usage instead of showing infinity/omitting
the bucket.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@tmck-code
tmck-code marked this pull request as ready for review August 30, 2026 10:38
The new opt-in 5h/7d usage simulator had no entry in the example
config, unlike every other config table.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@tmck-code
tmck-code merged commit 71d035d into main Aug 30, 2026
6 checks passed
@tmck-code
tmck-code deleted the feat/simulated-usage-limits branch August 30, 2026 10:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant