claude-usage-limits 1.5.0 → 1.5.2

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.0",
4
+ "version": "1.5.2",
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
@@ -466,6 +466,12 @@ Good enough to plan with, not a bill. The honest caveats:
466
466
  - A rebuilt figure only counts what this machine did. If you also worked on
467
467
  another device it reads low, which is the dangerous direction, so treat it as
468
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`.
469
475
 
470
476
  [how-it-works.md](skills/usage-limits/references/how-it-works.md) has the field
471
477
  names, the formulas, and the rest of it.
@@ -491,7 +497,7 @@ test/ node --test, no dependencies
491
497
  node --test
492
498
  ```
493
499
 
494
- 157 tests over the pricing, the window arithmetic, plan and credit detection,
500
+ 164 tests over the pricing, the window arithmetic, plan and credit detection,
495
501
  the status line, the before-prompt line, job forecasting, per-project
496
502
  attribution, the CLI, packaging, and the settings save/restore.
497
503
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-usage-limits",
3
- "version": "1.5.0",
3
+ "version": "1.5.2",
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",
@@ -195,7 +195,7 @@ function sessionSpend(events, sessionId) {
195
195
  function describeWindow(window) {
196
196
  if (!window) return null;
197
197
  if (window.stale) return window.label + ' rolling over';
198
- const about = window.estimated ? ' about ' : ' ';
198
+ const about = window.estimated || window.adjusted ? ' about ' : ' ';
199
199
  return window.label + about + window.percentUsed + '%';
200
200
  }
201
201
 
@@ -237,6 +237,19 @@ function briefText(parts) {
237
237
  'That figure was rebuilt from local history because the ' +
238
238
  'snapshot is ' + parts.snapshotAge + ' old; run /usage to refresh it.'
239
239
  );
240
+ } else if (parts.pointsSinceSnapshot) {
241
+ sentences.push(
242
+ 'That includes about ' + parts.pointsSinceSnapshot + ' points spent since the ' +
243
+ 'snapshot was taken ' + parts.snapshotAge + ' ago, which it does not know about yet.'
244
+ );
245
+ } else if (parts.staleWindows) {
246
+ // Do not quietly carry on with a window we know is wrong and could not
247
+ // rebuild. Say it is unknown and point at the one command that fixes it.
248
+ sentences.push(
249
+ 'A window is past its reset and could not be rebuilt from local history, ' +
250
+ 'so its reading is unknown rather than current; the snapshot is ' +
251
+ parts.snapshotAge + ' old, so run /usage before trusting the rest.'
252
+ );
240
253
  }
241
254
  if (parts.othersSummary) sentences.push('Other windows: ' + parts.othersSummary + '.');
242
255
  if (parts.session) {
@@ -290,6 +303,7 @@ async function run(now, hookInput) {
290
303
  session: sessionSpend(events, sessionId),
291
304
  othersSummary: summariseOthers(windows, binding && binding.key),
292
305
  sessions: usage.activeSessions(events, now, usage.CONCURRENT_WINDOW_MS),
306
+ staleWindows: windows.filter((w) => w.stale).length,
293
307
  binding: binding
294
308
  ? {
295
309
  key: binding.key,
@@ -297,6 +311,8 @@ async function run(now, hookInput) {
297
311
  percentUsed: binding.percentUsed,
298
312
  stale: binding.stale,
299
313
  estimated: binding.estimated,
314
+ adjusted: binding.adjusted,
315
+ pointsSinceSnapshot: binding.pointsSinceSnapshot,
300
316
  resetsAt: binding.resetsAt,
301
317
  verdict: binding.verdict,
302
318
  windowStart: binding.windowStart,
@@ -324,6 +340,8 @@ async function run(now, hookInput) {
324
340
  : null,
325
341
  session: view.session,
326
342
  rebuilt: Boolean(binding && binding.estimated),
343
+ staleWindows: view.staleWindows || 0,
344
+ pointsSinceSnapshot: (binding && binding.pointsSinceSnapshot) || 0,
327
345
  snapshotAge: usage.formatDuration(base.snapshotAgeMs),
328
346
  pressure: pressure(binding, now, config, view.turnsLeft),
329
347
  });
@@ -535,7 +535,7 @@ function shareOf(sessions, sessionId) {
535
535
  // Everything the report needs about one limit window.
536
536
  function buildWindow(spec, snapshot, events, now, options) {
537
537
  const extra = options || {};
538
- const percent =
538
+ const rawPercent =
539
539
  snapshot && typeof snapshot.utilization === 'number' ? snapshot.utilization : null;
540
540
  const resetsAt = snapshot && snapshot.resets_at ? Date.parse(snapshot.resets_at) : null;
541
541
  const hasReset = Number.isFinite(resetsAt);
@@ -556,8 +556,8 @@ function buildWindow(spec, snapshot, events, now, options) {
556
556
  const window = {
557
557
  key: spec.key,
558
558
  label: spec.label,
559
- percentUsed: percent,
560
- percentLeft: percent === null ? null : Math.max(0, 100 - percent),
559
+ percentUsed: rawPercent,
560
+ percentLeft: rawPercent === null ? null : Math.max(0, 100 - rawPercent),
561
561
  resetsAt: hasReset ? resetsAt : null,
562
562
  msToReset: hasReset ? resetsAt - now : null,
563
563
  windowStart: start,
@@ -579,13 +579,50 @@ function buildWindow(spec, snapshot, events, now, options) {
579
579
  // True when the percentage was rebuilt from local history because the
580
580
  // snapshot had gone stale, rather than read from the snapshot itself.
581
581
  estimated: Boolean(extra.estimated),
582
+ // True when spend since the snapshot was added to its reading.
583
+ adjusted: false,
584
+ pointsSinceSnapshot: 0,
582
585
  verdict: 'unknown',
583
586
  };
584
587
 
588
+ // Worked out before anything reads it: the adjustment below and the verdict
589
+ // chain both branch on whether this window has already rolled over.
590
+ window.stale = hasReset && resetsAt <= now;
591
+
585
592
  // Calibrate against this account: how many dollars of measured traffic
586
593
  // moved the meter one point. A rebuilt window hands its own figure in,
587
594
  // because rounding to 0% would otherwise leave it unpriced and drop it out
588
595
  // of the binding choice just after a reset.
596
+ // The snapshot is a reading from a moment in the past, not from now. Spend
597
+ // since then is real and uncounted, and with several sessions running it adds
598
+ // up fast: forty points went missing in nine minutes once, so a window that
599
+ // was truly at 88% was reported at 49%. Calibrate on spend up to the reading
600
+ // only, or the very spend being accounted for inflates the price per point
601
+ // and shrinks its own correction.
602
+ let percent = rawPercent;
603
+ let sinceSnapshot = 0;
604
+ if (
605
+ rawPercent !== null &&
606
+ rawPercent > 0 &&
607
+ !window.stale &&
608
+ Number.isFinite(extra.fetchedAt) &&
609
+ extra.fetchedAt > start
610
+ ) {
611
+ const upTo = totals(inWindow.filter((e) => e.at <= extra.fetchedAt));
612
+ const after = totals(inWindow.filter((e) => e.at > extra.fetchedAt));
613
+ if (upTo.cost > 0 && after.cost > 0) {
614
+ const pricePerPoint = upTo.cost / rawPercent;
615
+ sinceSnapshot = after.cost / pricePerPoint;
616
+ if (sinceSnapshot >= 1) {
617
+ percent = Math.min(100, Math.round(rawPercent + sinceSnapshot));
618
+ window.adjusted = true;
619
+ window.pointsSinceSnapshot = Math.round(sinceSnapshot);
620
+ window.percentUsed = percent;
621
+ window.percentLeft = Math.max(0, 100 - percent);
622
+ }
623
+ }
624
+ }
625
+
589
626
  const derived = percent !== null && percent > 0 && spent.cost > 0 ? spent.cost / percent : null;
590
627
  const priced =
591
628
  derived !== null
@@ -615,7 +652,6 @@ function buildWindow(spec, snapshot, events, now, options) {
615
652
  // A reset time in the past means the window already turned over and the
616
653
  // cached percentage describes a window that no longer exists. Reporting it
617
654
  // as current would claim the budget is gone when it has just come back.
618
- window.stale = hasReset && resetsAt <= now;
619
655
  if (window.stale) {
620
656
  window.remainingUSD = null;
621
657
  window.turnsLeft = null;
@@ -754,6 +790,7 @@ function collect(now) {
754
790
  planTier: plan.tier,
755
791
  planAdvice: plan.advice,
756
792
  snapshotAgeMs: cache && cache.fetchedAtMs ? now - cache.fetchedAtMs : null,
793
+ snapshotFetchedAt: cache && cache.fetchedAtMs ? cache.fetchedAtMs : null,
757
794
  utilization,
758
795
  settings: {
759
796
  model: settings.model || 'default',
@@ -772,6 +809,9 @@ function collect(now) {
772
809
  // was spent inside the window it describes equalled its percentage. That
773
810
  // dollars-per-point figure is a property of the plan, not of the moment, so it
774
811
  // still prices the window running now.
812
+ // Anything above this and the calibration, not the budget, is what is full.
813
+ const SATURATION_LIMIT = 105;
814
+
775
815
  function reconstructWindow(spec, snapshot, events, now) {
776
816
  if (!snapshot || typeof snapshot.utilization !== 'number') return null;
777
817
  if (snapshot.utilization <= 0) return null;
@@ -784,11 +824,25 @@ function reconstructWindow(spec, snapshot, events, now) {
784
824
  if (past.cost <= 0) return null;
785
825
 
786
826
  const usdPerPercent = past.cost / snapshot.utilization;
787
- const liveStart = now - spec.span;
827
+
828
+ // The window running now began when the old one reset, not five hours ago.
829
+ // Summing a rolling span sweeps in the window that already expired: three
830
+ // minutes after a reset that meant counting 103 turns instead of 6, and
831
+ // reporting a fresh window as completely full.
832
+ const liveStart = Math.max(now - spec.span, resetsAt);
788
833
  const live = totals(events.filter((e) => e.at >= liveStart && e.at <= now));
834
+ const raw = live.cost / usdPerPercent;
835
+
836
+ // A rebuild that overflows the window is not a full window, it is a broken
837
+ // calibration. Local transcripts only see this machine, so if the closed
838
+ // window was mostly spent elsewhere its price per point comes out far too
839
+ // small and any live spend divides to hundreds of percent. Capping that at
840
+ // 100 would report a full budget to someone sitting at half, which is worse
841
+ // than admitting the reading cannot be rebuilt.
842
+ if (raw > SATURATION_LIMIT) return null;
789
843
 
790
844
  return {
791
- percentUsed: Math.min(100, Math.round(live.cost / usdPerPercent)),
845
+ percentUsed: Math.min(100, Math.round(raw)),
792
846
  usdPerPercent,
793
847
  spentUSD: live.cost,
794
848
  turns: live.turns,
@@ -798,14 +852,14 @@ function reconstructWindow(spec, snapshot, events, now) {
798
852
 
799
853
  // No snapshot at all means no windows, which is what tells the report to
800
854
  // explain itself rather than print a table of dashes.
801
- function buildWindows(utilization, events, now) {
855
+ function buildWindows(utilization, events, now, fetchedAt) {
802
856
  if (!utilization) return [];
803
857
  return WINDOWS.map((spec) => {
804
858
  const snapshot = utilization[spec.key];
805
859
  // The per-model weekly windows only exist on some plans.
806
860
  if (spec.key !== 'five_hour' && spec.key !== 'seven_day' && !snapshot) return null;
807
861
 
808
- const window = buildWindow(spec, snapshot, events, now);
862
+ const window = buildWindow(spec, snapshot, events, now, { fetchedAt });
809
863
  if (!window.stale) return window;
810
864
 
811
865
  // Rolled over. Rebuild from local history rather than going blind on it.
@@ -832,7 +886,7 @@ async function report(now) {
832
886
  const earliest = now - 8 * DAY;
833
887
  const events = await readEvents(earliest);
834
888
 
835
- const windows = buildWindows(base.utilization, events, now);
889
+ const windows = buildWindows(base.utilization, events, now, base.snapshotFetchedAt);
836
890
 
837
891
  const recentEvents = events.filter((event) => event.at >= now - HOUR);
838
892
  const recent = totals(recentEvents);
@@ -993,7 +1047,7 @@ function render(data) {
993
1047
  ? 'stale'
994
1048
  : window.percentUsed === null
995
1049
  ? '-'
996
- : (window.estimated ? '~' : '') + window.percentUsed + '%',
1050
+ : (window.estimated || window.adjusted ? '~' : '') + window.percentUsed + '%',
997
1051
  6
998
1052
  ) +
999
1053
  padLeft(formatDuration(window.msToReset), 12) +
@@ -1071,6 +1125,15 @@ function render(data) {
1071
1125
  }
1072
1126
  lines.push(' Measured ' + formatCount(data.measuredTurns) + ' turns of local transcript');
1073
1127
 
1128
+ if (data.windows.some((window) => window.adjusted)) {
1129
+ lines.push(
1130
+ ' Note ~ includes spend since the snapshot was taken, which its own'
1131
+ );
1132
+ lines.push(
1133
+ ' reading does not cover yet. Run /usage for a fresh one.'
1134
+ );
1135
+ }
1136
+
1074
1137
  if (data.windows.some((window) => window.estimated)) {
1075
1138
  lines.push(
1076
1139
  ' Note ~ means the snapshot had gone stale and that window was rebuilt'
@@ -1236,6 +1299,7 @@ module.exports = {
1236
1299
  readEvents,
1237
1300
  buildWindow,
1238
1301
  reconstructWindow,
1302
+ SATURATION_LIMIT,
1239
1303
  buildWindows,
1240
1304
  bindingWindow,
1241
1305
  dominantEffort,