claude-usage-limits 1.4.0 → 1.5.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.0",
4
+ "version": "1.5.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,26 @@ node skills/usage-limits/scripts/usage.js
167
167
  node skills/usage-limits/scripts/usage.js --json
168
168
  ```
169
169
 
170
+ ## When another Claude is working too
171
+
172
+ Two Claude Code windows share one limit, so headroom measured in turns is
173
+ optimistic while another session is also spending. It watches for that:
174
+
175
+ ```
176
+ Sharing 2 sessions have spent in the last 15m, splitting this budget 75% / 25%
177
+ The turns above are the whole window, not your slice of it.
178
+ ```
179
+
180
+ and the before-prompt line says how many of those turns are actually yours:
181
+
182
+ ```
183
+ about 38 turns of headroom (2 sessions active, roughly 10 of them yours)
184
+ ```
185
+
186
+ The split comes from measured spend rather than an assumption that everyone is
187
+ working equally hard, because they usually are not. A session that has gone
188
+ quiet for a quarter of an hour is not counted as competing.
189
+
170
190
  ## Which limit it watches
171
191
 
172
192
  Two windows run at once and the 5-hour one is usually what actually stops you,
@@ -471,7 +491,7 @@ test/ node --test, no dependencies
471
491
  node --test
472
492
  ```
473
493
 
474
- 148 tests over the pricing, the window arithmetic, plan and credit detection,
494
+ 157 tests over the pricing, the window arithmetic, plan and credit detection,
475
495
  the status line, the before-prompt line, job forecasting, per-project
476
496
  attribution, the CLI, packaging, and the settings save/restore.
477
497
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-usage-limits",
3
- "version": "1.4.0",
3
+ "version": "1.5.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",
@@ -215,7 +215,14 @@ function briefText(parts) {
215
215
  const described = describeWindow(parts.binding);
216
216
  if (described) bound.push(described + (parts.binding.stale ? '' : ' used'));
217
217
  if (Number.isFinite(parts.turnsLeft)) {
218
- bound.push('about ' + parts.turnsLeft + ' turns of headroom');
218
+ // Another session spending the same budget means fewer of those turns are
219
+ // yours, so say both numbers rather than the flattering one.
220
+ const shared =
221
+ parts.sessions > 1 && Number.isFinite(parts.yourTurnsLeft)
222
+ ? ' (' + parts.sessions + ' sessions active, roughly ' + parts.yourTurnsLeft +
223
+ ' of them yours)'
224
+ : '';
225
+ bound.push('about ' + parts.turnsLeft + ' turns of headroom' + shared);
219
226
  }
220
227
  if (parts.resetsIn) bound.push('resets in ' + parts.resetsIn);
221
228
 
@@ -282,6 +289,7 @@ async function run(now, hookInput) {
282
289
  turnsLeft: binding && Number.isFinite(binding.turnsLeft) ? binding.turnsLeft : null,
283
290
  session: sessionSpend(events, sessionId),
284
291
  othersSummary: summariseOthers(windows, binding && binding.key),
292
+ sessions: usage.activeSessions(events, now, usage.CONCURRENT_WINDOW_MS),
285
293
  binding: binding
286
294
  ? {
287
295
  key: binding.key,
@@ -300,7 +308,13 @@ async function run(now, hookInput) {
300
308
  }
301
309
 
302
310
  const binding = view.binding;
311
+ const sessions = view.sessions || [];
312
+ const share = usage.shareOf(sessions, sessionId);
303
313
  return briefText({
314
+ sessions: sessions.length,
315
+ yourTurnsLeft: Number.isFinite(view.turnsLeft)
316
+ ? Math.max(1, Math.round(view.turnsLeft * share))
317
+ : null,
304
318
  binding,
305
319
  othersSummary: view.othersSummary,
306
320
  turnsLeft: view.turnsLeft,
@@ -499,6 +499,39 @@ function forecastWindow(window, turns, rates) {
499
499
  };
500
500
  }
501
501
 
502
+ const CONCURRENT_WINDOW_MS = 15 * MINUTE;
503
+
504
+ // Sessions that have spent something recently. Two Claude Code windows share
505
+ // one limit, so headroom measured in "turns" is optimistic when another one is
506
+ // also working: the budget drains while you are not the one spending it.
507
+ function activeSessions(events, now, windowMs) {
508
+ const since = now - (Number.isFinite(windowMs) ? windowMs : CONCURRENT_WINDOW_MS);
509
+ const bySession = new Map();
510
+
511
+ for (const event of events) {
512
+ if (event.at < since || event.at > now) continue;
513
+ const id = event.sessionId || 'unknown';
514
+ if (!bySession.has(id)) bySession.set(id, { sessionId: id, turns: 0, cost: 0 });
515
+ const row = bySession.get(id);
516
+ row.turns += 1;
517
+ row.cost += event.cost;
518
+ }
519
+
520
+ const rows = [...bySession.values()].sort((a, b) => b.cost - a.cost);
521
+ const total = rows.reduce((sum, row) => sum + row.cost, 0);
522
+ for (const row of rows) row.share = total > 0 ? row.cost / total : 0;
523
+ return rows;
524
+ }
525
+
526
+ // The slice of the shared budget this session is actually getting. With
527
+ // another session spending half of it, only half those turns are yours.
528
+ function shareOf(sessions, sessionId) {
529
+ if (!sessions || sessions.length < 2) return 1;
530
+ const mine = sessions.find((row) => row.sessionId === sessionId);
531
+ if (!mine) return 1 / sessions.length;
532
+ return mine.share > 0 ? mine.share : 1 / sessions.length;
533
+ }
534
+
502
535
  // Everything the report needs about one limit window.
503
536
  function buildWindow(spec, snapshot, events, now, options) {
504
537
  const extra = options || {};
@@ -550,9 +583,19 @@ function buildWindow(spec, snapshot, events, now, options) {
550
583
  };
551
584
 
552
585
  // Calibrate against this account: how many dollars of measured traffic
553
- // moved the meter one point.
554
- if (percent !== null && percent > 0 && spent.cost > 0) {
555
- window.usdPerPercent = spent.cost / percent;
586
+ // moved the meter one point. A rebuilt window hands its own figure in,
587
+ // because rounding to 0% would otherwise leave it unpriced and drop it out
588
+ // of the binding choice just after a reset.
589
+ const derived = percent !== null && percent > 0 && spent.cost > 0 ? spent.cost / percent : null;
590
+ const priced =
591
+ derived !== null
592
+ ? derived
593
+ : Number.isFinite(extra.usdPerPercent) && extra.usdPerPercent > 0
594
+ ? extra.usdPerPercent
595
+ : null;
596
+
597
+ if (percent !== null && priced !== null) {
598
+ window.usdPerPercent = priced;
556
599
  window.remainingUSD = window.usdPerPercent * window.percentLeft;
557
600
  // The API reports whole numbers, so a low reading is a wide bracket.
558
601
  window.coarse = percent < 5;
@@ -773,7 +816,11 @@ function buildWindows(utilization, events, now) {
773
816
  { utilization: rebuilt.percentUsed, resets_at: null },
774
817
  events,
775
818
  now,
776
- { estimated: true, windowStart: rebuilt.windowStart }
819
+ {
820
+ estimated: true,
821
+ windowStart: rebuilt.windowStart,
822
+ usdPerPercent: rebuilt.usdPerPercent,
823
+ }
777
824
  );
778
825
  }).filter(Boolean);
779
826
  }
@@ -799,6 +846,7 @@ async function report(now) {
799
846
  windows,
800
847
  binding,
801
848
  credits: creditsFrom(base.utilization),
849
+ sessions: activeSessions(events, now, CONCURRENT_WINDOW_MS),
802
850
  rates: costPercentiles(recentEvents.length >= 5 ? recentEvents : scoped),
803
851
  resumeAt: binding ? binding.resetsAt : null,
804
852
  models: byModel(scoped),
@@ -1001,6 +1049,17 @@ function render(data) {
1001
1049
  lines.push('');
1002
1050
  }
1003
1051
 
1052
+ if (data.sessions && data.sessions.length > 1) {
1053
+ const split = data.sessions.map((row) => Math.round(row.share * 100) + '%').join(' / ');
1054
+ lines.push(
1055
+ ' Sharing ' + data.sessions.length + ' sessions have spent in the last 15m, ' +
1056
+ 'splitting this budget ' + split
1057
+ );
1058
+ lines.push(
1059
+ ' The turns above are the whole window, not your slice of it.'
1060
+ );
1061
+ }
1062
+
1004
1063
  if (data.recent.turns) {
1005
1064
  lines.push(
1006
1065
  ' Recent pace ' + data.recent.turns + ' turns in the last hour, ' +
@@ -1181,6 +1240,9 @@ module.exports = {
1181
1240
  bindingWindow,
1182
1241
  dominantEffort,
1183
1242
  typicalTurnCost,
1243
+ activeSessions,
1244
+ shareOf,
1245
+ CONCURRENT_WINDOW_MS,
1184
1246
  MIN_PACE_SAMPLE,
1185
1247
  formatDuration,
1186
1248
  formatUSD,