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
|
@@ -13,6 +13,7 @@ const fs = require('fs');
|
|
|
13
13
|
const os = require('os');
|
|
14
14
|
const path = require('path');
|
|
15
15
|
|
|
16
|
+
const atomic = require('./atomic.js');
|
|
16
17
|
const usage = require('./usage.js');
|
|
17
18
|
const host = require('./host.js');
|
|
18
19
|
const tally = require('./tally.js');
|
|
@@ -23,6 +24,7 @@ const reading = require('./reading.js');
|
|
|
23
24
|
const voice = require('./voice.js');
|
|
24
25
|
const mode = require('./mode.js');
|
|
25
26
|
const feed = require('./feed.js');
|
|
27
|
+
const lowpri = require('./lowpri.js');
|
|
26
28
|
|
|
27
29
|
const SECOND = 1000;
|
|
28
30
|
const DAY = 24 * 60 * 60 * 1000;
|
|
@@ -510,6 +512,11 @@ const CACHED_BINDING_FIELDS = [
|
|
|
510
512
|
'msToReset',
|
|
511
513
|
'refusedAt',
|
|
512
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',
|
|
513
520
|
];
|
|
514
521
|
|
|
515
522
|
function cacheableBinding(binding) {
|
|
@@ -1002,6 +1009,74 @@ function briefText(input) {
|
|
|
1002
1009
|
);
|
|
1003
1010
|
}
|
|
1004
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
|
+
}
|
|
1005
1080
|
if (parts.session) {
|
|
1006
1081
|
sentences.push(
|
|
1007
1082
|
'This session: ' + parts.session.turns + ' turns, ' +
|
|
@@ -1101,6 +1176,23 @@ function briefText(input) {
|
|
|
1101
1176
|
);
|
|
1102
1177
|
}
|
|
1103
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
|
+
}
|
|
1104
1196
|
if (carry && carry.last) {
|
|
1105
1197
|
relaySentences.push(
|
|
1106
1198
|
'The last relay ' +
|
|
@@ -1123,7 +1215,25 @@ function briefText(input) {
|
|
|
1123
1215
|
// near the wall is to make being cut off cheap - order the work, save as you
|
|
1124
1216
|
// go, keep a note of where things stand - not to shrink the work until it is
|
|
1125
1217
|
// guaranteed to fit.
|
|
1126
|
-
|
|
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
|
+
:
|
|
1127
1237
|
// The escape outranks everything below it. A session that stops while a
|
|
1128
1238
|
// command would have carried it on has not been careful, it has quit - and
|
|
1129
1239
|
// that is a real session: the Fable weekly hit 89 per cent, the line said
|
|
@@ -1251,6 +1361,7 @@ function briefText(input) {
|
|
|
1251
1361
|
return (
|
|
1252
1362
|
sentences[0] + caveat + (parts.tier ? ' ' + parts.tier : '') + (bounded ? ' ' + bounded : '') + adviceText +
|
|
1253
1363
|
(escapeSentence ? ' ' + escapeSentence : '') +
|
|
1364
|
+
(lowPrioritySentence ? ' ' + lowPrioritySentence : '') +
|
|
1254
1365
|
(parts.pressure !== 'roomy' && relaySentences.length ? ' ' + relaySentences.join(' ') : '') +
|
|
1255
1366
|
'\n' + instruction + directive
|
|
1256
1367
|
);
|
|
@@ -1376,6 +1487,41 @@ function relayState(now, hookInput, binding, sessionId) {
|
|
|
1376
1487
|
}
|
|
1377
1488
|
}
|
|
1378
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
|
+
|
|
1379
1525
|
// The one line `off` will ever say, and only when it has been asked for.
|
|
1380
1526
|
//
|
|
1381
1527
|
// `off` means off, including at 100 per cent: that is what was asked and it is
|
|
@@ -1450,6 +1596,20 @@ const GEMINI_UNREADABLE =
|
|
|
1450
1596
|
const LIMIT_REACHED =
|
|
1451
1597
|
'[usage-limits] The meter says the usage limit is reached. Start nothing new: save the work, write the hand-off note in this turn, and end the turn.';
|
|
1452
1598
|
|
|
1599
|
+
// Temporary files a killed process left behind: a hook that ran into its
|
|
1600
|
+
// timeout between writing one and renaming it. Nothing else can clean them,
|
|
1601
|
+
// and on 2026-09-25 about seventy had piled up in ~/.claude. Once per prompt
|
|
1602
|
+
// is often enough and costs one directory listing per home. The clock here
|
|
1603
|
+
// is always the real one, never the run's `now`: a stale file is judged by
|
|
1604
|
+
// its mtime, which is real time too. Never throws.
|
|
1605
|
+
function sweepDebris() {
|
|
1606
|
+
try {
|
|
1607
|
+
return atomic.sweep([configDir(), host.codexHome()], Date.now());
|
|
1608
|
+
} catch (err) {
|
|
1609
|
+
return 0;
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1453
1613
|
async function run(now, hookInput, opts) {
|
|
1454
1614
|
if (String(process.env.USAGE_LIMITS_BRIEF || '').toLowerCase() === 'off') return '';
|
|
1455
1615
|
|
|
@@ -1463,6 +1623,8 @@ async function run(now, hookInput, opts) {
|
|
|
1463
1623
|
const budget = mode.forSession({ sessionId });
|
|
1464
1624
|
if (budget.policy.briefStyle === 'none') return guardLine(now, budget);
|
|
1465
1625
|
|
|
1626
|
+
sweepDebris();
|
|
1627
|
+
|
|
1466
1628
|
// Settle host here, before any file is read. A caller that already knows the
|
|
1467
1629
|
// host (agy-hook runs only inside Antigravity) says so, and that wins over
|
|
1468
1630
|
// detection: an empty or unparseable payload used to fall through to
|
|
@@ -1612,10 +1774,35 @@ async function run(now, hookInput, opts) {
|
|
|
1612
1774
|
writeCache(mergeCache(all, sessionId, view, KEEP_SESSIONS));
|
|
1613
1775
|
}
|
|
1614
1776
|
|
|
1615
|
-
|
|
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 || [];
|
|
1616
1803
|
const { active, share } = activeShare(view.sessions, all, now, sessionId);
|
|
1617
|
-
const yourTurnsLeft = Number.isFinite(
|
|
1618
|
-
? Math.max(1, Math.round(
|
|
1804
|
+
const yourTurnsLeft = Number.isFinite(turnsLeftNow)
|
|
1805
|
+
? Math.max(1, Math.round(turnsLeftNow * share))
|
|
1619
1806
|
: null;
|
|
1620
1807
|
// Only a short runway is worth saying. Quoting it when there are hours left
|
|
1621
1808
|
// would make the line longer without making it more useful.
|
|
@@ -1664,7 +1851,7 @@ async function run(now, hookInput, opts) {
|
|
|
1664
1851
|
const offering = advice.ok && !advice.alreadyOffered;
|
|
1665
1852
|
if (offering) mode.adviceOffer(advice.id, sessionId, now);
|
|
1666
1853
|
|
|
1667
|
-
const pressureNow = pressure(binding, now, config, Number.isFinite(yourTurnsLeft) ? yourTurnsLeft :
|
|
1854
|
+
const pressureNow = pressure(binding, now, config, Number.isFinite(yourTurnsLeft) ? yourTurnsLeft : turnsLeftNow);
|
|
1668
1855
|
// Fast mode changes what the window's figures mean, so a toggle is a change
|
|
1669
1856
|
// worth saying even when nothing else has moved.
|
|
1670
1857
|
const fastMode = fastModeFor(sessionId);
|
|
@@ -1679,12 +1866,17 @@ async function run(now, hookInput, opts) {
|
|
|
1679
1866
|
budget.name,
|
|
1680
1867
|
pressureNow,
|
|
1681
1868
|
binding && Number.isFinite(binding.percentUsed) ? Math.round(binding.percentUsed / 5) * 5 : 'x',
|
|
1682
|
-
|
|
1869
|
+
escapeNow ? escapeNow.kind : '-',
|
|
1683
1870
|
tier || '-',
|
|
1684
1871
|
active > 1 ? 'shared' : 'solo',
|
|
1685
1872
|
carry && carry.armed ? 'relay' : '-',
|
|
1686
1873
|
offering ? 'advice' : '-',
|
|
1687
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' : '-',
|
|
1688
1880
|
].join('|');
|
|
1689
1881
|
if (!budget.policy.briefWhenUnchanged && pressureNow === 'roomy') {
|
|
1690
1882
|
const slots = readCache();
|
|
@@ -1724,10 +1916,10 @@ async function run(now, hookInput, opts) {
|
|
|
1724
1916
|
binding && binding.family && Number.isFinite(binding.percentUsed) && binding.percentUsed >= HALF_SPENT
|
|
1725
1917
|
? familyLabel(binding.family)
|
|
1726
1918
|
: null,
|
|
1727
|
-
othersSummary:
|
|
1728
|
-
escape:
|
|
1919
|
+
othersSummary: othersSummaryNow,
|
|
1920
|
+
escape: escapeNow,
|
|
1729
1921
|
host: usage.currentHost(),
|
|
1730
|
-
turnsLeft:
|
|
1922
|
+
turnsLeft: turnsLeftNow,
|
|
1731
1923
|
effortWarning: view.effortWarning || null,
|
|
1732
1924
|
// Once per setting, and once per session, and never after a decline: the
|
|
1733
1925
|
// advice rules above decide, and the sentence itself is unchanged.
|
|
@@ -1743,13 +1935,17 @@ async function run(now, hookInput, opts) {
|
|
|
1743
1935
|
rebuilt: Boolean(binding && binding.estimated),
|
|
1744
1936
|
staleWindows: view.staleWindows || 0,
|
|
1745
1937
|
planChanged: Boolean(view.planChanged),
|
|
1746
|
-
critical:
|
|
1938
|
+
critical: criticalNow,
|
|
1747
1939
|
pointsSinceSnapshot: (binding && binding.pointsSinceSnapshot) || 0,
|
|
1748
1940
|
correctionUnreliable: Boolean(binding && binding.correctionUnreliable),
|
|
1749
1941
|
pointsBeyondSnapshot: (binding && binding.pointsBeyondSnapshot) || 0,
|
|
1750
1942
|
snapshotAge: view.snapshotAge,
|
|
1751
1943
|
snapshotStale: Number.isFinite(view.snapshotAgeMs) && view.snapshotAgeMs >= SNAPSHOT_TRUST_MS,
|
|
1752
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,
|
|
1753
1949
|
// The turn count that matters for this session is its share of a shared
|
|
1754
1950
|
// budget, not the whole window's. Escalating on the whole window meant a
|
|
1755
1951
|
// count that looked comfortable while the part actually available here was
|
|
@@ -1804,7 +2000,7 @@ function withBugcheck(text) {
|
|
|
1804
2000
|
return text;
|
|
1805
2001
|
}
|
|
1806
2002
|
|
|
1807
|
-
module.exports = { withBugcheck, sayOnce, shapeOf, saidFile, REPEAT_MS, staleVersionFor, relayNewsFor, installedVersion, runningVersion,
|
|
2003
|
+
module.exports = { sweepDebris, withBugcheck, sayOnce, shapeOf, saidFile, REPEAT_MS, staleVersionFor, relayNewsFor, installedVersion, runningVersion,
|
|
1808
2004
|
readSaid, standingSaid, markStanding, standingShortFor, STANDING_SHORT, cacheMissWhyFor, missReason, MISS_RECENT_MS,
|
|
1809
2005
|
DEFAULTS,
|
|
1810
2006
|
HOOK_BUDGET_MS,
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const fs = require('node:fs');
|
|
4
4
|
const path = require('node:path');
|
|
5
5
|
const os = require('node:os');
|
|
6
|
+
const atomicWrite = require('./atomic.js');
|
|
6
7
|
const KEYS = ['model', 'model_reasoning_effort'];
|
|
7
8
|
const EFFORTS = ['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra'];
|
|
8
9
|
|
|
@@ -118,9 +119,8 @@ function read(file, fallback) {
|
|
|
118
119
|
try { return fs.readFileSync(file, 'utf8'); } catch (e) { if (e.code === 'ENOENT') return fallback; throw e; }
|
|
119
120
|
}
|
|
120
121
|
function atomic(file, text) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
finally { if (fs.existsSync(temp)) fs.unlinkSync(temp); }
|
|
122
|
+
// Throws on failure, as before, with the temporary file already removed.
|
|
123
|
+
atomicWrite.writeFileAtomic(file, text, { encoding: 'utf8', mode: 0o600 });
|
|
124
124
|
}
|
|
125
125
|
function checkState(state) {
|
|
126
126
|
if (!state) return;
|
|
@@ -23,6 +23,7 @@ const os = require('os');
|
|
|
23
23
|
const path = require('path');
|
|
24
24
|
const readline = require('readline');
|
|
25
25
|
|
|
26
|
+
const atomic = require('./atomic.js');
|
|
26
27
|
const host = require('./host.js');
|
|
27
28
|
|
|
28
29
|
const MINUTE = 60 * 1000;
|
|
@@ -815,13 +816,9 @@ function readLiveMeter() {
|
|
|
815
816
|
|
|
816
817
|
function writeLiveMeter(reading) {
|
|
817
818
|
try {
|
|
818
|
-
|
|
819
|
-
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
820
|
-
const temp = file + '.' + process.pid + '.usage-limits-tmp';
|
|
821
|
-
fs.writeFileSync(temp, JSON.stringify(reading), 'utf8');
|
|
822
|
-
fs.renameSync(temp, file);
|
|
823
|
-
return true;
|
|
819
|
+
return atomic.tryWriteFileAtomic(liveFile(), JSON.stringify(reading));
|
|
824
820
|
} catch (err) {
|
|
821
|
+
// A reading that will not serialise is not written either.
|
|
825
822
|
return false;
|
|
826
823
|
}
|
|
827
824
|
}
|
|
@@ -19,6 +19,7 @@ const fs = require('fs');
|
|
|
19
19
|
const os = require('os');
|
|
20
20
|
const path = require('path');
|
|
21
21
|
|
|
22
|
+
const atomic = require('./atomic.js');
|
|
22
23
|
const host = require('./host.js');
|
|
23
24
|
const codex = require('./codex.js');
|
|
24
25
|
|
|
@@ -102,13 +103,11 @@ function modeSummary(codexHome) {
|
|
|
102
103
|
.sort((a, b) => b.turns - a.turns);
|
|
103
104
|
}
|
|
104
105
|
|
|
106
|
+
// Same beside-and-rename as reading.js: the pulse and the prompt hook can both
|
|
107
|
+
// land here in the same second. A refused rename is retried, and on failure
|
|
108
|
+
// the temporary file is removed before this throws: see atomic.js.
|
|
105
109
|
function writeAtomic(file, data) {
|
|
106
|
-
|
|
107
|
-
// Same beside-and-rename as reading.js: the pulse and the prompt hook can
|
|
108
|
-
// both land here in the same second.
|
|
109
|
-
const tmp = file + '.' + process.pid + '.tmp';
|
|
110
|
-
fs.writeFileSync(tmp, JSON.stringify(data));
|
|
111
|
-
fs.renameSync(tmp, file);
|
|
110
|
+
atomic.writeFileAtomic(file, JSON.stringify(data));
|
|
112
111
|
}
|
|
113
112
|
|
|
114
113
|
// `previous` and `next` are both entries in reading.js's own shape - see
|
|
@@ -31,6 +31,7 @@ const fs = require('fs');
|
|
|
31
31
|
const os = require('os');
|
|
32
32
|
const path = require('path');
|
|
33
33
|
|
|
34
|
+
const atomic = require('./atomic.js');
|
|
34
35
|
const host = require('./host.js');
|
|
35
36
|
|
|
36
37
|
const PLUGIN = 'usage-limits';
|
|
@@ -144,10 +145,7 @@ function rulesText() {
|
|
|
144
145
|
}
|
|
145
146
|
|
|
146
147
|
function writeFile(file, text) {
|
|
147
|
-
|
|
148
|
-
const tmp = file + '.' + process.pid + '.tmp';
|
|
149
|
-
fs.writeFileSync(tmp, text, 'utf8');
|
|
150
|
-
fs.renameSync(tmp, file);
|
|
148
|
+
atomic.writeFileAtomic(file, text);
|
|
151
149
|
}
|
|
152
150
|
|
|
153
151
|
function installed() {
|
|
@@ -26,6 +26,8 @@ const http = require('http');
|
|
|
26
26
|
const https = require('https');
|
|
27
27
|
const { execFileSync } = require('child_process');
|
|
28
28
|
|
|
29
|
+
const atomic = require('./atomic.js');
|
|
30
|
+
|
|
29
31
|
const USAGE_URL = 'https://api.anthropic.com/api/oauth/usage';
|
|
30
32
|
const BETA = 'oauth-2025-04-20';
|
|
31
33
|
// Claude Code gives the call five seconds; so does this.
|
|
@@ -328,22 +330,10 @@ function readLive() {
|
|
|
328
330
|
|
|
329
331
|
// Through a temporary file named for this process, so a reader never sees
|
|
330
332
|
// half a reading and two writers never share a temp file. A rename that fails
|
|
331
|
-
// (Windows, with the target held open)
|
|
333
|
+
// (Windows, with the target held open) is retried, and one that still fails
|
|
334
|
+
// leaves nothing behind: see atomic.js.
|
|
332
335
|
function writeAtomic(file, text) {
|
|
333
|
-
|
|
334
|
-
try {
|
|
335
|
-
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
336
|
-
fs.writeFileSync(temp, text, 'utf8');
|
|
337
|
-
fs.renameSync(temp, file);
|
|
338
|
-
return true;
|
|
339
|
-
} catch (err) {
|
|
340
|
-
try {
|
|
341
|
-
fs.unlinkSync(temp);
|
|
342
|
-
} catch (gone) {
|
|
343
|
-
// Nothing to clean up.
|
|
344
|
-
}
|
|
345
|
-
return false;
|
|
346
|
-
}
|
|
336
|
+
return atomic.tryWriteFileAtomic(file, text);
|
|
347
337
|
}
|
|
348
338
|
|
|
349
339
|
function writeLive(snapshot) {
|
|
@@ -22,6 +22,8 @@ const fs = require('fs');
|
|
|
22
22
|
const os = require('os');
|
|
23
23
|
const path = require('path');
|
|
24
24
|
|
|
25
|
+
const atomic = require('./atomic.js');
|
|
26
|
+
|
|
25
27
|
const EFFORT_LEVELS = ['low', 'medium', 'high', 'xhigh', 'max'];
|
|
26
28
|
const MANAGED_KEYS = ['effortLevel', 'model'];
|
|
27
29
|
const DEFAULTS = { effortLevel: 'low' };
|
|
@@ -50,10 +52,7 @@ function readJson(file) {
|
|
|
50
52
|
// Replace through a temporary file so an interrupted run cannot leave
|
|
51
53
|
// settings.json half written.
|
|
52
54
|
function writeJson(file, value) {
|
|
53
|
-
|
|
54
|
-
const temp = file + '.usage-limits-tmp';
|
|
55
|
-
fs.writeFileSync(temp, JSON.stringify(value, null, 2) + '\n', 'utf8');
|
|
56
|
-
fs.renameSync(temp, file);
|
|
55
|
+
atomic.writeFileAtomic(file, JSON.stringify(value, null, 2) + '\n');
|
|
57
56
|
}
|
|
58
57
|
|
|
59
58
|
function parseArgs(argv) {
|