claude-usage-limits 1.4.0 → 1.5.1
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.1",
|
|
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,
|
|
@@ -446,6 +466,12 @@ Good enough to plan with, not a bill. The honest caveats:
|
|
|
446
466
|
- A rebuilt figure only counts what this machine did. If you also worked on
|
|
447
467
|
another device it reads low, which is the dangerous direction, so treat it as
|
|
448
468
|
a floor until you refresh.
|
|
469
|
+
- If a rebuild comes out above a full window, it is refused rather than capped.
|
|
470
|
+
Local transcripts only see this machine, so a window mostly spent elsewhere
|
|
471
|
+
makes a point look far too cheap and any live spend divides to hundreds of
|
|
472
|
+
percent. Capping that at 100 would tell someone sitting at half their budget
|
|
473
|
+
that it was gone. The window is reported as unknown instead, with a nudge to
|
|
474
|
+
run `/usage`.
|
|
449
475
|
|
|
450
476
|
[how-it-works.md](skills/usage-limits/references/how-it-works.md) has the field
|
|
451
477
|
names, the formulas, and the rest of it.
|
|
@@ -471,7 +497,7 @@ test/ node --test, no dependencies
|
|
|
471
497
|
node --test
|
|
472
498
|
```
|
|
473
499
|
|
|
474
|
-
|
|
500
|
+
164 tests over the pricing, the window arithmetic, plan and credit detection,
|
|
475
501
|
the status line, the before-prompt line, job forecasting, per-project
|
|
476
502
|
attribution, the CLI, packaging, and the settings save/restore.
|
|
477
503
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-usage-limits",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.1",
|
|
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",
|
|
@@ -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
|
|
|
@@ -230,6 +237,14 @@ function briefText(parts) {
|
|
|
230
237
|
'That figure was rebuilt from local history because the ' +
|
|
231
238
|
'snapshot is ' + parts.snapshotAge + ' old; run /usage to refresh it.'
|
|
232
239
|
);
|
|
240
|
+
} else if (parts.staleWindows) {
|
|
241
|
+
// Do not quietly carry on with a window we know is wrong and could not
|
|
242
|
+
// rebuild. Say it is unknown and point at the one command that fixes it.
|
|
243
|
+
sentences.push(
|
|
244
|
+
'A window is past its reset and could not be rebuilt from local history, ' +
|
|
245
|
+
'so its reading is unknown rather than current; the snapshot is ' +
|
|
246
|
+
parts.snapshotAge + ' old, so run /usage before trusting the rest.'
|
|
247
|
+
);
|
|
233
248
|
}
|
|
234
249
|
if (parts.othersSummary) sentences.push('Other windows: ' + parts.othersSummary + '.');
|
|
235
250
|
if (parts.session) {
|
|
@@ -282,6 +297,8 @@ async function run(now, hookInput) {
|
|
|
282
297
|
turnsLeft: binding && Number.isFinite(binding.turnsLeft) ? binding.turnsLeft : null,
|
|
283
298
|
session: sessionSpend(events, sessionId),
|
|
284
299
|
othersSummary: summariseOthers(windows, binding && binding.key),
|
|
300
|
+
sessions: usage.activeSessions(events, now, usage.CONCURRENT_WINDOW_MS),
|
|
301
|
+
staleWindows: windows.filter((w) => w.stale).length,
|
|
285
302
|
binding: binding
|
|
286
303
|
? {
|
|
287
304
|
key: binding.key,
|
|
@@ -300,7 +317,13 @@ async function run(now, hookInput) {
|
|
|
300
317
|
}
|
|
301
318
|
|
|
302
319
|
const binding = view.binding;
|
|
320
|
+
const sessions = view.sessions || [];
|
|
321
|
+
const share = usage.shareOf(sessions, sessionId);
|
|
303
322
|
return briefText({
|
|
323
|
+
sessions: sessions.length,
|
|
324
|
+
yourTurnsLeft: Number.isFinite(view.turnsLeft)
|
|
325
|
+
? Math.max(1, Math.round(view.turnsLeft * share))
|
|
326
|
+
: null,
|
|
304
327
|
binding,
|
|
305
328
|
othersSummary: view.othersSummary,
|
|
306
329
|
turnsLeft: view.turnsLeft,
|
|
@@ -310,6 +333,7 @@ async function run(now, hookInput) {
|
|
|
310
333
|
: null,
|
|
311
334
|
session: view.session,
|
|
312
335
|
rebuilt: Boolean(binding && binding.estimated),
|
|
336
|
+
staleWindows: view.staleWindows || 0,
|
|
313
337
|
snapshotAge: usage.formatDuration(base.snapshotAgeMs),
|
|
314
338
|
pressure: pressure(binding, now, config, view.turnsLeft),
|
|
315
339
|
});
|
|
@@ -499,6 +499,39 @@ function forecastWindow(window, turns, rates) {
|
|
|
499
499
|
};
|
|
500
500
|
}
|
|
501
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
|
+
|
|
502
535
|
// Everything the report needs about one limit window.
|
|
503
536
|
function buildWindow(spec, snapshot, events, now, options) {
|
|
504
537
|
const extra = options || {};
|
|
@@ -550,9 +583,19 @@ function buildWindow(spec, snapshot, events, now, options) {
|
|
|
550
583
|
};
|
|
551
584
|
|
|
552
585
|
// Calibrate against this account: how many dollars of measured traffic
|
|
553
|
-
// moved the meter one point.
|
|
554
|
-
|
|
555
|
-
|
|
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;
|
|
556
599
|
window.remainingUSD = window.usdPerPercent * window.percentLeft;
|
|
557
600
|
// The API reports whole numbers, so a low reading is a wide bracket.
|
|
558
601
|
window.coarse = percent < 5;
|
|
@@ -729,6 +772,9 @@ function collect(now) {
|
|
|
729
772
|
// was spent inside the window it describes equalled its percentage. That
|
|
730
773
|
// dollars-per-point figure is a property of the plan, not of the moment, so it
|
|
731
774
|
// still prices the window running now.
|
|
775
|
+
// Anything above this and the calibration, not the budget, is what is full.
|
|
776
|
+
const SATURATION_LIMIT = 105;
|
|
777
|
+
|
|
732
778
|
function reconstructWindow(spec, snapshot, events, now) {
|
|
733
779
|
if (!snapshot || typeof snapshot.utilization !== 'number') return null;
|
|
734
780
|
if (snapshot.utilization <= 0) return null;
|
|
@@ -743,9 +789,18 @@ function reconstructWindow(spec, snapshot, events, now) {
|
|
|
743
789
|
const usdPerPercent = past.cost / snapshot.utilization;
|
|
744
790
|
const liveStart = now - spec.span;
|
|
745
791
|
const live = totals(events.filter((e) => e.at >= liveStart && e.at <= now));
|
|
792
|
+
const raw = live.cost / usdPerPercent;
|
|
793
|
+
|
|
794
|
+
// A rebuild that overflows the window is not a full window, it is a broken
|
|
795
|
+
// calibration. Local transcripts only see this machine, so if the closed
|
|
796
|
+
// window was mostly spent elsewhere its price per point comes out far too
|
|
797
|
+
// small and any live spend divides to hundreds of percent. Capping that at
|
|
798
|
+
// 100 would report a full budget to someone sitting at half, which is worse
|
|
799
|
+
// than admitting the reading cannot be rebuilt.
|
|
800
|
+
if (raw > SATURATION_LIMIT) return null;
|
|
746
801
|
|
|
747
802
|
return {
|
|
748
|
-
percentUsed: Math.min(100, Math.round(
|
|
803
|
+
percentUsed: Math.min(100, Math.round(raw)),
|
|
749
804
|
usdPerPercent,
|
|
750
805
|
spentUSD: live.cost,
|
|
751
806
|
turns: live.turns,
|
|
@@ -773,7 +828,11 @@ function buildWindows(utilization, events, now) {
|
|
|
773
828
|
{ utilization: rebuilt.percentUsed, resets_at: null },
|
|
774
829
|
events,
|
|
775
830
|
now,
|
|
776
|
-
{
|
|
831
|
+
{
|
|
832
|
+
estimated: true,
|
|
833
|
+
windowStart: rebuilt.windowStart,
|
|
834
|
+
usdPerPercent: rebuilt.usdPerPercent,
|
|
835
|
+
}
|
|
777
836
|
);
|
|
778
837
|
}).filter(Boolean);
|
|
779
838
|
}
|
|
@@ -799,6 +858,7 @@ async function report(now) {
|
|
|
799
858
|
windows,
|
|
800
859
|
binding,
|
|
801
860
|
credits: creditsFrom(base.utilization),
|
|
861
|
+
sessions: activeSessions(events, now, CONCURRENT_WINDOW_MS),
|
|
802
862
|
rates: costPercentiles(recentEvents.length >= 5 ? recentEvents : scoped),
|
|
803
863
|
resumeAt: binding ? binding.resetsAt : null,
|
|
804
864
|
models: byModel(scoped),
|
|
@@ -1001,6 +1061,17 @@ function render(data) {
|
|
|
1001
1061
|
lines.push('');
|
|
1002
1062
|
}
|
|
1003
1063
|
|
|
1064
|
+
if (data.sessions && data.sessions.length > 1) {
|
|
1065
|
+
const split = data.sessions.map((row) => Math.round(row.share * 100) + '%').join(' / ');
|
|
1066
|
+
lines.push(
|
|
1067
|
+
' Sharing ' + data.sessions.length + ' sessions have spent in the last 15m, ' +
|
|
1068
|
+
'splitting this budget ' + split
|
|
1069
|
+
);
|
|
1070
|
+
lines.push(
|
|
1071
|
+
' The turns above are the whole window, not your slice of it.'
|
|
1072
|
+
);
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1004
1075
|
if (data.recent.turns) {
|
|
1005
1076
|
lines.push(
|
|
1006
1077
|
' Recent pace ' + data.recent.turns + ' turns in the last hour, ' +
|
|
@@ -1177,10 +1248,14 @@ module.exports = {
|
|
|
1177
1248
|
readEvents,
|
|
1178
1249
|
buildWindow,
|
|
1179
1250
|
reconstructWindow,
|
|
1251
|
+
SATURATION_LIMIT,
|
|
1180
1252
|
buildWindows,
|
|
1181
1253
|
bindingWindow,
|
|
1182
1254
|
dominantEffort,
|
|
1183
1255
|
typicalTurnCost,
|
|
1256
|
+
activeSessions,
|
|
1257
|
+
shareOf,
|
|
1258
|
+
CONCURRENT_WINDOW_MS,
|
|
1184
1259
|
MIN_PACE_SAMPLE,
|
|
1185
1260
|
formatDuration,
|
|
1186
1261
|
formatUSD,
|