claude-usage-limits 1.3.0 → 1.5.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.
|
|
4
|
+
"version": "1.5.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,26 @@ node skills/usage-limits/scripts/usage.js
|
|
|
167
167
|
node skills/usage-limits/scripts/usage.js --json
|
|
168
168
|
```
|
|
169
169
|
|
|
170
|
+
## When another Claude is working too
|
|
171
|
+
|
|
172
|
+
Two Claude Code windows share one limit, so headroom measured in turns is
|
|
173
|
+
optimistic while another session is also spending. It watches for that:
|
|
174
|
+
|
|
175
|
+
```
|
|
176
|
+
Sharing 2 sessions have spent in the last 15m, splitting this budget 75% / 25%
|
|
177
|
+
The turns above are the whole window, not your slice of it.
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
and the before-prompt line says how many of those turns are actually yours:
|
|
181
|
+
|
|
182
|
+
```
|
|
183
|
+
about 38 turns of headroom (2 sessions active, roughly 10 of them yours)
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
The split comes from measured spend rather than an assumption that everyone is
|
|
187
|
+
working equally hard, because they usually are not. A session that has gone
|
|
188
|
+
quiet for a quarter of an hour is not counted as competing.
|
|
189
|
+
|
|
170
190
|
## Which limit it watches
|
|
171
191
|
|
|
172
192
|
Two windows run at once and the 5-hour one is usually what actually stops you,
|
|
@@ -428,6 +448,13 @@ Good enough to plan with, not a bill. The honest caveats:
|
|
|
428
448
|
- A model released after this table was written is priced at its family's
|
|
429
449
|
average rate, and the report marks those rows with an asterisk rather than
|
|
430
450
|
passing the guess off as a published price.
|
|
451
|
+
- Time of day is not modelled. Anthropic used to shrink the five-hour limit
|
|
452
|
+
during peak hours, but removed that on 6 May 2026 for Pro and Max while
|
|
453
|
+
doubling the limits. If demand-based limits ever return, the numbers here
|
|
454
|
+
follow automatically, because they are calibrated from what your traffic did
|
|
455
|
+
to the meter rather than from an assumption about the clock.
|
|
456
|
+
- The turn cost behind "turns of headroom" is a median over at least five
|
|
457
|
+
turns, so one compaction cannot define your pace.
|
|
431
458
|
- The cache only refreshes when Claude Code talks to the API, so after a gap it
|
|
432
459
|
can be hours old and its 5-hour window long since rolled over. Dropping that
|
|
433
460
|
window would hide the limit that actually stops short work, so it gets rebuilt
|
|
@@ -464,7 +491,7 @@ test/ node --test, no dependencies
|
|
|
464
491
|
node --test
|
|
465
492
|
```
|
|
466
493
|
|
|
467
|
-
|
|
494
|
+
157 tests over the pricing, the window arithmetic, plan and credit detection,
|
|
468
495
|
the status line, the before-prompt line, job forecasting, per-project
|
|
469
496
|
attribution, the CLI, packaging, and the settings save/restore.
|
|
470
497
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-usage-limits",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.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.
|
|
@@ -215,7 +215,14 @@ function briefText(parts) {
|
|
|
215
215
|
const described = describeWindow(parts.binding);
|
|
216
216
|
if (described) bound.push(described + (parts.binding.stale ? '' : ' used'));
|
|
217
217
|
if (Number.isFinite(parts.turnsLeft)) {
|
|
218
|
-
|
|
218
|
+
// Another session spending the same budget means fewer of those turns are
|
|
219
|
+
// yours, so say both numbers rather than the flattering one.
|
|
220
|
+
const shared =
|
|
221
|
+
parts.sessions > 1 && Number.isFinite(parts.yourTurnsLeft)
|
|
222
|
+
? ' (' + parts.sessions + ' sessions active, roughly ' + parts.yourTurnsLeft +
|
|
223
|
+
' of them yours)'
|
|
224
|
+
: '';
|
|
225
|
+
bound.push('about ' + parts.turnsLeft + ' turns of headroom' + shared);
|
|
219
226
|
}
|
|
220
227
|
if (parts.resetsIn) bound.push('resets in ' + parts.resetsIn);
|
|
221
228
|
|
|
@@ -282,6 +289,7 @@ async function run(now, hookInput) {
|
|
|
282
289
|
turnsLeft: binding && Number.isFinite(binding.turnsLeft) ? binding.turnsLeft : null,
|
|
283
290
|
session: sessionSpend(events, sessionId),
|
|
284
291
|
othersSummary: summariseOthers(windows, binding && binding.key),
|
|
292
|
+
sessions: usage.activeSessions(events, now, usage.CONCURRENT_WINDOW_MS),
|
|
285
293
|
binding: binding
|
|
286
294
|
? {
|
|
287
295
|
key: binding.key,
|
|
@@ -300,7 +308,13 @@ async function run(now, hookInput) {
|
|
|
300
308
|
}
|
|
301
309
|
|
|
302
310
|
const binding = view.binding;
|
|
311
|
+
const sessions = view.sessions || [];
|
|
312
|
+
const share = usage.shareOf(sessions, sessionId);
|
|
303
313
|
return briefText({
|
|
314
|
+
sessions: sessions.length,
|
|
315
|
+
yourTurnsLeft: Number.isFinite(view.turnsLeft)
|
|
316
|
+
? Math.max(1, Math.round(view.turnsLeft * share))
|
|
317
|
+
: null,
|
|
304
318
|
binding,
|
|
305
319
|
othersSummary: view.othersSummary,
|
|
306
320
|
turnsLeft: view.turnsLeft,
|
|
@@ -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) {
|
|
@@ -469,6 +499,39 @@ function forecastWindow(window, turns, rates) {
|
|
|
469
499
|
};
|
|
470
500
|
}
|
|
471
501
|
|
|
502
|
+
const CONCURRENT_WINDOW_MS = 15 * MINUTE;
|
|
503
|
+
|
|
504
|
+
// Sessions that have spent something recently. Two Claude Code windows share
|
|
505
|
+
// one limit, so headroom measured in "turns" is optimistic when another one is
|
|
506
|
+
// also working: the budget drains while you are not the one spending it.
|
|
507
|
+
function activeSessions(events, now, windowMs) {
|
|
508
|
+
const since = now - (Number.isFinite(windowMs) ? windowMs : CONCURRENT_WINDOW_MS);
|
|
509
|
+
const bySession = new Map();
|
|
510
|
+
|
|
511
|
+
for (const event of events) {
|
|
512
|
+
if (event.at < since || event.at > now) continue;
|
|
513
|
+
const id = event.sessionId || 'unknown';
|
|
514
|
+
if (!bySession.has(id)) bySession.set(id, { sessionId: id, turns: 0, cost: 0 });
|
|
515
|
+
const row = bySession.get(id);
|
|
516
|
+
row.turns += 1;
|
|
517
|
+
row.cost += event.cost;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const rows = [...bySession.values()].sort((a, b) => b.cost - a.cost);
|
|
521
|
+
const total = rows.reduce((sum, row) => sum + row.cost, 0);
|
|
522
|
+
for (const row of rows) row.share = total > 0 ? row.cost / total : 0;
|
|
523
|
+
return rows;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// The slice of the shared budget this session is actually getting. With
|
|
527
|
+
// another session spending half of it, only half those turns are yours.
|
|
528
|
+
function shareOf(sessions, sessionId) {
|
|
529
|
+
if (!sessions || sessions.length < 2) return 1;
|
|
530
|
+
const mine = sessions.find((row) => row.sessionId === sessionId);
|
|
531
|
+
if (!mine) return 1 / sessions.length;
|
|
532
|
+
return mine.share > 0 ? mine.share : 1 / sessions.length;
|
|
533
|
+
}
|
|
534
|
+
|
|
472
535
|
// Everything the report needs about one limit window.
|
|
473
536
|
function buildWindow(spec, snapshot, events, now, options) {
|
|
474
537
|
const extra = options || {};
|
|
@@ -520,19 +583,28 @@ function buildWindow(spec, snapshot, events, now, options) {
|
|
|
520
583
|
};
|
|
521
584
|
|
|
522
585
|
// Calibrate against this account: how many dollars of measured traffic
|
|
523
|
-
// moved the meter one point.
|
|
524
|
-
|
|
525
|
-
|
|
586
|
+
// moved the meter one point. A rebuilt window hands its own figure in,
|
|
587
|
+
// because rounding to 0% would otherwise leave it unpriced and drop it out
|
|
588
|
+
// of the binding choice just after a reset.
|
|
589
|
+
const derived = percent !== null && percent > 0 && spent.cost > 0 ? spent.cost / percent : null;
|
|
590
|
+
const priced =
|
|
591
|
+
derived !== null
|
|
592
|
+
? derived
|
|
593
|
+
: Number.isFinite(extra.usdPerPercent) && extra.usdPerPercent > 0
|
|
594
|
+
? extra.usdPerPercent
|
|
595
|
+
: null;
|
|
596
|
+
|
|
597
|
+
if (percent !== null && priced !== null) {
|
|
598
|
+
window.usdPerPercent = priced;
|
|
526
599
|
window.remainingUSD = window.usdPerPercent * window.percentLeft;
|
|
527
600
|
// The API reports whole numbers, so a low reading is a wide bracket.
|
|
528
601
|
window.coarse = percent < 5;
|
|
529
602
|
|
|
530
|
-
const perTurn =
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
window.percentPerTurn = perTurn / window.usdPerPercent;
|
|
603
|
+
const perTurn = typicalTurnCost(recentEvents, inWindow, events, MIN_PACE_SAMPLE);
|
|
604
|
+
window.percentPerTurn = perTurn === null ? null : perTurn / window.usdPerPercent;
|
|
605
|
+
window.typicalTurnUSD = perTurn;
|
|
534
606
|
window.percentPerHour = window.recentUSDPerHour / window.usdPerPercent;
|
|
535
|
-
if (window.percentPerTurn > 0) {
|
|
607
|
+
if (window.percentPerTurn !== null && window.percentPerTurn > 0) {
|
|
536
608
|
window.turnsLeft = Math.floor(window.percentLeft / window.percentPerTurn);
|
|
537
609
|
}
|
|
538
610
|
if (window.percentPerHour > 0) {
|
|
@@ -744,7 +816,11 @@ function buildWindows(utilization, events, now) {
|
|
|
744
816
|
{ utilization: rebuilt.percentUsed, resets_at: null },
|
|
745
817
|
events,
|
|
746
818
|
now,
|
|
747
|
-
{
|
|
819
|
+
{
|
|
820
|
+
estimated: true,
|
|
821
|
+
windowStart: rebuilt.windowStart,
|
|
822
|
+
usdPerPercent: rebuilt.usdPerPercent,
|
|
823
|
+
}
|
|
748
824
|
);
|
|
749
825
|
}).filter(Boolean);
|
|
750
826
|
}
|
|
@@ -770,6 +846,7 @@ async function report(now) {
|
|
|
770
846
|
windows,
|
|
771
847
|
binding,
|
|
772
848
|
credits: creditsFrom(base.utilization),
|
|
849
|
+
sessions: activeSessions(events, now, CONCURRENT_WINDOW_MS),
|
|
773
850
|
rates: costPercentiles(recentEvents.length >= 5 ? recentEvents : scoped),
|
|
774
851
|
resumeAt: binding ? binding.resetsAt : null,
|
|
775
852
|
models: byModel(scoped),
|
|
@@ -972,6 +1049,17 @@ function render(data) {
|
|
|
972
1049
|
lines.push('');
|
|
973
1050
|
}
|
|
974
1051
|
|
|
1052
|
+
if (data.sessions && data.sessions.length > 1) {
|
|
1053
|
+
const split = data.sessions.map((row) => Math.round(row.share * 100) + '%').join(' / ');
|
|
1054
|
+
lines.push(
|
|
1055
|
+
' Sharing ' + data.sessions.length + ' sessions have spent in the last 15m, ' +
|
|
1056
|
+
'splitting this budget ' + split
|
|
1057
|
+
);
|
|
1058
|
+
lines.push(
|
|
1059
|
+
' The turns above are the whole window, not your slice of it.'
|
|
1060
|
+
);
|
|
1061
|
+
}
|
|
1062
|
+
|
|
975
1063
|
if (data.recent.turns) {
|
|
976
1064
|
lines.push(
|
|
977
1065
|
' Recent pace ' + data.recent.turns + ' turns in the last hour, ' +
|
|
@@ -1151,6 +1239,11 @@ module.exports = {
|
|
|
1151
1239
|
buildWindows,
|
|
1152
1240
|
bindingWindow,
|
|
1153
1241
|
dominantEffort,
|
|
1242
|
+
typicalTurnCost,
|
|
1243
|
+
activeSessions,
|
|
1244
|
+
shareOf,
|
|
1245
|
+
CONCURRENT_WINDOW_MS,
|
|
1246
|
+
MIN_PACE_SAMPLE,
|
|
1154
1247
|
formatDuration,
|
|
1155
1248
|
formatUSD,
|
|
1156
1249
|
formatCount,
|