Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,60 @@
# Changelog

## v0.37.0 — 2026-09-01 — what a point buys

**Two pools drain toward one wall and nothing said which one binds.** The
live reading that started this: the account at 81%, the model-scoped
weekly at 63%, both resetting at the same instant. Line 1 has printed the
two numbers for months and never their relation — and the relation is the
decision, because 22 of the model's remaining points are already
unreachable at the rate this week is mixing them.

**The ratio is the mix, and it needs no history.** Both counters are
cumulative from one reset instant, so `scope / seven` IS this week's rate
of scoped points per account point. `reachable = round((100 - seven) *
scope / seven)` is what the account's remaining points can still buy in
the scoped pool, and `strand = (100 - scope) - reachable` is the rest of
it. At 81/63: mix 0.78, reachable 15, strand 22 — against 0.77 mined from
the corpus at week level. One division, and the strand is its remainder,
because both numbers print beside the total they have to add to. No new
`forecast.cache` field and no schema concern: the inputs are already on
screen.

**A new `+` notice at rank 58.** `fb ~22% expires at this mix`, and in
full: `7d caps before fb: this mix reaches ~15% of its 37% left · run fb
heavier to extract more`. Above the 5h tail, below the fleet hint, and it
can never outrank the 7d pressure that causes it — the pairing it was
built for is the dry warning pinned on row 2 with the strand flashing on
row 3. It is the mirror of `fb 91% vs 7d 55%` and the two cannot coexist:
that one needs the model ahead of the account, this one needs the account
ahead of the model.

**Gates, because a ratio is easy to say and easy to get wrong.** One wall
(`SCOPE_SAME_WALL_SEC = 120`: the pools have always reset together to the
microsecond, but Anthropic could split them, and a ratio across two
different weeks is a fluent lie); the account at 60% or deeper, where
"which cap binds" is a question at all; the model at 5% or more, since a
model untouched this week belongs to the underuse voice; both pools under
100; ten stranded points or more; and the 7d window past the young guard
every projection shares. Two mutes on top, statusline-only: a projected
scoped cap, because "fb caps first" and "fb strands" cannot both be true
in one frame, and a live re-base, which just moved one of the two counters
for a reason the ratio cannot see.

**`report` prices the same thing in the other currency.** One line after
the price and spend block: `fb mix: ~0.78 fb-pt per 7d-pt this week ·
7d's 19% left carries ~15% of fb's 37%`. No session runs there to name a
model, so it takes the binding scoped limit, else the deepest.

The four constants are shared rules, not local thresholds:
`SCOPE_STRAND_MIN_PCT`, `SCOPE_MIX_MIN_7D`, `SCOPE_MIX_MIN_SCOPE` and
`SCOPE_SAME_WALL_SEC` are documented in `docs/api/state-dir.md` as
READING RULES computed live rather than cache fields, and ccpace v0.5.0
ships the same four with the same closed forms. The coupling in the other
direction — what a scoped point costs the account — stays unpublished:
n=22 in a 0.2-0.5 band is fluent enough to be believed and too noisy to
be true.

## v0.36.0 — 2026-09-01 — the night is not runway

**The forecast learns when you work, not just how much.** Claude Code can
Expand Down
3 changes: 3 additions & 0 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,9 @@ fb capped ~Thu 07:00 the running model is gone until then
last 5h of the week no later window exists to spend the remainder through
fb 91% vs 7d 55% the MODEL caps first, not the account: switch, and the
week's remaining capacity comes back
fb ~22% expires the ACCOUNT caps first: at this week's mix the 7d pool
runs out before the model's does, and what it cannot
reach expires unspent
5h ~40m left throughput you cannot bank — said only while the week
has slack, since an unspent 5h window is otherwise
headroom, not waste
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,7 @@ clauses about one window can never disagree.
| `! 5h caps ~05:18` | Linear projection off this window's own pace: you hit the wall before the reset. Long form adds how long you'd sit blocked. |
| `! 7d dry ~Thu 09:00 · hard stop` | The learned per-weekday burn profile (EWMA over your own history), not a straight line — your heavy Tuesday counts more than an average, and the learned hour-of-day shape puts the dry point where you are awake to act on it instead of at 03:00. Tail says what happens at 100%: extra billing, or a stop. |
| `! fb 91% vs 7d 55% · go op` | **The relation** between two badges: the *model* caps before the account does. Switching models buys the week's remaining capacity back; the roomiest other `weekly_scoped` limit in the payload gets named. |
| `+ fb ~22% expires at this mix` | **The mirror of the row above**: the *account* caps first, so the model's remaining pool cannot all be spent. Both counters run from one reset instant, so their live ratio is this week's mix rate: at 81% / 63% the 19 account points left carry ~15 of the model's 37, and the other 22 expire where nothing can reach them. Running the scoped model heavier is what moves the mix. |
| `+ 7d rebased 53%→12%` | Utilization fell *inside* one window instance: burn never runs backwards, so this is a plan change or an out-of-band reset. The projections restart; the ledger still draws the old period. |
| `+ last 5h of the week · 47% unused` | The 7d window resets inside this 5h window: no later window exists to spend the remainder through. |
| `+ 47% unused · spend it` (or `· ~33% expires even at full burn`) | Expiring surplus, feasibility-checked against your learned `pct_per_window`: "spend it" only when full-tilt burn can actually consume it. |
Expand Down
45 changes: 44 additions & 1 deletion docs/api/state-dir.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ read concurrently.

- Contract version: **2** (bump on any breaking layout/field change; this
file is the changelog)
- Synced with: statusline.sh v0.36.0
- Synced with: statusline.sh v0.37.0
- Permissions: the script runs under `umask 077` — files are owner-only.
Caches hold account PII (email, uuid, org names).

Expand Down Expand Up @@ -100,6 +100,49 @@ when idle); a `usage.err` JSON file appears during fetch-failure cooldowns
Treat `fetched_at` as authoritative staleness; anything older than ~1h
means no active session is feeding this account.

#### Two pools, one wall

The account `seven_day` limit and a `weekly_scoped` limit are two counters
over one week: the same reset instant, both cumulative from it. Their live
ratio is therefore this week's MIX RATE, and no history is needed to read
it. Everything in this section is COMPUTED LIVE from the two numbers above
— nothing here is a stored field, and no cache changes when it changes.

```
mix = scope / seven scoped points per 7d point
reachable = round((100 - seven) * scope / seven) of the scoped pool
strand = (100 - scope) - reachable = round(100 * (seven - scope) / seven)
```

`reachable` is what the account's remaining points can still buy in the
scoped pool; `strand` is the rest of that pool, which expires because
nothing can reach it. Round ONCE and take the strand as the remainder: the
two numbers are printed beside the total they must add to.

Live check: `seven = 81`, `scope = 63` gives mix 0.78, reachable 15, strand
22 — against 0.77 mined from the corpus at week level.

The reading speaks only when ALL of these hold:

| Rule | Value | Why |
|------|-------|-----|
| `SCOPE_SAME_WALL_SEC` | 120 | Resets no further apart than this are one wall. The two have always reset together to the microsecond, but Anthropic could split them, and a ratio taken across two different weeks is a fluent lie. |
| `SCOPE_MIX_MIN_7D` | 60 | "Which cap binds" is only a question once the account's own end is in sight, and the ratio is well established by then. |
| `SCOPE_MIX_MIN_SCOPE` | 5 | A model untouched this week belongs to the underuse reading, not to a mix. |
| `SCOPE_STRAND_MIN_PCT` | 10 | Under ten points there is nothing to act on. |

Plus: both pools under 100 (a capped pool is its own message), and the 7d
window past the young guard every projection shares (a day of this
window's own evidence).

Selecting the scoped limit where no running model names one (`report`,
ccpace): prefer `is_active == true`, else the deepest `weekly_scoped`. A
surface that knows which model the session runs matches on that instead.

The coupling in the other direction — what a scoped point costs the
account — is deliberately NOT published. Measured at n=22 it sits in a
0.2-0.5 band: fluent enough to be believed, too noisy to be true.

### profile.cache

Raw `/api/oauth/profile` response: `account.{uuid,email,display_name,…}`,
Expand Down
97 changes: 97 additions & 0 deletions statusline.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2096,6 +2096,20 @@ FORECAST_SCHEMA=2
# that disagree about which hours are spendable disagree about the ration.
REST_MULT_MAX=0.25

# The account 7d pool and the model-scoped weekly pool drain at different
# speeds toward the SAME wall. Both counters are cumulative from one reset
# instant, so their live ratio IS this week's mix — `scope / seven` is
# scoped points per account point — and what the account cannot reach before
# the wall arrives strands in the scoped pool. These are READING RULES, not
# cache fields: nothing is stored, the numbers are computed from usage.cache
# on every frame. They live here beside the cache contract because ccpace
# says the same thing from the same numbers (docs/api/state-dir.md), and two
# tools that disagree about what strands disagree about the week.
SCOPE_STRAND_MIN_PCT=10 # scoped points the mix strands before it is worth a row
SCOPE_MIX_MIN_7D=60 # ...and only once the account's own end is in sight
SCOPE_MIX_MIN_SCOPE=5 # a model untouched this week is the underuse voice's job
SCOPE_SAME_WALL_SEC=120 # resets this close are one wall; further apart, no ratio

# Rotate usage.jsonl when it exceeds the cap. Single .1 backup; the profile
# builder reads .1 + current, so rotation never costs learned history.
rotate_usage_log() {
Expand Down Expand Up @@ -2757,6 +2771,51 @@ run_usage_report() {
awk -v a="${usd24:-0}" -v b="${usd7d:-0}" \
'BEGIN{ if (a > 0 || b > 0) printf "spent: $%.2f in 24h · $%.2f in 7d\n", a, b }'

# What a 7d point buys in the pool beside it. The account pool and the
# model-scoped pool are cumulative from one reset instant, so their live
# ratio is this week's mix rate, and the account reaching its wall first
# is what strands the rest of the model's pool. No history is needed and
# nothing is cached — the reading rules are the SCOPE_* constants, shared
# with ccpace (docs/api/state-dir.md). No session runs here, so there is
# no model to match: the binding scoped limit speaks, else the deepest.
local mix_cache="$CLAUDE_ACCOUNT_DIR/usage.cache" mix_tsv=""
if [ -f "$mix_cache" ]; then
mix_tsv=$(jq -r '. as $u
| [$u.limits[]? | select(.kind == "weekly_scoped"
and (.scope.model.display_name // "") != "")]
| ((map(select(.is_active == true)) | first) // (sort_by(.percent // 0) | last) // empty)
| [(.scope.model.display_name | ascii_downcase), (.percent // 0), (.resets_at // ""),
($u.seven_day.utilization // 0), ($u.seven_day.resets_at // "")] | @tsv' \
"$mix_cache" 2>/dev/null) || true
fi
if [ -n "$mix_tsv" ]; then
local mx_name mx_pct mx_reset mx_su mx_sreset
IFS=$'\t' read -r mx_name mx_pct mx_reset mx_su mx_sreset <<<"$mix_tsv"
local mx_int mx_seven mx_secs mx_ssecs
mx_int=$(printf '%.0f' "$mx_pct" 2>/dev/null || echo 0)
mx_seven=$(printf '%.0f' "$mx_su" 2>/dev/null || echo 0)
mx_secs=$(get_reset_seconds "$mx_reset")
mx_ssecs=$(get_reset_seconds "$mx_sreset")
if [ -n "$mx_secs" ] && [ -n "$mx_ssecs" ] && [ "$mx_ssecs" -gt 0 ] 2>/dev/null \
&& [ "$mx_int" -ge "$SCOPE_MIX_MIN_SCOPE" ] && [ "$mx_int" -lt 100 ] 2>/dev/null \
&& [ "$mx_seven" -ge "$SCOPE_MIX_MIN_7D" ] && [ "$mx_seven" -lt 100 ] 2>/dev/null \
&& [ $((SEVEN_DAY_WINDOW_SECS - mx_ssecs)) -ge "$SEVEN_DAY_YOUNG_SECS" ] 2>/dev/null; then
local mx_wall=$((mx_secs - mx_ssecs))
[ "$mx_wall" -lt 0 ] && mx_wall=$((-mx_wall))
local mx_reach=$(( ((100 - mx_seven) * mx_int + mx_seven / 2) / mx_seven ))
local mx_strand=$((100 - mx_int - mx_reach))
if [ "$mx_wall" -le "$SCOPE_SAME_WALL_SEC" ] \
&& [ "$mx_strand" -ge "$SCOPE_STRAND_MIN_PCT" ]; then
local mx_ab
mx_ab=$(model_scope_abbrev "$mx_name")
printf "%s mix: ~%s %s-pt per 7d-pt this week · 7d's %s%% left carries ~%s%% of %s's %s%%\n" \
"$mx_ab" \
"$(awk -v s="$mx_int" -v v="$mx_seven" 'BEGIN{printf "%.2f", s / v}')" \
"$mx_ab" "$((100 - mx_seven))" "$mx_reach" "$mx_ab" "$((100 - mx_int))"
fi
fi
fi

# The rhythm the walk below is now shaped by. Stated because it changes
# every projection in this report and is the one learned fact the reader
# can check against their own week.
Expand Down Expand Up @@ -4342,6 +4401,44 @@ notice_collect() {
pressure=1
fi

# The other way round: the ACCOUNT caps first, and the model's remaining
# points expire in a pool nothing can reach them from. Both counters run
# from the same reset instant, so their live ratio is this week's mix
# rate — no history, no cache field, just the two numbers line 1 already
# shows. Measured at 81/63: mix 0.78 against 0.77 mined from the corpus.
# It is the mirror of the steering notice above and cannot coexist with
# it — that one needs the model ahead of the account, this one needs the
# account ahead of the model.
#
# Rank 58: above the 5h tail, below the fleet hint, and never above the
# 7d pressure that CAUSES it. The pairing this is built for is the dry
# warning pinned on row 2 with the strand flashing on row 3.
if [ -z "$sc_str" ] && [ -z "$rebase_at" ] \
&& [ -n "$scope_int" ] && [ "$scope_int" -ge "$SCOPE_MIX_MIN_SCOPE" ] 2>/dev/null \
&& [ "$scope_int" -lt 100 ] 2>/dev/null \
&& [ "$seven_int" -ge "$SCOPE_MIX_MIN_7D" ] && [ "$seven_int" -lt 100 ] \
&& [ -n "$scope_secs" ] && [ -n "$seven_secs" ] && [ "$seven_secs" -gt 0 ] 2>/dev/null \
&& [ $((SEVEN_DAY_WINDOW_SECS - seven_secs)) -ge "$SEVEN_DAY_YOUNG_SECS" ] 2>/dev/null; then
# One wall, or no ratio: the two pools have always reset together to
# the microsecond, but Anthropic could split them someday, and a mix
# taken across two different weeks is a fluent lie.
local wall_gap=$((scope_secs - seven_secs))
[ "$wall_gap" -lt 0 ] && wall_gap=$((-wall_gap))
if [ "$wall_gap" -le "$SCOPE_SAME_WALL_SEC" ]; then
# One division, not two. Both numbers land on one line beside the
# total they must add to, so the strand is the REMAINDER of what
# the mix reaches, never its own rounding.
local mix_left=$((100 - scope_int))
local mix_reach=$(( ((100 - seven_int) * scope_int + seven_int / 2) / seven_int ))
local mix_strand=$((mix_left - mix_reach))
if [ "$mix_strand" -ge "$SCOPE_STRAND_MIN_PCT" ] 2>/dev/null; then
notice_add 58 '+' fb "fb.strand.$((mix_strand / 5))" "~${mix_strand}%" \
"${scope_ab} ~${mix_strand}% expires at this mix" \
"7d caps before ${scope_ab}: this mix reaches ~${mix_reach}% of its ${mix_left}% left · run ${scope_ab} heavier to extract more"
fi
fi
fi

# Expiring surplus — use it or lose it. Inside the last day of the 7d
# window a big green remainder is forfeiture, not headroom. The tail is
# feasibility-checked against the learned pct_per_window ratio: burning
Expand Down
4 changes: 4 additions & 0 deletions t/helpers.bash
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ MULT_GLYPH="✕"
LEDGER_BASE_GLYPH="▁"
FORECAST_SCHEMA=2
REST_MULT_MAX=0.25
SCOPE_STRAND_MIN_PCT=10
SCOPE_MIX_MIN_7D=60
SCOPE_MIX_MIN_SCOPE=5
SCOPE_SAME_WALL_SEC=120
ADVISOR_FLEET_FRESH_SECS=3600
ADVISOR_FLEET_FREE_PCT=40
ADVISOR_EXPIRY_HORIZON_SECS=86400
Expand Down
Loading
Loading