claude-usage-limits 1.7.1 → 1.9.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.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +118 -12
- package/bin/cli.js +3 -0
- package/commands/session.md +13 -0
- package/hooks/hooks.json +23 -1
- package/package.json +1 -1
- package/skills/usage-limits/SKILL.md +69 -1
- package/skills/usage-limits/references/how-it-works.md +15 -0
- package/skills/usage-limits/references/tactics.md +8 -1
- package/skills/usage-limits/scripts/brief.js +147 -11
- package/skills/usage-limits/scripts/lowpower.js +9 -0
- package/skills/usage-limits/scripts/pulse.js +6 -4
- package/skills/usage-limits/scripts/recommend.js +295 -0
- package/skills/usage-limits/scripts/sessionend.js +47 -0
- package/skills/usage-limits/scripts/stop.js +55 -0
- package/skills/usage-limits/scripts/tally.js +378 -0
- package/skills/usage-limits/scripts/usage.js +488 -70
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "usage-limits",
|
|
3
3
|
"displayName": "Usage Limits",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.9.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",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "usage-limits",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
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/README.md
CHANGED
|
@@ -307,7 +307,7 @@ one line into Claude's context:
|
|
|
307
307
|
|
|
308
308
|
```
|
|
309
309
|
[usage-limits] binding window is 5-hour 47% used, about 75 turns of headroom,
|
|
310
|
-
resets in 3h 52m. Other windows: weekly 16%. This session: 229 turns, $64.16.
|
|
310
|
+
resets in 3h 52m. Other windows: weekly 16%. This session: 229 turns, 14.2M tokens, $64.16.
|
|
311
311
|
```
|
|
312
312
|
|
|
313
313
|
It names the window that will stop the work first and hangs the figures off
|
|
@@ -344,12 +344,67 @@ minute, so it costs about 400ms cold and 120ms warm.
|
|
|
344
344
|
|
|
345
345
|
| Variable | Default | Effect |
|
|
346
346
|
| --- | --- | --- |
|
|
347
|
-
| `USAGE_LIMITS_BRIEF` | on | Set to `off` to turn the line off entirely. |
|
|
348
|
-
| `USAGE_LIMITS_NEAR` |
|
|
349
|
-
| `
|
|
350
|
-
| `
|
|
347
|
+
| `USAGE_LIMITS_BRIEF` | on | Set to `off` to turn the before-prompt line off entirely. |
|
|
348
|
+
| `USAGE_LIMITS_NEAR` | 90 | Percent used at which the budget counts as tight. Nothing below it is discouraged. |
|
|
349
|
+
| `USAGE_LIMITS_FEW_TURNS` | 10 | Turns of headroom at or below which the budget counts as tight. |
|
|
350
|
+
| `USAGE_LIMITS_RUNWAY` | 10 | Minutes of runway at the current pace below which the budget counts as tight. |
|
|
351
351
|
| `USAGE_LIMITS_CACHE` | 60 | Seconds the measured half stays good for. |
|
|
352
|
-
| `
|
|
352
|
+
| `USAGE_LIMITS_FLOOR`, `USAGE_LIMITS_AHEAD` | 40, 15 | Only feed the reported pace figure; they no longer change the wording. |
|
|
353
|
+
| `USAGE_LIMITS_PULSE` | on | `off` silences the mid-turn line; `always` prints it even when there is room. |
|
|
354
|
+
| `USAGE_LIMITS_PULSE_SECONDS` | 120 | How often the mid-turn line can fire. |
|
|
355
|
+
| `USAGE_LIMITS_TALLY` | on | Set to `off` to turn off the after-reply tally, the closing line and the session history. |
|
|
356
|
+
|
|
357
|
+
## What a session cost
|
|
358
|
+
|
|
359
|
+
The other half of the question. After every reply, a `Stop` hook shows you one
|
|
360
|
+
line with what that reply cost and what the session has cost so far, tokens
|
|
361
|
+
first because that is what people ask:
|
|
362
|
+
|
|
363
|
+
```
|
|
364
|
+
[usage-limits] this reply: 6 turns, 210k tokens, $0.95. This session: 9 prompts,
|
|
365
|
+
48 turns, 3.1M tokens (2.9M cache read, 61k output), about $12.40, roughly 31
|
|
366
|
+
points of the 5-hour window. Context is now about 130k tokens.
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
It goes to you, not into the context, so it costs the model nothing. It reads
|
|
370
|
+
only the bytes of the transcript written since the previous reply, including
|
|
371
|
+
any subagent transcripts under the session's own folder, so it takes a few
|
|
372
|
+
milliseconds however long the session has run. When the session closes, a
|
|
373
|
+
`SessionEnd` hook prints the closing line:
|
|
374
|
+
|
|
375
|
+
```
|
|
376
|
+
[usage-limits] session closed after 1h 42m: 9 prompts, 48 turns, 3.1M tokens, about $12.40.
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
Claude is also asked to end finished work with the total in its own words, one
|
|
380
|
+
plain line, and to skip it on partial progress. The before-prompt line carries
|
|
381
|
+
the session's tokens, what the last reply cost, and how large the context has
|
|
382
|
+
become, with one clause of advice once it passes 150k tokens, because the
|
|
383
|
+
context is re-sent on every call and past a point it is the cost of the session.
|
|
384
|
+
|
|
385
|
+
The history is kept in `usage-limits-sessions.json` beside the other caches:
|
|
386
|
+
|
|
387
|
+
```
|
|
388
|
+
node skills/usage-limits/scripts/usage.js --sessions
|
|
389
|
+
node skills/usage-limits/scripts/usage.js --session last
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
```
|
|
393
|
+
Id When Project Prompts Turns Tokens Cost
|
|
394
|
+
4940f126 9m ago C--Users-OWNER 1 16 2.8M $11.36 open
|
|
395
|
+
380e664a 9m ago C--Users-OWNER 10 112+35 51.0M $93.11 open
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
`--session last` (or an id, or a unique prefix of one) shows one session in
|
|
399
|
+
full: the token split, the model mix, the subagent calls and the context size.
|
|
400
|
+
Installed as a plugin, `/usage-limits:session` reads the same thing back.
|
|
401
|
+
Turns are main-thread calls; `+N` is what subagents made on top. Set
|
|
402
|
+
`USAGE_LIMITS_TALLY=off` to turn all of this off.
|
|
403
|
+
|
|
404
|
+
If you installed the plain skill rather than the plugin, add the two hooks
|
|
405
|
+
beside the first one in `settings.json`: `Stop` running `scripts/stop.js` and
|
|
406
|
+
`SessionEnd` running `scripts/sessionend.js`.
|
|
407
|
+
|
|
353
408
|
|
|
354
409
|
## What would this job cost
|
|
355
410
|
|
|
@@ -375,6 +430,44 @@ times one that answers from context. The upper end is the honest one for a long
|
|
|
375
430
|
run, since turns get dearer as the context grows.
|
|
376
431
|
|
|
377
432
|
|
|
433
|
+
## Which effort and model should this run at
|
|
434
|
+
|
|
435
|
+
```
|
|
436
|
+
node skills/usage-limits/scripts/usage.js --recommend # against the headroom
|
|
437
|
+
node skills/usage-limits/scripts/usage.js --recommend 15 # against a 15 turn job
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
```
|
|
441
|
+
Recommendation for 15 turns
|
|
442
|
+
|
|
443
|
+
Posture tight - a 15 turn job fits, but only just (5-hour window, 12% left, resets in 1h 40m)
|
|
444
|
+
Effort xhigh -> medium; one notch covers the mechanical stretches; keep judgement calls at full effort
|
|
445
|
+
this session: /effort medium (only the user can run it)
|
|
446
|
+
new sessions: node scripts/lowpower.js on --effort medium
|
|
447
|
+
Model keep opus for the judgement; the saving is in where the mechanical bulk runs
|
|
448
|
+
dispatch self-contained mechanical work to a subagent on sonnet at low effort, and keep the judgement here
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
It weighs the binding window, the measured cost of a turn, and how much of the
|
|
452
|
+
output is actually reasoning, then names a posture - roomy, tight, critical, or
|
|
453
|
+
reset-first - and the exact commands. When there is room it says to keep
|
|
454
|
+
everything as it is, out loud, because turning effort down when the budget is
|
|
455
|
+
not tight buys nothing and costs quality. When reasoning is only a sliver of
|
|
456
|
+
the output it says so too, and leaves effort alone: the reasoning share is the
|
|
457
|
+
ceiling on what lowering effort can save. `--json` returns the decision as an
|
|
458
|
+
object.
|
|
459
|
+
|
|
460
|
+
The three levers it recommends across belong to different hands. The running
|
|
461
|
+
session's effort and model are the user's alone (`/effort`, `/model`, applied
|
|
462
|
+
immediately); new sessions belong to `lowpower.js`, which writes `settings.json`
|
|
463
|
+
for the next launch; and delegated work belongs to the agent itself, which can
|
|
464
|
+
dispatch a subagent on any model at any effort, mid-session, with no one asked.
|
|
465
|
+
No script or hook can change the model or effort of a session already running -
|
|
466
|
+
`settings.json` is read at launch and hook output has no model field - which is
|
|
467
|
+
why the recommendation separates "this session" from "new sessions" instead of
|
|
468
|
+
pretending one command covers both.
|
|
469
|
+
|
|
470
|
+
|
|
378
471
|
## Plans
|
|
379
472
|
|
|
380
473
|
It reads which plan you are on and adjusts what it tells you, because the
|
|
@@ -490,6 +583,10 @@ overwrite the saved originals.
|
|
|
490
583
|
The file change applies to new sessions. For a session already running,
|
|
491
584
|
`/effort low` does the same thing immediately.
|
|
492
585
|
|
|
586
|
+
`--effort max` is refused: `settings.json` does not accept `max`, so saving it
|
|
587
|
+
would store a value the next session silently ignores. `max` lives in
|
|
588
|
+
`/effort` and `CLAUDE_CODE_EFFORT_LEVEL` only.
|
|
589
|
+
|
|
493
590
|
That covers the setting. The larger saving is behavioural, and the skill file
|
|
494
591
|
spells it out: batch tool calls, read line ranges instead of whole files, skip
|
|
495
592
|
subagents when the context already exists, stop retrying a fix that is not
|
|
@@ -528,6 +625,12 @@ Good enough to plan with, not a bill. The honest caveats:
|
|
|
528
625
|
doubling the limits. If demand-based limits ever return, the numbers here
|
|
529
626
|
follow automatically, because they are calibrated from what your traffic did
|
|
530
627
|
to the meter rather than from an assumption about the clock.
|
|
628
|
+
- Subagent transcripts, written under the session's own folder, are read too.
|
|
629
|
+
Their calls count in the money and the tokens and are reported apart from
|
|
630
|
+
the turns, because a turn is one main-thread call.
|
|
631
|
+
- The account's own list of limits is read as well as the per-window buckets,
|
|
632
|
+
so a per-model weekly such as `weekly (Fable)` shows up as a window of its
|
|
633
|
+
own, priced from that model's calls alone.
|
|
531
634
|
- The turn cost behind "turns of headroom" is a median over at least five
|
|
532
635
|
turns, so one compaction cannot define your pace.
|
|
533
636
|
- What a point of a window costs is learned once from the best sample seen and
|
|
@@ -565,12 +668,15 @@ names, the formulas, and the rest of it.
|
|
|
565
668
|
agents/openai.yaml how Codex lists the plugin
|
|
566
669
|
skills/usage-limits/SKILL.md what the agent reads
|
|
567
670
|
skills/usage-limits/agents/ how Codex lists the skill
|
|
568
|
-
skills/usage-limits/scripts/ usage.js, brief.js, pulse.js,
|
|
569
|
-
|
|
671
|
+
skills/usage-limits/scripts/ usage.js, brief.js, pulse.js, stop.js,
|
|
672
|
+
sessionend.js, tally.js, codex.js, host.js,
|
|
673
|
+
lowpower.js, install-codex-hook.js
|
|
570
674
|
skills/usage-limits/references/ the longer notes
|
|
571
|
-
hooks/hooks.json runs brief.js before each prompt,
|
|
572
|
-
|
|
675
|
+
hooks/hooks.json runs brief.js before each prompt, pulse.js
|
|
676
|
+
during long turns, stop.js after each reply
|
|
677
|
+
and sessionend.js when the session closes
|
|
573
678
|
commands/check.md the /usage-limits:check command
|
|
679
|
+
commands/session.md the /usage-limits:session command
|
|
574
680
|
bin/cli.js the npx entry point
|
|
575
681
|
tools/sync-version.js keeps the manifest version in step
|
|
576
682
|
test/ node --test, no dependencies
|
|
@@ -582,8 +688,8 @@ test/ node --test, no dependencies
|
|
|
582
688
|
node --test
|
|
583
689
|
```
|
|
584
690
|
|
|
585
|
-
|
|
586
|
-
the status line, the before-prompt line, the mid-turn pulse, job forecasting,
|
|
691
|
+
308 tests over the pricing, the window arithmetic, plan and credit detection,
|
|
692
|
+
the status line, the before-prompt line, the mid-turn pulse, the after-reply tally and the session history, job forecasting,
|
|
587
693
|
per-project attribution, the Codex reader and its installer, the CLI,
|
|
588
694
|
packaging, and the settings save/restore.
|
|
589
695
|
|
package/bin/cli.js
CHANGED
|
@@ -11,6 +11,9 @@ const HELP = `claude-usage-limits - how much agent usage is left, and whether th
|
|
|
11
11
|
claude-usage-limits --json the same numbers, machine readable
|
|
12
12
|
claude-usage-limits --status one short line, for a status line
|
|
13
13
|
claude-usage-limits --forecast 15 what a 15 turn job would cost
|
|
14
|
+
claude-usage-limits --recommend [15] which effort and model this budget calls for
|
|
15
|
+
claude-usage-limits --sessions what recent sessions cost
|
|
16
|
+
claude-usage-limits --session last one session in full, or give its id
|
|
14
17
|
|
|
15
18
|
claude-usage-limits --host codex read Codex's limits instead
|
|
16
19
|
claude-usage-limits --host codex --refresh ask Codex for a live reading
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Report what this session has cost so far in prompts, turns, tokens and money
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Run `node "${CLAUDE_PLUGIN_ROOT}/skills/usage-limits/scripts/usage.js" --session last`
|
|
6
|
+
and read it back to me in one or two plain lines: prompts, turns (and subagent
|
|
7
|
+
turns if any), tokens with the split, the cost, and how large the context is now.
|
|
8
|
+
|
|
9
|
+
`last` is the most recently active session on this machine, which is normally
|
|
10
|
+
this one. If more than one window is open and the project named does not look
|
|
11
|
+
like this one, run `--sessions` and pick the right id.
|
|
12
|
+
|
|
13
|
+
Report the numbers and stop. Do not start other work as part of this command.
|
package/hooks/hooks.json
CHANGED
|
@@ -1,6 +1,28 @@
|
|
|
1
1
|
{
|
|
2
|
-
"description": "Tells Claude how much usage budget is left: once before each prompt, and again during long turns so the figure it is working from does not go stale.",
|
|
2
|
+
"description": "Tells Claude how much usage budget is left: once before each prompt, and again during long turns so the figure it is working from does not go stale. Tells you what each reply and each session cost, after the reply and when the session closes.",
|
|
3
3
|
"hooks": {
|
|
4
|
+
"Stop": [
|
|
5
|
+
{
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/skills/usage-limits/scripts/stop.js\"",
|
|
10
|
+
"timeout": 10
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"SessionEnd": [
|
|
16
|
+
{
|
|
17
|
+
"hooks": [
|
|
18
|
+
{
|
|
19
|
+
"type": "command",
|
|
20
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/skills/usage-limits/scripts/sessionend.js\"",
|
|
21
|
+
"timeout": 5
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
],
|
|
4
26
|
"UserPromptSubmit": [
|
|
5
27
|
{
|
|
6
28
|
"hooks": [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-usage-limits",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.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",
|
|
@@ -225,6 +225,31 @@ context grows.
|
|
|
225
225
|
|
|
226
226
|
Reach for it whenever the answer to "will this finish" is not obvious.
|
|
227
227
|
|
|
228
|
+
## Closing with what it cost
|
|
229
|
+
|
|
230
|
+
The budget line opens the reply; the cost closes it. When a reply completes
|
|
231
|
+
what was asked, or wraps up the session, end it with one plain line:
|
|
232
|
+
|
|
233
|
+
> Spent this session: 48 turns, 3.1M tokens, about $12.40.
|
|
234
|
+
|
|
235
|
+
The figures come from the `This session:` part of the budget line, so there is
|
|
236
|
+
nothing to run. They are exact as of the start of this turn, and the Stop hook
|
|
237
|
+
prints the up-to-date figure to the user the moment you finish, so do not go
|
|
238
|
+
and re-measure for the closing line.
|
|
239
|
+
|
|
240
|
+
Three rules keep it useful rather than noisy:
|
|
241
|
+
|
|
242
|
+
- **Finished work only.** A progress note mid-task, a clarifying question, or
|
|
243
|
+
a reply that ends with "shall I go on" is not the moment. The line marks a
|
|
244
|
+
goal reached or a session wrapped up.
|
|
245
|
+
- **One line, at the end.** It is a footer, not a section, and it never
|
|
246
|
+
replaces the recap of what was actually done.
|
|
247
|
+
- **Plain text.** No symbols, no formatting tricks. The number is the point.
|
|
248
|
+
|
|
249
|
+
The user can also ask any time with `/usage-limits:session`, or run
|
|
250
|
+
`node scripts/usage.js --session last` for the full breakdown and
|
|
251
|
+
`--sessions` for the history of recent sessions on this machine.
|
|
252
|
+
|
|
228
253
|
|
|
229
254
|
## 4. Low power
|
|
230
255
|
|
|
@@ -247,6 +272,45 @@ to `low` is the largest per-turn saving available without changing model or
|
|
|
247
272
|
scope. The file change applies to new sessions; for the session already
|
|
248
273
|
running, `/effort low` takes effect immediately.
|
|
249
274
|
|
|
275
|
+
### Choosing the level
|
|
276
|
+
|
|
277
|
+
Do not guess which effort or model the budget calls for; the report can say:
|
|
278
|
+
|
|
279
|
+
```
|
|
280
|
+
node scripts/usage.js --recommend # against the headroom in general
|
|
281
|
+
node scripts/usage.js --recommend 15 # against a 15 turn job
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
It weighs the binding window, the measured cost of a turn, and how much of
|
|
285
|
+
the output is actually reasoning, then names the posture (roomy, tight,
|
|
286
|
+
critical, or reset-first) and the exact commands. When there is room it says
|
|
287
|
+
to keep everything as it is, out loud, so cheapness never becomes a habit.
|
|
288
|
+
Add `--json` for the decision as fields.
|
|
289
|
+
|
|
290
|
+
The three levers it recommends across belong to different hands, and keeping
|
|
291
|
+
that straight is the whole trick:
|
|
292
|
+
|
|
293
|
+
| Lever | Whose hand | When it acts |
|
|
294
|
+
| --- | --- | --- |
|
|
295
|
+
| `/effort`, `/model` | the user's only | this session, immediately |
|
|
296
|
+
| `lowpower.js on` (writes settings.json) | yours, right now | new sessions, at launch |
|
|
297
|
+
| subagent model and effort | yours, freely | that dispatch, immediately |
|
|
298
|
+
|
|
299
|
+
The running session's own model and effort cannot be changed by any script or
|
|
300
|
+
hook: settings.json is read at launch and hook output has no model field. So
|
|
301
|
+
when the recommendation says `/effort low`, put that in front of the user as
|
|
302
|
+
one short line and keep working; do not wait on it. What can be done without
|
|
303
|
+
asking anyone is the other two rows: write the next session's settings with
|
|
304
|
+
`lowpower.js`, and send self-contained mechanical bulk to a subagent on a
|
|
305
|
+
cheaper model at low effort, which is a change of model that needs nobody's
|
|
306
|
+
permission. The cold start still costs (see `references/tactics.md` on
|
|
307
|
+
subagents), so delegate work that is big and mechanical, not quick questions.
|
|
308
|
+
|
|
309
|
+
One catch to know about: `settings.json` does not accept `max`, so a saved
|
|
310
|
+
effort level tops out at `xhigh`. `max` only survives through `/effort` or the
|
|
311
|
+
`CLAUDE_CODE_EFFORT_LEVEL` environment variable, and `lowpower.js` refuses to
|
|
312
|
+
write it rather than save a value the next session would silently ignore.
|
|
313
|
+
|
|
250
314
|
Do not take that on trust: the report measures it. Under the model table it
|
|
251
315
|
says how much of the output was reasoning and what that cost, for example
|
|
252
316
|
|
|
@@ -359,12 +423,16 @@ a turn, which is the thing it is trying to save.
|
|
|
359
423
|
|
|
360
424
|
| Path | What it is |
|
|
361
425
|
| --- | --- |
|
|
362
|
-
| `scripts/usage.js` | The report. `--json` for raw fields, `--status` for a one-line readout that skips the transcript scan, `--forecast N` for what an N turn job would cost, `--host codex` to read Codex's limits, `--refresh` to ask Codex for a live figure. |
|
|
426
|
+
| `scripts/usage.js` | The report. `--json` for raw fields, `--status` for a one-line readout that skips the transcript scan, `--forecast N` for what an N turn job would cost, `--sessions` for what recent sessions cost, `--session last` (or an id) for one session in full, `--host codex` to read Codex's limits, `--refresh` to ask Codex for a live figure. |
|
|
363
427
|
| `scripts/brief.js` | What the hook runs before each prompt. Not meant to be called by hand. |
|
|
364
428
|
| `scripts/pulse.js` | What runs after tool calls, to re-check the budget during a long turn. Not meant to be called by hand. |
|
|
429
|
+
| `scripts/stop.js` | What runs after each reply: shows the user what the reply and the session have cost. Not meant to be called by hand. |
|
|
430
|
+
| `scripts/sessionend.js` | What runs when the session closes: the closing line, and the session marked closed in the history. Not meant to be called by hand. |
|
|
431
|
+
| `scripts/tally.js` | The running per-session total behind those two, kept in `usage-limits-sessions.json` and read incrementally. |
|
|
365
432
|
| `scripts/host.js` | Works out which agent this is running inside, so one host's percentages are never reported against the other's turns. |
|
|
366
433
|
| `scripts/codex.js` | The Codex reader: the meter and the pace out of `~/.codex/sessions`, plus the live `--refresh` call. |
|
|
367
434
|
| `scripts/install-codex-hook.js` | `status`, `on`, `off`. Installs the Codex-side instruction, which Claude Code does not need. |
|
|
368
435
|
| `scripts/lowpower.js` | `status`, `on`, `off`. Restores what it replaced. Claude Code only. |
|
|
436
|
+
| `scripts/recommend.js` | The chooser behind `usage.js --recommend`: posture, then the effort and model commands for each lever. Not meant to be called by hand. |
|
|
369
437
|
| `references/tactics.md` | Every lever that lowers cost, and why it works. |
|
|
370
438
|
| `references/how-it-works.md` | Where the numbers come from and where they are soft. |
|
|
@@ -28,6 +28,14 @@ the report includes them when they are present.
|
|
|
28
28
|
If the key is missing, run `/usage` once inside Claude Code. That is what
|
|
29
29
|
populates it.
|
|
30
30
|
|
|
31
|
+
The same object carries a `limits` array: one entry per limit the account
|
|
32
|
+
enforces, each with `kind` (`session`, `weekly_all`, `weekly_scoped`),
|
|
33
|
+
`percent`, `severity`, `resets_at`, `is_active`, and for the scoped weeklies
|
|
34
|
+
which model they cover. The report reads it too. A scoped weekly becomes a
|
|
35
|
+
window of its own, labelled `weekly (Fable)` and priced from that model's
|
|
36
|
+
calls alone; `is_active` breaks ties in the binding choice; and a bucket that
|
|
37
|
+
quotes `limit_dollars` is priced from that directly instead of being calibrated.
|
|
38
|
+
|
|
31
39
|
The plan name comes from `oauthAccount.organizationType` in the same file.
|
|
32
40
|
Current effort and model come from `settings.json` in the config directory.
|
|
33
41
|
|
|
@@ -64,6 +72,13 @@ Assistant turns carry a usage record:
|
|
|
64
72
|
}
|
|
65
73
|
```
|
|
66
74
|
|
|
75
|
+
Subagents write their own transcripts under
|
|
76
|
+
`<project>/<session id>/subagents/agent-*.jsonl`, with `isSidechain: true` and
|
|
77
|
+
the parent's `sessionId`. They are read too: their calls count in the money and
|
|
78
|
+
the tokens, and are kept apart from the turns, because a turn is one main-thread
|
|
79
|
+
call. Lines from a model called `<synthetic>` are interrupts and client-side
|
|
80
|
+
errors, not calls, and are skipped.
|
|
81
|
+
|
|
67
82
|
Files whose modification time predates the window are skipped. Turns are keyed
|
|
68
83
|
by `message.id` plus `requestId` and counted once, because resuming or forking
|
|
69
84
|
a session copies earlier turns into a new file.
|
|
@@ -47,7 +47,7 @@ Per million tokens, input and output:
|
|
|
47
47
|
| Model | Input | Output |
|
|
48
48
|
| --- | --- | --- |
|
|
49
49
|
| Opus 5 | $5 | $25 |
|
|
50
|
-
| Sonnet 5 | $
|
|
50
|
+
| Sonnet 5 | $2 | $10 |
|
|
51
51
|
| Haiku 4.5 | $1 | $5 |
|
|
52
52
|
|
|
53
53
|
Renaming symbols, writing boilerplate tests, formatting, mechanical
|
|
@@ -56,6 +56,13 @@ Switch at a task boundary rather than mid-task, because a model switch
|
|
|
56
56
|
invalidates the prompt cache and the rebuild can cost more than the saving on
|
|
57
57
|
a short remaining task.
|
|
58
58
|
|
|
59
|
+
The switch needs no boundary at all when the work is delegated: a subagent
|
|
60
|
+
can be dispatched on a cheaper model at low effort mid-session, and that is
|
|
61
|
+
the one model change the agent can make without anyone's hand on `/model`.
|
|
62
|
+
It pays the cold start from lever 7, so it is for self-contained bulky work,
|
|
63
|
+
not quick questions. `node scripts/usage.js --recommend` prices whether the
|
|
64
|
+
budget calls for it.
|
|
65
|
+
|
|
59
66
|
### 3. Read less, and read it once
|
|
60
67
|
|
|
61
68
|
- Line ranges instead of whole files. Grep with a result limit instead of
|
|
@@ -15,6 +15,7 @@ const path = require('path');
|
|
|
15
15
|
|
|
16
16
|
const usage = require('./usage.js');
|
|
17
17
|
const host = require('./host.js');
|
|
18
|
+
const tally = require('./tally.js');
|
|
18
19
|
|
|
19
20
|
const SECOND = 1000;
|
|
20
21
|
const DAY = 24 * 60 * 60 * 1000;
|
|
@@ -133,6 +134,20 @@ function liveSessions(all, now, windowMs, exceptId) {
|
|
|
133
134
|
return count;
|
|
134
135
|
}
|
|
135
136
|
|
|
137
|
+
// How many sessions are sharing the budget, and how much of it is this one's.
|
|
138
|
+
// One place for both hooks. The pulse once counted only sessions that had
|
|
139
|
+
// spent while the brief also counted those that had merely prompted, and the
|
|
140
|
+
// two lines disagreed about how many sessions there were minutes apart. The
|
|
141
|
+
// spend-derived count is the accurate one when it has caught up; the
|
|
142
|
+
// open-session count is the one that is right immediately. Take whichever is
|
|
143
|
+
// higher, because under-counting is what makes the headroom read as more
|
|
144
|
+
// yours than it is.
|
|
145
|
+
function activeShare(sessions, slots, now, sessionId) {
|
|
146
|
+
const spent = sessions || [];
|
|
147
|
+
const active = Math.max(spent.length, liveSessions(slots, now, LIVE_WINDOW_MS, sessionId) + 1);
|
|
148
|
+
return { active, share: active > 1 ? usage.shareOf(spent, sessionId, active) : 1 };
|
|
149
|
+
}
|
|
150
|
+
|
|
136
151
|
// Keep the newest few so a machine with many sessions does not grow the file
|
|
137
152
|
// without bound.
|
|
138
153
|
function mergeCache(all, sessionId, entry, keep) {
|
|
@@ -252,6 +267,7 @@ const CACHED_BINDING_FIELDS = [
|
|
|
252
267
|
'adjusted',
|
|
253
268
|
'pointsSinceSnapshot',
|
|
254
269
|
'correctionUnreliable',
|
|
270
|
+
'pointsBeyondSnapshot',
|
|
255
271
|
'resetsAt',
|
|
256
272
|
'verdict',
|
|
257
273
|
'windowStart',
|
|
@@ -332,7 +348,10 @@ function briefText(parts) {
|
|
|
332
348
|
// the binding window is named and its figures are attached to it.
|
|
333
349
|
const bound = [];
|
|
334
350
|
const described = describeWindow(parts.binding);
|
|
335
|
-
|
|
351
|
+
// When spending since the snapshot has outrun what the snapshot said was
|
|
352
|
+
// left, the percentage is the last real reading, not the current one, and
|
|
353
|
+
// the headline has to say so or every later prompt repeats a stale number.
|
|
354
|
+
if (described) bound.push(described + (parts.binding.stale ? '' : (parts.correctionUnreliable ? ' used at the last real reading' : ' used')));
|
|
336
355
|
if (Number.isFinite(parts.turnsLeft)) {
|
|
337
356
|
// Another session spending the same budget means fewer of those turns are
|
|
338
357
|
// yours, so say both numbers rather than the flattering one.
|
|
@@ -387,6 +406,16 @@ function briefText(parts) {
|
|
|
387
406
|
parts.snapshotAge + ' old, so run /usage before trusting the rest.'
|
|
388
407
|
);
|
|
389
408
|
}
|
|
409
|
+
if (parts.correctionUnreliable) {
|
|
410
|
+
// A reading three hours old that has been spent past its own remainder is
|
|
411
|
+
// not 82%; it is unknown, with 82% as the floor. Say exactly that.
|
|
412
|
+
sentences.push(
|
|
413
|
+
'That percentage is a floor, not a current reading: the last real snapshot is ' +
|
|
414
|
+
parts.snapshotAge + ' old and about ' + parts.pointsBeyondSnapshot +
|
|
415
|
+
' points have been spent since, more than it said was left. Either the window is ' +
|
|
416
|
+
'already exhausted or the snapshot is wrong; /usage refreshes it.'
|
|
417
|
+
);
|
|
418
|
+
}
|
|
390
419
|
// Work having actually been stopped is the most useful thing that can be said
|
|
391
420
|
// about a budget, and the percentages stop showing it the moment the window
|
|
392
421
|
// turns over. Saying it plainly is what stops the next session opening with
|
|
@@ -412,9 +441,45 @@ function briefText(parts) {
|
|
|
412
441
|
if (parts.session) {
|
|
413
442
|
sentences.push(
|
|
414
443
|
'This session: ' + parts.session.turns + ' turns, ' +
|
|
444
|
+
(Number.isFinite(parts.session.tokens) ? usage.formatTokens(parts.session.tokens) + ' tokens, ' : '') +
|
|
415
445
|
usage.formatUSD(parts.session.cost) + '.'
|
|
416
446
|
);
|
|
417
447
|
}
|
|
448
|
+
// From the tally the Stop hook keeps, so these are exact as of the last reply.
|
|
449
|
+
if (parts.lastReply && Number.isFinite(parts.lastReply.cost)) {
|
|
450
|
+
sentences.push('Last reply ' + usage.formatMoney(parts.lastReply.cost) + '.');
|
|
451
|
+
}
|
|
452
|
+
if (Number.isFinite(parts.context) && parts.context > 0) {
|
|
453
|
+
// The context is re-sent on every call, so past a point it is the cost of
|
|
454
|
+
// the session. One clause, once it is large; no advice while it is not.
|
|
455
|
+
sentences.push(
|
|
456
|
+
'Context about ' + usage.formatTokens(parts.context) + ' tokens' +
|
|
457
|
+
(parts.context >= LARGE_CONTEXT_TOKENS
|
|
458
|
+
? '; each turn re-reads that, so a fresh session or /compact at the next clean boundary cuts per-turn cost.'
|
|
459
|
+
: '.')
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
// Said once, on a session's first prompt, because the previous session's
|
|
463
|
+
// total is the one figure nothing else ever shows.
|
|
464
|
+
if (parts.lastSession && parts.lastSession.turns) {
|
|
465
|
+
const last = parts.lastSession;
|
|
466
|
+
sentences.push(
|
|
467
|
+
'Last session: ' + last.turns + ' turns, ' + usage.formatTokens(last.tokens) + ' tokens, ' +
|
|
468
|
+
roundMoney(last.cost) +
|
|
469
|
+
(last.project || last.endedAgo
|
|
470
|
+
? ' (' +
|
|
471
|
+
[
|
|
472
|
+
last.project,
|
|
473
|
+
last.endedAgo
|
|
474
|
+
? (last.open ? 'still open, last active ' : 'ended ') + last.endedAgo + ' ago'
|
|
475
|
+
: null,
|
|
476
|
+
]
|
|
477
|
+
.filter(Boolean)
|
|
478
|
+
.join(', ') +
|
|
479
|
+
')'
|
|
480
|
+
: '') + '.'
|
|
481
|
+
);
|
|
482
|
+
}
|
|
418
483
|
|
|
419
484
|
// Three states, and only the last one stops anything.
|
|
420
485
|
//
|
|
@@ -437,7 +502,9 @@ function briefText(parts) {
|
|
|
437
502
|
'save at clean boundaries as you go, and keep a short running note of ' +
|
|
438
503
|
'what is done, what is next, and which files are mid-change, so that ' +
|
|
439
504
|
'stopping at any moment loses nothing. Say in one line what may not ' +
|
|
440
|
-
'land before the reset, then keep working.'
|
|
505
|
+
'land before the reset, then keep working. If part of what remains is ' +
|
|
506
|
+
'mechanical, node scripts/usage.js --recommend (from the skill directory) ' +
|
|
507
|
+
'names the effort and model it should run at.'
|
|
441
508
|
: 'Open your reply with one short line stating this and confirming the ' +
|
|
442
509
|
'request fits, then get on with the work. Keep it to a single line. ' +
|
|
443
510
|
'There is room, so use it: work at full quality, take on the whole ' +
|
|
@@ -451,7 +518,72 @@ function briefText(parts) {
|
|
|
451
518
|
'and reset time above belong to the binding window alone; do not read them ' +
|
|
452
519
|
'against another window percentage.';
|
|
453
520
|
|
|
454
|
-
|
|
521
|
+
// Finished work closes with what it cost. Not every reply: a progress note
|
|
522
|
+
// mid-task is not the moment, and once the budget is gone nothing further
|
|
523
|
+
// runs, so there is no reply to close.
|
|
524
|
+
const closing =
|
|
525
|
+
parts.pressure === 'gone'
|
|
526
|
+
? ''
|
|
527
|
+
: ' When this reply completes what was asked, or wraps up the session, end it ' +
|
|
528
|
+
'with one plain line giving the session total above (turns, tokens and cost). ' +
|
|
529
|
+
'Skip it on partial progress; the hook prints the exact figure after you stop.';
|
|
530
|
+
|
|
531
|
+
return sentences.join(' ') + '\n' + instruction + closing + care;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Past this the context is the cost of the session, not a detail of it.
|
|
535
|
+
const LARGE_CONTEXT_TOKENS = 150000;
|
|
536
|
+
|
|
537
|
+
// A previous session's total reads better in whole dollars once it is large.
|
|
538
|
+
function roundMoney(value) {
|
|
539
|
+
if (!Number.isFinite(value)) return '-';
|
|
540
|
+
return value >= 10 ? '$' + Math.round(value) : usage.formatMoney(value);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function isTallyEntry(value) {
|
|
544
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// What the Stop hook's tally can add to the line: this session's last reply
|
|
548
|
+
// and context when it is a session the tally has seen, or the previous
|
|
549
|
+
// session's total when it has not, which is how a first prompt gets told what
|
|
550
|
+
// the last session cost.
|
|
551
|
+
function tallyContext(all, sessionId, now) {
|
|
552
|
+
const none = { lastReply: null, context: null, lastSession: null };
|
|
553
|
+
if (!all || typeof all !== 'object') return none;
|
|
554
|
+
|
|
555
|
+
const mine = sessionId && isTallyEntry(all[sessionId]) ? all[sessionId] : null;
|
|
556
|
+
if (mine) {
|
|
557
|
+
return {
|
|
558
|
+
lastReply: mine.lastReply || null,
|
|
559
|
+
context: Number.isFinite(mine.context) ? mine.context : null,
|
|
560
|
+
lastSession: null,
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const others = Object.keys(all)
|
|
565
|
+
.filter((key) => key !== tally.IDS_KEY && isTallyEntry(all[key]))
|
|
566
|
+
.map((key) => all[key])
|
|
567
|
+
.sort((a, b) => (b.lastAt || 0) - (a.lastAt || 0));
|
|
568
|
+
const last = others[0];
|
|
569
|
+
if (!last || !last.turns) return none;
|
|
570
|
+
|
|
571
|
+
const open = !Number.isFinite(last.endedAt);
|
|
572
|
+
const ended = open ? last.lastAt : last.endedAt;
|
|
573
|
+
return {
|
|
574
|
+
lastReply: null,
|
|
575
|
+
context: null,
|
|
576
|
+
lastSession: {
|
|
577
|
+
turns: last.turns,
|
|
578
|
+
tokens: tally.totalTokens(last.tokens),
|
|
579
|
+
cost: last.cost,
|
|
580
|
+
project: last.project || null,
|
|
581
|
+
// Another window may still be in it, so say when it was last active
|
|
582
|
+
// rather than claiming it ended.
|
|
583
|
+
open,
|
|
584
|
+
endedAgo: Number.isFinite(ended) ? usage.formatDuration(now - ended) : null,
|
|
585
|
+
},
|
|
586
|
+
};
|
|
455
587
|
}
|
|
456
588
|
|
|
457
589
|
async function run(now, hookInput) {
|
|
@@ -498,14 +630,7 @@ async function run(now, hookInput) {
|
|
|
498
630
|
}
|
|
499
631
|
|
|
500
632
|
const binding = view.binding;
|
|
501
|
-
const
|
|
502
|
-
// The spend-derived count is the accurate one when it has caught up; the
|
|
503
|
-
// open-session count is the one that is right immediately. Take whichever is
|
|
504
|
-
// higher rather than the one that happens to be handy, because under-counting
|
|
505
|
-
// is what makes the headroom read as more yours than it is.
|
|
506
|
-
const active = Math.max(sessions.length, liveSessions(all, now, LIVE_WINDOW_MS, sessionId) + 1);
|
|
507
|
-
const share =
|
|
508
|
-
sessions.length > 1 ? usage.shareOf(sessions, sessionId) : active > 1 ? 1 / active : 1;
|
|
633
|
+
const { active, share } = activeShare(view.sessions, all, now, sessionId);
|
|
509
634
|
const yourTurnsLeft = Number.isFinite(view.turnsLeft)
|
|
510
635
|
? Math.max(1, Math.round(view.turnsLeft * share))
|
|
511
636
|
: null;
|
|
@@ -514,7 +639,12 @@ async function run(now, hookInput) {
|
|
|
514
639
|
const shortRunway =
|
|
515
640
|
binding && Number.isFinite(binding.headroomMs) &&
|
|
516
641
|
binding.headroomMs <= RUNWAY_MENTION_MS;
|
|
642
|
+
// Outside the cache on purpose: the tally moves after every reply.
|
|
643
|
+
const found = tallyContext(tally.readState(), sessionId, now);
|
|
517
644
|
return briefText({
|
|
645
|
+
lastReply: found.lastReply,
|
|
646
|
+
context: found.context,
|
|
647
|
+
lastSession: found.lastSession,
|
|
518
648
|
sessions: active,
|
|
519
649
|
yourTurnsLeft,
|
|
520
650
|
runsOutIn: shortRunway ? usage.formatDuration(binding.headroomMs) : null,
|
|
@@ -537,6 +667,8 @@ async function run(now, hookInput) {
|
|
|
537
667
|
planChanged: Boolean(view.planChanged),
|
|
538
668
|
critical: view.critical || [],
|
|
539
669
|
pointsSinceSnapshot: (binding && binding.pointsSinceSnapshot) || 0,
|
|
670
|
+
correctionUnreliable: Boolean(binding && binding.correctionUnreliable),
|
|
671
|
+
pointsBeyondSnapshot: (binding && binding.pointsBeyondSnapshot) || 0,
|
|
540
672
|
snapshotAge: view.snapshotAge,
|
|
541
673
|
// The turn count that matters for this session is its share of a shared
|
|
542
674
|
// budget, not the whole window's. Escalating on the whole window meant a
|
|
@@ -574,11 +706,15 @@ module.exports = {
|
|
|
574
706
|
describeWindow,
|
|
575
707
|
summariseOthers,
|
|
576
708
|
briefText,
|
|
709
|
+
tallyContext,
|
|
710
|
+
LARGE_CONTEXT_TOKENS,
|
|
577
711
|
settings,
|
|
578
712
|
keepSlots,
|
|
579
713
|
pickCached,
|
|
580
714
|
mergeCache,
|
|
581
715
|
liveSessions,
|
|
716
|
+
activeShare,
|
|
717
|
+
readCache,
|
|
582
718
|
cacheableBinding,
|
|
583
719
|
pressureInputs,
|
|
584
720
|
CACHED_BINDING_FIELDS,
|