claude-usage-limits 1.1.4 → 1.3.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.3.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,43 @@ 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
|
+
|
|
185
|
+
## When you keep typing
|
|
186
|
+
|
|
187
|
+
Every message sent while work is already running starts another turn, and each
|
|
188
|
+
turn re-sends the whole conversation. Three follow-ups during one task can cost
|
|
189
|
+
more than the task did.
|
|
190
|
+
|
|
191
|
+
So when several additions arrive mid-task and the binding window is tight,
|
|
192
|
+
Claude says so once and keeps working:
|
|
193
|
+
|
|
194
|
+
> I have got all three. While the weekly window is this tight, sending them
|
|
195
|
+
> together costs a good deal less than one at a time, so I will fold these in
|
|
196
|
+
> and carry on.
|
|
197
|
+
|
|
198
|
+
It asks once, never repeatedly, and only when the budget is actually tight.
|
|
199
|
+
Asking someone to hold their thoughts when there is room to spare is rude for
|
|
200
|
+
no gain.
|
|
201
|
+
|
|
202
|
+
The important exclusion: it never discourages a correction, a stop, or a bug
|
|
203
|
+
report. Those are the messages that save the most work, and a rule that trains
|
|
204
|
+
people out of interrupting to say "that is wrong" costs far more than the turns
|
|
205
|
+
it saves. Only additive scope is worth batching.
|
|
206
|
+
|
|
170
207
|
## Credits, and what happens at the wall
|
|
171
208
|
|
|
172
209
|
The report says which of two things happens when the plan allowance runs out,
|
|
@@ -274,6 +311,7 @@ minute, so it costs about 400ms cold and 120ms warm.
|
|
|
274
311
|
| `USAGE_LIMITS_FLOOR` | 40 | Below this, pace is ignored. |
|
|
275
312
|
| `USAGE_LIMITS_AHEAD` | 15 | Points ahead of pace that count as burning fast. |
|
|
276
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. |
|
|
277
315
|
|
|
278
316
|
## What would this job cost
|
|
279
317
|
|
|
@@ -390,10 +428,17 @@ Good enough to plan with, not a bill. The honest caveats:
|
|
|
390
428
|
- A model released after this table was written is priced at its family's
|
|
391
429
|
average rate, and the report marks those rows with an asterisk rather than
|
|
392
430
|
passing the guess off as a published price.
|
|
393
|
-
- The cache only refreshes when Claude Code talks to the API, so after
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
431
|
+
- The cache only refreshes when Claude Code talks to the API, so after a gap it
|
|
432
|
+
can be hours old and its 5-hour window long since rolled over. Dropping that
|
|
433
|
+
window would hide the limit that actually stops short work, so it gets rebuilt
|
|
434
|
+
from your transcripts instead: whatever was spent inside the window the stale
|
|
435
|
+
reading describes equalled its percentage, and that price per point still
|
|
436
|
+
values the window running now. Rebuilt figures are written `~41%` in the
|
|
437
|
+
report and "about 41%" in the before-prompt line, and they say how old the
|
|
438
|
+
snapshot is so you can run `/usage` and replace the estimate with a reading.
|
|
439
|
+
- A rebuilt figure only counts what this machine did. If you also worked on
|
|
440
|
+
another device it reads low, which is the dangerous direction, so treat it as
|
|
441
|
+
a floor until you refresh.
|
|
397
442
|
|
|
398
443
|
[how-it-works.md](skills/usage-limits/references/how-it-works.md) has the field
|
|
399
444
|
names, the formulas, and the rest of it.
|
|
@@ -419,7 +464,7 @@ test/ node --test, no dependencies
|
|
|
419
464
|
node --test
|
|
420
465
|
```
|
|
421
466
|
|
|
422
|
-
|
|
467
|
+
142 tests over the pricing, the window arithmetic, plan and credit detection,
|
|
423
468
|
the status line, the before-prompt line, job forecasting, per-project
|
|
424
469
|
attribution, the CLI, packaging, and the settings save/restore.
|
|
425
470
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-usage-limits",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.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",
|
|
@@ -78,6 +78,37 @@ 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
|
+
## When messages stack up
|
|
82
|
+
|
|
83
|
+
Every message sent while work is already running starts another turn, and every
|
|
84
|
+
turn re-sends the whole conversation. Three follow-ups during one task can cost
|
|
85
|
+
more than the task did.
|
|
86
|
+
|
|
87
|
+
So when several additions arrive while you are still working and the binding
|
|
88
|
+
window is tight, say it once, then keep going:
|
|
89
|
+
|
|
90
|
+
> I have got all three. While the weekly window is this tight, sending them
|
|
91
|
+
> together costs a good deal less than one at a time, so I will fold these in
|
|
92
|
+
> and carry on. Send the rest in one go if you can.
|
|
93
|
+
|
|
94
|
+
Four rules keep that from being obnoxious, and they matter more than the saving.
|
|
95
|
+
|
|
96
|
+
**Ask once per stretch of work.** A second reminder costs more goodwill than
|
|
97
|
+
the tokens it saves.
|
|
98
|
+
|
|
99
|
+
**Only when the budget is actually tight.** With room to spare, asking someone
|
|
100
|
+
to hold their thoughts is rude for no gain. Say nothing.
|
|
101
|
+
|
|
102
|
+
**Never discourage a correction, a stop, or a bug report.** Those are the
|
|
103
|
+
messages that save the most work. Someone interrupting to say the approach is
|
|
104
|
+
wrong has just paid for their own interruption many times over, and a rule that
|
|
105
|
+
trains people out of that is far more expensive than the turns it saves. Only
|
|
106
|
+
additive scope, the "also do X" and "and can you Y", is worth batching.
|
|
107
|
+
|
|
108
|
+
**Never make it about you.** The cost lands on their limit, not yours. Frame it
|
|
109
|
+
as their budget, offer the saving, and let them decide. Then work. Do not stop
|
|
110
|
+
to negotiate about whether to stop.
|
|
111
|
+
|
|
81
112
|
## Credits
|
|
82
113
|
|
|
83
114
|
The `Credits` line in the report says what actually happens at the limit, and
|
|
@@ -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
|
-
|
|
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
|
-
|
|
127
|
-
|
|
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
|
|
|
@@ -175,9 +194,9 @@ function sessionSpend(events, sessionId) {
|
|
|
175
194
|
|
|
176
195
|
function describeWindow(window) {
|
|
177
196
|
if (!window) return null;
|
|
178
|
-
return window.
|
|
179
|
-
|
|
180
|
-
|
|
197
|
+
if (window.stale) return window.label + ' rolling over';
|
|
198
|
+
const about = window.estimated ? ' about ' : ' ';
|
|
199
|
+
return window.label + about + window.percentUsed + '%';
|
|
181
200
|
}
|
|
182
201
|
|
|
183
202
|
// Everything except the window that will actually stop the work.
|
|
@@ -206,6 +225,12 @@ function briefText(parts) {
|
|
|
206
225
|
? '[usage-limits] binding window is ' + bound.join(', ') + '.'
|
|
207
226
|
: '[usage-limits] no usable window reading.'
|
|
208
227
|
);
|
|
228
|
+
if (parts.rebuilt) {
|
|
229
|
+
sentences.push(
|
|
230
|
+
'That figure was rebuilt from local history because the ' +
|
|
231
|
+
'snapshot is ' + parts.snapshotAge + ' old; run /usage to refresh it.'
|
|
232
|
+
);
|
|
233
|
+
}
|
|
209
234
|
if (parts.othersSummary) sentences.push('Other windows: ' + parts.othersSummary + '.');
|
|
210
235
|
if (parts.session) {
|
|
211
236
|
sentences.push(
|
|
@@ -218,7 +243,9 @@ function briefText(parts) {
|
|
|
218
243
|
parts.pressure === 'tight' || parts.pressure === 'gone'
|
|
219
244
|
? 'Open your reply with one line on where this leaves the budget, then say ' +
|
|
220
245
|
'what you will do now and what you will leave for after the reset. ' +
|
|
221
|
-
'Do not start work that clearly will not finish.'
|
|
246
|
+
'Do not start work that clearly will not finish. If several additions ' +
|
|
247
|
+
'arrive while you are working, say once that sending them together ' +
|
|
248
|
+
'costs less, then carry on; never say it about a correction or a stop.'
|
|
222
249
|
: 'Open your reply with one short line stating this and confirming the ' +
|
|
223
250
|
'request fits, then get on with the work. Keep it to a single line.';
|
|
224
251
|
|
|
@@ -240,39 +267,51 @@ async function run(now, hookInput) {
|
|
|
240
267
|
if (!base.utilization) return '';
|
|
241
268
|
|
|
242
269
|
const sessionId = hookInput && hookInput.session_id ? hookInput.session_id : null;
|
|
243
|
-
|
|
244
|
-
// The percentages are cheap: one small file, no transcripts.
|
|
245
|
-
const cheap = usage.buildWindows(base.utilization, [], now);
|
|
246
|
-
if (!cheap.length) return '';
|
|
247
|
-
|
|
248
270
|
const all = readCache();
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
let turnsLeft = cached ? cached.turnsLeft : null;
|
|
252
|
-
let session = cached ? cached.session : null;
|
|
253
|
-
let windows = cheap;
|
|
271
|
+
let view = pickCached(all, sessionId, now, config.cacheSeconds * SECOND);
|
|
254
272
|
|
|
255
|
-
|
|
273
|
+
// Everything shown has to come from one pass. Deriving the turns from a
|
|
274
|
+
// full scan and the binding window from somewhere cheaper is how the two
|
|
275
|
+
// end up describing different windows.
|
|
276
|
+
if (!view || !view.binding) {
|
|
256
277
|
const events = await usage.readEvents(now - 8 * DAY);
|
|
257
|
-
windows = usage.buildWindows(base.utilization, events, now);
|
|
278
|
+
const windows = usage.buildWindows(base.utilization, events, now);
|
|
258
279
|
const binding = usage.bindingWindow(windows);
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
280
|
+
view = {
|
|
281
|
+
at: now,
|
|
282
|
+
turnsLeft: binding && Number.isFinite(binding.turnsLeft) ? binding.turnsLeft : null,
|
|
283
|
+
session: sessionSpend(events, sessionId),
|
|
284
|
+
othersSummary: summariseOthers(windows, binding && binding.key),
|
|
285
|
+
binding: binding
|
|
286
|
+
? {
|
|
287
|
+
key: binding.key,
|
|
288
|
+
label: binding.label,
|
|
289
|
+
percentUsed: binding.percentUsed,
|
|
290
|
+
stale: binding.stale,
|
|
291
|
+
estimated: binding.estimated,
|
|
292
|
+
resetsAt: binding.resetsAt,
|
|
293
|
+
verdict: binding.verdict,
|
|
294
|
+
windowStart: binding.windowStart,
|
|
295
|
+
spanMs: binding.spanMs,
|
|
296
|
+
}
|
|
297
|
+
: null,
|
|
298
|
+
};
|
|
299
|
+
writeCache(mergeCache(all, sessionId, view, KEEP_SESSIONS));
|
|
262
300
|
}
|
|
263
301
|
|
|
264
|
-
const binding =
|
|
265
|
-
|
|
302
|
+
const binding = view.binding;
|
|
266
303
|
return briefText({
|
|
267
304
|
binding,
|
|
268
|
-
othersSummary:
|
|
269
|
-
turnsLeft,
|
|
305
|
+
othersSummary: view.othersSummary,
|
|
306
|
+
turnsLeft: view.turnsLeft,
|
|
270
307
|
resetsIn:
|
|
271
|
-
binding && !binding.stale && binding.
|
|
272
|
-
? usage.formatDuration(binding.
|
|
308
|
+
binding && !binding.stale && Number.isFinite(binding.resetsAt)
|
|
309
|
+
? usage.formatDuration(binding.resetsAt - now)
|
|
273
310
|
: null,
|
|
274
|
-
session,
|
|
275
|
-
|
|
311
|
+
session: view.session,
|
|
312
|
+
rebuilt: Boolean(binding && binding.estimated),
|
|
313
|
+
snapshotAge: usage.formatDuration(base.snapshotAgeMs),
|
|
314
|
+
pressure: pressure(binding, now, config, view.turnsLeft),
|
|
276
315
|
});
|
|
277
316
|
}
|
|
278
317
|
|
|
@@ -470,12 +470,17 @@ function forecastWindow(window, turns, rates) {
|
|
|
470
470
|
}
|
|
471
471
|
|
|
472
472
|
// Everything the report needs about one limit window.
|
|
473
|
-
function buildWindow(spec, snapshot, events, now) {
|
|
473
|
+
function buildWindow(spec, snapshot, events, now, options) {
|
|
474
|
+
const extra = options || {};
|
|
474
475
|
const percent =
|
|
475
476
|
snapshot && typeof snapshot.utilization === 'number' ? snapshot.utilization : null;
|
|
476
477
|
const resetsAt = snapshot && snapshot.resets_at ? Date.parse(snapshot.resets_at) : null;
|
|
477
478
|
const hasReset = Number.isFinite(resetsAt);
|
|
478
|
-
const start =
|
|
479
|
+
const start = Number.isFinite(extra.windowStart)
|
|
480
|
+
? extra.windowStart
|
|
481
|
+
: hasReset
|
|
482
|
+
? resetsAt - spec.span
|
|
483
|
+
: now - spec.span;
|
|
479
484
|
|
|
480
485
|
const inWindow = events.filter((event) => event.at >= start && event.at <= now);
|
|
481
486
|
const spent = totals(inWindow);
|
|
@@ -508,6 +513,9 @@ function buildWindow(spec, snapshot, events, now) {
|
|
|
508
513
|
headroomMs: null,
|
|
509
514
|
coarse: false,
|
|
510
515
|
stale: false,
|
|
516
|
+
// True when the percentage was rebuilt from local history because the
|
|
517
|
+
// snapshot had gone stale, rather than read from the snapshot itself.
|
|
518
|
+
estimated: Boolean(extra.estimated),
|
|
511
519
|
verdict: 'unknown',
|
|
512
520
|
};
|
|
513
521
|
|
|
@@ -563,11 +571,27 @@ function bindingWindow(windows) {
|
|
|
563
571
|
const live = fresh.length ? fresh : known;
|
|
564
572
|
if (!live.length) return null;
|
|
565
573
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
574
|
+
// How soon this window stops the work. A window with no pace estimate is
|
|
575
|
+
// ranked by how full it is instead, because a nearly full window must never
|
|
576
|
+
// be passed over merely because nothing has been spent in it lately.
|
|
577
|
+
const soonest = (w) => {
|
|
578
|
+
if (Number.isFinite(w.headroomMs)) return w.headroomMs;
|
|
579
|
+
return w.percentUsed >= 90 ? 0 : Infinity;
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
return live.reduce((best, w) => {
|
|
583
|
+
const mine = soonest(w);
|
|
584
|
+
const theirs = soonest(best);
|
|
585
|
+
if (mine !== theirs) return mine < theirs ? w : best;
|
|
586
|
+
|
|
587
|
+
// Equally urgent: the shorter window is the one hit first in practice, so
|
|
588
|
+
// the 5-hour limit wins a tie against the weekly one.
|
|
589
|
+
const myspan = Number.isFinite(w.spanMs) ? w.spanMs : Infinity;
|
|
590
|
+
const theirspan = Number.isFinite(best.spanMs) ? best.spanMs : Infinity;
|
|
591
|
+
if (myspan !== theirspan) return myspan < theirspan ? w : best;
|
|
592
|
+
|
|
593
|
+
return w.percentUsed > best.percentUsed ? w : best;
|
|
594
|
+
});
|
|
571
595
|
}
|
|
572
596
|
|
|
573
597
|
function formatDuration(ms) {
|
|
@@ -667,6 +691,39 @@ function collect(now) {
|
|
|
667
691
|
};
|
|
668
692
|
}
|
|
669
693
|
|
|
694
|
+
// A snapshot only refreshes when Claude Code talks to the API, so after a
|
|
695
|
+
// gap it can be hours old and its 5-hour window long since rolled over.
|
|
696
|
+
// Dropping that window loses the limit that actually stops short work, so
|
|
697
|
+
// rebuild it from the transcripts instead.
|
|
698
|
+
//
|
|
699
|
+
// The trick is that the stale reading is still a usable calibration: whatever
|
|
700
|
+
// was spent inside the window it describes equalled its percentage. That
|
|
701
|
+
// dollars-per-point figure is a property of the plan, not of the moment, so it
|
|
702
|
+
// still prices the window running now.
|
|
703
|
+
function reconstructWindow(spec, snapshot, events, now) {
|
|
704
|
+
if (!snapshot || typeof snapshot.utilization !== 'number') return null;
|
|
705
|
+
if (snapshot.utilization <= 0) return null;
|
|
706
|
+
|
|
707
|
+
const resetsAt = snapshot.resets_at ? Date.parse(snapshot.resets_at) : null;
|
|
708
|
+
if (!Number.isFinite(resetsAt) || resetsAt > now) return null;
|
|
709
|
+
|
|
710
|
+
const pastStart = resetsAt - spec.span;
|
|
711
|
+
const past = totals(events.filter((e) => e.at >= pastStart && e.at <= resetsAt));
|
|
712
|
+
if (past.cost <= 0) return null;
|
|
713
|
+
|
|
714
|
+
const usdPerPercent = past.cost / snapshot.utilization;
|
|
715
|
+
const liveStart = now - spec.span;
|
|
716
|
+
const live = totals(events.filter((e) => e.at >= liveStart && e.at <= now));
|
|
717
|
+
|
|
718
|
+
return {
|
|
719
|
+
percentUsed: Math.min(100, Math.round(live.cost / usdPerPercent)),
|
|
720
|
+
usdPerPercent,
|
|
721
|
+
spentUSD: live.cost,
|
|
722
|
+
turns: live.turns,
|
|
723
|
+
windowStart: liveStart,
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
|
|
670
727
|
// No snapshot at all means no windows, which is what tells the report to
|
|
671
728
|
// explain itself rather than print a table of dashes.
|
|
672
729
|
function buildWindows(utilization, events, now) {
|
|
@@ -675,7 +732,20 @@ function buildWindows(utilization, events, now) {
|
|
|
675
732
|
const snapshot = utilization[spec.key];
|
|
676
733
|
// The per-model weekly windows only exist on some plans.
|
|
677
734
|
if (spec.key !== 'five_hour' && spec.key !== 'seven_day' && !snapshot) return null;
|
|
678
|
-
|
|
735
|
+
|
|
736
|
+
const window = buildWindow(spec, snapshot, events, now);
|
|
737
|
+
if (!window.stale) return window;
|
|
738
|
+
|
|
739
|
+
// Rolled over. Rebuild from local history rather than going blind on it.
|
|
740
|
+
const rebuilt = reconstructWindow(spec, snapshot, events, now);
|
|
741
|
+
if (!rebuilt) return window;
|
|
742
|
+
return buildWindow(
|
|
743
|
+
spec,
|
|
744
|
+
{ utilization: rebuilt.percentUsed, resets_at: null },
|
|
745
|
+
events,
|
|
746
|
+
now,
|
|
747
|
+
{ estimated: true, windowStart: rebuilt.windowStart }
|
|
748
|
+
);
|
|
679
749
|
}).filter(Boolean);
|
|
680
750
|
}
|
|
681
751
|
|
|
@@ -842,7 +912,11 @@ function render(data) {
|
|
|
842
912
|
lines.push(
|
|
843
913
|
' ' + pad(window.label, 15) +
|
|
844
914
|
padLeft(
|
|
845
|
-
window.stale
|
|
915
|
+
window.stale
|
|
916
|
+
? 'stale'
|
|
917
|
+
: window.percentUsed === null
|
|
918
|
+
? '-'
|
|
919
|
+
: (window.estimated ? '~' : '') + window.percentUsed + '%',
|
|
846
920
|
6
|
|
847
921
|
) +
|
|
848
922
|
padLeft(formatDuration(window.msToReset), 12) +
|
|
@@ -909,6 +983,15 @@ function render(data) {
|
|
|
909
983
|
}
|
|
910
984
|
lines.push(' Measured ' + formatCount(data.measuredTurns) + ' turns of local transcript');
|
|
911
985
|
|
|
986
|
+
if (data.windows.some((window) => window.estimated)) {
|
|
987
|
+
lines.push(
|
|
988
|
+
' Note ~ means the snapshot had gone stale and that window was rebuilt'
|
|
989
|
+
);
|
|
990
|
+
lines.push(
|
|
991
|
+
' from local history. Run /usage to replace it with a real reading.'
|
|
992
|
+
);
|
|
993
|
+
}
|
|
994
|
+
|
|
912
995
|
if (data.binding && data.binding.coarse) {
|
|
913
996
|
lines.push(' Note the meter reads in whole percent, so a low reading is a wide bracket');
|
|
914
997
|
}
|
|
@@ -1064,6 +1147,7 @@ module.exports = {
|
|
|
1064
1147
|
eventFrom,
|
|
1065
1148
|
readEvents,
|
|
1066
1149
|
buildWindow,
|
|
1150
|
+
reconstructWindow,
|
|
1067
1151
|
buildWindows,
|
|
1068
1152
|
bindingWindow,
|
|
1069
1153
|
dominantEffort,
|