claude-usage-limits 1.7.0 → 1.7.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.7.0",
4
+ "version": "1.7.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",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "usage-limits",
3
- "version": "1.7.0",
3
+ "version": "1.7.1",
4
4
  "description": "Reports how much of your Codex usage limit is left as turns of work rather than a percentage, prices a job before you start it, and counts the other agents sharing the same budget.",
5
5
  "author": {
6
6
  "name": "Ridelink",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-usage-limits",
3
- "version": "1.7.0",
3
+ "version": "1.7.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",
@@ -361,6 +361,13 @@ function briefText(parts) {
361
361
  ? '[usage-limits] binding window is ' + bound.join(', ') + '.'
362
362
  : '[usage-limits] no usable window reading.'
363
363
  );
364
+ if (parts.planChanged) {
365
+ sentences.push(
366
+ 'The plan has changed since these figures were learned, so the reading ' +
367
+ 'above was measured against a different allowance and may predate the ' +
368
+ 'change; run /usage before trusting it.'
369
+ );
370
+ }
364
371
  if (parts.rebuilt) {
365
372
  sentences.push(
366
373
  'That figure was rebuilt from local history because the ' +
@@ -478,6 +485,7 @@ async function run(now, hookInput) {
478
485
  othersSummary: summariseOthers(data.windows, binding && binding.key),
479
486
  sessions: data.sessions,
480
487
  staleWindows: data.staleWindows,
488
+ planChanged: data.planChanged,
481
489
  critical: usage.criticalOthers(data.windows, binding && binding.key).map((w) => ({
482
490
  label: w.label,
483
491
  percentUsed: w.percentUsed,
@@ -526,6 +534,7 @@ async function run(now, hookInput) {
526
534
  session: view.session,
527
535
  rebuilt: Boolean(binding && binding.estimated),
528
536
  staleWindows: view.staleWindows || 0,
537
+ planChanged: Boolean(view.planChanged),
529
538
  critical: view.critical || [],
530
539
  pointsSinceSnapshot: (binding && binding.pointsSinceSnapshot) || 0,
531
540
  snapshotAge: view.snapshotAge,
@@ -678,6 +678,53 @@ function writeCalibration(all) {
678
678
  }
679
679
  }
680
680
 
681
+ // Everything learned about a budget belongs to the plan it was learned on.
682
+ //
683
+ // A point of a window is a share of an allowance, so changing the allowance
684
+ // changes what a point is worth, and every figure derived from the old one is
685
+ // then wrong by the ratio between the plans. Upgrading Pro to Max 5x is roughly
686
+ // a fivefold move: a calibration saying a point costs $0.40 keeps being applied
687
+ // to a point now worth several times that, and the turn estimates built on it
688
+ // are wrong in the direction that promises room there is not.
689
+ //
690
+ // There is no timestamp anywhere that says when the plan changed.
691
+ // `subscriptionCreatedAt` is the original signup, not the upgrade. So the plan
692
+ // is stamped onto the calibration instead, and a stamp that no longer matches
693
+ // is itself the proof that it moved.
694
+ function calibrationForPlan(all, planId) {
695
+ const kept = {};
696
+ let dropped = false;
697
+ for (const key of Object.keys(all || {})) {
698
+ const entry = all[key];
699
+ if (!entry || typeof entry !== 'object') continue;
700
+ // A stamp that no longer matches is proof the plan moved, and worth saying
701
+ // so: the reading on disk was measured against the other allowance too.
702
+ if (entry.plan && planId && entry.plan !== planId) {
703
+ dropped = true;
704
+ continue;
705
+ }
706
+ // An entry saved before the stamp existed cannot be shown to belong to this
707
+ // plan. Keeping it would be assuming the answer, and the cost of assuming
708
+ // wrong is the whole bug this guards: a point priced for Pro applied to a
709
+ // Max window, promising several times the turns that exist. It is dropped
710
+ // instead, which costs one relearn and says "unknown" in the meantime.
711
+ // Unknown is not claimed as a plan change, because it is not evidence of
712
+ // one; every install upgrading to this version passes through here once.
713
+ if (!entry.plan && planId) continue;
714
+ kept[key] = entry;
715
+ }
716
+ return { learned: kept, planChanged: dropped };
717
+ }
718
+
719
+ function stampPlan(all, planId) {
720
+ if (!planId) return all;
721
+ const stamped = {};
722
+ for (const key of Object.keys(all || {})) {
723
+ stamped[key] = Object.assign({}, all[key], { plan: planId });
724
+ }
725
+ return stamped;
726
+ }
727
+
681
728
  // A sample is better when it rests on more turns. Percentages read in whole
682
729
  // numbers, so a bigger percentage also divides more precisely.
683
730
  function betterCalibration(current, candidate) {
@@ -1280,7 +1327,11 @@ async function report(now, options) {
1280
1327
  }
1281
1328
 
1282
1329
  const onDisk = readCalibration();
1283
- const learned = Object.assign({}, onDisk);
1330
+ // Anything learned on a different plan is void, and its absence is what makes
1331
+ // the report say so rather than quietly pricing this plan with the last one's
1332
+ // numbers.
1333
+ const calibrated = calibrationForPlan(onDisk, base.planId);
1334
+ const learned = Object.assign({}, calibrated.learned);
1284
1335
 
1285
1336
  // Codex logs the meter next to every request, so the price of a point can be
1286
1337
  // measured outright instead of inferred. A measurement from this session
@@ -1299,7 +1350,11 @@ async function report(now, options) {
1299
1350
  base.snapshotFetchedAt,
1300
1351
  learned,
1301
1352
  base.windowSpecs,
1302
- rejections
1353
+ // A refusal describes the allowance that refused it. After a plan change
1354
+ // that allowance is gone, so anchoring the new window to it, or warning
1355
+ // that the room "ran out last time", is describing a budget that no longer
1356
+ // exists.
1357
+ calibrated.planChanged ? new Map() : rejections
1303
1358
  );
1304
1359
 
1305
1360
  // Keep the best sample seen so far, so a thin baseline never has to guess.
@@ -1311,11 +1366,11 @@ async function report(now, options) {
1311
1366
  }
1312
1367
  // Compared against what is actually on disk, so a measurement taken during
1313
1368
  // this run is saved too rather than only the ones inferred from a window.
1314
- let changed = false;
1369
+ let changed = calibrated.planChanged;
1315
1370
  for (const key of Object.keys(updated)) {
1316
1371
  if (updated[key] !== onDisk[key]) changed = true;
1317
1372
  }
1318
- if (changed) writeCalibration(updated);
1373
+ if (changed) writeCalibration(stampPlan(updated, base.planId));
1319
1374
 
1320
1375
  const recentEvents = events.filter((event) => event.at >= now - HOUR);
1321
1376
  const recent = totals(recentEvents);
@@ -1332,6 +1387,10 @@ async function report(now, options) {
1332
1387
  windows,
1333
1388
  binding,
1334
1389
  otherLimits: otherLimits(base.utilization),
1390
+ // The plan moved since anything was last learned about it, so the cached
1391
+ // percentage was measured against a different allowance and everything
1392
+ // derived from the old one has been dropped.
1393
+ planChanged: calibrated.planChanged,
1335
1394
  // The last time the account actually refused work, so a report taken just
1336
1395
  // after a cutoff says so rather than describing the fresh window as though
1337
1396
  // nothing happened.
@@ -1462,6 +1521,12 @@ function render(data) {
1462
1521
  (data.snapshotAgeMs === null ? 'none on disk' : formatDuration(data.snapshotAgeMs) + ' old')
1463
1522
  );
1464
1523
  lines.push(' Settings model=' + data.settings.model + ' effort=' + data.settings.effortLevel);
1524
+ if (data.planChanged) {
1525
+ lines.push(' Plan change this is a different plan from the one the figures below');
1526
+ lines.push(' were learned on, so what a point of a window is worth has');
1527
+ lines.push(' changed with it. The cached reading may predate the change:');
1528
+ lines.push(' run /usage for one measured against this plan.');
1529
+ }
1465
1530
  const credits = data.credits;
1466
1531
  if (credits && credits.unlimited) {
1467
1532
  lines.push(' Credits unlimited');
@@ -1884,6 +1949,8 @@ module.exports = {
1884
1949
  bindingWindow,
1885
1950
  criticalOthers,
1886
1951
  betterCalibration,
1952
+ calibrationForPlan,
1953
+ stampPlan,
1887
1954
  calibrationFile,
1888
1955
  CRITICAL_PERCENT,
1889
1956
  dominantEffort,