claude-usage-limits 1.8.0 → 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 +42 -0
- package/bin/cli.js +1 -0
- package/package.json +1 -1
- package/skills/usage-limits/SKILL.md +40 -0
- package/skills/usage-limits/references/tactics.md +7 -0
- package/skills/usage-limits/scripts/brief.js +3 -1
- package/skills/usage-limits/scripts/lowpower.js +9 -0
- package/skills/usage-limits/scripts/recommend.js +295 -0
- package/skills/usage-limits/scripts/usage.js +66 -11
|
@@ -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
|
@@ -430,6 +430,44 @@ times one that answers from context. The upper end is the honest one for a long
|
|
|
430
430
|
run, since turns get dearer as the context grows.
|
|
431
431
|
|
|
432
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
|
+
|
|
433
471
|
## Plans
|
|
434
472
|
|
|
435
473
|
It reads which plan you are on and adjusts what it tells you, because the
|
|
@@ -545,6 +583,10 @@ overwrite the saved originals.
|
|
|
545
583
|
The file change applies to new sessions. For a session already running,
|
|
546
584
|
`/effort low` does the same thing immediately.
|
|
547
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
|
+
|
|
548
590
|
That covers the setting. The larger saving is behavioural, and the skill file
|
|
549
591
|
spells it out: batch tool calls, read line ranges instead of whole files, skip
|
|
550
592
|
subagents when the context already exists, stop retrying a fix that is not
|
package/bin/cli.js
CHANGED
|
@@ -11,6 +11,7 @@ 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
|
|
14
15
|
claude-usage-limits --sessions what recent sessions cost
|
|
15
16
|
claude-usage-limits --session last one session in full, or give its id
|
|
16
17
|
|
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",
|
|
@@ -272,6 +272,45 @@ to `low` is the largest per-turn saving available without changing model or
|
|
|
272
272
|
scope. The file change applies to new sessions; for the session already
|
|
273
273
|
running, `/effort low` takes effect immediately.
|
|
274
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
|
+
|
|
275
314
|
Do not take that on trust: the report measures it. Under the model table it
|
|
276
315
|
says how much of the output was reasoning and what that cost, for example
|
|
277
316
|
|
|
@@ -394,5 +433,6 @@ a turn, which is the thing it is trying to save.
|
|
|
394
433
|
| `scripts/codex.js` | The Codex reader: the meter and the pace out of `~/.codex/sessions`, plus the live `--refresh` call. |
|
|
395
434
|
| `scripts/install-codex-hook.js` | `status`, `on`, `off`. Installs the Codex-side instruction, which Claude Code does not need. |
|
|
396
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. |
|
|
397
437
|
| `references/tactics.md` | Every lever that lowers cost, and why it works. |
|
|
398
438
|
| `references/how-it-works.md` | Where the numbers come from and where they are soft. |
|
|
@@ -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
|
|
@@ -502,7 +502,9 @@ function briefText(parts) {
|
|
|
502
502
|
'save at clean boundaries as you go, and keep a short running note of ' +
|
|
503
503
|
'what is done, what is next, and which files are mid-change, so that ' +
|
|
504
504
|
'stopping at any moment loses nothing. Say in one line what may not ' +
|
|
505
|
-
'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.'
|
|
506
508
|
: 'Open your reply with one short line stating this and confirming the ' +
|
|
507
509
|
'request fits, then get on with the work. Keep it to a single line. ' +
|
|
508
510
|
'There is room, so use it: work at full quality, take on the whole ' +
|
|
@@ -81,6 +81,15 @@ function planApply(settings, options, existingState) {
|
|
|
81
81
|
if (EFFORT_LEVELS.indexOf(effort) === -1) {
|
|
82
82
|
throw new Error('unknown effort "' + effort + '", expected one of ' + EFFORT_LEVELS.join(', '));
|
|
83
83
|
}
|
|
84
|
+
// Claude Code refuses 'max' in settings.json; it only survives through
|
|
85
|
+
// /effort or CLAUDE_CODE_EFFORT_LEVEL. Writing it here would save a value
|
|
86
|
+
// the next session silently ignores, which is worse than an error.
|
|
87
|
+
if (effort === 'max') {
|
|
88
|
+
throw new Error(
|
|
89
|
+
"settings.json does not accept 'max'. Use --effort xhigh here, and /effort max " +
|
|
90
|
+
'or CLAUDE_CODE_EFFORT_LEVEL=max for the sessions that need it.'
|
|
91
|
+
);
|
|
92
|
+
}
|
|
84
93
|
wanted.effortLevel = effort;
|
|
85
94
|
if (options.model) wanted.model = options.model;
|
|
86
95
|
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Turns the budget figures into a choice of effort and model, and says where
|
|
4
|
+
// each half of that choice can actually be applied. There are three levers and
|
|
5
|
+
// they belong to different hands:
|
|
6
|
+
//
|
|
7
|
+
// - The running session's effort and model are the user's: only /effort and
|
|
8
|
+
// /model change them, and they change them immediately.
|
|
9
|
+
// - New sessions are the script's: lowpower.js writes effortLevel and model
|
|
10
|
+
// into settings.json, which Claude Code reads at launch.
|
|
11
|
+
// - Delegated work is Claude's alone: a subagent can be dispatched on any
|
|
12
|
+
// model at any effort, mid-session, with no one asked.
|
|
13
|
+
//
|
|
14
|
+
// Nothing here touches disk. decide() is a pure function over the same report
|
|
15
|
+
// data usage.js already gathers, so the reasoning can be tested without a
|
|
16
|
+
// transcript in sight.
|
|
17
|
+
|
|
18
|
+
// One notch down, not a cliff. Dropping xhigh to low on work that still has
|
|
19
|
+
// judgement in it costs more in rework than it saves; the ladder loses height
|
|
20
|
+
// a step at a time and 'critical' is the only posture that goes straight to
|
|
21
|
+
// the floor.
|
|
22
|
+
const NEXT_LOWER = { max: 'high', xhigh: 'medium', high: 'medium', medium: 'low', low: 'low' };
|
|
23
|
+
|
|
24
|
+
// Below this share of output, reasoning is not where the money is going, and
|
|
25
|
+
// turning effort down would trade quality for a saving that is not there.
|
|
26
|
+
const REASONING_FLOOR = 0.1;
|
|
27
|
+
|
|
28
|
+
// Turns-left walls used when no job size is given. Ten turns is barely a
|
|
29
|
+
// feature; twenty-five is room for one, carefully.
|
|
30
|
+
const CRITICAL_TURNS = 10;
|
|
31
|
+
const TIGHT_TURNS = 25;
|
|
32
|
+
|
|
33
|
+
const HOUR = 60 * 60 * 1000;
|
|
34
|
+
|
|
35
|
+
// Where the mechanical bulk should go when it is delegated. One tier down
|
|
36
|
+
// from whatever is doing the judgement; haiku is already the floor.
|
|
37
|
+
function delegateModel(model) {
|
|
38
|
+
const name = String(model || '').toLowerCase();
|
|
39
|
+
if (name.indexOf('haiku') !== -1) return 'haiku';
|
|
40
|
+
if (name.indexOf('sonnet') !== -1) return 'haiku';
|
|
41
|
+
return 'sonnet';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// The effort actually in force. settings.json says 'default' when nothing is
|
|
45
|
+
// set, and the measured dominant effort of recent turns is better evidence
|
|
46
|
+
// than a guess; xhigh is what Claude Code defaults to when neither knows.
|
|
47
|
+
function currentEffort(settings, recentEffort) {
|
|
48
|
+
const set = settings && settings.effortLevel;
|
|
49
|
+
if (set && set !== 'default') return set;
|
|
50
|
+
if (recentEffort) return recentEffort;
|
|
51
|
+
return 'xhigh';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function decide(inputs) {
|
|
55
|
+
const binding = inputs.binding;
|
|
56
|
+
const rates = inputs.rates;
|
|
57
|
+
const settings = inputs.settings || {};
|
|
58
|
+
const effortNow = currentEffort(settings, inputs.recentEffort);
|
|
59
|
+
const modelNow = (settings.model && settings.model !== 'default' && settings.model) || 'default';
|
|
60
|
+
|
|
61
|
+
const base = {
|
|
62
|
+
posture: 'unknown',
|
|
63
|
+
reason: null,
|
|
64
|
+
turnsLeft: null,
|
|
65
|
+
effort: { current: effortNow, target: effortNow, changes: false, why: null },
|
|
66
|
+
model: { current: modelNow, delegate: null, nextSession: null, why: null },
|
|
67
|
+
apply: { now: null, next: null, delegate: null },
|
|
68
|
+
notes: [],
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
if (!binding || binding.stale) {
|
|
72
|
+
base.reason = 'no fresh reading of the binding window';
|
|
73
|
+
return base;
|
|
74
|
+
}
|
|
75
|
+
if (!rates || !Number.isFinite(rates.median) || rates.median <= 0) {
|
|
76
|
+
base.reason = 'no measured turn cost to price the budget in turns';
|
|
77
|
+
return base;
|
|
78
|
+
}
|
|
79
|
+
if (!Number.isFinite(binding.usdPerPercent) || binding.usdPerPercent <= 0) {
|
|
80
|
+
base.reason = 'the binding window has no calibrated price per point yet';
|
|
81
|
+
return base;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const percentLeft = Number.isFinite(binding.percentLeft) ? binding.percentLeft : 0;
|
|
85
|
+
const turnsLeft = Math.floor((percentLeft * binding.usdPerPercent) / rates.median);
|
|
86
|
+
base.turnsLeft = turnsLeft;
|
|
87
|
+
|
|
88
|
+
// When the clock wins the race, the limit is not the constraint and there
|
|
89
|
+
// is nothing to buy by economising: whatever is left at the reset is lost.
|
|
90
|
+
const pace = inputs.recentTurnsPerHour;
|
|
91
|
+
if (
|
|
92
|
+
Number.isFinite(binding.msToReset) &&
|
|
93
|
+
binding.msToReset > 0 &&
|
|
94
|
+
Number.isFinite(pace) &&
|
|
95
|
+
pace > 0 &&
|
|
96
|
+
(binding.msToReset / HOUR) * pace < turnsLeft * 0.8
|
|
97
|
+
) {
|
|
98
|
+
base.posture = 'reset-first';
|
|
99
|
+
base.reason = 'the window resets before this pace can spend it';
|
|
100
|
+
base.effort.why = 'the budget is not the constraint';
|
|
101
|
+
base.model.why = 'the budget is not the constraint';
|
|
102
|
+
return base;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// With a job size, the forecast arithmetic decides. Without one, the raw
|
|
106
|
+
// turns of headroom do. Both use the expensive end of the measured spread,
|
|
107
|
+
// because that is the honest number for a long run.
|
|
108
|
+
if (Number.isFinite(inputs.turns) && inputs.turns > 0) {
|
|
109
|
+
const percentHigh = (inputs.turns * rates.high) / binding.usdPerPercent;
|
|
110
|
+
if (percentHigh > percentLeft) {
|
|
111
|
+
base.posture = 'critical';
|
|
112
|
+
base.reason = 'a ' + inputs.turns + ' turn job does not fit in what is left';
|
|
113
|
+
} else if (percentHigh > percentLeft * 0.75) {
|
|
114
|
+
base.posture = 'tight';
|
|
115
|
+
base.reason = 'a ' + inputs.turns + ' turn job fits, but only just';
|
|
116
|
+
} else {
|
|
117
|
+
base.posture = 'roomy';
|
|
118
|
+
base.reason = 'a ' + inputs.turns + ' turn job fits with room to spare';
|
|
119
|
+
}
|
|
120
|
+
} else if (percentLeft <= 0 || turnsLeft <= 0) {
|
|
121
|
+
base.posture = 'critical';
|
|
122
|
+
base.reason = 'the binding window is spent';
|
|
123
|
+
} else if (turnsLeft <= CRITICAL_TURNS) {
|
|
124
|
+
base.posture = 'critical';
|
|
125
|
+
base.reason = 'about ' + turnsLeft + ' turns of headroom';
|
|
126
|
+
} else if (turnsLeft <= TIGHT_TURNS) {
|
|
127
|
+
base.posture = 'tight';
|
|
128
|
+
base.reason = 'about ' + turnsLeft + ' turns of headroom';
|
|
129
|
+
} else {
|
|
130
|
+
base.posture = 'roomy';
|
|
131
|
+
base.reason = 'about ' + turnsLeft + ' turns of headroom';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (base.posture === 'roomy') {
|
|
135
|
+
base.effort.why = 'cheapness is not a virtue when the budget is not tight';
|
|
136
|
+
base.model.why = 'cheapness is not a virtue when the budget is not tight';
|
|
137
|
+
return base;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Effort is the biggest per-turn lever, but only when reasoning is actually
|
|
141
|
+
// where the money goes. The reasoning share is the ceiling on the saving,
|
|
142
|
+
// so a small share means the honest advice is to leave effort alone.
|
|
143
|
+
const share = inputs.reasoningShare;
|
|
144
|
+
if (Number.isFinite(share) && share < REASONING_FLOOR) {
|
|
145
|
+
base.effort.why =
|
|
146
|
+
'reasoning is only ' + Math.round(share * 100) +
|
|
147
|
+
'% of output, so effort is not where the money is going';
|
|
148
|
+
} else {
|
|
149
|
+
const target = base.posture === 'critical' ? 'low' : NEXT_LOWER[effortNow] || 'medium';
|
|
150
|
+
if (target !== effortNow) {
|
|
151
|
+
base.effort.target = target;
|
|
152
|
+
base.effort.changes = true;
|
|
153
|
+
base.effort.why =
|
|
154
|
+
base.posture === 'critical'
|
|
155
|
+
? 'reasoning is billed as output, and low is the largest saving that changes nothing else'
|
|
156
|
+
: 'one notch covers the mechanical stretches; keep judgement calls at full effort';
|
|
157
|
+
} else {
|
|
158
|
+
base.effort.why = 'already at the floor for this posture';
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// The main model is only worth flipping when things are critical, and even
|
|
163
|
+
// then it lands in settings.json for the next session: switching the running
|
|
164
|
+
// session's model mid-task invalidates the prompt cache, so the change
|
|
165
|
+
// belongs at a session boundary.
|
|
166
|
+
base.model.delegate = delegateModel(modelNow);
|
|
167
|
+
base.model.why =
|
|
168
|
+
'keep ' + (modelNow === 'default' ? 'the current model' : modelNow) +
|
|
169
|
+
' for the judgement; the saving is in where the mechanical bulk runs';
|
|
170
|
+
if (base.posture === 'critical' && base.model.delegate !== 'haiku') {
|
|
171
|
+
base.model.nextSession = 'sonnet';
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// The commands, spelled out, because the point of a recommendation is that
|
|
175
|
+
// it can be acted on without working anything out.
|
|
176
|
+
if (base.effort.changes) {
|
|
177
|
+
base.apply.now = '/effort ' + base.effort.target;
|
|
178
|
+
}
|
|
179
|
+
if (inputs.codex) {
|
|
180
|
+
base.notes.push(
|
|
181
|
+
'Under Codex, settings.json is not in play: change model or effort through ' +
|
|
182
|
+
"Codex's own controls."
|
|
183
|
+
);
|
|
184
|
+
} else if (base.effort.changes || base.model.nextSession) {
|
|
185
|
+
const settingsEffort = base.effort.changes ? base.effort.target : effortNow;
|
|
186
|
+
base.apply.next =
|
|
187
|
+
'node scripts/lowpower.js on --effort ' +
|
|
188
|
+
(settingsEffort === 'max' ? 'xhigh' : settingsEffort) +
|
|
189
|
+
(base.model.nextSession ? ' --model ' + base.model.nextSession : '');
|
|
190
|
+
}
|
|
191
|
+
base.apply.delegate =
|
|
192
|
+
'dispatch self-contained mechanical work to a subagent on ' +
|
|
193
|
+
base.model.delegate +
|
|
194
|
+
' at low effort, and keep the judgement here';
|
|
195
|
+
|
|
196
|
+
if (Number.isFinite(inputs.sessions) && inputs.sessions > 1) {
|
|
197
|
+
base.notes.push(
|
|
198
|
+
inputs.sessions + ' sessions are spending this budget at once, so the headroom ' +
|
|
199
|
+
'drains faster than these figures alone suggest.'
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
if (effortNow === 'max') {
|
|
203
|
+
base.notes.push(
|
|
204
|
+
"settings.json does not accept 'max', so a saved level can only go up to " +
|
|
205
|
+
'xhigh; max survives only through /effort or CLAUDE_CODE_EFFORT_LEVEL.'
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return base;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// The report data usage.js gathers, reduced to what decide() reads.
|
|
213
|
+
function fromReport(data, turns) {
|
|
214
|
+
return {
|
|
215
|
+
binding: (data && data.binding) || null,
|
|
216
|
+
rates: (data && data.rates) || null,
|
|
217
|
+
settings: (data && data.settings) || {},
|
|
218
|
+
recentEffort: data && data.recent ? data.recent.effort : null,
|
|
219
|
+
recentTurnsPerHour: data && data.recent ? data.recent.turns : null,
|
|
220
|
+
reasoningShare: data && data.reasoning ? data.reasoning.shareOfOutput : null,
|
|
221
|
+
sessions: data && data.sessions ? data.sessions.length : 1,
|
|
222
|
+
codex: Boolean(data && data.money === false),
|
|
223
|
+
turns: Number.isFinite(turns) ? turns : null,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Local and small on purpose: requiring usage.js back for its formatters
|
|
228
|
+
// would make the two modules a cycle.
|
|
229
|
+
function fmtDuration(ms) {
|
|
230
|
+
if (!Number.isFinite(ms) || ms <= 0) return 'now';
|
|
231
|
+
const minutes = Math.round(ms / 60000);
|
|
232
|
+
if (minutes < 60) return minutes + 'm';
|
|
233
|
+
const hours = Math.floor(minutes / 60);
|
|
234
|
+
if (hours < 48) return hours + 'h ' + (minutes % 60) + 'm';
|
|
235
|
+
return Math.floor(hours / 24) + 'd ' + (hours % 24) + 'h';
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function renderRecommend(data, turns) {
|
|
239
|
+
const decision = decide(fromReport(data, turns));
|
|
240
|
+
const lines = [];
|
|
241
|
+
lines.push('Recommendation' + (Number.isFinite(turns) && turns > 0 ? ' for ' + turns + ' turns' : ''));
|
|
242
|
+
lines.push('');
|
|
243
|
+
|
|
244
|
+
if (decision.posture === 'unknown') {
|
|
245
|
+
lines.push(' Nothing to recommend yet: ' + decision.reason + '.');
|
|
246
|
+
lines.push(' Run /usage once, do a little work, then ask again.');
|
|
247
|
+
return lines.join('\n');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const binding = data.binding;
|
|
251
|
+
const where =
|
|
252
|
+
binding.label +
|
|
253
|
+
' window, ' +
|
|
254
|
+
(Number.isFinite(binding.percentLeft) ? Math.max(0, Math.round(binding.percentLeft)) : '?') +
|
|
255
|
+
'% left' +
|
|
256
|
+
(Number.isFinite(binding.msToReset) ? ', resets in ' + fmtDuration(binding.msToReset) : '');
|
|
257
|
+
lines.push(' Posture ' + decision.posture + ' - ' + decision.reason + ' (' + where + ')');
|
|
258
|
+
|
|
259
|
+
if (decision.posture === 'roomy' || decision.posture === 'reset-first') {
|
|
260
|
+
lines.push(' Effort keep ' + decision.effort.current + '; ' + decision.effort.why);
|
|
261
|
+
lines.push(' Model keep ' + decision.model.current + '; do not economise');
|
|
262
|
+
return lines.join('\n');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (decision.effort.changes) {
|
|
266
|
+
lines.push(' Effort ' + decision.effort.current + ' -> ' + decision.effort.target + '; ' + decision.effort.why);
|
|
267
|
+
lines.push(' this session: ' + decision.apply.now + ' (only the user can run it)');
|
|
268
|
+
} else {
|
|
269
|
+
lines.push(' Effort keep ' + decision.effort.current + '; ' + decision.effort.why);
|
|
270
|
+
}
|
|
271
|
+
if (decision.apply.next) {
|
|
272
|
+
lines.push(' new sessions: ' + decision.apply.next);
|
|
273
|
+
}
|
|
274
|
+
lines.push(' Model ' + decision.model.why);
|
|
275
|
+
lines.push(' ' + decision.apply.delegate);
|
|
276
|
+
if (decision.model.nextSession) {
|
|
277
|
+
lines.push(' new sessions: main model to ' + decision.model.nextSession + ' until the window resets');
|
|
278
|
+
}
|
|
279
|
+
for (const note of decision.notes) {
|
|
280
|
+
lines.push(' Note ' + note);
|
|
281
|
+
}
|
|
282
|
+
return lines.join('\n');
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
module.exports = {
|
|
286
|
+
decide,
|
|
287
|
+
fromReport,
|
|
288
|
+
renderRecommend,
|
|
289
|
+
delegateModel,
|
|
290
|
+
currentEffort,
|
|
291
|
+
NEXT_LOWER,
|
|
292
|
+
REASONING_FLOOR,
|
|
293
|
+
CRITICAL_TURNS,
|
|
294
|
+
TIGHT_TURNS,
|
|
295
|
+
};
|
|
@@ -812,13 +812,24 @@ function stampPlan(all, planId) {
|
|
|
812
812
|
return stamped;
|
|
813
813
|
}
|
|
814
814
|
|
|
815
|
-
// A sample is better when it rests on more
|
|
816
|
-
// numbers, so a
|
|
815
|
+
// A sample is better when it rests on more of the meter. Percentages read in
|
|
816
|
+
// whole numbers, so a reading at 1% prices a point against a bracket that is
|
|
817
|
+
// mostly rounding, while one at 60% divides by a number that means something.
|
|
818
|
+
// Turn count only breaks the tie: it says how much local spend sat behind the
|
|
819
|
+
// reading, not how precise the denominator was, and preferring it outright is
|
|
820
|
+
// how a 44-turn baseline read at 1% once beat every honest sample after it.
|
|
817
821
|
function betterCalibration(current, candidate) {
|
|
818
822
|
if (!candidate || !Number.isFinite(candidate.usdPerPercent) || candidate.usdPerPercent <= 0) {
|
|
819
823
|
return current || null;
|
|
820
824
|
}
|
|
821
825
|
if (!current || !Number.isFinite(current.turns)) return candidate;
|
|
826
|
+
if (
|
|
827
|
+
Number.isFinite(candidate.percent) &&
|
|
828
|
+
Number.isFinite(current.percent) &&
|
|
829
|
+
candidate.percent !== current.percent
|
|
830
|
+
) {
|
|
831
|
+
return candidate.percent > current.percent ? candidate : current;
|
|
832
|
+
}
|
|
822
833
|
return candidate.turns > current.turns ? candidate : current;
|
|
823
834
|
}
|
|
824
835
|
|
|
@@ -933,16 +944,23 @@ function buildWindow(spec, snapshot, events, now, options) {
|
|
|
933
944
|
// holding. The learned price covers it: what a point costs is a property
|
|
934
945
|
// of the plan, not of this reading.
|
|
935
946
|
const selfPriced =
|
|
936
|
-
rawPercent
|
|
947
|
+
rawPercent >= MIN_BASELINE_PERCENT && upTo.cost > 0 && upTo.turns >= MIN_BASELINE_TURNS
|
|
937
948
|
? { usdPerPercent: upTo.cost / rawPercent, turns: upTo.turns, percent: rawPercent }
|
|
938
949
|
: null;
|
|
939
950
|
// A metered window has nothing to learn: its price per point is stated.
|
|
940
951
|
if (selfPriced && !extra.metered) window.calibration = selfPriced;
|
|
941
952
|
|
|
942
953
|
const known = extra.knownCalibration;
|
|
954
|
+
// A remembered price read off a near-empty meter is the same rounding
|
|
955
|
+
// bracket in disguise, so it is no more usable than measuring one now.
|
|
943
956
|
const usable =
|
|
944
|
-
known &&
|
|
945
|
-
|
|
957
|
+
known &&
|
|
958
|
+
Number.isFinite(known.usdPerPercent) &&
|
|
959
|
+
known.usdPerPercent > 0 &&
|
|
960
|
+
!(Number.isFinite(known.percent) && known.percent < MIN_BASELINE_PERCENT)
|
|
961
|
+
? known
|
|
962
|
+
: null;
|
|
963
|
+
// Trust the better-measured of the two, whichever that is; a stated price
|
|
946
964
|
// beats both.
|
|
947
965
|
const stated =
|
|
948
966
|
extra.metered && Number.isFinite(extra.usdPerPercent) && extra.usdPerPercent > 0
|
|
@@ -950,9 +968,7 @@ function buildWindow(spec, snapshot, events, now, options) {
|
|
|
950
968
|
: null;
|
|
951
969
|
const chosen =
|
|
952
970
|
stated ||
|
|
953
|
-
(selfPriced && usable
|
|
954
|
-
? (usable.turns > selfPriced.turns ? usable : selfPriced)
|
|
955
|
-
: selfPriced || usable);
|
|
971
|
+
(selfPriced && usable ? betterCalibration(usable, selfPriced) : selfPriced || usable);
|
|
956
972
|
|
|
957
973
|
if (after.cost > 0 && chosen) {
|
|
958
974
|
const pricePerPoint = chosen.usdPerPercent;
|
|
@@ -988,7 +1004,12 @@ function buildWindow(spec, snapshot, events, now, options) {
|
|
|
988
1004
|
// so when the two disagree that badly it is the local history that is
|
|
989
1005
|
// incomplete, not the meter.
|
|
990
1006
|
const thin = spent.turns < MIN_BASELINE_TURNS && percent >= UNEXPLAINED_PERCENT;
|
|
991
|
-
|
|
1007
|
+
// The same rounding bracket again, in the optimistic direction this time: a
|
|
1008
|
+
// window reading 1% divided a full hour of spend by one and priced the
|
|
1009
|
+
// remaining 99 points at thousands of dollars. Below the floor the learned
|
|
1010
|
+
// price takes over through the fallback chain.
|
|
1011
|
+
const measured =
|
|
1012
|
+
percent !== null && percent >= MIN_BASELINE_PERCENT && spent.cost > 0 && !thin;
|
|
992
1013
|
const derived = measured ? spent.cost / percent : null;
|
|
993
1014
|
const known = extra.knownCalibration;
|
|
994
1015
|
const metered =
|
|
@@ -1004,11 +1025,14 @@ function buildWindow(spec, snapshot, events, now, options) {
|
|
|
1004
1025
|
: null;
|
|
1005
1026
|
window.metered = metered;
|
|
1006
1027
|
|
|
1028
|
+
// The API reports whole numbers, so a low reading is a wide bracket. A fact
|
|
1029
|
+
// about the reading, not the pricing, so it is set whether or not a price
|
|
1030
|
+
// per point could be found.
|
|
1031
|
+
window.coarse = percent !== null && percent < MIN_BASELINE_PERCENT;
|
|
1032
|
+
|
|
1007
1033
|
if (percent !== null && priced !== null) {
|
|
1008
1034
|
window.usdPerPercent = priced;
|
|
1009
1035
|
window.remainingUSD = window.usdPerPercent * window.percentLeft;
|
|
1010
|
-
// The API reports whole numbers, so a low reading is a wide bracket.
|
|
1011
|
-
window.coarse = percent < 5;
|
|
1012
1036
|
|
|
1013
1037
|
const perTurn = typicalTurnCost(recentEvents, inWindow, events, MIN_PACE_SAMPLE);
|
|
1014
1038
|
window.percentPerTurn = perTurn === null ? null : perTurn / window.usdPerPercent;
|
|
@@ -1313,6 +1337,15 @@ const SATURATION_LIMIT = 105;
|
|
|
1313
1337
|
// Fewer turns than this before the snapshot and a point cannot be priced.
|
|
1314
1338
|
const MIN_BASELINE_TURNS = 5;
|
|
1315
1339
|
|
|
1340
|
+
// A reading below this cannot price a point either. The API reports whole
|
|
1341
|
+
// numbers, so at 1% the denominator is mostly rounding: the true figure is
|
|
1342
|
+
// anywhere in a bracket as wide as the reading itself, and a point priced
|
|
1343
|
+
// against it converts later spend into several times the points it really
|
|
1344
|
+
// moved. A snapshot taken just after a reset is the common case - one sat at
|
|
1345
|
+
// 1% while the local spend divided by it asserted 97% of a window that was
|
|
1346
|
+
// truly at 35.
|
|
1347
|
+
const MIN_BASELINE_PERCENT = 5;
|
|
1348
|
+
|
|
1316
1349
|
// Past this much of a window, a handful of local turns is not what spent it,
|
|
1317
1350
|
// so their total is not a fair price for a point.
|
|
1318
1351
|
const UNEXPLAINED_PERCENT = 20;
|
|
@@ -1328,6 +1361,10 @@ function reconstructWindow(spec, snapshot, events, now) {
|
|
|
1328
1361
|
const past = totals(events.filter((e) => e.at >= pastStart && e.at <= resetsAt));
|
|
1329
1362
|
if (past.cost <= 0) return null;
|
|
1330
1363
|
|
|
1364
|
+
// The same rounding bracket that poisons the live correction poisons a
|
|
1365
|
+
// rebuild: a closed window that read 1% prices a point off almost nothing.
|
|
1366
|
+
if (snapshot.utilization < MIN_BASELINE_PERCENT) return null;
|
|
1367
|
+
|
|
1331
1368
|
const usdPerPercent = past.cost / snapshot.utilization;
|
|
1332
1369
|
|
|
1333
1370
|
// The window running now began when the old one reset, not five hours ago.
|
|
@@ -2261,6 +2298,23 @@ async function main(argv) {
|
|
|
2261
2298
|
}
|
|
2262
2299
|
return 0;
|
|
2263
2300
|
}
|
|
2301
|
+
const recommendAt = argv.indexOf('--recommend');
|
|
2302
|
+
if (recommendAt !== -1) {
|
|
2303
|
+
// The turn count is optional: with one the verdict is about that job,
|
|
2304
|
+
// without one it is about the headroom in general.
|
|
2305
|
+
const next = argv[recommendAt + 1];
|
|
2306
|
+
const turns = next && next.indexOf('--') !== 0 ? Number(next) : null;
|
|
2307
|
+
const recommend = require('./recommend.js');
|
|
2308
|
+
if (wantsJson) {
|
|
2309
|
+
process.stdout.write(
|
|
2310
|
+
JSON.stringify(recommend.decide(recommend.fromReport(data, turns)), null, 2) + '\n'
|
|
2311
|
+
);
|
|
2312
|
+
} else {
|
|
2313
|
+
process.stdout.write(recommend.renderRecommend(data, turns) + '\n');
|
|
2314
|
+
}
|
|
2315
|
+
return 0;
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2264
2318
|
if (wantsJson) {
|
|
2265
2319
|
process.stdout.write(JSON.stringify(data, null, 2) + '\n');
|
|
2266
2320
|
} else {
|
|
@@ -2295,6 +2349,7 @@ module.exports = {
|
|
|
2295
2349
|
reconstructWindow,
|
|
2296
2350
|
SATURATION_LIMIT,
|
|
2297
2351
|
MIN_BASELINE_TURNS,
|
|
2352
|
+
MIN_BASELINE_PERCENT,
|
|
2298
2353
|
buildWindows,
|
|
2299
2354
|
limitWindows,
|
|
2300
2355
|
lastRejections,
|