claude-usage-limits 1.39.5 → 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/activity.js +4 -16
- package/skills/usage-limits/scripts/atomic.js +173 -0
- package/skills/usage-limits/scripts/brief.js +207 -11
- package/skills/usage-limits/scripts/codex-lowpower.js +3 -3
- package/skills/usage-limits/scripts/codex.js +3 -6
- package/skills/usage-limits/scripts/drift.js +5 -6
- package/skills/usage-limits/scripts/install-antigravity.js +2 -4
- package/skills/usage-limits/scripts/live.js +5 -15
- package/skills/usage-limits/scripts/lowpower.js +3 -4
- package/skills/usage-limits/scripts/lowpri.js +423 -0
- package/skills/usage-limits/scripts/mode.js +100 -4
- package/skills/usage-limits/scripts/reading.js +4 -4
- package/skills/usage-limits/scripts/relay.js +31 -3
- package/skills/usage-limits/scripts/statusline.js +3 -4
- package/skills/usage-limits/scripts/usage.js +3 -10
|
@@ -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
|
|
@@ -14,6 +14,8 @@ const fs = require('fs');
|
|
|
14
14
|
const os = require('os');
|
|
15
15
|
const path = require('path');
|
|
16
16
|
|
|
17
|
+
const atomic = require('./atomic.js');
|
|
18
|
+
|
|
17
19
|
const KEEP_SESSIONS = 8;
|
|
18
20
|
// A session that has said nothing for this long is not working, whatever its
|
|
19
21
|
// last word was: a crash never sends Stop.
|
|
@@ -68,22 +70,8 @@ function mark(state, sessionId, extra, now) {
|
|
|
68
70
|
if (extra && extra.model) entry.model = String(extra.model);
|
|
69
71
|
else if (previous.model) entry.model = previous.model;
|
|
70
72
|
all[sessionId || '_'] = entry;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
try {
|
|
74
|
-
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
75
|
-
fs.writeFileSync(temp, JSON.stringify(trim(all)), 'utf8');
|
|
76
|
-
fs.renameSync(temp, file);
|
|
77
|
-
} catch (err) {
|
|
78
|
-
// A rename Windows refused leaves nothing behind.
|
|
79
|
-
try {
|
|
80
|
-
fs.unlinkSync(temp);
|
|
81
|
-
} catch (gone) {
|
|
82
|
-
// Nothing to clean up.
|
|
83
|
-
}
|
|
84
|
-
return false;
|
|
85
|
-
}
|
|
86
|
-
return true;
|
|
73
|
+
// A rename Windows refused leaves nothing behind: see atomic.js.
|
|
74
|
+
return atomic.tryWriteFileAtomic(activityFile(), JSON.stringify(trim(all)));
|
|
87
75
|
} catch (err) {
|
|
88
76
|
return false;
|
|
89
77
|
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Every file this plugin replaces goes through here: written beside the target
|
|
4
|
+
// under a name that is this process's own, then renamed into place, so a
|
|
5
|
+
// reader sees either the old whole file or the new whole file.
|
|
6
|
+
//
|
|
7
|
+
// On Windows that rename is the step that fails. Two processes renaming onto
|
|
8
|
+
// the same target at the same moment - the prompt hook, the pulse and the
|
|
9
|
+
// status line do exactly that - get EPERM back from MoveFileEx for the one
|
|
10
|
+
// that loses. Measured on 2026-09-25 with four processes writing one file for
|
|
11
|
+
// three seconds: 2109 renames refused with EPERM against 469 that went
|
|
12
|
+
// through. The writers that did not delete their temporary file on that
|
|
13
|
+
// failure left one behind every time, and about seventy of them had piled up
|
|
14
|
+
// in ~/.claude by then. The same retry on EPERM, EACCES and EBUSY is what
|
|
15
|
+
// graceful-fs does for npm on Windows; here it is bounded tightly, because
|
|
16
|
+
// the hooks that call this have ten seconds between them and the prompt.
|
|
17
|
+
//
|
|
18
|
+
// What no code path can clean is a process killed between creating its
|
|
19
|
+
// temporary file and renaming it - a hook that ran into its timeout. sweep()
|
|
20
|
+
// removes those at the next prompt, once they are old enough that no live
|
|
21
|
+
// writer can still own them.
|
|
22
|
+
|
|
23
|
+
const fs = require('fs');
|
|
24
|
+
const path = require('path');
|
|
25
|
+
|
|
26
|
+
// One suffix for every temporary file, and nobody else's: anything ending in
|
|
27
|
+
// it is this plugin's, so the sweep never has to guess.
|
|
28
|
+
const SUFFIX = '.usage-limits-tmp';
|
|
29
|
+
|
|
30
|
+
// The codes Windows returns while another process has the target, or is
|
|
31
|
+
// replacing it. Anything else - ENOSPC, ENOENT, EISDIR - will not change on a
|
|
32
|
+
// second try.
|
|
33
|
+
const RETRY_CODES = new Set(['EPERM', 'EACCES', 'EBUSY']);
|
|
34
|
+
|
|
35
|
+
// About a third of a second in all. Contention clears in milliseconds; a
|
|
36
|
+
// target that is still held after this is held by something that will not let
|
|
37
|
+
// go soon, and the caller carries on with what is on disk.
|
|
38
|
+
const RETRY_DELAYS_MS = [5, 10, 20, 40, 60, 80, 100];
|
|
39
|
+
|
|
40
|
+
// A write takes milliseconds and the longest hook is killed at ten seconds, so
|
|
41
|
+
// nothing alive owns a temporary file this old.
|
|
42
|
+
const STALE_MS = 10 * 60 * 1000;
|
|
43
|
+
|
|
44
|
+
// Temporary names this plugin has used. The first is the one written now; the
|
|
45
|
+
// second is what reading.js, drift.js, mode.js and relay.js wrote before
|
|
46
|
+
// 1.39.6 - `usage-limits-<name>.json.<pid>.tmp` - and is matched only with the
|
|
47
|
+
// usage-limits- prefix and the pid, because a bare .tmp could be anyone's.
|
|
48
|
+
const OWN_TEMP = [
|
|
49
|
+
/^.+\.usage-limits-tmp$/,
|
|
50
|
+
/^usage-limits-[A-Za-z0-9_-]+\.json\.\d+\.tmp$/,
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
function sleepSync(ms) {
|
|
54
|
+
try {
|
|
55
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
56
|
+
} catch (err) {
|
|
57
|
+
// No shared memory here: spin for the same time instead.
|
|
58
|
+
const until = Date.now() + ms;
|
|
59
|
+
while (Date.now() < until) {
|
|
60
|
+
// Waiting.
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function tempFor(file) {
|
|
66
|
+
return file + '.' + process.pid + SUFFIX;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function renameWithRetry(from, to) {
|
|
70
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
71
|
+
try {
|
|
72
|
+
fs.renameSync(from, to);
|
|
73
|
+
return;
|
|
74
|
+
} catch (err) {
|
|
75
|
+
if (!RETRY_CODES.has(err && err.code) || attempt >= RETRY_DELAYS_MS.length) throw err;
|
|
76
|
+
sleepSync(RETRY_DELAYS_MS[attempt]);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Never throws. True when the file is gone, whoever removed it.
|
|
82
|
+
function removeQuietly(file) {
|
|
83
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
84
|
+
try {
|
|
85
|
+
fs.unlinkSync(file);
|
|
86
|
+
return true;
|
|
87
|
+
} catch (err) {
|
|
88
|
+
if (err && err.code === 'ENOENT') return true;
|
|
89
|
+
if (!RETRY_CODES.has(err && err.code) || attempt >= 3) return false;
|
|
90
|
+
sleepSync(RETRY_DELAYS_MS[attempt]);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Throws what the write or the last rename threw, after removing the
|
|
96
|
+
// temporary file. `options` is passed to writeFileSync (encoding, mode).
|
|
97
|
+
function writeFileAtomic(file, text, options) {
|
|
98
|
+
const temp = tempFor(file);
|
|
99
|
+
try {
|
|
100
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
101
|
+
fs.writeFileSync(temp, text, options === undefined ? 'utf8' : options);
|
|
102
|
+
renameWithRetry(temp, file);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
removeQuietly(temp);
|
|
105
|
+
throw err;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// The same, for callers that carry on either way.
|
|
110
|
+
function tryWriteFileAtomic(file, text, options) {
|
|
111
|
+
try {
|
|
112
|
+
writeFileAtomic(file, text, options);
|
|
113
|
+
return true;
|
|
114
|
+
} catch (err) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function isOwnTemp(name) {
|
|
120
|
+
return OWN_TEMP.some((pattern) => pattern.test(name));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Removes this plugin's temporary files older than STALE_MS from each
|
|
124
|
+
// directory. Only names isOwnTemp() accepts are ever looked at. Never throws;
|
|
125
|
+
// returns how many were removed.
|
|
126
|
+
function sweep(dirs, now, staleMs) {
|
|
127
|
+
const at = Number.isFinite(now) ? now : Date.now();
|
|
128
|
+
const age = Number.isFinite(staleMs) ? staleMs : STALE_MS;
|
|
129
|
+
let removed = 0;
|
|
130
|
+
const seen = new Set();
|
|
131
|
+
for (const dir of [].concat(dirs || [])) {
|
|
132
|
+
if (!dir || seen.has(path.resolve(dir))) continue;
|
|
133
|
+
seen.add(path.resolve(dir));
|
|
134
|
+
let names;
|
|
135
|
+
try {
|
|
136
|
+
names = fs.readdirSync(dir);
|
|
137
|
+
} catch (err) {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
for (const name of names) {
|
|
141
|
+
if (!isOwnTemp(name)) continue;
|
|
142
|
+
const file = path.join(dir, name);
|
|
143
|
+
try {
|
|
144
|
+
const stat = fs.statSync(file);
|
|
145
|
+
if (!stat.isFile() || at - stat.mtimeMs < age) continue;
|
|
146
|
+
} catch (err) {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
fs.unlinkSync(file);
|
|
151
|
+
removed += 1;
|
|
152
|
+
} catch (err) {
|
|
153
|
+
// Gone already is someone else's sweep; held is worth one more try.
|
|
154
|
+
if (err && err.code !== 'ENOENT' && removeQuietly(file)) removed += 1;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return removed;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
module.exports = {
|
|
162
|
+
SUFFIX,
|
|
163
|
+
STALE_MS,
|
|
164
|
+
RETRY_CODES,
|
|
165
|
+
RETRY_DELAYS_MS,
|
|
166
|
+
tempFor,
|
|
167
|
+
renameWithRetry,
|
|
168
|
+
removeQuietly,
|
|
169
|
+
writeFileAtomic,
|
|
170
|
+
tryWriteFileAtomic,
|
|
171
|
+
isOwnTemp,
|
|
172
|
+
sweep,
|
|
173
|
+
};
|