claude-usage-limits 1.7.1 → 1.8.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 +76 -12
- package/bin/cli.js +2 -0
- package/commands/session.md +13 -0
- package/hooks/hooks.json +23 -1
- package/package.json +1 -1
- package/skills/usage-limits/SKILL.md +29 -1
- package/skills/usage-limits/references/how-it-works.md +15 -0
- package/skills/usage-limits/references/tactics.md +1 -1
- package/skills/usage-limits/scripts/brief.js +144 -10
- package/skills/usage-limits/scripts/pulse.js +6 -4
- 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 +425 -62
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "usage-limits",
|
|
3
3
|
"displayName": "Usage Limits",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.8.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.8.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
|
|
|
@@ -528,6 +583,12 @@ Good enough to plan with, not a bill. The honest caveats:
|
|
|
528
583
|
doubling the limits. If demand-based limits ever return, the numbers here
|
|
529
584
|
follow automatically, because they are calibrated from what your traffic did
|
|
530
585
|
to the meter rather than from an assumption about the clock.
|
|
586
|
+
- Subagent transcripts, written under the session's own folder, are read too.
|
|
587
|
+
Their calls count in the money and the tokens and are reported apart from
|
|
588
|
+
the turns, because a turn is one main-thread call.
|
|
589
|
+
- The account's own list of limits is read as well as the per-window buckets,
|
|
590
|
+
so a per-model weekly such as `weekly (Fable)` shows up as a window of its
|
|
591
|
+
own, priced from that model's calls alone.
|
|
531
592
|
- The turn cost behind "turns of headroom" is a median over at least five
|
|
532
593
|
turns, so one compaction cannot define your pace.
|
|
533
594
|
- What a point of a window costs is learned once from the best sample seen and
|
|
@@ -565,12 +626,15 @@ names, the formulas, and the rest of it.
|
|
|
565
626
|
agents/openai.yaml how Codex lists the plugin
|
|
566
627
|
skills/usage-limits/SKILL.md what the agent reads
|
|
567
628
|
skills/usage-limits/agents/ how Codex lists the skill
|
|
568
|
-
skills/usage-limits/scripts/ usage.js, brief.js, pulse.js,
|
|
569
|
-
|
|
629
|
+
skills/usage-limits/scripts/ usage.js, brief.js, pulse.js, stop.js,
|
|
630
|
+
sessionend.js, tally.js, codex.js, host.js,
|
|
631
|
+
lowpower.js, install-codex-hook.js
|
|
570
632
|
skills/usage-limits/references/ the longer notes
|
|
571
|
-
hooks/hooks.json runs brief.js before each prompt,
|
|
572
|
-
|
|
633
|
+
hooks/hooks.json runs brief.js before each prompt, pulse.js
|
|
634
|
+
during long turns, stop.js after each reply
|
|
635
|
+
and sessionend.js when the session closes
|
|
573
636
|
commands/check.md the /usage-limits:check command
|
|
637
|
+
commands/session.md the /usage-limits:session command
|
|
574
638
|
bin/cli.js the npx entry point
|
|
575
639
|
tools/sync-version.js keeps the manifest version in step
|
|
576
640
|
test/ node --test, no dependencies
|
|
@@ -582,8 +646,8 @@ test/ node --test, no dependencies
|
|
|
582
646
|
node --test
|
|
583
647
|
```
|
|
584
648
|
|
|
585
|
-
|
|
586
|
-
the status line, the before-prompt line, the mid-turn pulse, job forecasting,
|
|
649
|
+
308 tests over the pricing, the window arithmetic, plan and credit detection,
|
|
650
|
+
the status line, the before-prompt line, the mid-turn pulse, the after-reply tally and the session history, job forecasting,
|
|
587
651
|
per-project attribution, the Codex reader and its installer, the CLI,
|
|
588
652
|
packaging, and the settings save/restore.
|
|
589
653
|
|
package/bin/cli.js
CHANGED
|
@@ -11,6 +11,8 @@ 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 --sessions what recent sessions cost
|
|
15
|
+
claude-usage-limits --session last one session in full, or give its id
|
|
14
16
|
|
|
15
17
|
claude-usage-limits --host codex read Codex's limits instead
|
|
16
18
|
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.8.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
|
|
|
@@ -359,9 +384,12 @@ a turn, which is the thing it is trying to save.
|
|
|
359
384
|
|
|
360
385
|
| Path | What it is |
|
|
361
386
|
| --- | --- |
|
|
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. |
|
|
387
|
+
| `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
388
|
| `scripts/brief.js` | What the hook runs before each prompt. Not meant to be called by hand. |
|
|
364
389
|
| `scripts/pulse.js` | What runs after tool calls, to re-check the budget during a long turn. Not meant to be called by hand. |
|
|
390
|
+
| `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. |
|
|
391
|
+
| `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. |
|
|
392
|
+
| `scripts/tally.js` | The running per-session total behind those two, kept in `usage-limits-sessions.json` and read incrementally. |
|
|
365
393
|
| `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
394
|
| `scripts/codex.js` | The Codex reader: the meter and the pace out of `~/.codex/sessions`, plus the live `--refresh` call. |
|
|
367
395
|
| `scripts/install-codex-hook.js` | `status`, `on`, `off`. Installs the Codex-side instruction, which Claude Code does not need. |
|
|
@@ -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
|
|
@@ -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
|
//
|
|
@@ -451,7 +516,72 @@ function briefText(parts) {
|
|
|
451
516
|
'and reset time above belong to the binding window alone; do not read them ' +
|
|
452
517
|
'against another window percentage.';
|
|
453
518
|
|
|
454
|
-
|
|
519
|
+
// Finished work closes with what it cost. Not every reply: a progress note
|
|
520
|
+
// mid-task is not the moment, and once the budget is gone nothing further
|
|
521
|
+
// runs, so there is no reply to close.
|
|
522
|
+
const closing =
|
|
523
|
+
parts.pressure === 'gone'
|
|
524
|
+
? ''
|
|
525
|
+
: ' When this reply completes what was asked, or wraps up the session, end it ' +
|
|
526
|
+
'with one plain line giving the session total above (turns, tokens and cost). ' +
|
|
527
|
+
'Skip it on partial progress; the hook prints the exact figure after you stop.';
|
|
528
|
+
|
|
529
|
+
return sentences.join(' ') + '\n' + instruction + closing + care;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Past this the context is the cost of the session, not a detail of it.
|
|
533
|
+
const LARGE_CONTEXT_TOKENS = 150000;
|
|
534
|
+
|
|
535
|
+
// A previous session's total reads better in whole dollars once it is large.
|
|
536
|
+
function roundMoney(value) {
|
|
537
|
+
if (!Number.isFinite(value)) return '-';
|
|
538
|
+
return value >= 10 ? '$' + Math.round(value) : usage.formatMoney(value);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function isTallyEntry(value) {
|
|
542
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// What the Stop hook's tally can add to the line: this session's last reply
|
|
546
|
+
// and context when it is a session the tally has seen, or the previous
|
|
547
|
+
// session's total when it has not, which is how a first prompt gets told what
|
|
548
|
+
// the last session cost.
|
|
549
|
+
function tallyContext(all, sessionId, now) {
|
|
550
|
+
const none = { lastReply: null, context: null, lastSession: null };
|
|
551
|
+
if (!all || typeof all !== 'object') return none;
|
|
552
|
+
|
|
553
|
+
const mine = sessionId && isTallyEntry(all[sessionId]) ? all[sessionId] : null;
|
|
554
|
+
if (mine) {
|
|
555
|
+
return {
|
|
556
|
+
lastReply: mine.lastReply || null,
|
|
557
|
+
context: Number.isFinite(mine.context) ? mine.context : null,
|
|
558
|
+
lastSession: null,
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const others = Object.keys(all)
|
|
563
|
+
.filter((key) => key !== tally.IDS_KEY && isTallyEntry(all[key]))
|
|
564
|
+
.map((key) => all[key])
|
|
565
|
+
.sort((a, b) => (b.lastAt || 0) - (a.lastAt || 0));
|
|
566
|
+
const last = others[0];
|
|
567
|
+
if (!last || !last.turns) return none;
|
|
568
|
+
|
|
569
|
+
const open = !Number.isFinite(last.endedAt);
|
|
570
|
+
const ended = open ? last.lastAt : last.endedAt;
|
|
571
|
+
return {
|
|
572
|
+
lastReply: null,
|
|
573
|
+
context: null,
|
|
574
|
+
lastSession: {
|
|
575
|
+
turns: last.turns,
|
|
576
|
+
tokens: tally.totalTokens(last.tokens),
|
|
577
|
+
cost: last.cost,
|
|
578
|
+
project: last.project || null,
|
|
579
|
+
// Another window may still be in it, so say when it was last active
|
|
580
|
+
// rather than claiming it ended.
|
|
581
|
+
open,
|
|
582
|
+
endedAgo: Number.isFinite(ended) ? usage.formatDuration(now - ended) : null,
|
|
583
|
+
},
|
|
584
|
+
};
|
|
455
585
|
}
|
|
456
586
|
|
|
457
587
|
async function run(now, hookInput) {
|
|
@@ -498,14 +628,7 @@ async function run(now, hookInput) {
|
|
|
498
628
|
}
|
|
499
629
|
|
|
500
630
|
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;
|
|
631
|
+
const { active, share } = activeShare(view.sessions, all, now, sessionId);
|
|
509
632
|
const yourTurnsLeft = Number.isFinite(view.turnsLeft)
|
|
510
633
|
? Math.max(1, Math.round(view.turnsLeft * share))
|
|
511
634
|
: null;
|
|
@@ -514,7 +637,12 @@ async function run(now, hookInput) {
|
|
|
514
637
|
const shortRunway =
|
|
515
638
|
binding && Number.isFinite(binding.headroomMs) &&
|
|
516
639
|
binding.headroomMs <= RUNWAY_MENTION_MS;
|
|
640
|
+
// Outside the cache on purpose: the tally moves after every reply.
|
|
641
|
+
const found = tallyContext(tally.readState(), sessionId, now);
|
|
517
642
|
return briefText({
|
|
643
|
+
lastReply: found.lastReply,
|
|
644
|
+
context: found.context,
|
|
645
|
+
lastSession: found.lastSession,
|
|
518
646
|
sessions: active,
|
|
519
647
|
yourTurnsLeft,
|
|
520
648
|
runsOutIn: shortRunway ? usage.formatDuration(binding.headroomMs) : null,
|
|
@@ -537,6 +665,8 @@ async function run(now, hookInput) {
|
|
|
537
665
|
planChanged: Boolean(view.planChanged),
|
|
538
666
|
critical: view.critical || [],
|
|
539
667
|
pointsSinceSnapshot: (binding && binding.pointsSinceSnapshot) || 0,
|
|
668
|
+
correctionUnreliable: Boolean(binding && binding.correctionUnreliable),
|
|
669
|
+
pointsBeyondSnapshot: (binding && binding.pointsBeyondSnapshot) || 0,
|
|
540
670
|
snapshotAge: view.snapshotAge,
|
|
541
671
|
// The turn count that matters for this session is its share of a shared
|
|
542
672
|
// budget, not the whole window's. Escalating on the whole window meant a
|
|
@@ -574,11 +704,15 @@ module.exports = {
|
|
|
574
704
|
describeWindow,
|
|
575
705
|
summariseOthers,
|
|
576
706
|
briefText,
|
|
707
|
+
tallyContext,
|
|
708
|
+
LARGE_CONTEXT_TOKENS,
|
|
577
709
|
settings,
|
|
578
710
|
keepSlots,
|
|
579
711
|
pickCached,
|
|
580
712
|
mergeCache,
|
|
581
713
|
liveSessions,
|
|
714
|
+
activeShare,
|
|
715
|
+
readCache,
|
|
582
716
|
cacheableBinding,
|
|
583
717
|
pressureInputs,
|
|
584
718
|
CACHED_BINDING_FIELDS,
|
|
@@ -129,10 +129,12 @@ async function run(now, hookInput) {
|
|
|
129
129
|
const binding = data.binding;
|
|
130
130
|
if (!binding) return '';
|
|
131
131
|
|
|
132
|
-
|
|
133
|
-
|
|
132
|
+
// The same count and the same split as the brief, so the two lines never
|
|
133
|
+
// disagree about how many sessions there are or how much of the budget is
|
|
134
|
+
// this one's.
|
|
135
|
+
const { active, share } = brief.activeShare(data.sessions, brief.readCache(), now, sessionId);
|
|
134
136
|
const turnsLeft = Number.isFinite(binding.turnsLeft)
|
|
135
|
-
?
|
|
137
|
+
? active > 1
|
|
136
138
|
? Math.max(1, Math.round(binding.turnsLeft * share))
|
|
137
139
|
: binding.turnsLeft
|
|
138
140
|
: null;
|
|
@@ -156,7 +158,7 @@ async function run(now, hookInput) {
|
|
|
156
158
|
Number.isFinite(binding.headroomMs) && binding.headroomMs <= runwayMs
|
|
157
159
|
? usage.formatDuration(binding.headroomMs)
|
|
158
160
|
: null,
|
|
159
|
-
sessions:
|
|
161
|
+
sessions: active,
|
|
160
162
|
pressure,
|
|
161
163
|
});
|
|
162
164
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// The closing line.
|
|
5
|
+
//
|
|
6
|
+
// Runs as the SessionEnd hook. Brings the session's total up to date one last
|
|
7
|
+
// time, marks it closed, and prints one line saying how long it ran and what
|
|
8
|
+
// it cost. SessionEnd shows plain stdout to the user and gives hooks a short
|
|
9
|
+
// shared budget, which the incremental read fits inside comfortably.
|
|
10
|
+
|
|
11
|
+
const usage = require('./usage.js');
|
|
12
|
+
const host = require('./host.js');
|
|
13
|
+
const tally = require('./tally.js');
|
|
14
|
+
|
|
15
|
+
async function run(now, hookInput) {
|
|
16
|
+
if (String(process.env.USAGE_LIMITS_TALLY || '').toLowerCase() === 'off') return '';
|
|
17
|
+
usage.setHost(host.detect(process.argv.slice(2), process.env));
|
|
18
|
+
|
|
19
|
+
const sessionId = hookInput && hookInput.session_id ? hookInput.session_id : null;
|
|
20
|
+
const transcript = hookInput && hookInput.transcript_path ? hookInput.transcript_path : null;
|
|
21
|
+
if (!sessionId || !transcript) return '';
|
|
22
|
+
|
|
23
|
+
const all = tally.readState();
|
|
24
|
+
const { session } = tally.update(all, sessionId, transcript, now, { cwd: hookInput.cwd || null });
|
|
25
|
+
session.endedAt = now;
|
|
26
|
+
session.reason = typeof hookInput.reason === 'string' ? hookInput.reason : null;
|
|
27
|
+
tally.writeState(tally.trim(all));
|
|
28
|
+
|
|
29
|
+
return tally.formatClosed(session, now);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (require.main === module) {
|
|
33
|
+
tally
|
|
34
|
+
.readHookInput()
|
|
35
|
+
.then((input) => run(Date.now(), input))
|
|
36
|
+
.then(
|
|
37
|
+
(text) => {
|
|
38
|
+
if (text) process.stdout.write(text + '\n');
|
|
39
|
+
process.exit(0);
|
|
40
|
+
},
|
|
41
|
+
() => {
|
|
42
|
+
process.exit(0);
|
|
43
|
+
}
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports = { run };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// The end-of-reply tally.
|
|
5
|
+
//
|
|
6
|
+
// Runs as the Stop hook, after Claude has finished a reply, and puts one line
|
|
7
|
+
// in front of the user saying what that reply cost and what the session has
|
|
8
|
+
// cost so far. It costs the model nothing: the line goes to the person, not
|
|
9
|
+
// into the context, and the numbers come from bytes of the transcript that
|
|
10
|
+
// have already been written.
|
|
11
|
+
//
|
|
12
|
+
// It must never exit with code 2. On this event that would stop Claude from
|
|
13
|
+
// stopping.
|
|
14
|
+
|
|
15
|
+
const usage = require('./usage.js');
|
|
16
|
+
const host = require('./host.js');
|
|
17
|
+
const tally = require('./tally.js');
|
|
18
|
+
|
|
19
|
+
async function run(now, hookInput) {
|
|
20
|
+
if (String(process.env.USAGE_LIMITS_TALLY || '').toLowerCase() === 'off') return '';
|
|
21
|
+
usage.setHost(host.detect(process.argv.slice(2), process.env));
|
|
22
|
+
|
|
23
|
+
const sessionId = hookInput && hookInput.session_id ? hookInput.session_id : null;
|
|
24
|
+
const transcript = hookInput && hookInput.transcript_path ? hookInput.transcript_path : null;
|
|
25
|
+
if (!sessionId || !transcript) return '';
|
|
26
|
+
|
|
27
|
+
const all = tally.readState();
|
|
28
|
+
const { session, delta, created } = tally.update(all, sessionId, transcript, now, {
|
|
29
|
+
cwd: hookInput.cwd || null,
|
|
30
|
+
});
|
|
31
|
+
tally.writeState(tally.trim(all));
|
|
32
|
+
|
|
33
|
+
// The first time a session is seen, everything read is history rather than
|
|
34
|
+
// the reply that just finished, so only the total is shown.
|
|
35
|
+
return JSON.stringify({
|
|
36
|
+
systemMessage: tally.formatTally(session, created ? null : delta, tally.pricing(now)),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (require.main === module) {
|
|
41
|
+
tally
|
|
42
|
+
.readHookInput()
|
|
43
|
+
.then((input) => run(Date.now(), input))
|
|
44
|
+
.then(
|
|
45
|
+
(text) => {
|
|
46
|
+
if (text) process.stdout.write(text + '\n');
|
|
47
|
+
process.exit(0);
|
|
48
|
+
},
|
|
49
|
+
() => {
|
|
50
|
+
process.exit(0);
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = { run };
|