claude-usage-limits 1.39.6 → 1.40.1
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 +106 -3
- package/commands/usage-mode.md +23 -0
- package/package.json +1 -1
- package/skills/usage-limits/SKILL.md +45 -1
- package/skills/usage-limits/scripts/brief.js +189 -10
- package/skills/usage-limits/scripts/lowpri.js +423 -0
- package/skills/usage-limits/scripts/mode.js +98 -0
- package/skills/usage-limits/scripts/relay.js +29 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "usage-limits",
|
|
3
3
|
"displayName": "Usage Limits",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.40.1",
|
|
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.40.1",
|
|
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
|
@@ -350,6 +350,106 @@ The skill also requires Claude to say up front when a job will not fit in what
|
|
|
350
350
|
is left, name what it is doing now, what it is leaving, and when the rest can
|
|
351
351
|
happen, rather than starting and stopping halfway through an edit.
|
|
352
352
|
|
|
353
|
+
## /low-priority, and Claude Code's own features at the wall
|
|
354
|
+
|
|
355
|
+
Claude Code has grown its own machinery for the moment the 5-hour limit is hit.
|
|
356
|
+
Some of it a hook can read and some of it cannot, and the whole of this
|
|
357
|
+
section is about keeping that line straight — a plugin that guesses at the half
|
|
358
|
+
it cannot see is worse at this than one that says nothing.
|
|
359
|
+
|
|
360
|
+
**`/low-priority`** is a hidden toggle the CLI offers when the session (5-hour)
|
|
361
|
+
limit is reached. It keeps the session working at reduced priority and spends
|
|
362
|
+
your **weekly** limit, plus a separate weekly lower-priority allowance. Replies
|
|
363
|
+
can pause while it waits for spare capacity. It appears in no changelog and in
|
|
364
|
+
no documentation — there are zero mentions across all 405 versions of the
|
|
365
|
+
bundled changelog back to 0.2.21 — and it is declared `isHidden: true`, so the
|
|
366
|
+
only way anyone learns of it is the offer line at the wall.
|
|
367
|
+
|
|
368
|
+
What the plugin can read is whether **your account is provisioned** for it, from
|
|
369
|
+
one field in the same `~/.claude.json` it already parses for the meter:
|
|
370
|
+
`cachedGrowthBookFeatures.tengu_toasty_breeze`. That costs no extra I/O, and it
|
|
371
|
+
is re-read on every prompt rather than remembered, because the grant can be
|
|
372
|
+
withdrawn mid-week.
|
|
373
|
+
|
|
374
|
+
What the plugin **cannot** read is whether it is on right now. That state lives
|
|
375
|
+
in the CLI's process memory and is written to no file — not `~/.claude.json`,
|
|
376
|
+
not `~/.claude/state`, and no hook payload or status-line field carries it. So
|
|
377
|
+
there are three states and never a fourth:
|
|
378
|
+
|
|
379
|
+
| state | how it is known |
|
|
380
|
+
| --- | --- |
|
|
381
|
+
| **absent** | the account is not provisioned, and the plugin never mentions the command |
|
|
382
|
+
| **offered** | provisioned, so the brief says it exists at the wall — as a possibility |
|
|
383
|
+
| **acknowledged** | **you** said you switched it on: `mode --low-priority on` |
|
|
384
|
+
|
|
385
|
+
Two further gates sit in front of the offer that nothing here will ever see: the
|
|
386
|
+
experiment arm arrives in a response header, and the CLI withholds the offer
|
|
387
|
+
during a cooloff and once the weekly allowance is spent. So the brief says "if
|
|
388
|
+
the wall offers it", never "you can run it". And it never says the plugin or the
|
|
389
|
+
model can switch it on: Claude cannot type a slash command, and the command is
|
|
390
|
+
declared `supportsNonInteractive: false`, so it is unusable in a headless or
|
|
391
|
+
relayed run either.
|
|
392
|
+
|
|
393
|
+
At the wall with weekly headroom, the line looks like this:
|
|
394
|
+
|
|
395
|
+
```
|
|
396
|
+
If the wall offers it, /low-priority carries this session past the 5-hour limit
|
|
397
|
+
at lower priority instead of stopping: it spends the weekly limit, which is at
|
|
398
|
+
45% and so has room, and replies may pause while it waits for spare capacity -
|
|
399
|
+
the wait and its ceiling are set by the server per request. It is a toggle: you
|
|
400
|
+
type it yourself and run it again to stop, and nothing here can switch it on
|
|
401
|
+
for you.
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
**The recommendation has a number behind it.** It is offered only while the
|
|
405
|
+
weekly is at or below **80 per cent** and the 5-hour window is the binding wall.
|
|
406
|
+
Above that the brief actively says not to, and cites the figure: low-priority
|
|
407
|
+
spends the weekly *and* draws on a weekly allowance whose size is exposed to no
|
|
408
|
+
hook and no file, and a real user measured it emptying most of a week in a
|
|
409
|
+
couple of hours. No wait time is ever printed, because the retry and the ceiling
|
|
410
|
+
come from `lowPriorityRetryAfterSeconds` and `lowPriorityMaxWaitSeconds` on each
|
|
411
|
+
response — any fixed "20 seconds, 20 minutes" would be invented.
|
|
412
|
+
|
|
413
|
+
Once you acknowledge it, three things change. The brief brakes on the **weekly**
|
|
414
|
+
window instead of the 5-hour one, because that is the only brake left. It stops
|
|
415
|
+
telling you to wind down at the 5-hour wall. And the relay **will not book a
|
|
416
|
+
wake for the 5-hour reset** — this session carries straight past it, so a wake
|
|
417
|
+
would fire into a session that never stopped. A weekly wall still arms. The
|
|
418
|
+
acknowledgement lapses by itself when the 5-hour window it was made against
|
|
419
|
+
resets.
|
|
420
|
+
|
|
421
|
+
**The graceful wrap-up note.** Claude Code can inject its own "finish up"
|
|
422
|
+
instruction at the wall. The mechanism is real, and the gate is
|
|
423
|
+
`tengu_lantern_wick_mode` — the bundle's own normalizer keeps only `"wrap-up"`
|
|
424
|
+
and `"next-steps"` and maps everything else to `"off"`. When it is on, this
|
|
425
|
+
plugin stands down and says who is speaking, because two agents telling the
|
|
426
|
+
model to wind down in different words is worse than one. When it is off, the
|
|
427
|
+
plugin's own instruction stands. It is read, not assumed, so the right thing
|
|
428
|
+
happens whichever way the flag is set. (The near-limit variant is a separate
|
|
429
|
+
flag, `tengu_vellum_anchor`.)
|
|
430
|
+
|
|
431
|
+
**`autoContinueAtUsageLimit`.** Since 2.1.234 Claude Code waits out the reset
|
|
432
|
+
and continues the same open session by itself, and the setting is **on by
|
|
433
|
+
default**. In place, with the context intact, that is better than any scheduled
|
|
434
|
+
wake. So when a resume relay is armed and this is on, the brief says so: the
|
|
435
|
+
wake is the route for a session that will be **closed** at the reset, and both
|
|
436
|
+
firing for the same reset would start the work twice and spend the weekly twice.
|
|
437
|
+
|
|
438
|
+
**`/limit-reset`**, the once-weekly manual session reset, is detected but not
|
|
439
|
+
built on: this account holds no grant (`tengu_cedar_ember` absent,
|
|
440
|
+
`cachedUsageUtilization.cedar_ember` null), so a feature resting on it would be
|
|
441
|
+
untestable. If a grant appears, the brief names it at the wall, says it only
|
|
442
|
+
works while you are actually at a limit, and says the work it unlocks still
|
|
443
|
+
spends the weekly. It never reports how many are left — `resets_left` comes from
|
|
444
|
+
a live endpoint and appears in no file a hook can read.
|
|
445
|
+
|
|
446
|
+
**Grace is not weekly spend.** The allowance the wall gives you is metered in
|
|
447
|
+
its own per-window meters (`anthropic-ratelimit-unified-grace-5h-utilization`
|
|
448
|
+
and `-grace-7d-utilization`, with a `rateLimitGraceZone` naming which window it
|
|
449
|
+
belongs to), not billed to the 7-day window. Its size arrives in response
|
|
450
|
+
headers and appears in no hook payload, no status-line field and no file, so the
|
|
451
|
+
plugin says nothing about how much of it is left rather than estimating.
|
|
452
|
+
|
|
353
453
|
## The relay: carrying a project across the reset
|
|
354
454
|
|
|
355
455
|
The handoff has always had the same flaw. It gets written, and then it sits in
|
|
@@ -1170,7 +1270,8 @@ skills/usage-limits/scripts/ usage.js, brief.js, pulse.js, stop.js,
|
|
|
1170
1270
|
recommend.js, panel.js, feed.js,
|
|
1171
1271
|
statusline.js, live.js, view.js, bars.js,
|
|
1172
1272
|
activity.js, reading.js, drift.js,
|
|
1173
|
-
mode.js, voice.js, relay.js, wake.js
|
|
1273
|
+
mode.js, voice.js, relay.js, wake.js,
|
|
1274
|
+
lowpri.js
|
|
1174
1275
|
skills/usage-limits/references/ the longer notes
|
|
1175
1276
|
hooks/hooks.json runs brief.js before each prompt, pulse.js
|
|
1176
1277
|
during long turns, stop.js after each reply
|
|
@@ -1182,6 +1283,7 @@ commands/statusline.md the /usage-limits:statusline command
|
|
|
1182
1283
|
commands/usage-mode.md the /usage-mode command
|
|
1183
1284
|
bin/cli.js the npx entry point
|
|
1184
1285
|
tools/sync-version.js keeps the manifest version in step
|
|
1286
|
+
tools/test-tempdirs.js temp directories the suite deletes on exit
|
|
1185
1287
|
vscode/ the VS Code extension; build.js copies the
|
|
1186
1288
|
scripts into vscode/lib and makes the vsix
|
|
1187
1289
|
test/ node --test, no dependencies
|
|
@@ -1193,10 +1295,11 @@ test/ node --test, no dependencies
|
|
|
1193
1295
|
node --test
|
|
1194
1296
|
```
|
|
1195
1297
|
|
|
1196
|
-
|
|
1298
|
+
895 tests over the pricing, the window arithmetic, plan and credit detection,
|
|
1197
1299
|
the status line, the before-prompt line, the mid-turn pulse, the after-reply tally and the session history, job forecasting,
|
|
1198
1300
|
per-project attribution, the Codex reader and its installer, the CLI,
|
|
1199
|
-
packaging,
|
|
1301
|
+
packaging, the settings save/restore, and what Claude Code's own wall-time
|
|
1302
|
+
features are readable as (/low-priority, the wrap-up note, autoContinueAtUsageLimit).
|
|
1200
1303
|
|
|
1201
1304
|
The budget modes are checked as rules rather than examples: the whole stopping
|
|
1202
1305
|
matrix is walked in every mode (640 lines), `off` must inject nothing at any
|
package/commands/usage-mode.md
CHANGED
|
@@ -55,6 +55,29 @@ Commands:
|
|
|
55
55
|
the last one, naming it first.
|
|
56
56
|
- `--ledger` - measured turns and cost per turn, per mode, from what replies
|
|
57
57
|
actually cost.
|
|
58
|
+
- `--low-priority` / `--low-priority on` / `--low-priority off` - whether YOU
|
|
59
|
+
have switched Claude Code's `/low-priority` on. With no argument it reports
|
|
60
|
+
what is recorded, whether this account is provisioned for the command at all,
|
|
61
|
+
whether Claude Code will inject its own wrap-up note at the wall on this
|
|
62
|
+
machine, and whether the CLI's own continue-after-reset is on.
|
|
63
|
+
|
|
64
|
+
**This is a record of something the user did, never a reading.** Claude Code
|
|
65
|
+
keeps the live low-priority state in process memory and writes it to no file:
|
|
66
|
+
not `~/.claude.json`, not `~/.claude/state`, and no hook payload or
|
|
67
|
+
status-line field carries it. So the plugin knows three things and not a
|
|
68
|
+
fourth - the account is provisioned (`tengu_toasty_breeze.enabled`), the user
|
|
69
|
+
has said it is on, or neither. It never claims it is running.
|
|
70
|
+
|
|
71
|
+
`on` changes three behaviours: the brief brakes on the **weekly** window
|
|
72
|
+
instead of the 5-hour one, it stops telling you to wind down at the 5-hour
|
|
73
|
+
wall, and the relay will not book a wake for the 5-hour reset - that reset is
|
|
74
|
+
no longer a wall this session stops at. The record lapses on its own when the
|
|
75
|
+
5-hour window it was made against resets, because that is when low-priority
|
|
76
|
+
ends. Say `off` if it ends sooner.
|
|
77
|
+
|
|
78
|
+
The model cannot type a slash command, and `/low-priority` is declared
|
|
79
|
+
`supportsNonInteractive: false`, so it is also unusable in a headless or
|
|
80
|
+
relayed run. Nothing here can switch it on.
|
|
58
81
|
|
|
59
82
|
Two rules that hold in every mode:
|
|
60
83
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-usage-limits",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.40.1",
|
|
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",
|
|
@@ -591,6 +591,49 @@ was not caution; it was quitting with a reason that sounded like one.
|
|
|
591
591
|
Only when the switch is genuinely unavailable - no other window has room, or the
|
|
592
592
|
user has ruled it out - does the checkpoint below apply.
|
|
593
593
|
|
|
594
|
+
### /low-priority: the user's lever, never yours
|
|
595
|
+
|
|
596
|
+
Claude Code has a hidden toggle, `/low-priority`, offered when the 5-hour session
|
|
597
|
+
limit is reached. It keeps the session working at reduced priority and spends the
|
|
598
|
+
**weekly** limit, plus a separate weekly lower-priority allowance; replies can
|
|
599
|
+
pause while it waits for spare capacity.
|
|
600
|
+
|
|
601
|
+
**You cannot switch it on.** You cannot type a slash command, and the command is
|
|
602
|
+
declared `supportsNonInteractive: false`, so it does not work in a headless or
|
|
603
|
+
relayed run either. What you may do is say, in one line, that it exists and what
|
|
604
|
+
it costs - and only when the budget line has already said so, because the line is
|
|
605
|
+
the only thing that knows whether this account is provisioned for it at all.
|
|
606
|
+
Never volunteer it otherwise: telling a user to run a command their account does
|
|
607
|
+
not have is worse than saying nothing.
|
|
608
|
+
|
|
609
|
+
Three rules when the line does mention it:
|
|
610
|
+
|
|
611
|
+
- **Word it as a possibility.** The offer is gated on an experiment arm that
|
|
612
|
+
arrives in a response header, and the CLI also withholds it during a cooloff
|
|
613
|
+
and once the weekly allowance is spent. "If the wall offers it", not "run it".
|
|
614
|
+
- **Never invent a wait time.** The retry interval and its ceiling come from the
|
|
615
|
+
server on each response. There is no fixed number.
|
|
616
|
+
- **If the line says not to, say not to, and why.** Above 80 per cent weekly it
|
|
617
|
+
is the wrong move: it spends the window that takes days to come back in order
|
|
618
|
+
to save one that comes back in hours.
|
|
619
|
+
|
|
620
|
+
If the user tells you they have switched it on, the honest answer is to record
|
|
621
|
+
it: `node scripts/mode.js --low-priority on`. From then the budget line brakes on
|
|
622
|
+
the weekly instead of the 5-hour window, stops telling you to wind down at the
|
|
623
|
+
5-hour wall, and the relay stops booking a wake for a reset this session no
|
|
624
|
+
longer stops at. **Record it only when they said so.** Whether it is running is
|
|
625
|
+
in the CLI's process memory and readable nowhere, so nothing here may infer it,
|
|
626
|
+
and `mode --low-priority off` is how it is taken back.
|
|
627
|
+
|
|
628
|
+
The same applies to `/limit-reset`, the once-weekly manual session reset: the
|
|
629
|
+
line names it only if a grant is actually readable, it only works while you are
|
|
630
|
+
at a limit, the work it unlocks still spends the weekly, and how many are left is
|
|
631
|
+
not knowable from here.
|
|
632
|
+
|
|
633
|
+
And if the budget line says Claude Code is injecting its own wrap-up note at this
|
|
634
|
+
wall, follow that note: it is more specific than anything here, and two
|
|
635
|
+
instructions to wind down in different words is worse than one.
|
|
636
|
+
|
|
594
637
|
## 6. Checkpoint before the wall
|
|
595
638
|
|
|
596
639
|
When the binding window is under roughly 10 percent, or under about ten turns
|
|
@@ -865,7 +908,7 @@ stop.
|
|
|
865
908
|
| `scripts/host.js` | Works out which agent this is running inside, so one host's percentages are never reported against the other's turns. |
|
|
866
909
|
| `scripts/codex.js` | The Codex reader: the meter and the pace out of `~/.codex/sessions`, plus the live `--refresh` call. |
|
|
867
910
|
| `scripts/install-codex-hook.js` | `status`, `on`, `off`. Installs the Codex-side instruction, which Claude Code does not need. |
|
|
868
|
-
| `scripts/mode.js` | The budget mode: no arguments to report it, `max`/`high`/`standard`/`off` to set it, `auto`, `off --guard 95`, `--list`, `--explain <name>`, `--floor`/`--ceiling`/`--pin`, `--baseline`, `--advice`/`--no-advice`, `--history`, `undo`, `--ledger`. Reads settings.json and never writes it. |
|
|
911
|
+
| `scripts/mode.js` | The budget mode: no arguments to report it, `max`/`high`/`standard`/`off` to set it, `auto`, `off --guard 95`, `--list`, `--explain <name>`, `--floor`/`--ceiling`/`--pin`, `--baseline`, `--advice`/`--no-advice`, `--history`, `undo`, `--ledger`, `--low-priority [on|off]`. Reads settings.json and never writes it. |
|
|
869
912
|
| `scripts/lowpower.js` | `status`, `on`, `off`. Restores what it replaced. Claude Code only. |
|
|
870
913
|
| `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. |
|
|
871
914
|
| `references/tactics.md` | Every lever that lowers cost, and why it works. |
|
|
@@ -879,6 +922,7 @@ stop.
|
|
|
879
922
|
| `scripts/net.js` | Can this machine reach the API, and was a failed run the network's fault. Three probes, TLS-interception detection, and the backoff the offline retries use. |
|
|
880
923
|
| `scripts/wake.js` | What the scheduler runs after the reset: re-checks the meter, then notifies or resumes. Never called by hand. |
|
|
881
924
|
| `scripts/voice.js` | The local writing profile: `show`, `card`, `set "<instruction>"`, `clear`, `off`/`on`, `forget`. |
|
|
925
|
+
| `scripts/lowpri.js` | What Claude Code's own features at the wall are readable as: whether this account is provisioned for `/low-priority`, whether a manual session reset grant exists, whether the CLI injects its own wrap-up note here, whether usage credits are available, whether `autoContinueAtUsageLimit` is on, and the record of the user saying they switched low-priority on. Reads only; never writes anything but that one record. |
|
|
882
926
|
| `references/how-it-works.md` | Where the numbers come from and where they are soft. |
|
|
883
927
|
|
|
884
928
|
## Codex controls
|
|
@@ -24,6 +24,7 @@ const reading = require('./reading.js');
|
|
|
24
24
|
const voice = require('./voice.js');
|
|
25
25
|
const mode = require('./mode.js');
|
|
26
26
|
const feed = require('./feed.js');
|
|
27
|
+
const lowpri = require('./lowpri.js');
|
|
27
28
|
|
|
28
29
|
const SECOND = 1000;
|
|
29
30
|
const DAY = 24 * 60 * 60 * 1000;
|
|
@@ -511,6 +512,11 @@ const CACHED_BINDING_FIELDS = [
|
|
|
511
512
|
'msToReset',
|
|
512
513
|
'refusedAt',
|
|
513
514
|
'refusedResetsAt',
|
|
515
|
+
// What the room that is left buys, per window. Needed because the binding
|
|
516
|
+
// window can be swapped for the weekly once low-priority is acknowledged, and
|
|
517
|
+
// a swapped window with the old window's turn count would be the brief
|
|
518
|
+
// quoting two different budgets in one sentence.
|
|
519
|
+
'turnsLeft',
|
|
514
520
|
];
|
|
515
521
|
|
|
516
522
|
function cacheableBinding(binding) {
|
|
@@ -1003,6 +1009,74 @@ function briefText(input) {
|
|
|
1003
1009
|
);
|
|
1004
1010
|
}
|
|
1005
1011
|
}
|
|
1012
|
+
// /low-priority: the other thing that is not stopping.
|
|
1013
|
+
//
|
|
1014
|
+
// Three rules this block exists to keep. It says nothing at all unless the
|
|
1015
|
+
// account is provisioned for the command, because advertising a slash command
|
|
1016
|
+
// an account does not have is worse than silence. It never implies that the
|
|
1017
|
+
// plugin or the model can switch it on - Claude cannot type a slash command,
|
|
1018
|
+
// and the command is declared supportsNonInteractive:false, so it is unusable
|
|
1019
|
+
// in a headless or relayed run as well. And it words the offer as a
|
|
1020
|
+
// possibility, because the real gate is an experiment arm in a response
|
|
1021
|
+
// header that nothing here will ever see.
|
|
1022
|
+
//
|
|
1023
|
+
// No wait time is printed. The retry and the ceiling come from
|
|
1024
|
+
// lowPriorityRetryAfterSeconds and lowPriorityMaxWaitSeconds on each
|
|
1025
|
+
// response, so any fixed number would be invented.
|
|
1026
|
+
// Kept in a variable as well as pushed, for the same reason as the escape: it
|
|
1027
|
+
// is the difference between stopping at the 5-hour wall and not, and the terse
|
|
1028
|
+
// style drops only what reads the same every turn. This does not.
|
|
1029
|
+
const lp = parts.lowPriority || null;
|
|
1030
|
+
let lowPrioritySentence = null;
|
|
1031
|
+
if (lp && lp.state === 'acknowledged') {
|
|
1032
|
+
lowPrioritySentence = (
|
|
1033
|
+
'You have said /low-priority is on, so the brake is the weekly window and not the 5-hour one: ' +
|
|
1034
|
+
(Number.isFinite(lp.fiveHourPercent)
|
|
1035
|
+
? 'the 5-hour limit is at ' + lp.fiveHourPercent + '% and no longer stops this session, '
|
|
1036
|
+
: 'the 5-hour limit no longer stops this session, ') +
|
|
1037
|
+
'the figures above are the weekly, and replies may pause while lower priority waits for spare ' +
|
|
1038
|
+
'capacity. Whether it is still on cannot be read from here, so if it has ended say so with ' +
|
|
1039
|
+
'usage-mode --low-priority off and the 5-hour wall counts again. It also draws on a separate ' +
|
|
1040
|
+
'weekly lower-priority allowance that is exposed to no hook and no file, so nothing here can ' +
|
|
1041
|
+
'track how much of that is left.'
|
|
1042
|
+
);
|
|
1043
|
+
} else if (lp && lp.advise && lp.advise.kind === 'offer') {
|
|
1044
|
+
lowPrioritySentence = (
|
|
1045
|
+
'If the wall offers it, /low-priority carries this session past the 5-hour limit at lower ' +
|
|
1046
|
+
'priority instead of stopping: it spends the weekly limit, which is at ' +
|
|
1047
|
+
lp.advise.weeklyPercent + '% and so has room, and replies may pause while it waits for spare ' +
|
|
1048
|
+
'capacity - the wait and its ceiling are set by the server per request. It is a toggle: you ' +
|
|
1049
|
+
'type it yourself and run it again to stop, and nothing here can switch it on for you. Say ' +
|
|
1050
|
+
'in one line that it is there; if it is taken, usage-mode --low-priority on is what makes ' +
|
|
1051
|
+
'this line brake on the weekly instead.'
|
|
1052
|
+
);
|
|
1053
|
+
} else if (lp && lp.advise && lp.advise.kind === 'hold') {
|
|
1054
|
+
lowPrioritySentence = (
|
|
1055
|
+
'The wall may offer /low-priority here, and it is not worth taking: it spends the weekly limit, ' +
|
|
1056
|
+
'which is already at ' + lp.advise.weeklyPercent + '% - past the ' + lp.advise.threshold +
|
|
1057
|
+
' per cent this plugin will recommend it at - and it draws on a weekly lower-priority ' +
|
|
1058
|
+
'allowance as well, which has been measured emptying most of a week in a couple of hours. ' +
|
|
1059
|
+
'Say in one line that the answer is no and why; waiting out the 5-hour reset is the cheaper move.'
|
|
1060
|
+
);
|
|
1061
|
+
}
|
|
1062
|
+
if (lowPrioritySentence) sentences.push(lowPrioritySentence);
|
|
1063
|
+
// A manual session reset, if this account ever gets one.
|
|
1064
|
+
//
|
|
1065
|
+
// /limit-reset refills the 5-hour window, works only AT a limit, and is once a
|
|
1066
|
+
// week - and the work it unlocks still spends the weekly, which is the part
|
|
1067
|
+
// worth saying out loud. This account holds no grant today
|
|
1068
|
+
// (tengu_cedar_ember absent, cachedUsageUtilization.cedar_ember null), so the
|
|
1069
|
+
// sentence is a detector rather than a feature: nothing is claimed about how
|
|
1070
|
+
// many resets are left, because resets_left is served by a live endpoint and
|
|
1071
|
+
// appears in no file a hook can read.
|
|
1072
|
+
if (lp && lp.resetGrant && (parts.pressure === 'tight' || parts.pressure === 'gone')) {
|
|
1073
|
+
sentences.push(
|
|
1074
|
+
'A once-weekly manual session reset appears to be available on this account (/limit-reset). It ' +
|
|
1075
|
+
'only works while you are actually AT a limit, and the work it unlocks still spends the weekly, ' +
|
|
1076
|
+
'so it moves the 5-hour wall rather than adding budget. How many are left is not readable from ' +
|
|
1077
|
+
'here - the CLI asks the server for that. It is yours to type, like /low-priority.'
|
|
1078
|
+
);
|
|
1079
|
+
}
|
|
1006
1080
|
if (parts.session) {
|
|
1007
1081
|
sentences.push(
|
|
1008
1082
|
'This session: ' + parts.session.turns + ' turns, ' +
|
|
@@ -1102,6 +1176,23 @@ function briefText(input) {
|
|
|
1102
1176
|
);
|
|
1103
1177
|
}
|
|
1104
1178
|
}
|
|
1179
|
+
// The relay was built for a CLI that stopped dead at the wall. Since 2.1.234
|
|
1180
|
+
// Claude Code waits out the reset and continues the same open session by
|
|
1181
|
+
// itself, and the setting that does it is ON BY DEFAULT
|
|
1182
|
+
// (autoContinueAtUsageLimit, code.claude.com/docs/en/settings-reference). In
|
|
1183
|
+
// place, with the context intact, that is strictly better than a wake: no
|
|
1184
|
+
// hand-off file to re-read and nothing lost. So the wake is the route for a
|
|
1185
|
+
// session that will be CLOSED, and saying so is what stops both firing for the
|
|
1186
|
+
// same reset - which would run the work twice and spend the weekly twice.
|
|
1187
|
+
if (carry && carry.armed && carry.armed.mode === 'resume' && parts.autoContinue && parts.autoContinue.value) {
|
|
1188
|
+
relaySentences.push(
|
|
1189
|
+
'Claude Code\'s own "Continue automatically at usage limit" is on (' + parts.autoContinue.source +
|
|
1190
|
+
'), so if this terminal is still open at the reset the CLI carries THIS session across by ' +
|
|
1191
|
+
'itself, in place, with its context intact - better than any wake. The wake is the route for a ' +
|
|
1192
|
+
'session that is closed by then. Both firing for the same reset would start the work twice and ' +
|
|
1193
|
+
'spend the weekly twice, so if the terminal is staying open, one of the two is worth standing down.'
|
|
1194
|
+
);
|
|
1195
|
+
}
|
|
1105
1196
|
if (carry && carry.last) {
|
|
1106
1197
|
relaySentences.push(
|
|
1107
1198
|
'The last relay ' +
|
|
@@ -1124,7 +1215,25 @@ function briefText(input) {
|
|
|
1124
1215
|
// near the wall is to make being cut off cheap - order the work, save as you
|
|
1125
1216
|
// go, keep a note of where things stand - not to shrink the work until it is
|
|
1126
1217
|
// guaranteed to fit.
|
|
1127
|
-
|
|
1218
|
+
// Whether Claude Code itself is giving the wrap-up instruction at this wall.
|
|
1219
|
+
//
|
|
1220
|
+
// Confirmed mechanism, gated on a flag this plugin reads rather than assumed:
|
|
1221
|
+
// the CLI injects its own "finish up" note when tengu_lantern_wick_mode is
|
|
1222
|
+
// "wrap-up" or "next-steps". On this machine that flag reads "off", so the
|
|
1223
|
+
// plugin's own instruction stands and nothing changes. When it flips, two
|
|
1224
|
+
// agents telling the model to wind down in different words is worse than one,
|
|
1225
|
+
// so this line stands down and says who is speaking instead.
|
|
1226
|
+
//
|
|
1227
|
+
// Not when an escape is live: the escape says the budget is still there, and
|
|
1228
|
+
// deferring to a note that says the opposite would be the plugin handing over
|
|
1229
|
+
// at the one moment it disagrees.
|
|
1230
|
+
const hostWrapsUpNow =
|
|
1231
|
+
Boolean(parts.hostWrapsUp) && !escapeText && (parts.pressure === 'tight' || parts.pressure === 'gone');
|
|
1232
|
+
const instruction = hostWrapsUpNow
|
|
1233
|
+
? 'Claude Code is injecting its own wrap-up note at this wall, so it gives the instruction and this ' +
|
|
1234
|
+
'line does not repeat it. Follow that note; the figures above are the ones to quote, and the ' +
|
|
1235
|
+
'session total below is what to close with.'
|
|
1236
|
+
:
|
|
1128
1237
|
// The escape outranks everything below it. A session that stops while a
|
|
1129
1238
|
// command would have carried it on has not been careful, it has quit - and
|
|
1130
1239
|
// that is a real session: the Fable weekly hit 89 per cent, the line said
|
|
@@ -1252,6 +1361,7 @@ function briefText(input) {
|
|
|
1252
1361
|
return (
|
|
1253
1362
|
sentences[0] + caveat + (parts.tier ? ' ' + parts.tier : '') + (bounded ? ' ' + bounded : '') + adviceText +
|
|
1254
1363
|
(escapeSentence ? ' ' + escapeSentence : '') +
|
|
1364
|
+
(lowPrioritySentence ? ' ' + lowPrioritySentence : '') +
|
|
1255
1365
|
(parts.pressure !== 'roomy' && relaySentences.length ? ' ' + relaySentences.join(' ') : '') +
|
|
1256
1366
|
'\n' + instruction + directive
|
|
1257
1367
|
);
|
|
@@ -1377,6 +1487,41 @@ function relayState(now, hookInput, binding, sessionId) {
|
|
|
1377
1487
|
}
|
|
1378
1488
|
}
|
|
1379
1489
|
|
|
1490
|
+
// What Claude Code's own wall-time features change about this brief.
|
|
1491
|
+
//
|
|
1492
|
+
// Only one of them changes a number. An acknowledged /low-priority retires the
|
|
1493
|
+
// 5-hour wall - the session carries straight past that reset at lower priority,
|
|
1494
|
+
// spending the weekly - so the window that actually stops the work becomes the
|
|
1495
|
+
// weekly, and the brief has to brake on that one instead. The swap happens here,
|
|
1496
|
+
// once, so the pressure, the relay and the sentence all describe the same
|
|
1497
|
+
// window; doing it in three places is how they come to describe two.
|
|
1498
|
+
//
|
|
1499
|
+
// Never on a guess. `acknowledged` means the user said so through
|
|
1500
|
+
// usage-mode --low-priority on, and that record lapses at the reset of the
|
|
1501
|
+
// window it was recorded against. Whether low-priority is really running is in
|
|
1502
|
+
// the CLI's process memory and readable nowhere; see lowpri.js.
|
|
1503
|
+
function wallFeatures(now, binding, windows) {
|
|
1504
|
+
const out = { binding, lowPriority: null, hostWrapsUp: false, swapped: false };
|
|
1505
|
+
try {
|
|
1506
|
+
const account = lowpri.snapshot();
|
|
1507
|
+
const info = lowpri.forBrief({ account, now, binding, windows });
|
|
1508
|
+
out.lowPriority = info;
|
|
1509
|
+
out.hostWrapsUp = lowpri.wrapUp(account).hostWrapsUp;
|
|
1510
|
+
out.autoContinue = info.autoContinue;
|
|
1511
|
+
if (info.state === 'acknowledged' && binding && binding.key === 'five_hour') {
|
|
1512
|
+
const weekly = lowpri.weeklyOf(windows);
|
|
1513
|
+
if (weekly && Number.isFinite(weekly.percentUsed) && !weekly.stale && Number.isFinite(weekly.resetsAt)) {
|
|
1514
|
+
out.binding = weekly;
|
|
1515
|
+
out.swapped = true;
|
|
1516
|
+
out.lowPriority = Object.assign({}, info, { weeklyBinding: true });
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
} catch (err) {
|
|
1520
|
+
// Nothing about these features is worth a failed prompt.
|
|
1521
|
+
}
|
|
1522
|
+
return out;
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1380
1525
|
// The one line `off` will ever say, and only when it has been asked for.
|
|
1381
1526
|
//
|
|
1382
1527
|
// `off` means off, including at 100 per cent: that is what was asked and it is
|
|
@@ -1629,10 +1774,35 @@ async function run(now, hookInput, opts) {
|
|
|
1629
1774
|
writeCache(mergeCache(all, sessionId, view, KEEP_SESSIONS));
|
|
1630
1775
|
}
|
|
1631
1776
|
|
|
1632
|
-
|
|
1777
|
+
// Settled before the pressure, the relay and the line are decided, so all
|
|
1778
|
+
// three describe the same window.
|
|
1779
|
+
const wall = wallFeatures(now, view.binding, view.windows);
|
|
1780
|
+
const binding = wall.binding;
|
|
1781
|
+
// The turn count belongs to whichever window is now binding. Carrying the
|
|
1782
|
+
// 5-hour count onto a weekly window would put two budgets in one sentence.
|
|
1783
|
+
const turnsLeftNow = wall.swapped
|
|
1784
|
+
? (Number.isFinite(binding.turnsLeft) ? binding.turnsLeft : null)
|
|
1785
|
+
: view.turnsLeft;
|
|
1786
|
+
const othersSummaryNow = wall.swapped
|
|
1787
|
+
? summariseOthers(view.windows || [], binding.key)
|
|
1788
|
+
: view.othersSummary;
|
|
1789
|
+
// Everything derived from the window that WAS binding has to go with it.
|
|
1790
|
+
//
|
|
1791
|
+
// The escape route was worked out for the 5-hour window - "this window is
|
|
1792
|
+
// scoped to one model, so switching model retires it" - and the sentence that
|
|
1793
|
+
// prints it is gated on the binding window's percentage. Left in place after
|
|
1794
|
+
// the swap it would describe one window while the figures beside it described
|
|
1795
|
+
// another, which is the exact confusion the binding window exists to prevent.
|
|
1796
|
+
// Same for the critical list, which excludes whichever window was binding: the
|
|
1797
|
+
// weekly would otherwise be both the binding window and a "note that" warning
|
|
1798
|
+
// about itself.
|
|
1799
|
+
const escapeNow = wall.swapped ? null : view.escape || null;
|
|
1800
|
+
const criticalNow = wall.swapped
|
|
1801
|
+
? (view.critical || []).filter((other) => other.label !== binding.label)
|
|
1802
|
+
: view.critical || [];
|
|
1633
1803
|
const { active, share } = activeShare(view.sessions, all, now, sessionId);
|
|
1634
|
-
const yourTurnsLeft = Number.isFinite(
|
|
1635
|
-
? Math.max(1, Math.round(
|
|
1804
|
+
const yourTurnsLeft = Number.isFinite(turnsLeftNow)
|
|
1805
|
+
? Math.max(1, Math.round(turnsLeftNow * share))
|
|
1636
1806
|
: null;
|
|
1637
1807
|
// Only a short runway is worth saying. Quoting it when there are hours left
|
|
1638
1808
|
// would make the line longer without making it more useful.
|
|
@@ -1681,7 +1851,7 @@ async function run(now, hookInput, opts) {
|
|
|
1681
1851
|
const offering = advice.ok && !advice.alreadyOffered;
|
|
1682
1852
|
if (offering) mode.adviceOffer(advice.id, sessionId, now);
|
|
1683
1853
|
|
|
1684
|
-
const pressureNow = pressure(binding, now, config, Number.isFinite(yourTurnsLeft) ? yourTurnsLeft :
|
|
1854
|
+
const pressureNow = pressure(binding, now, config, Number.isFinite(yourTurnsLeft) ? yourTurnsLeft : turnsLeftNow);
|
|
1685
1855
|
// Fast mode changes what the window's figures mean, so a toggle is a change
|
|
1686
1856
|
// worth saying even when nothing else has moved.
|
|
1687
1857
|
const fastMode = fastModeFor(sessionId);
|
|
@@ -1696,12 +1866,17 @@ async function run(now, hookInput, opts) {
|
|
|
1696
1866
|
budget.name,
|
|
1697
1867
|
pressureNow,
|
|
1698
1868
|
binding && Number.isFinite(binding.percentUsed) ? Math.round(binding.percentUsed / 5) * 5 : 'x',
|
|
1699
|
-
|
|
1869
|
+
escapeNow ? escapeNow.kind : '-',
|
|
1700
1870
|
tier || '-',
|
|
1701
1871
|
active > 1 ? 'shared' : 'solo',
|
|
1702
1872
|
carry && carry.armed ? 'relay' : '-',
|
|
1703
1873
|
offering ? 'advice' : '-',
|
|
1704
1874
|
fastMode ? 'fast' : '-',
|
|
1875
|
+
// An acknowledgement, or the wall starting to offer the toggle, changes what
|
|
1876
|
+
// the line says and which window it brakes on. Leaving it out of the digest
|
|
1877
|
+
// would let `max` swallow exactly the prompt on which that changed.
|
|
1878
|
+
wall.lowPriority ? wall.lowPriority.state + (wall.lowPriority.advise ? ':' + wall.lowPriority.advise.kind : '') : '-',
|
|
1879
|
+
wall.hostWrapsUp ? 'wrapup' : '-',
|
|
1705
1880
|
].join('|');
|
|
1706
1881
|
if (!budget.policy.briefWhenUnchanged && pressureNow === 'roomy') {
|
|
1707
1882
|
const slots = readCache();
|
|
@@ -1741,10 +1916,10 @@ async function run(now, hookInput, opts) {
|
|
|
1741
1916
|
binding && binding.family && Number.isFinite(binding.percentUsed) && binding.percentUsed >= HALF_SPENT
|
|
1742
1917
|
? familyLabel(binding.family)
|
|
1743
1918
|
: null,
|
|
1744
|
-
othersSummary:
|
|
1745
|
-
escape:
|
|
1919
|
+
othersSummary: othersSummaryNow,
|
|
1920
|
+
escape: escapeNow,
|
|
1746
1921
|
host: usage.currentHost(),
|
|
1747
|
-
turnsLeft:
|
|
1922
|
+
turnsLeft: turnsLeftNow,
|
|
1748
1923
|
effortWarning: view.effortWarning || null,
|
|
1749
1924
|
// Once per setting, and once per session, and never after a decline: the
|
|
1750
1925
|
// advice rules above decide, and the sentence itself is unchanged.
|
|
@@ -1760,13 +1935,17 @@ async function run(now, hookInput, opts) {
|
|
|
1760
1935
|
rebuilt: Boolean(binding && binding.estimated),
|
|
1761
1936
|
staleWindows: view.staleWindows || 0,
|
|
1762
1937
|
planChanged: Boolean(view.planChanged),
|
|
1763
|
-
critical:
|
|
1938
|
+
critical: criticalNow,
|
|
1764
1939
|
pointsSinceSnapshot: (binding && binding.pointsSinceSnapshot) || 0,
|
|
1765
1940
|
correctionUnreliable: Boolean(binding && binding.correctionUnreliable),
|
|
1766
1941
|
pointsBeyondSnapshot: (binding && binding.pointsBeyondSnapshot) || 0,
|
|
1767
1942
|
snapshotAge: view.snapshotAge,
|
|
1768
1943
|
snapshotStale: Number.isFinite(view.snapshotAgeMs) && view.snapshotAgeMs >= SNAPSHOT_TRUST_MS,
|
|
1769
1944
|
fastMode,
|
|
1945
|
+
// Claude Code's own wall-time features, as far as they are readable.
|
|
1946
|
+
lowPriority: wall.lowPriority,
|
|
1947
|
+
hostWrapsUp: wall.hostWrapsUp,
|
|
1948
|
+
autoContinue: wall.autoContinue || null,
|
|
1770
1949
|
// The turn count that matters for this session is its share of a shared
|
|
1771
1950
|
// budget, not the whole window's. Escalating on the whole window meant a
|
|
1772
1951
|
// count that looked comfortable while the part actually available here was
|
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Claude Code's own features at the usage wall, as far as a hook can read them.
|
|
4
|
+
//
|
|
5
|
+
// There are five of them and they are not equally knowable, so this module
|
|
6
|
+
// exists mostly to keep the difference straight. Everything here was verified
|
|
7
|
+
// against the installed CLI on 2026-09-25 (@anthropic-ai/claude-code 2.1.283,
|
|
8
|
+
// C:/Users/OWNER/AppData/Roaming/npm/node_modules/@anthropic-ai/claude-code).
|
|
9
|
+
//
|
|
10
|
+
// /low-priority - a hidden toggle offered when the 5-hour session limit is
|
|
11
|
+
// reached: it keeps the session running at lower priority and spends the
|
|
12
|
+
// WEEKLY limit, plus a separate weekly lower-priority allowance. Whether the
|
|
13
|
+
// account is PROVISIONED for it is readable, from one field in the same
|
|
14
|
+
// ~/.claude.json this plugin already parses:
|
|
15
|
+
// cachedGrowthBookFeatures.tengu_toasty_breeze
|
|
16
|
+
// In the bundle: Cj="tengu_toasty_breeze", AKe()=Au().enabled===!0, and the
|
|
17
|
+
// command declares isEnabled:()=>pt()&&(WL()||AKe()).
|
|
18
|
+
//
|
|
19
|
+
// Whether it is ON is NOT readable. The state lives in process memory only -
|
|
20
|
+
// WL()=mr().state.phase==="active", phases idle/armed/active/stale - and is
|
|
21
|
+
// written to no file. ~/.claude/state holds only mcp-discover-verdicts.json
|
|
22
|
+
// and skill-runs.jsonl; ~/.claude/usage-limits is empty; the documented hook
|
|
23
|
+
// payload (code.claude.com/docs/en/hooks) carries no usage field on any
|
|
24
|
+
// event; the statusLine payload has no low_priority field. So this module
|
|
25
|
+
// offers three states and never a fourth: absent, offered, and acknowledged
|
|
26
|
+
// by the user in their own words. activeKnown is false in all of them.
|
|
27
|
+
//
|
|
28
|
+
// Two further gates sit in front of the offer that nothing here can see: the
|
|
29
|
+
// experiment arm arrives in a response header (ZIe() requires
|
|
30
|
+
// lowPriorityOffer==="treatment"), and the CLI withholds the offer during a
|
|
31
|
+
// cooloff and once the weekly allowance is spent. So every sentence built
|
|
32
|
+
// from this is a possibility, never a promise - and the grant can be
|
|
33
|
+
// withdrawn mid-week (github.com/anthropics/claude-code/issues/95470), which
|
|
34
|
+
// is why the flag is re-read on every brief instead of remembered.
|
|
35
|
+
//
|
|
36
|
+
// The wait and retry are per-request, from lowPriorityRetryAfterSeconds and
|
|
37
|
+
// lowPriorityMaxWaitSeconds on the response headers. There is no client-side
|
|
38
|
+
// constant, so no number is printed for them. ($Fn=1800000 in the bundle is a
|
|
39
|
+
// 30-minute freshness cutoff on cached window readings, nothing to do with
|
|
40
|
+
// this.)
|
|
41
|
+
//
|
|
42
|
+
// /limit-reset - a once-weekly manual refill of the 5-hour window, usable
|
|
43
|
+
// only AT a limit, whose work still spends the weekly. Gated on
|
|
44
|
+
// tengu_cedar_ember, which is ABSENT from this account's feature cache, with
|
|
45
|
+
// cachedUsageUtilization.utilization.cedar_ember null. So there is nothing to
|
|
46
|
+
// spend here and nothing is built on it: only a detector that starts
|
|
47
|
+
// reporting if a grant ever appears. resets_left is served by a live
|
|
48
|
+
// endpoint, never a file, so the count is never claimed.
|
|
49
|
+
//
|
|
50
|
+
// The graceful wrap-up note - the CLI injecting "finish up" at the wall. The
|
|
51
|
+
// mechanism is real and the treatment TEXT is provisioned on this machine
|
|
52
|
+
// (tengu_lantern_wick_text), but the gate is the MODE flag, and the bundle's
|
|
53
|
+
// own normalizer keeps only "wrap-up" and "next-steps" and maps everything
|
|
54
|
+
// else to "off":
|
|
55
|
+
// function kuo(e){let n=typeof e==="string"?e.trim().toLowerCase():e;
|
|
56
|
+
// return n==="wrap-up"||n==="next-steps"?n:"off"}
|
|
57
|
+
// tengu_lantern_wick_mode reads "off" here, so the note does not fire on this
|
|
58
|
+
// machine today and the plugin's own near-wall instruction must stay. When
|
|
59
|
+
// the flag flips, hostWrapsUp goes true and the plugin stands down rather
|
|
60
|
+
// than telling the model to wrap up in different words. (tengu_lantern_wick
|
|
61
|
+
// is in the cache but is not a string this build contains at all, so it reads
|
|
62
|
+
// nothing; the near-limit note is a separate flag, tengu_vellum_anchor,
|
|
63
|
+
// T1()=x("tengu_vellum_anchor",!1), which reads false here.)
|
|
64
|
+
//
|
|
65
|
+
// Usage credits - blocked at the org level on this account
|
|
66
|
+
// (cachedExtraUsageDisabledReason "org_level_disabled", extra_usage
|
|
67
|
+
// is_enabled false), so nothing should offer /usage-credits here.
|
|
68
|
+
//
|
|
69
|
+
// autoContinueAtUsageLimit - documented at
|
|
70
|
+
// code.claude.com/docs/en/settings-reference, shipped in 2.1.234, and
|
|
71
|
+
// DEFAULTED TO TRUE in the bundle (value:r?.autoContinueAtUsageLimit??!0). It
|
|
72
|
+
// waits out the reset and continues the same open session. That is better
|
|
73
|
+
// than a scheduled wake when the terminal stays open, and it is why the relay
|
|
74
|
+
// has to say so rather than presenting its wake as the only route.
|
|
75
|
+
|
|
76
|
+
const fs = require('fs');
|
|
77
|
+
const os = require('os');
|
|
78
|
+
const path = require('path');
|
|
79
|
+
|
|
80
|
+
const atomic = require('./atomic.js');
|
|
81
|
+
|
|
82
|
+
const FLAG = 'tengu_toasty_breeze';
|
|
83
|
+
const RESET_FLAG = 'tengu_cedar_ember';
|
|
84
|
+
const WRAPUP_MODE_FLAG = 'tengu_lantern_wick_mode';
|
|
85
|
+
const WRAPUP_TEXT_FLAG = 'tengu_lantern_wick_text';
|
|
86
|
+
const NEAR_WALL_FLAG = 'tengu_vellum_anchor';
|
|
87
|
+
|
|
88
|
+
// The only two mode values the CLI's own normalizer keeps.
|
|
89
|
+
const WRAPUP_MODES = new Set(['wrap-up', 'next-steps']);
|
|
90
|
+
|
|
91
|
+
// The client default in the bundle (xj=10), clamped there to 1440 minutes.
|
|
92
|
+
const DEFAULT_COOLOFF_MINUTES = 10;
|
|
93
|
+
const MAX_COOLOFF_MINUTES = 1440;
|
|
94
|
+
|
|
95
|
+
// The recommendation rule, in one number so it can be argued with.
|
|
96
|
+
//
|
|
97
|
+
// /low-priority spends the weekly and draws on a weekly allowance whose size is
|
|
98
|
+
// exposed nowhere a hook can read, and a real user measured it burning "almost
|
|
99
|
+
// a week of usage in a couple hours"
|
|
100
|
+
// (github.com/anthropics/claude-code/issues/92544). So it is worth naming only
|
|
101
|
+
// while the weekly still has real room. At or below this it is offered; above
|
|
102
|
+
// it the brief says not to, and says why.
|
|
103
|
+
const WEEKLY_HEADROOM_MAX = 80;
|
|
104
|
+
|
|
105
|
+
// Past this the 5-hour window is the wall, which is the only place the CLI
|
|
106
|
+
// offers the toggle at all.
|
|
107
|
+
const WALL_PERCENT = 90;
|
|
108
|
+
|
|
109
|
+
function configDir() {
|
|
110
|
+
return process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function stateFile() {
|
|
114
|
+
return path.join(configDir(), 'usage-limits-lowpri.json');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function readJson(file) {
|
|
118
|
+
try {
|
|
119
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
120
|
+
} catch (err) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// The same choice usage.js and host.js make, for the same reason: a migration
|
|
126
|
+
// leaves a small ~/.claude/.claude.json with machine ids and no meter while the
|
|
127
|
+
// account state stays in the home file, so pick the one that carries a
|
|
128
|
+
// snapshot and only fall back to existence.
|
|
129
|
+
function accountFiles() {
|
|
130
|
+
const scoped = path.join(configDir(), '.claude.json');
|
|
131
|
+
const home = path.join(os.homedir(), '.claude.json');
|
|
132
|
+
return scoped === home ? [home] : [scoped, home];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function snapshot() {
|
|
136
|
+
const files = accountFiles();
|
|
137
|
+
let fallback = null;
|
|
138
|
+
for (const file of files) {
|
|
139
|
+
const parsed = readJson(file);
|
|
140
|
+
if (!parsed) continue;
|
|
141
|
+
if (parsed.cachedUsageUtilization) return parsed;
|
|
142
|
+
if (!fallback) fallback = parsed;
|
|
143
|
+
}
|
|
144
|
+
return fallback;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function features(account) {
|
|
148
|
+
return account && account.cachedGrowthBookFeatures && typeof account.cachedGrowthBookFeatures === 'object'
|
|
149
|
+
? account.cachedGrowthBookFeatures
|
|
150
|
+
: null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function utilization(account) {
|
|
154
|
+
const cache = account && account.cachedUsageUtilization;
|
|
155
|
+
return cache && cache.utilization && typeof cache.utilization === 'object' ? cache.utilization : null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// A string the server sent, or null. Never a default: the wording is
|
|
159
|
+
// Anthropic's and it can change, so a sentence built on a made-up version of it
|
|
160
|
+
// would be quoting the plugin to itself.
|
|
161
|
+
function copy(value) {
|
|
162
|
+
return typeof value === 'string' && value.trim() ? value : null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Whether this ACCOUNT is provisioned for /low-priority, plus the copy the
|
|
166
|
+
// server shipped with it.
|
|
167
|
+
//
|
|
168
|
+
// A missing key is "not offered" and known:false - it is the absence of a fact,
|
|
169
|
+
// not the fact of an absence, and the difference decides whether the docs can
|
|
170
|
+
// say anything about this machine at all.
|
|
171
|
+
function offer(account) {
|
|
172
|
+
const gb = features(account);
|
|
173
|
+
const raw = gb && Object.prototype.hasOwnProperty.call(gb, FLAG) ? gb[FLAG] : undefined;
|
|
174
|
+
if (raw === undefined) {
|
|
175
|
+
return {
|
|
176
|
+
known: false,
|
|
177
|
+
offered: false,
|
|
178
|
+
version: null,
|
|
179
|
+
label: null,
|
|
180
|
+
noticeLine: null,
|
|
181
|
+
statusLine: null,
|
|
182
|
+
allowanceNote: null,
|
|
183
|
+
waitBanner: null,
|
|
184
|
+
budgetExhaustedCopy: null,
|
|
185
|
+
cooloffMinutes: DEFAULT_COOLOFF_MINUTES,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
const flag = raw && typeof raw === 'object' ? raw : {};
|
|
189
|
+
const cooloff = Number(flag.cooloffMinutes);
|
|
190
|
+
return {
|
|
191
|
+
known: true,
|
|
192
|
+
offered: flag.enabled === true,
|
|
193
|
+
version: Number.isFinite(Number(flag.version)) ? Number(flag.version) : null,
|
|
194
|
+
label: copy(flag.label),
|
|
195
|
+
noticeLine: copy(flag.noticeLine),
|
|
196
|
+
statusLine: copy(flag.statusLine),
|
|
197
|
+
allowanceNote: copy(flag.allowanceNote),
|
|
198
|
+
waitBanner: copy(flag.waitBanner),
|
|
199
|
+
budgetExhaustedCopy: copy(flag.budgetExhaustedCopy),
|
|
200
|
+
cooloffMinutes: Number.isFinite(cooloff)
|
|
201
|
+
? Math.round(Math.min(MAX_COOLOFF_MINUTES, Math.max(0, cooloff)))
|
|
202
|
+
: DEFAULT_COOLOFF_MINUTES,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// The manual session reset. Detected, never spent: this account holds no grant,
|
|
207
|
+
// so a feature built on it would be untestable here.
|
|
208
|
+
function sessionReset(account) {
|
|
209
|
+
const gb = features(account);
|
|
210
|
+
const util = utilization(account);
|
|
211
|
+
const flagged = Boolean(gb && Object.prototype.hasOwnProperty.call(gb, RESET_FLAG) && gb[RESET_FLAG]);
|
|
212
|
+
const grant = util && util.cedar_ember ? util.cedar_ember : null;
|
|
213
|
+
return {
|
|
214
|
+
known: Boolean(gb || util),
|
|
215
|
+
present: Boolean(flagged || grant),
|
|
216
|
+
// resets_left is served from /api/organizations/<uuid>/reset_rate_limits,
|
|
217
|
+
// not from any file a hook can read, so it stays unreported.
|
|
218
|
+
resetsLeft: null,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Whether the CLI itself will tell the model to wrap up at the wall.
|
|
223
|
+
function wrapUp(account) {
|
|
224
|
+
const gb = features(account);
|
|
225
|
+
const rawMode = gb ? gb[WRAPUP_MODE_FLAG] : undefined;
|
|
226
|
+
const normalised = typeof rawMode === 'string' ? rawMode.trim().toLowerCase() : rawMode;
|
|
227
|
+
const mode = WRAPUP_MODES.has(normalised) ? normalised : 'off';
|
|
228
|
+
return {
|
|
229
|
+
known: Boolean(gb && (Object.prototype.hasOwnProperty.call(gb, WRAPUP_MODE_FLAG) || Object.prototype.hasOwnProperty.call(gb, WRAPUP_TEXT_FLAG))),
|
|
230
|
+
mode,
|
|
231
|
+
hostWrapsUp: mode !== 'off',
|
|
232
|
+
textProvisioned: Boolean(gb && copy(gb[WRAPUP_TEXT_FLAG])),
|
|
233
|
+
nearWallNote: Boolean(gb && gb[NEAR_WALL_FLAG] === true),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Whether usage credits are a lever on this account at all.
|
|
238
|
+
function credits(account) {
|
|
239
|
+
const util = utilization(account);
|
|
240
|
+
const extra = util && util.extra_usage ? util.extra_usage : null;
|
|
241
|
+
const reason =
|
|
242
|
+
account && typeof account.cachedExtraUsageDisabledReason === 'string'
|
|
243
|
+
? account.cachedExtraUsageDisabledReason
|
|
244
|
+
: (extra && typeof extra.disabled_reason === 'string' ? extra.disabled_reason : null);
|
|
245
|
+
if (!extra) return { available: null, reason: reason };
|
|
246
|
+
if (extra.is_enabled === true) return { available: true, reason: null };
|
|
247
|
+
if (extra.is_enabled === false) return { available: false, reason: reason };
|
|
248
|
+
return { available: null, reason: reason };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function settingsFiles() {
|
|
252
|
+
return [path.join(configDir(), 'settings.local.json'), path.join(configDir(), 'settings.json')];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// The CLI's own wait-and-continue. Documented as user-or-managed scope, and the
|
|
256
|
+
// bundle defaults it to true, so an absent key is TRUE - the one reading a
|
|
257
|
+
// relay must not get wrong, because it decides whether the wake is the only
|
|
258
|
+
// route across the reset or a second one.
|
|
259
|
+
function autoContinue() {
|
|
260
|
+
for (const file of settingsFiles()) {
|
|
261
|
+
const parsed = readJson(file);
|
|
262
|
+
if (parsed && typeof parsed.autoContinueAtUsageLimit === 'boolean') {
|
|
263
|
+
return { value: parsed.autoContinueAtUsageLimit, source: path.basename(file) };
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return { value: true, source: 'default' };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ---------------------------------------------------------------------------
|
|
270
|
+
// The acknowledgement. The plugin's own record that the USER said he turned
|
|
271
|
+
// low-priority on, because nothing else can tell it.
|
|
272
|
+
//
|
|
273
|
+
// It carries the window it belongs to and that window's reset, and it lapses at
|
|
274
|
+
// that reset: low-priority ends when the limit does, so a fact that outlived
|
|
275
|
+
// its window would go on redirecting the headroom maths at the weekly in a
|
|
276
|
+
// session where the 5-hour wall is real again.
|
|
277
|
+
// ---------------------------------------------------------------------------
|
|
278
|
+
|
|
279
|
+
function readState() {
|
|
280
|
+
const parsed = readJson(stateFile());
|
|
281
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function writeState(state) {
|
|
285
|
+
try {
|
|
286
|
+
atomic.writeFileAtomic(stateFile(), JSON.stringify(state, null, 2) + '\n');
|
|
287
|
+
return true;
|
|
288
|
+
} catch (err) {
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function acknowledge(input) {
|
|
294
|
+
const options = input || {};
|
|
295
|
+
const now = Number.isFinite(options.now) ? options.now : Date.now();
|
|
296
|
+
if (!options.on) {
|
|
297
|
+
const state = readState();
|
|
298
|
+
delete state.ack;
|
|
299
|
+
writeState(state);
|
|
300
|
+
return { on: false, at: now };
|
|
301
|
+
}
|
|
302
|
+
const record = {
|
|
303
|
+
on: true,
|
|
304
|
+
at: now,
|
|
305
|
+
windowKey: options.windowKey || null,
|
|
306
|
+
resetsAt: Number.isFinite(options.resetsAt) ? options.resetsAt : null,
|
|
307
|
+
};
|
|
308
|
+
const state = readState();
|
|
309
|
+
state.ack = record;
|
|
310
|
+
writeState(state);
|
|
311
|
+
return record;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function readAck(now) {
|
|
315
|
+
const at = Number.isFinite(now) ? now : Date.now();
|
|
316
|
+
const state = readState();
|
|
317
|
+
const ack = state.ack;
|
|
318
|
+
if (!ack || ack.on !== true) return null;
|
|
319
|
+
// Past the reset of the window it was recorded against, the fact is spent.
|
|
320
|
+
if (Number.isFinite(ack.resetsAt) && at >= ack.resetsAt) return null;
|
|
321
|
+
return ack;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Exactly three states, and activeKnown is false in every one of them.
|
|
325
|
+
function stateOf(input) {
|
|
326
|
+
const options = input || {};
|
|
327
|
+
const now = Number.isFinite(options.now) ? options.now : Date.now();
|
|
328
|
+
const account = options.account !== undefined ? options.account : snapshot();
|
|
329
|
+
const provisioned = offer(account);
|
|
330
|
+
const ack = readAck(now);
|
|
331
|
+
return {
|
|
332
|
+
state: ack ? 'acknowledged' : provisioned.offered ? 'offered' : 'absent',
|
|
333
|
+
offered: provisioned.offered,
|
|
334
|
+
known: provisioned.known,
|
|
335
|
+
ack,
|
|
336
|
+
// Never readable. Said out loud here so no caller can talk itself into it.
|
|
337
|
+
activeKnown: false,
|
|
338
|
+
offer: provisioned,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function weeklyOf(windows) {
|
|
343
|
+
return (windows || []).find((w) => w && w.key === 'seven_day') || null;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Whether to say anything about /low-priority on this prompt, and which way.
|
|
347
|
+
//
|
|
348
|
+
// Null unless the account is provisioned, the 5-hour window is the binding one,
|
|
349
|
+
// it is at the wall, and there is a weekly reading to judge against. A guess at
|
|
350
|
+
// any of those would be the plugin recommending a spend it cannot price.
|
|
351
|
+
function advise(input) {
|
|
352
|
+
const options = input || {};
|
|
353
|
+
const now = Number.isFinite(options.now) ? options.now : Date.now();
|
|
354
|
+
const account = options.account !== undefined ? options.account : snapshot();
|
|
355
|
+
if (!offer(account).offered) return null;
|
|
356
|
+
const binding = options.binding;
|
|
357
|
+
if (!binding || binding.key !== 'five_hour') return null;
|
|
358
|
+
if (binding.stale) return null;
|
|
359
|
+
if (!Number.isFinite(binding.percentUsed) || binding.percentUsed < WALL_PERCENT) return null;
|
|
360
|
+
const weekly = weeklyOf(options.windows);
|
|
361
|
+
if (!weekly || !Number.isFinite(weekly.percentUsed) || weekly.stale) return null;
|
|
362
|
+
const percent = Math.round(weekly.percentUsed);
|
|
363
|
+
return {
|
|
364
|
+
kind: percent <= WEEKLY_HEADROOM_MAX ? 'offer' : 'hold',
|
|
365
|
+
weeklyPercent: percent,
|
|
366
|
+
threshold: WEEKLY_HEADROOM_MAX,
|
|
367
|
+
weeklyLabel: weekly.label || 'weekly',
|
|
368
|
+
now,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Everything the brief needs, in one read of one file.
|
|
373
|
+
function forBrief(input) {
|
|
374
|
+
const options = input || {};
|
|
375
|
+
const now = Number.isFinite(options.now) ? options.now : Date.now();
|
|
376
|
+
const account = options.account !== undefined ? options.account : snapshot();
|
|
377
|
+
const state = stateOf({ account, now });
|
|
378
|
+
const weekly = weeklyOf(options.windows);
|
|
379
|
+
const five = (options.windows || []).find((w) => w && w.key === 'five_hour') || null;
|
|
380
|
+
return {
|
|
381
|
+
state: state.state,
|
|
382
|
+
activeKnown: false,
|
|
383
|
+
advise: advise({ account, now, binding: options.binding, windows: options.windows }),
|
|
384
|
+
notice: state.offer.noticeLine,
|
|
385
|
+
cooloffMinutes: state.offer.cooloffMinutes,
|
|
386
|
+
budgetExhaustedCopy: state.offer.budgetExhaustedCopy,
|
|
387
|
+
weeklyLabel: weekly ? weekly.label || 'weekly' : 'weekly',
|
|
388
|
+
weeklyPercent: weekly && Number.isFinite(weekly.percentUsed) ? Math.round(weekly.percentUsed) : null,
|
|
389
|
+
fiveHourPercent: five && Number.isFinite(five.percentUsed) ? Math.round(five.percentUsed) : null,
|
|
390
|
+
resetGrant: sessionReset(account).present,
|
|
391
|
+
credits: credits(account),
|
|
392
|
+
autoContinue: autoContinue(),
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
module.exports = {
|
|
397
|
+
FLAG,
|
|
398
|
+
RESET_FLAG,
|
|
399
|
+
WRAPUP_MODE_FLAG,
|
|
400
|
+
WRAPUP_TEXT_FLAG,
|
|
401
|
+
NEAR_WALL_FLAG,
|
|
402
|
+
WRAPUP_MODES,
|
|
403
|
+
DEFAULT_COOLOFF_MINUTES,
|
|
404
|
+
MAX_COOLOFF_MINUTES,
|
|
405
|
+
WEEKLY_HEADROOM_MAX,
|
|
406
|
+
WALL_PERCENT,
|
|
407
|
+
configDir,
|
|
408
|
+
stateFile,
|
|
409
|
+
accountFiles,
|
|
410
|
+
snapshot,
|
|
411
|
+
offer,
|
|
412
|
+
sessionReset,
|
|
413
|
+
wrapUp,
|
|
414
|
+
credits,
|
|
415
|
+
autoContinue,
|
|
416
|
+
acknowledge,
|
|
417
|
+
readAck,
|
|
418
|
+
readState,
|
|
419
|
+
stateOf,
|
|
420
|
+
advise,
|
|
421
|
+
forBrief,
|
|
422
|
+
weeklyOf,
|
|
423
|
+
};
|
|
@@ -34,6 +34,7 @@ const path = require('path');
|
|
|
34
34
|
const atomic = require('./atomic.js');
|
|
35
35
|
const host = require('./host.js');
|
|
36
36
|
const codex = require('./codex.js');
|
|
37
|
+
const lowpri = require('./lowpri.js');
|
|
37
38
|
|
|
38
39
|
// ---------------------------------------------------------------------------
|
|
39
40
|
// The policy table
|
|
@@ -1460,6 +1461,103 @@ function main(argv) {
|
|
|
1460
1461
|
logChange({ plane: 'mode', key: 'advice', from: 'off', to: 'on', by: 'user', reason: null }, now);
|
|
1461
1462
|
return 'Recommendations are back on, capped at one per session and never repeated once declined.';
|
|
1462
1463
|
}
|
|
1464
|
+
// Whether the user has switched /low-priority on.
|
|
1465
|
+
//
|
|
1466
|
+
// This is a RECORD of something the user said, not a reading. Claude Code
|
|
1467
|
+
// keeps the live state in process memory and writes it nowhere - not to
|
|
1468
|
+
// ~/.claude.json, not to ~/.claude/state, and no hook payload or status-line
|
|
1469
|
+
// field carries it - so the plugin cannot know. What it can do is take the
|
|
1470
|
+
// user's word, stamp it with the window it belongs to, and let it lapse at
|
|
1471
|
+
// that window's reset, which is when low-priority ends anyway.
|
|
1472
|
+
//
|
|
1473
|
+
// Nothing else may write this. The model cannot type a slash command, so it
|
|
1474
|
+
// cannot have turned low-priority on, so it has nothing to acknowledge.
|
|
1475
|
+
if (flag('--low-priority')) {
|
|
1476
|
+
const asked = String(value('--low-priority') || '').trim().toLowerCase();
|
|
1477
|
+
const account = lowpri.snapshot();
|
|
1478
|
+
const provisioned = lowpri.offer(account);
|
|
1479
|
+
const detail = provisioned.offered
|
|
1480
|
+
? 'This account is provisioned for it (' + lowpri.FLAG + ' enabled' +
|
|
1481
|
+
(provisioned.version ? ', config version ' + provisioned.version : '') + '), though the ' +
|
|
1482
|
+
'offer itself also depends on an experiment arm that arrives in a response header, so it ' +
|
|
1483
|
+
'is not guaranteed to appear at every wall.'
|
|
1484
|
+
: provisioned.known
|
|
1485
|
+
? 'This account is NOT provisioned for it right now (' + lowpri.FLAG + ' is present and disabled), ' +
|
|
1486
|
+
'so the wall will not offer it.'
|
|
1487
|
+
: 'Whether this account is provisioned for it is unknown: ' + lowpri.FLAG + ' has never been ' +
|
|
1488
|
+
'seen in the feature cache, which is the absence of a fact rather than the fact of an absence.';
|
|
1489
|
+
if (asked === 'on' || asked === 'yes' || asked === 'true') {
|
|
1490
|
+
// Stamp it with the 5-hour window it belongs to, so the fact lapses when
|
|
1491
|
+
// that window resets instead of steering the headroom maths for ever.
|
|
1492
|
+
let windowKey = null;
|
|
1493
|
+
let resetsAt = null;
|
|
1494
|
+
try {
|
|
1495
|
+
const usage = require('./usage.js');
|
|
1496
|
+
const windows = usage.snapshotWindows(usage.collect(now), now, null) || [];
|
|
1497
|
+
const five = windows.find((w) => w && w.key === 'five_hour');
|
|
1498
|
+
if (five && Number.isFinite(five.resetsAt)) {
|
|
1499
|
+
windowKey = 'five_hour';
|
|
1500
|
+
resetsAt = five.resetsAt;
|
|
1501
|
+
}
|
|
1502
|
+
} catch (err) {
|
|
1503
|
+
// No reading is not a reason to refuse the user's own statement; the
|
|
1504
|
+
// record simply has no expiry to hang on.
|
|
1505
|
+
}
|
|
1506
|
+
lowpri.acknowledge({ on: true, windowKey, resetsAt, now });
|
|
1507
|
+
logChange({ plane: 'mode', key: 'low-priority', from: 'unknown', to: 'on', by: 'user', reason: null }, now);
|
|
1508
|
+
return [
|
|
1509
|
+
'Recorded: you have switched /low-priority on.',
|
|
1510
|
+
'',
|
|
1511
|
+
'From here the brief brakes on your WEEKLY window rather than the 5-hour one, stops telling ' +
|
|
1512
|
+
'you to wind down at the 5-hour wall, says replies may pause while lower priority waits for ' +
|
|
1513
|
+
'spare capacity, and will not book a relay wake for the 5-hour reset - that reset is no ' +
|
|
1514
|
+
'longer a wall this session stops at.',
|
|
1515
|
+
'',
|
|
1516
|
+
'What it costs, in Anthropic\'s own words: it uses your weekly limit, and it draws on a ' +
|
|
1517
|
+
'separate weekly lower-priority allowance whose size is exposed to no hook and no file, so ' +
|
|
1518
|
+
'the plugin cannot track it. Nothing here can tell whether it is still on, so when you ' +
|
|
1519
|
+
'turn it off: mode --low-priority off',
|
|
1520
|
+
'',
|
|
1521
|
+
resetsAt
|
|
1522
|
+
? 'This lapses on its own at ' + new Date(resetsAt).toLocaleString() + ', when the 5-hour window resets.'
|
|
1523
|
+
: 'No 5-hour reset time was readable, so this has no expiry: clear it by hand when it ends.',
|
|
1524
|
+
'',
|
|
1525
|
+
detail,
|
|
1526
|
+
].join('\n');
|
|
1527
|
+
}
|
|
1528
|
+
if (asked === 'off' || asked === 'no' || asked === 'false') {
|
|
1529
|
+
const had = lowpri.readAck(now);
|
|
1530
|
+
lowpri.acknowledge({ on: false, now });
|
|
1531
|
+
logChange({ plane: 'mode', key: 'low-priority', from: had ? 'on' : 'unknown', to: 'off', by: 'user', reason: null }, now);
|
|
1532
|
+
return had
|
|
1533
|
+
? 'Cleared: low-priority is off again, so the 5-hour window counts as a wall from the next prompt.'
|
|
1534
|
+
: 'Nothing was recorded, so there was nothing to clear. Low-priority is off as far as this plugin knows.';
|
|
1535
|
+
}
|
|
1536
|
+
if (asked) {
|
|
1537
|
+
return 'mode --low-priority takes "on" or "off". Say which; this is a record of what YOU did, so it is never guessed.';
|
|
1538
|
+
}
|
|
1539
|
+
const ack = lowpri.readAck(now);
|
|
1540
|
+
const wrap = lowpri.wrapUp(account);
|
|
1541
|
+
const auto = lowpri.autoContinue();
|
|
1542
|
+
return [
|
|
1543
|
+
ack
|
|
1544
|
+
? 'You have acknowledged low-priority ON, at ' + new Date(ack.at).toLocaleString() +
|
|
1545
|
+
(Number.isFinite(ack.resetsAt) ? ', lapsing at ' + new Date(ack.resetsAt).toLocaleString() : ', with no expiry') + '.'
|
|
1546
|
+
: 'Low-priority has not been acknowledged, so the plugin treats the 5-hour window as a real wall.',
|
|
1547
|
+
'',
|
|
1548
|
+
'It is a toggle you type yourself. The model cannot run a slash command, and /low-priority is ' +
|
|
1549
|
+
'declared supportsNonInteractive:false, so it is unusable in a headless or relayed run as well.',
|
|
1550
|
+
'',
|
|
1551
|
+
detail,
|
|
1552
|
+
'',
|
|
1553
|
+
'Claude Code will ' + (wrap.hostWrapsUp ? '' : 'NOT ') + 'inject its own wrap-up note at the wall on ' +
|
|
1554
|
+
'this machine (' + lowpri.WRAPUP_MODE_FLAG + ' is "' + wrap.mode + '")' +
|
|
1555
|
+
(wrap.textProvisioned && !wrap.hostWrapsUp ? ', although the note text is provisioned' : '') + '.',
|
|
1556
|
+
'Claude Code\'s own continue-after-reset is ' + (auto.value ? 'ON' : 'OFF') + ' (' + auto.source + ').',
|
|
1557
|
+
'',
|
|
1558
|
+
'To set it: mode --low-priority on / mode --low-priority off',
|
|
1559
|
+
].join('\n');
|
|
1560
|
+
}
|
|
1463
1561
|
if (flag('--pin') || flag('--no-pin')) {
|
|
1464
1562
|
const state = read();
|
|
1465
1563
|
const before = state.pin;
|
|
@@ -37,6 +37,7 @@ const { spawn, spawnSync } = require('child_process');
|
|
|
37
37
|
const atomic = require('./atomic.js');
|
|
38
38
|
const host = require('./host.js');
|
|
39
39
|
const voice = require('./voice.js');
|
|
40
|
+
const lowpri = require('./lowpri.js');
|
|
40
41
|
|
|
41
42
|
const MINUTE = 60 * 1000;
|
|
42
43
|
|
|
@@ -1173,6 +1174,34 @@ function armable(input) {
|
|
|
1173
1174
|
const raw = binding.percentUsed - Math.max(0, beyond);
|
|
1174
1175
|
if (raw < config.at) return { ok: false, why: 'the account reads ' + Math.round(raw) + ' per cent; only the local estimate (' + Math.round(binding.percentUsed) + ') is past ' + config.at };
|
|
1175
1176
|
if (!Number.isFinite(binding.resetsAt)) return { ok: false, why: 'the window has no known reset time' };
|
|
1177
|
+
// /low-priority retires the 5-hour wall while it is on: the session carries
|
|
1178
|
+
// straight past that reset at lower priority, spending the weekly. A wake
|
|
1179
|
+
// booked for that reset would fire into a session that was never stopped -
|
|
1180
|
+
// and with Claude Code's own autoContinueAtUsageLimit on by default, two
|
|
1181
|
+
// things starting the same work is the one bug here that costs real weekly
|
|
1182
|
+
// budget. So it is refused, in plain words, and only for that window: the
|
|
1183
|
+
// weekly is a different wall and still arms.
|
|
1184
|
+
//
|
|
1185
|
+
// This reads the plugin's own acknowledgement, never a guess. Whether
|
|
1186
|
+
// low-priority is actually running lives in the CLI's process memory and is
|
|
1187
|
+
// readable nowhere; see lowpri.js.
|
|
1188
|
+
if (binding.key === 'five_hour') {
|
|
1189
|
+
let ack = null;
|
|
1190
|
+
try {
|
|
1191
|
+
ack = lowpri.readAck(options.now);
|
|
1192
|
+
} catch (err) {
|
|
1193
|
+
ack = null;
|
|
1194
|
+
}
|
|
1195
|
+
if (ack) {
|
|
1196
|
+
return {
|
|
1197
|
+
ok: false,
|
|
1198
|
+
why:
|
|
1199
|
+
'low-priority is acknowledged on, so the 5-hour reset is not a wall to wake after - ' +
|
|
1200
|
+
'this session carries past it and spends the weekly instead. Only the weekly window ' +
|
|
1201
|
+
'would be worth a wake; say so with usage-mode --low-priority off if it has been switched back off',
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1176
1205
|
if (!options.sessionId) return { ok: false, why: 'no session id' };
|
|
1177
1206
|
const work = workWithContinuation(options.work, options.sessionId);
|
|
1178
1207
|
if (!work.hasWork) return { ok: false, why: 'no plan or unfinished todo list to carry, and no saved continuation' };
|