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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "usage-limits",
3
3
  "displayName": "Usage Limits",
4
- "version": "1.7.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.7.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` | 80 | Percent used that always counts as tight. |
349
- | `USAGE_LIMITS_FLOOR` | 40 | Below this, pace is ignored. |
350
- | `USAGE_LIMITS_AHEAD` | 15 | Points ahead of pace that count as burning fast. |
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
- | `USAGE_LIMITS_FEW_TURNS` | 20 | Turn count at or below which the budget counts as tight. |
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, codex.js,
569
- host.js, lowpower.js, install-codex-hook.js
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, and
572
- pulse.js during long turns
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
- 248 tests over the pricing, the window arithmetic, plan and credit detection,
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.7.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 | $3 | $15 |
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
- if (described) bound.push(described + (parts.binding.stale ? '' : ' used'));
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
- return sentences.join(' ') + '\n' + instruction + care;
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 sessions = view.sessions || [];
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,