claude-usage-limits 1.1.4 → 1.2.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.1.4",
4
+ "version": "1.2.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,28 @@ node skills/usage-limits/scripts/usage.js
167
167
  node skills/usage-limits/scripts/usage.js --json
168
168
  ```
169
169
 
170
+ ## When you keep typing
171
+
172
+ Every message sent while work is already running starts another turn, and each
173
+ turn re-sends the whole conversation. Three follow-ups during one task can cost
174
+ more than the task did.
175
+
176
+ So when several additions arrive mid-task and the binding window is tight,
177
+ Claude says so once and keeps working:
178
+
179
+ > I have got all three. While the weekly window is this tight, sending them
180
+ > together costs a good deal less than one at a time, so I will fold these in
181
+ > and carry on.
182
+
183
+ It asks once, never repeatedly, and only when the budget is actually tight.
184
+ Asking someone to hold their thoughts when there is room to spare is rude for
185
+ no gain.
186
+
187
+ The important exclusion: it never discourages a correction, a stop, or a bug
188
+ report. Those are the messages that save the most work, and a rule that trains
189
+ people out of interrupting to say "that is wrong" costs far more than the turns
190
+ it saves. Only additive scope is worth batching.
191
+
170
192
  ## Credits, and what happens at the wall
171
193
 
172
194
  The report says which of two things happens when the plan allowance runs out,
@@ -390,10 +412,17 @@ Good enough to plan with, not a bill. The honest caveats:
390
412
  - A model released after this table was written is priced at its family's
391
413
  average rate, and the report marks those rows with an asterisk rather than
392
414
  passing the guess off as a published price.
393
- - The cache only refreshes when Claude Code talks to the API, so after an idle
394
- spell a window can sit past its own reset time. When that happens the report
395
- says `stale` and the status line says `rolling` rather than reporting a
396
- percentage for a window that has already turned over.
415
+ - The cache only refreshes when Claude Code talks to the API, so after a gap it
416
+ can be hours old and its 5-hour window long since rolled over. Dropping that
417
+ window would hide the limit that actually stops short work, so it gets rebuilt
418
+ from your transcripts instead: whatever was spent inside the window the stale
419
+ reading describes equalled its percentage, and that price per point still
420
+ values the window running now. Rebuilt figures are written `~41%` in the
421
+ report and "about 41%" in the before-prompt line, and they say how old the
422
+ snapshot is so you can run `/usage` and replace the estimate with a reading.
423
+ - A rebuilt figure only counts what this machine did. If you also worked on
424
+ another device it reads low, which is the dangerous direction, so treat it as
425
+ a floor until you refresh.
397
426
 
398
427
  [how-it-works.md](skills/usage-limits/references/how-it-works.md) has the field
399
428
  names, the formulas, and the rest of it.
@@ -419,7 +448,7 @@ test/ node --test, no dependencies
419
448
  node --test
420
449
  ```
421
450
 
422
- 119 tests over the pricing, the window arithmetic, plan and credit detection,
451
+ 130 tests over the pricing, the window arithmetic, plan and credit detection,
423
452
  the status line, the before-prompt line, job forecasting, per-project
424
453
  attribution, the CLI, packaging, and the settings save/restore.
425
454
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-usage-limits",
3
- "version": "1.1.4",
3
+ "version": "1.2.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
@@ -175,9 +175,9 @@ function sessionSpend(events, sessionId) {
175
175
 
176
176
  function describeWindow(window) {
177
177
  if (!window) return null;
178
- return window.stale
179
- ? window.label + ' rolling over'
180
- : window.label + ' ' + window.percentUsed + '%';
178
+ if (window.stale) return window.label + ' rolling over';
179
+ const about = window.estimated ? ' about ' : ' ';
180
+ return window.label + about + window.percentUsed + '%';
181
181
  }
182
182
 
183
183
  // Everything except the window that will actually stop the work.
@@ -206,6 +206,12 @@ function briefText(parts) {
206
206
  ? '[usage-limits] binding window is ' + bound.join(', ') + '.'
207
207
  : '[usage-limits] no usable window reading.'
208
208
  );
209
+ if (parts.rebuilt) {
210
+ sentences.push(
211
+ 'That figure was rebuilt from local history because the ' +
212
+ 'snapshot is ' + parts.snapshotAge + ' old; run /usage to refresh it.'
213
+ );
214
+ }
209
215
  if (parts.othersSummary) sentences.push('Other windows: ' + parts.othersSummary + '.');
210
216
  if (parts.session) {
211
217
  sentences.push(
@@ -218,7 +224,9 @@ function briefText(parts) {
218
224
  parts.pressure === 'tight' || parts.pressure === 'gone'
219
225
  ? 'Open your reply with one line on where this leaves the budget, then say ' +
220
226
  'what you will do now and what you will leave for after the reset. ' +
221
- 'Do not start work that clearly will not finish.'
227
+ 'Do not start work that clearly will not finish. If several additions ' +
228
+ 'arrive while you are working, say once that sending them together ' +
229
+ 'costs less, then carry on; never say it about a correction or a stop.'
222
230
  : 'Open your reply with one short line stating this and confirming the ' +
223
231
  'request fits, then get on with the work. Keep it to a single line.';
224
232
 
@@ -240,38 +248,50 @@ async function run(now, hookInput) {
240
248
  if (!base.utilization) return '';
241
249
 
242
250
  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
251
  const all = readCache();
249
- const cached = pickCached(all, sessionId, now, config.cacheSeconds * SECOND);
252
+ let view = pickCached(all, sessionId, now, config.cacheSeconds * SECOND);
250
253
 
251
- let turnsLeft = cached ? cached.turnsLeft : null;
252
- let session = cached ? cached.session : null;
253
- let windows = cheap;
254
-
255
- if (!cached) {
254
+ // Everything shown has to come from one pass. Deriving the turns from a
255
+ // full scan and the binding window from somewhere cheaper is how the two
256
+ // end up describing different windows.
257
+ if (!view || !view.binding) {
256
258
  const events = await usage.readEvents(now - 8 * DAY);
257
- windows = usage.buildWindows(base.utilization, events, now);
259
+ const windows = usage.buildWindows(base.utilization, events, now);
258
260
  const binding = usage.bindingWindow(windows);
259
- turnsLeft = binding && Number.isFinite(binding.turnsLeft) ? binding.turnsLeft : null;
260
- session = sessionSpend(events, sessionId);
261
- writeCache(mergeCache(all, sessionId, { at: now, turnsLeft, session }, KEEP_SESSIONS));
261
+ view = {
262
+ at: now,
263
+ turnsLeft: binding && Number.isFinite(binding.turnsLeft) ? binding.turnsLeft : null,
264
+ session: sessionSpend(events, sessionId),
265
+ othersSummary: summariseOthers(windows, binding && binding.key),
266
+ binding: binding
267
+ ? {
268
+ key: binding.key,
269
+ label: binding.label,
270
+ percentUsed: binding.percentUsed,
271
+ stale: binding.stale,
272
+ estimated: binding.estimated,
273
+ resetsAt: binding.resetsAt,
274
+ verdict: binding.verdict,
275
+ windowStart: binding.windowStart,
276
+ spanMs: binding.spanMs,
277
+ }
278
+ : null,
279
+ };
280
+ writeCache(mergeCache(all, sessionId, view, KEEP_SESSIONS));
262
281
  }
263
282
 
264
- const binding = usage.bindingWindow(windows) || windows[0];
265
-
283
+ const binding = view.binding;
266
284
  return briefText({
267
285
  binding,
268
- othersSummary: summariseOthers(windows, binding && binding.key),
269
- turnsLeft,
286
+ othersSummary: view.othersSummary,
287
+ turnsLeft: view.turnsLeft,
270
288
  resetsIn:
271
- binding && !binding.stale && binding.msToReset !== null
272
- ? usage.formatDuration(binding.msToReset)
289
+ binding && !binding.stale && Number.isFinite(binding.resetsAt)
290
+ ? usage.formatDuration(binding.resetsAt - now)
273
291
  : null,
274
- session,
292
+ session: view.session,
293
+ rebuilt: Boolean(binding && binding.estimated),
294
+ snapshotAge: usage.formatDuration(base.snapshotAgeMs),
275
295
  pressure: pressure(binding, now, config),
276
296
  });
277
297
  }
@@ -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 = hasReset ? resetsAt - spec.span : now - spec.span;
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
 
@@ -667,6 +675,39 @@ function collect(now) {
667
675
  };
668
676
  }
669
677
 
678
+ // A snapshot only refreshes when Claude Code talks to the API, so after a
679
+ // gap it can be hours old and its 5-hour window long since rolled over.
680
+ // Dropping that window loses the limit that actually stops short work, so
681
+ // rebuild it from the transcripts instead.
682
+ //
683
+ // The trick is that the stale reading is still a usable calibration: whatever
684
+ // was spent inside the window it describes equalled its percentage. That
685
+ // dollars-per-point figure is a property of the plan, not of the moment, so it
686
+ // still prices the window running now.
687
+ function reconstructWindow(spec, snapshot, events, now) {
688
+ if (!snapshot || typeof snapshot.utilization !== 'number') return null;
689
+ if (snapshot.utilization <= 0) return null;
690
+
691
+ const resetsAt = snapshot.resets_at ? Date.parse(snapshot.resets_at) : null;
692
+ if (!Number.isFinite(resetsAt) || resetsAt > now) return null;
693
+
694
+ const pastStart = resetsAt - spec.span;
695
+ const past = totals(events.filter((e) => e.at >= pastStart && e.at <= resetsAt));
696
+ if (past.cost <= 0) return null;
697
+
698
+ const usdPerPercent = past.cost / snapshot.utilization;
699
+ const liveStart = now - spec.span;
700
+ const live = totals(events.filter((e) => e.at >= liveStart && e.at <= now));
701
+
702
+ return {
703
+ percentUsed: Math.min(100, Math.round(live.cost / usdPerPercent)),
704
+ usdPerPercent,
705
+ spentUSD: live.cost,
706
+ turns: live.turns,
707
+ windowStart: liveStart,
708
+ };
709
+ }
710
+
670
711
  // No snapshot at all means no windows, which is what tells the report to
671
712
  // explain itself rather than print a table of dashes.
672
713
  function buildWindows(utilization, events, now) {
@@ -675,7 +716,20 @@ function buildWindows(utilization, events, now) {
675
716
  const snapshot = utilization[spec.key];
676
717
  // The per-model weekly windows only exist on some plans.
677
718
  if (spec.key !== 'five_hour' && spec.key !== 'seven_day' && !snapshot) return null;
678
- return buildWindow(spec, snapshot, events, now);
719
+
720
+ const window = buildWindow(spec, snapshot, events, now);
721
+ if (!window.stale) return window;
722
+
723
+ // Rolled over. Rebuild from local history rather than going blind on it.
724
+ const rebuilt = reconstructWindow(spec, snapshot, events, now);
725
+ if (!rebuilt) return window;
726
+ return buildWindow(
727
+ spec,
728
+ { utilization: rebuilt.percentUsed, resets_at: null },
729
+ events,
730
+ now,
731
+ { estimated: true, windowStart: rebuilt.windowStart }
732
+ );
679
733
  }).filter(Boolean);
680
734
  }
681
735
 
@@ -842,7 +896,11 @@ function render(data) {
842
896
  lines.push(
843
897
  ' ' + pad(window.label, 15) +
844
898
  padLeft(
845
- window.stale ? 'stale' : window.percentUsed === null ? '-' : window.percentUsed + '%',
899
+ window.stale
900
+ ? 'stale'
901
+ : window.percentUsed === null
902
+ ? '-'
903
+ : (window.estimated ? '~' : '') + window.percentUsed + '%',
846
904
  6
847
905
  ) +
848
906
  padLeft(formatDuration(window.msToReset), 12) +
@@ -909,6 +967,15 @@ function render(data) {
909
967
  }
910
968
  lines.push(' Measured ' + formatCount(data.measuredTurns) + ' turns of local transcript');
911
969
 
970
+ if (data.windows.some((window) => window.estimated)) {
971
+ lines.push(
972
+ ' Note ~ means the snapshot had gone stale and that window was rebuilt'
973
+ );
974
+ lines.push(
975
+ ' from local history. Run /usage to replace it with a real reading.'
976
+ );
977
+ }
978
+
912
979
  if (data.binding && data.binding.coarse) {
913
980
  lines.push(' Note the meter reads in whole percent, so a low reading is a wide bracket');
914
981
  }
@@ -1064,6 +1131,7 @@ module.exports = {
1064
1131
  eventFrom,
1065
1132
  readEvents,
1066
1133
  buildWindow,
1134
+ reconstructWindow,
1067
1135
  buildWindows,
1068
1136
  bindingWindow,
1069
1137
  dominantEffort,