claude-usage-limits 1.5.2 → 1.5.4
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.5.
|
|
4
|
+
"version": "1.5.4",
|
|
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
|
@@ -187,6 +187,11 @@ The split comes from measured spend rather than an assumption that everyone is
|
|
|
187
187
|
working equally hard, because they usually are not. A session that has gone
|
|
188
188
|
quiet for a quarter of an hour is not counted as competing.
|
|
189
189
|
|
|
190
|
+
Every session's spend also feeds the reading itself, not just the split. The
|
|
191
|
+
percentage is corrected using all spend recorded since the snapshot was taken,
|
|
192
|
+
whichever window produced it, so another Claude burning budget in the next
|
|
193
|
+
terminal moves your number too.
|
|
194
|
+
|
|
190
195
|
## Which limit it watches
|
|
191
196
|
|
|
192
197
|
Two windows run at once and the 5-hour one is usually what actually stops you,
|
|
@@ -320,6 +325,13 @@ explaining is pace: two days into a week you should be near 29 percent spent, so
|
|
|
320
325
|
60 percent means you will not last the week, and that is worth hearing at 60
|
|
321
326
|
rather than at 85.
|
|
322
327
|
|
|
328
|
+
One limit worth knowing: the hook fires when a prompt is submitted, so a
|
|
329
|
+
message sent while Claude is already working does not refresh it. Claude Code
|
|
330
|
+
delivers those into the running turn without re-running hooks, which no plugin
|
|
331
|
+
can intercept. The skill handles it by telling Claude the figures age during a
|
|
332
|
+
turn, and to re-read them before claiming a job fits rather than trusting a
|
|
333
|
+
number from several tool calls ago.
|
|
334
|
+
|
|
323
335
|
It has to be cheap, because it runs on every prompt. The percentages come from
|
|
324
336
|
one small file. The transcript scan behind "turns of headroom" is cached for a
|
|
325
337
|
minute, so it costs about 400ms cold and 120ms warm.
|
|
@@ -497,7 +509,7 @@ test/ node --test, no dependencies
|
|
|
497
509
|
node --test
|
|
498
510
|
```
|
|
499
511
|
|
|
500
|
-
|
|
512
|
+
181 tests over the pricing, the window arithmetic, plan and credit detection,
|
|
501
513
|
the status line, the before-prompt line, job forecasting, per-project
|
|
502
514
|
attribution, the CLI, packaging, and the settings save/restore.
|
|
503
515
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-usage-limits",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.4",
|
|
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",
|
|
@@ -78,6 +78,29 @@ abandoned mid-edit.
|
|
|
78
78
|
**The window resets first.** If the reset lands before the budget runs out,
|
|
79
79
|
the limit is not the constraint. Say that and stop optimising for it.
|
|
80
80
|
|
|
81
|
+
## The budget line ages during a turn
|
|
82
|
+
|
|
83
|
+
The hook runs when a prompt is submitted, so the figures you were given are
|
|
84
|
+
from the start of the turn. A message sent while you are already working does
|
|
85
|
+
not fire it again: it arrives without a budget line, and the numbers you are
|
|
86
|
+
holding are now older than they look.
|
|
87
|
+
|
|
88
|
+
That matters most when it is the one thing you are about to assert. Re-check
|
|
89
|
+
before saying a job fits, if any of these are true:
|
|
90
|
+
|
|
91
|
+
- the turn has run long, or through many tool calls
|
|
92
|
+
- more requests arrived while you were working
|
|
93
|
+
- other sessions are active, so the budget is draining without you
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
node scripts/usage.js --status
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
That is one cheap read of a small file, no transcript scan, and it costs far
|
|
100
|
+
less than promising to finish something and stopping halfway. If you have no
|
|
101
|
+
budget line at all this turn, you are mid-turn: use the last one you were given
|
|
102
|
+
and treat it as a ceiling, not a reading.
|
|
103
|
+
|
|
81
104
|
## When messages stack up
|
|
82
105
|
|
|
83
106
|
Every message sent while work is already running starts another turn, and every
|
|
@@ -180,17 +180,8 @@ function readHookInput() {
|
|
|
180
180
|
});
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
let cost = 0;
|
|
186
|
-
let turns = 0;
|
|
187
|
-
for (const event of events) {
|
|
188
|
-
if (event.sessionId !== sessionId) continue;
|
|
189
|
-
cost += event.cost;
|
|
190
|
-
turns += 1;
|
|
191
|
-
}
|
|
192
|
-
return turns ? { turns, cost } : null;
|
|
193
|
-
}
|
|
183
|
+
// Kept as a re-export so there is exactly one implementation.
|
|
184
|
+
const sessionSpend = usage.sessionSpend;
|
|
194
185
|
|
|
195
186
|
function describeWindow(window) {
|
|
196
187
|
if (!window) return null;
|
|
@@ -294,16 +285,18 @@ async function run(now, hookInput) {
|
|
|
294
285
|
// full scan and the binding window from somewhere cheaper is how the two
|
|
295
286
|
// end up describing different windows.
|
|
296
287
|
if (!view || !view.binding) {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
const
|
|
288
|
+
// One call, shared with the report. Building the view twice is how the
|
|
289
|
+
// snapshot correction reached the report and never reached the hook.
|
|
290
|
+
const data = await usage.report(now, { sessionId });
|
|
291
|
+
const binding = data.binding;
|
|
300
292
|
view = {
|
|
301
293
|
at: now,
|
|
302
294
|
turnsLeft: binding && Number.isFinite(binding.turnsLeft) ? binding.turnsLeft : null,
|
|
303
|
-
session:
|
|
304
|
-
othersSummary: summariseOthers(windows, binding && binding.key),
|
|
305
|
-
sessions:
|
|
306
|
-
staleWindows:
|
|
295
|
+
session: data.session,
|
|
296
|
+
othersSummary: summariseOthers(data.windows, binding && binding.key),
|
|
297
|
+
sessions: data.sessions,
|
|
298
|
+
staleWindows: data.staleWindows,
|
|
299
|
+
snapshotAge: usage.formatDuration(data.snapshotAgeMs),
|
|
307
300
|
binding: binding
|
|
308
301
|
? {
|
|
309
302
|
key: binding.key,
|
|
@@ -313,6 +306,7 @@ async function run(now, hookInput) {
|
|
|
313
306
|
estimated: binding.estimated,
|
|
314
307
|
adjusted: binding.adjusted,
|
|
315
308
|
pointsSinceSnapshot: binding.pointsSinceSnapshot,
|
|
309
|
+
correctionUnreliable: binding.correctionUnreliable,
|
|
316
310
|
resetsAt: binding.resetsAt,
|
|
317
311
|
verdict: binding.verdict,
|
|
318
312
|
windowStart: binding.windowStart,
|
|
@@ -342,7 +336,7 @@ async function run(now, hookInput) {
|
|
|
342
336
|
rebuilt: Boolean(binding && binding.estimated),
|
|
343
337
|
staleWindows: view.staleWindows || 0,
|
|
344
338
|
pointsSinceSnapshot: (binding && binding.pointsSinceSnapshot) || 0,
|
|
345
|
-
snapshotAge:
|
|
339
|
+
snapshotAge: view.snapshotAge,
|
|
346
340
|
pressure: pressure(binding, now, config, view.turnsLeft),
|
|
347
341
|
});
|
|
348
342
|
}
|
|
@@ -582,6 +582,8 @@ function buildWindow(spec, snapshot, events, now, options) {
|
|
|
582
582
|
// True when spend since the snapshot was added to its reading.
|
|
583
583
|
adjusted: false,
|
|
584
584
|
pointsSinceSnapshot: 0,
|
|
585
|
+
// Set when spend since the snapshot could not be priced sensibly.
|
|
586
|
+
correctionUnreliable: false,
|
|
585
587
|
verdict: 'unknown',
|
|
586
588
|
};
|
|
587
589
|
|
|
@@ -610,10 +612,22 @@ function buildWindow(spec, snapshot, events, now, options) {
|
|
|
610
612
|
) {
|
|
611
613
|
const upTo = totals(inWindow.filter((e) => e.at <= extra.fetchedAt));
|
|
612
614
|
const after = totals(inWindow.filter((e) => e.at > extra.fetchedAt));
|
|
613
|
-
|
|
615
|
+
|
|
616
|
+
// The baseline has to be worth something. Pricing a point off two or three
|
|
617
|
+
// turns makes it far too cheap, and every dollar spent since then is then
|
|
618
|
+
// divided by that, which is how a window truly at 55% got corrected all the
|
|
619
|
+
// way to a confident 100.
|
|
620
|
+
if (upTo.cost > 0 && after.cost > 0 && upTo.turns >= MIN_BASELINE_TURNS) {
|
|
614
621
|
const pricePerPoint = upTo.cost / rawPercent;
|
|
615
622
|
sinceSnapshot = after.cost / pricePerPoint;
|
|
616
|
-
|
|
623
|
+
|
|
624
|
+
// Same rule as a rebuild: past this it is the calibration that is full,
|
|
625
|
+
// not the window. Better to leave the reading uncorrected and say the
|
|
626
|
+
// snapshot is old than to assert a budget that is gone.
|
|
627
|
+
if (rawPercent + sinceSnapshot > SATURATION_LIMIT) {
|
|
628
|
+
sinceSnapshot = 0;
|
|
629
|
+
window.correctionUnreliable = true;
|
|
630
|
+
} else if (sinceSnapshot >= 1) {
|
|
617
631
|
percent = Math.min(100, Math.round(rawPercent + sinceSnapshot));
|
|
618
632
|
window.adjusted = true;
|
|
619
633
|
window.pointsSinceSnapshot = Math.round(sinceSnapshot);
|
|
@@ -812,6 +826,9 @@ function collect(now) {
|
|
|
812
826
|
// Anything above this and the calibration, not the budget, is what is full.
|
|
813
827
|
const SATURATION_LIMIT = 105;
|
|
814
828
|
|
|
829
|
+
// Fewer turns than this before the snapshot and a point cannot be priced.
|
|
830
|
+
const MIN_BASELINE_TURNS = 5;
|
|
831
|
+
|
|
815
832
|
function reconstructWindow(spec, snapshot, events, now) {
|
|
816
833
|
if (!snapshot || typeof snapshot.utilization !== 'number') return null;
|
|
817
834
|
if (snapshot.utilization <= 0) return null;
|
|
@@ -879,7 +896,20 @@ function buildWindows(utilization, events, now, fetchedAt) {
|
|
|
879
896
|
}).filter(Boolean);
|
|
880
897
|
}
|
|
881
898
|
|
|
882
|
-
|
|
899
|
+
// What one session has spent, out of everything on record.
|
|
900
|
+
function sessionSpend(events, sessionId) {
|
|
901
|
+
if (!sessionId) return null;
|
|
902
|
+
let cost = 0;
|
|
903
|
+
let turns = 0;
|
|
904
|
+
for (const event of events) {
|
|
905
|
+
if (event.sessionId !== sessionId) continue;
|
|
906
|
+
cost += event.cost;
|
|
907
|
+
turns += 1;
|
|
908
|
+
}
|
|
909
|
+
return turns ? { turns, cost } : null;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
async function report(now, options) {
|
|
883
913
|
const base = collect(now);
|
|
884
914
|
// A stale snapshot can put a window's start slightly further back than
|
|
885
915
|
// seven days, so give the scan a day of slack.
|
|
@@ -901,6 +931,8 @@ async function report(now) {
|
|
|
901
931
|
binding,
|
|
902
932
|
credits: creditsFrom(base.utilization),
|
|
903
933
|
sessions: activeSessions(events, now, CONCURRENT_WINDOW_MS),
|
|
934
|
+
session: sessionSpend(events, options && options.sessionId),
|
|
935
|
+
staleWindows: windows.filter((w) => w.stale).length,
|
|
904
936
|
rates: costPercentiles(recentEvents.length >= 5 ? recentEvents : scoped),
|
|
905
937
|
resumeAt: binding ? binding.resetsAt : null,
|
|
906
938
|
models: byModel(scoped),
|
|
@@ -1300,11 +1332,13 @@ module.exports = {
|
|
|
1300
1332
|
buildWindow,
|
|
1301
1333
|
reconstructWindow,
|
|
1302
1334
|
SATURATION_LIMIT,
|
|
1335
|
+
MIN_BASELINE_TURNS,
|
|
1303
1336
|
buildWindows,
|
|
1304
1337
|
bindingWindow,
|
|
1305
1338
|
dominantEffort,
|
|
1306
1339
|
typicalTurnCost,
|
|
1307
1340
|
activeSessions,
|
|
1341
|
+
sessionSpend,
|
|
1308
1342
|
shareOf,
|
|
1309
1343
|
CONCURRENT_WINDOW_MS,
|
|
1310
1344
|
MIN_PACE_SAMPLE,
|