claude-usage-limits 1.2.0 → 1.4.0

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,7 +1,7 @@
1
1
  {
2
2
  "name": "usage-limits",
3
3
  "displayName": "Usage Limits",
4
- "version": "1.2.0",
4
+ "version": "1.4.0",
5
5
  "description": "Puts your remaining Claude Code usage limit into Claude's context before every prompt, so it opens with what fits in the budget instead of starting work that gets cut off. Reports headroom as turns rather than percentages, prices a job before you start it, and detects your plan tier.",
6
6
  "author": {
7
7
  "name": "Ridelink",
package/README.md CHANGED
@@ -167,6 +167,21 @@ node skills/usage-limits/scripts/usage.js
167
167
  node skills/usage-limits/scripts/usage.js --json
168
168
  ```
169
169
 
170
+ ## Which limit it watches
171
+
172
+ Two windows run at once and the 5-hour one is usually what actually stops you,
173
+ so it gets picked whenever it is tighter, and it wins a tie against the weekly
174
+ window because the shorter window is the one hit first in practice.
175
+
176
+ It is not forced, though. When the weekly window is genuinely the wall, at 99
177
+ percent with minutes left, that is what gets reported. Forcing the 5-hour there
178
+ would hide the limit about to stop the work, which is the same failure as
179
+ ignoring it.
180
+
181
+ A window with no recent spend to measure is ranked by how full it is rather
182
+ than being skipped, so a 5-hour window sitting at 95 percent is never passed
183
+ over just because nothing has gone through it in the last few minutes.
184
+
170
185
  ## When you keep typing
171
186
 
172
187
  Every message sent while work is already running starts another turn, and each
@@ -296,6 +311,7 @@ minute, so it costs about 400ms cold and 120ms warm.
296
311
  | `USAGE_LIMITS_FLOOR` | 40 | Below this, pace is ignored. |
297
312
  | `USAGE_LIMITS_AHEAD` | 15 | Points ahead of pace that count as burning fast. |
298
313
  | `USAGE_LIMITS_CACHE` | 60 | Seconds the measured half stays good for. |
314
+ | `USAGE_LIMITS_FEW_TURNS` | 20 | Turn count at or below which the budget counts as tight. |
299
315
 
300
316
  ## What would this job cost
301
317
 
@@ -412,6 +428,13 @@ Good enough to plan with, not a bill. The honest caveats:
412
428
  - A model released after this table was written is priced at its family's
413
429
  average rate, and the report marks those rows with an asterisk rather than
414
430
  passing the guess off as a published price.
431
+ - Time of day is not modelled. Anthropic used to shrink the five-hour limit
432
+ during peak hours, but removed that on 6 May 2026 for Pro and Max while
433
+ doubling the limits. If demand-based limits ever return, the numbers here
434
+ follow automatically, because they are calibrated from what your traffic did
435
+ to the meter rather than from an assumption about the clock.
436
+ - The turn cost behind "turns of headroom" is a median over at least five
437
+ turns, so one compaction cannot define your pace.
415
438
  - The cache only refreshes when Claude Code talks to the API, so after a gap it
416
439
  can be hours old and its 5-hour window long since rolled over. Dropping that
417
440
  window would hide the limit that actually stops short work, so it gets rebuilt
@@ -448,7 +471,7 @@ test/ node --test, no dependencies
448
471
  node --test
449
472
  ```
450
473
 
451
- 130 tests over the pricing, the window arithmetic, plan and credit detection,
474
+ 148 tests over the pricing, the window arithmetic, plan and credit detection,
452
475
  the status line, the before-prompt line, job forecasting, per-project
453
476
  attribution, the CLI, packaging, and the settings save/restore.
454
477
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-usage-limits",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "Puts your remaining Claude Code usage limit into Claude's context before every prompt, so it opens with what fits in the budget instead of starting work that gets cut off. Reports headroom as turns rather than percentages, prices a job before you start it, and detects your plan tier.",
5
5
  "keywords": [
6
6
  "claude",
@@ -124,6 +124,26 @@ window that no longer exists. Those are marked stale, excluded from the
124
124
  binding choice, and never used for projections, because treating one as
125
125
  current would report an empty budget at the exact moment the budget came back.
126
126
 
127
+ **Time of day is not modelled, and does not need to be.** Anthropic used to
128
+ shrink the Claude Code five-hour limit during peak hours, so the same work cost
129
+ more of it in the afternoon. That ended on 6 May 2026, when the five-hour
130
+ limits were doubled and, in Anthropic's words, they removed "the peak hours
131
+ limit reduction on Claude Code for Pro and Max accounts". So there is no
132
+ peak-hour penalty to model today.
133
+
134
+ If one ever returns, nothing here needs changing. Every figure is calibrated
135
+ from what your own traffic actually did to the meter, so if a point of budget
136
+ starts costing more at four in the afternoon, the measured dollars-per-point
137
+ moves with it and the headroom follows. That is the advantage of measuring
138
+ rather than assuming: the tool does not need to know why a point got dearer.
139
+
140
+ **One turn is not a pace.** The turn cost behind "turns of headroom" is the
141
+ median of a sample, not the mean, and never from fewer than five turns. A
142
+ compaction or a large file read can cost ten times an ordinary turn, and one of
143
+ those landing in a thin sample once put a window that was 13 percent full at
144
+ nine turns remaining. Thin samples widen to the whole window, and then to
145
+ everything on record.
146
+
127
147
  **Pace is not a promise.** Turns left assumes the next turns look like the last
128
148
  hour's. A debugging spiral or a large file read breaks that assumption
129
149
  immediately. Re-run the report if the shape of the work changes.
@@ -28,6 +28,8 @@ const DEFAULTS = {
28
28
  // How long the measured part stays good for. Prompts often arrive in
29
29
  // bursts, and a transcript scan per prompt would be wasteful.
30
30
  cacheSeconds: 60,
31
+ // Few enough turns that the count itself is the warning.
32
+ fewTurns: 20,
31
33
  };
32
34
 
33
35
  function configDir() {
@@ -104,6 +106,7 @@ function settings() {
104
106
  floor: number(env.USAGE_LIMITS_FLOOR, DEFAULTS.floor),
105
107
  ahead: number(env.USAGE_LIMITS_AHEAD, DEFAULTS.ahead),
106
108
  cacheSeconds: number(env.USAGE_LIMITS_CACHE, DEFAULTS.cacheSeconds),
109
+ fewTurns: number(env.USAGE_LIMITS_FEW_TURNS, DEFAULTS.fewTurns),
107
110
  };
108
111
  }
109
112
 
@@ -117,16 +120,32 @@ function aheadOfPace(window, now) {
117
120
  }
118
121
 
119
122
  // Not whether to speak, which is always, but how hard to lean on it.
120
- function pressure(window, now, config) {
123
+ // Not whether to speak, which is always, but how hard to lean on it.
124
+ function pressure(window, now, config, turnsLeft) {
121
125
  if (!window || window.percentUsed === null || window.stale) return 'unknown';
122
- if (window.verdict === 'exhausted') return 'gone';
126
+ if (window.verdict === 'exhausted' || window.percentUsed >= 100) return 'gone';
123
127
  if (window.verdict === 'runs-out') return 'tight';
124
- if (window.percentUsed >= config.near) return 'tight';
125
128
 
126
- const lead = aheadOfPace(window, now);
127
- if (window.percentUsed >= config.floor && lead !== null && lead >= config.ahead) {
129
+ // A rebuilt figure only counts this machine, so it reads low. React to it
130
+ // sooner than to a figure the API actually reported.
131
+ const near = window.estimated ? Math.min(config.near, 70) : config.near;
132
+ if (window.percentUsed >= near) return 'tight';
133
+
134
+ // Turns are the number the work is planned in, so a short count is tight
135
+ // whatever the percentage says.
136
+ if (Number.isFinite(turnsLeft) && turnsLeft <= config.fewTurns) {
128
137
  return 'tight';
129
138
  }
139
+
140
+ // Pace only means something for a window with a real start. A rebuilt one
141
+ // is anchored at now minus its span, so it is always "fully elapsed" and
142
+ // the comparison can never fire.
143
+ if (!window.estimated) {
144
+ const lead = aheadOfPace(window, now);
145
+ if (window.percentUsed >= config.floor && lead !== null && lead >= config.ahead) {
146
+ return 'tight';
147
+ }
148
+ }
130
149
  return 'roomy';
131
150
  }
132
151
 
@@ -292,7 +311,7 @@ async function run(now, hookInput) {
292
311
  session: view.session,
293
312
  rebuilt: Boolean(binding && binding.estimated),
294
313
  snapshotAge: usage.formatDuration(base.snapshotAgeMs),
295
- pressure: pressure(binding, now, config),
314
+ pressure: pressure(binding, now, config, view.turnsLeft),
296
315
  });
297
316
  }
298
317
 
@@ -355,6 +355,36 @@ function byModel(events) {
355
355
  return list;
356
356
  }
357
357
 
358
+ const MIN_PACE_SAMPLE = 5;
359
+
360
+ // The middle turn, not the mean, and never from a sample so small that one
361
+ // turn defines the pace. A compaction or a big file read can cost ten times an
362
+ // ordinary turn, and treating that as "the" turn cost sends the headroom
363
+ // estimate swinging: a single $7 turn once put a 13% full window at nine turns
364
+ // left. Too few recent turns to be sure, so widen to the whole window.
365
+ function typicalTurnCost(recentEvents, windowEvents, allEvents, minSample) {
366
+ const floor = Number.isFinite(minSample) ? minSample : MIN_PACE_SAMPLE;
367
+
368
+ // What a turn costs is a fact about how you work, not about which budget it
369
+ // is being measured against, so a thin window borrows from a wider sample
370
+ // rather than inventing a figure from two turns.
371
+ const tiers = [recentEvents, windowEvents, allEvents];
372
+ let pool = [];
373
+ for (const tier of tiers) {
374
+ if (tier && tier.length >= floor) {
375
+ pool = tier;
376
+ break;
377
+ }
378
+ if (tier && tier.length > pool.length) pool = tier;
379
+ }
380
+ const costs = pool
381
+ .map((event) => event.cost)
382
+ .filter((cost) => Number.isFinite(cost) && cost > 0)
383
+ .sort((a, b) => a - b);
384
+ if (!costs.length) return null;
385
+ return costs[Math.floor(costs.length / 2)];
386
+ }
387
+
358
388
  function dominantEffort(events) {
359
389
  const counts = new Map();
360
390
  for (const event of events) {
@@ -527,12 +557,11 @@ function buildWindow(spec, snapshot, events, now, options) {
527
557
  // The API reports whole numbers, so a low reading is a wide bracket.
528
558
  window.coarse = percent < 5;
529
559
 
530
- const perTurn = recent.turns
531
- ? recent.cost / recent.turns
532
- : spent.cost / Math.max(spent.turns, 1);
533
- window.percentPerTurn = perTurn / window.usdPerPercent;
560
+ const perTurn = typicalTurnCost(recentEvents, inWindow, events, MIN_PACE_SAMPLE);
561
+ window.percentPerTurn = perTurn === null ? null : perTurn / window.usdPerPercent;
562
+ window.typicalTurnUSD = perTurn;
534
563
  window.percentPerHour = window.recentUSDPerHour / window.usdPerPercent;
535
- if (window.percentPerTurn > 0) {
564
+ if (window.percentPerTurn !== null && window.percentPerTurn > 0) {
536
565
  window.turnsLeft = Math.floor(window.percentLeft / window.percentPerTurn);
537
566
  }
538
567
  if (window.percentPerHour > 0) {
@@ -571,11 +600,27 @@ function bindingWindow(windows) {
571
600
  const live = fresh.length ? fresh : known;
572
601
  if (!live.length) return null;
573
602
 
574
- const measured = live.filter((w) => w.headroomMs !== null);
575
- if (measured.length) {
576
- return measured.reduce((worst, w) => (w.headroomMs < worst.headroomMs ? w : worst));
577
- }
578
- return live.reduce((worst, w) => (w.percentUsed > worst.percentUsed ? w : worst));
603
+ // How soon this window stops the work. A window with no pace estimate is
604
+ // ranked by how full it is instead, because a nearly full window must never
605
+ // be passed over merely because nothing has been spent in it lately.
606
+ const soonest = (w) => {
607
+ if (Number.isFinite(w.headroomMs)) return w.headroomMs;
608
+ return w.percentUsed >= 90 ? 0 : Infinity;
609
+ };
610
+
611
+ return live.reduce((best, w) => {
612
+ const mine = soonest(w);
613
+ const theirs = soonest(best);
614
+ if (mine !== theirs) return mine < theirs ? w : best;
615
+
616
+ // Equally urgent: the shorter window is the one hit first in practice, so
617
+ // the 5-hour limit wins a tie against the weekly one.
618
+ const myspan = Number.isFinite(w.spanMs) ? w.spanMs : Infinity;
619
+ const theirspan = Number.isFinite(best.spanMs) ? best.spanMs : Infinity;
620
+ if (myspan !== theirspan) return myspan < theirspan ? w : best;
621
+
622
+ return w.percentUsed > best.percentUsed ? w : best;
623
+ });
579
624
  }
580
625
 
581
626
  function formatDuration(ms) {
@@ -1135,6 +1180,8 @@ module.exports = {
1135
1180
  buildWindows,
1136
1181
  bindingWindow,
1137
1182
  dominantEffort,
1183
+ typicalTurnCost,
1184
+ MIN_PACE_SAMPLE,
1138
1185
  formatDuration,
1139
1186
  formatUSD,
1140
1187
  formatCount,