claude-usage-limits 1.19.0 → 1.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +220 -2
- package/bin/cli.js +16 -0
- package/commands/defer.md +47 -0
- package/commands/usage-mode.md +64 -0
- package/hooks/hooks.json +1 -1
- package/package.json +1 -1
- package/skills/usage-limits/SKILL.md +196 -19
- package/skills/usage-limits/references/tactics.md +40 -9
- package/skills/usage-limits/scripts/agy-hook.js +175 -0
- package/skills/usage-limits/scripts/brief.js +406 -46
- package/skills/usage-limits/scripts/ceiling.js +191 -0
- package/skills/usage-limits/scripts/codex-lowpower.js +95 -4
- package/skills/usage-limits/scripts/codex.js +87 -6
- package/skills/usage-limits/scripts/defer.js +318 -0
- package/skills/usage-limits/scripts/drift.js +254 -0
- package/skills/usage-limits/scripts/feed.js +23 -1
- package/skills/usage-limits/scripts/host.js +23 -3
- package/skills/usage-limits/scripts/install-antigravity.js +215 -0
- package/skills/usage-limits/scripts/install-codex-hook.js +22 -2
- package/skills/usage-limits/scripts/lowpower.js +48 -0
- package/skills/usage-limits/scripts/mode.js +1637 -0
- package/skills/usage-limits/scripts/net.js +179 -0
- package/skills/usage-limits/scripts/pulse.js +254 -17
- package/skills/usage-limits/scripts/reading.js +12 -3
- package/skills/usage-limits/scripts/relay.js +266 -2
- package/skills/usage-limits/scripts/sessionend.js +8 -0
- package/skills/usage-limits/scripts/stop.js +145 -1
- package/skills/usage-limits/scripts/usage.js +244 -17
- package/skills/usage-limits/scripts/view.js +4 -0
- package/skills/usage-limits/scripts/voice.js +10 -1
- package/skills/usage-limits/scripts/wake.js +210 -30
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// The ceiling: the one thing in this plugin that is enforced rather than
|
|
5
|
+
// reported.
|
|
6
|
+
//
|
|
7
|
+
// Everything else here tells an agent where the budget stands and trusts it to
|
|
8
|
+
// act on that. For Claude Code that mostly works. For Codex it does not, and
|
|
9
|
+
// the reason is worth stating plainly rather than blaming the model: a number
|
|
10
|
+
// in the context is an input to a decision, and an agent part-way through a
|
|
11
|
+
// plan it was told to finish will weigh "the window is at 80 per cent" against
|
|
12
|
+
// "finish the whole list" and keep going, every time. It is not ignoring the
|
|
13
|
+
// figure. It is trading it off, and losing the trade.
|
|
14
|
+
//
|
|
15
|
+
// So the ceiling does not argue. Above it, the calls that MULTIPLY spend are
|
|
16
|
+
// refused at the hook, before the model's judgement is involved at all.
|
|
17
|
+
//
|
|
18
|
+
// Three rules shape what it refuses, and they are the whole design:
|
|
19
|
+
//
|
|
20
|
+
// 1. It never refuses work, only the expensive WAY of doing it. A fan-out of
|
|
21
|
+
// eight subagents and doing the same eight things in sequence reach the
|
|
22
|
+
// same place; only one of them can spend half a window between two
|
|
23
|
+
// readings with nothing able to speak in between. Refusing the fan-out
|
|
24
|
+
// leaves the task entirely possible, which is why this is a ceiling and
|
|
25
|
+
// not a stop button.
|
|
26
|
+
//
|
|
27
|
+
// 2. It never refuses the cheap calls. Reading a file, running a test,
|
|
28
|
+
// writing an edit - all of it stays allowed at any percentage, because an
|
|
29
|
+
// agent that cannot save its work is worse than one that overspends.
|
|
30
|
+
//
|
|
31
|
+
// 3. It says why, in terms of what to do instead. A denial that reads "over
|
|
32
|
+
// budget" gets retried. One that reads "do these sequentially yourself"
|
|
33
|
+
// gets obeyed.
|
|
34
|
+
//
|
|
35
|
+
// Off by default. A ceiling nobody set is not a ceiling, and this file returns
|
|
36
|
+
// `allow` for every call until someone names a number.
|
|
37
|
+
|
|
38
|
+
const MULTIPLIERS = [
|
|
39
|
+
// Claude Code, and the Codex equivalents, which use the same names.
|
|
40
|
+
'Agent',
|
|
41
|
+
'Task',
|
|
42
|
+
'Workflow',
|
|
43
|
+
// Codex names its fan-out differently depending on build; both have been
|
|
44
|
+
// seen. Matching the name is cheap and a miss only costs enforcement, never
|
|
45
|
+
// correctness.
|
|
46
|
+
'Subagent',
|
|
47
|
+
'Dispatch',
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
// Antigravity derives tool names by lowercasing the step type and stripping
|
|
51
|
+
// CORTEX_STEP_TYPE_, so its names are snake_case and cannot be matched by the
|
|
52
|
+
// list above.
|
|
53
|
+
const MULTIPLIER_PATTERN = /^(agent|task|workflow|subagent|dispatch|spawn_[a-z_]*agent|run_[a-z_]*agent)$/i;
|
|
54
|
+
|
|
55
|
+
// How far below the ceiling the warning starts. Ten points is about one long
|
|
56
|
+
// turn at xhigh on a 5-hour window, which is the last moment a warning can
|
|
57
|
+
// still change what happens next.
|
|
58
|
+
const NEAR_POINTS = 10;
|
|
59
|
+
|
|
60
|
+
function isMultiplier(tool) {
|
|
61
|
+
const name = String(tool || '').trim();
|
|
62
|
+
if (!name) return false;
|
|
63
|
+
if (MULTIPLIERS.includes(name)) return true;
|
|
64
|
+
return MULTIPLIER_PATTERN.test(name);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// The number, and who set it. An explicit environment value is the user saying
|
|
68
|
+
// it outright for this one session and beats the file, the same precedence the
|
|
69
|
+
// mode uses.
|
|
70
|
+
function ceilingFrom(state, env) {
|
|
71
|
+
const environment = env || process.env;
|
|
72
|
+
const raw = environment.USAGE_LIMITS_CEILING;
|
|
73
|
+
if (raw !== undefined && raw !== null && String(raw).trim() !== '') {
|
|
74
|
+
const text = String(raw).trim().toLowerCase();
|
|
75
|
+
if (text === 'off' || text === 'none' || text === 'no') return { percent: null, source: 'environment' };
|
|
76
|
+
const value = Number(text.replace(/%$/, ''));
|
|
77
|
+
if (Number.isFinite(value) && value > 0 && value <= 100) {
|
|
78
|
+
return { percent: value, source: 'environment' };
|
|
79
|
+
}
|
|
80
|
+
// A ceiling that cannot be read is not a ceiling of zero. Fall through to
|
|
81
|
+
// the file rather than enforcing a number nobody typed.
|
|
82
|
+
}
|
|
83
|
+
const stored = state && Number.isFinite(state.ceilingPercent) ? state.ceilingPercent : null;
|
|
84
|
+
if (stored !== null && stored > 0 && stored <= 100) return { percent: stored, source: 'the file' };
|
|
85
|
+
return { percent: null, source: null };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Where the binding window stands against the ceiling.
|
|
89
|
+
//
|
|
90
|
+
// `percent` is the BINDING window's, not the emptiest one's. A ceiling read
|
|
91
|
+
// against whichever window has the most left would never bind at all, which is
|
|
92
|
+
// the same mistake the brief was corrected for.
|
|
93
|
+
function assess(options) {
|
|
94
|
+
const opts = options || {};
|
|
95
|
+
const ceiling = ceilingFrom(opts.state, opts.env);
|
|
96
|
+
const percent = Number.isFinite(opts.percent) ? opts.percent : null;
|
|
97
|
+
if (ceiling.percent === null || percent === null) {
|
|
98
|
+
return {
|
|
99
|
+
set: ceiling.percent !== null,
|
|
100
|
+
source: ceiling.source,
|
|
101
|
+
ceiling: ceiling.percent,
|
|
102
|
+
percent,
|
|
103
|
+
over: false,
|
|
104
|
+
near: false,
|
|
105
|
+
headroomPoints: null,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
const headroomPoints = ceiling.percent - percent;
|
|
109
|
+
return {
|
|
110
|
+
set: true,
|
|
111
|
+
source: ceiling.source,
|
|
112
|
+
ceiling: ceiling.percent,
|
|
113
|
+
percent,
|
|
114
|
+
over: percent >= ceiling.percent,
|
|
115
|
+
near: headroomPoints > 0 && headroomPoints <= NEAR_POINTS,
|
|
116
|
+
headroomPoints,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function round(value) {
|
|
121
|
+
return Math.round(value * 10) / 10;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// What to do about one tool call.
|
|
125
|
+
//
|
|
126
|
+
// Returns `allow` for everything the ceiling does not cover, which is almost
|
|
127
|
+
// everything. The caller turns a `deny` into whatever its host's hook protocol
|
|
128
|
+
// wants; this file deliberately knows nothing about that.
|
|
129
|
+
function verdict(state, tool) {
|
|
130
|
+
if (!state || !state.set || !state.over) return { decision: 'allow', reason: null };
|
|
131
|
+
if (!isMultiplier(tool)) return { decision: 'allow', reason: null };
|
|
132
|
+
return {
|
|
133
|
+
decision: 'deny',
|
|
134
|
+
reason:
|
|
135
|
+
'Usage ceiling reached: the binding window is ' +
|
|
136
|
+
round(state.percent) +
|
|
137
|
+
'% used and the ceiling is ' +
|
|
138
|
+
state.ceiling +
|
|
139
|
+
'%. Fan-out calls are refused past the ceiling because subagents spend the same window ' +
|
|
140
|
+
'in parallel and nothing can report back until they stop. Nothing else is blocked. ' +
|
|
141
|
+
'Do this work yourself, in this session, one step at a time - that is the same work at ' +
|
|
142
|
+
'roughly a fifth of the spend. Do not retry this call and do not ask to raise the ' +
|
|
143
|
+
'ceiling; if the ceiling is genuinely wrong, say so in your reply and let the user change it.',
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// The line for a session that is close but not over. Advice, not enforcement -
|
|
148
|
+
// there is still room, and the point of saying it here is that it arrives
|
|
149
|
+
// BEFORE the expensive call rather than after it.
|
|
150
|
+
function warning(state) {
|
|
151
|
+
if (!state || !state.set || state.over || !state.near) return null;
|
|
152
|
+
return (
|
|
153
|
+
'Usage ceiling in ' +
|
|
154
|
+
round(state.headroomPoints) +
|
|
155
|
+
' points: the binding window is ' +
|
|
156
|
+
round(state.percent) +
|
|
157
|
+
'% used against a ceiling of ' +
|
|
158
|
+
state.ceiling +
|
|
159
|
+
'%. Past the ceiling, fan-out calls are refused outright. Land what is in flight and ' +
|
|
160
|
+
'prefer doing the next steps in this session over spawning agents for them.'
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// A short, honest description for the report and the panel.
|
|
165
|
+
function describe(state) {
|
|
166
|
+
if (!state || !state.set) return 'Ceiling not set';
|
|
167
|
+
const where = state.percent === null ? 'no reading' : round(state.percent) + '% used';
|
|
168
|
+
const status = state.over ? 'REACHED' : state.near ? 'close' : 'clear';
|
|
169
|
+
return (
|
|
170
|
+
'Ceiling ' +
|
|
171
|
+
state.ceiling +
|
|
172
|
+
'% (' +
|
|
173
|
+
status +
|
|
174
|
+
', ' +
|
|
175
|
+
where +
|
|
176
|
+
')' +
|
|
177
|
+
(state.source ? ' - set in ' + state.source : '')
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
module.exports = {
|
|
182
|
+
MULTIPLIERS,
|
|
183
|
+
MULTIPLIER_PATTERN,
|
|
184
|
+
NEAR_POINTS,
|
|
185
|
+
isMultiplier,
|
|
186
|
+
ceilingFrom,
|
|
187
|
+
assess,
|
|
188
|
+
verdict,
|
|
189
|
+
warning,
|
|
190
|
+
describe,
|
|
191
|
+
};
|
|
@@ -35,6 +35,85 @@ function rewrite(text, values) {
|
|
|
35
35
|
}
|
|
36
36
|
return prepend.join('') + lines.join('');
|
|
37
37
|
}
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// The subagent clamp
|
|
40
|
+
//
|
|
41
|
+
// Effort is the biggest lever on Codex and this is the second. Two facts, both
|
|
42
|
+
// read out of the model catalog Codex itself caches at
|
|
43
|
+
// CODEX_HOME/models_cache.json rather than taken from documentation:
|
|
44
|
+
//
|
|
45
|
+
// gpt-6-astra: default_reasoning_level = "low"
|
|
46
|
+
// multi_agent_reasoning_effort = "xhigh"
|
|
47
|
+
//
|
|
48
|
+
// So Astra's own default effort is the cheapest one, and its subagents run at
|
|
49
|
+
// the dearest one NO MATTER what the main session is set to. A session at
|
|
50
|
+
// medium that delegates is still paying xhigh for everything it delegates, and
|
|
51
|
+
// the "ultra" level is described in that same catalog as "maximum reasoning
|
|
52
|
+
// with automatic task delegation" - it spawns them on its own.
|
|
53
|
+
//
|
|
54
|
+
// The [agents] table is where that is bounded. It is a table rather than a
|
|
55
|
+
// top-level key, and the line editor above deliberately refuses to be a TOML
|
|
56
|
+
// parser, so this is handled the one way that is safe without one: a marked
|
|
57
|
+
// block appended at the end of the file. A TOML table runs until the next
|
|
58
|
+
// header, so appending at the end is always valid, and the editor above stops
|
|
59
|
+
// at the first '[' so the two never interfere.
|
|
60
|
+
//
|
|
61
|
+
// If the file already has an [agents] table this refuses outright rather than
|
|
62
|
+
// writing a second one, because duplicate tables are a TOML error and a config
|
|
63
|
+
// this tool broke would be worse than a window it failed to save.
|
|
64
|
+
const AGENTS_START = '# >>> usage-limits lowpower: subagent clamp';
|
|
65
|
+
const AGENTS_END = '# <<< usage-limits lowpower';
|
|
66
|
+
|
|
67
|
+
function hasAgentsTable(text) {
|
|
68
|
+
// Any [agents] or [agents.x] header that is not inside our own block.
|
|
69
|
+
const outside = stripAgentsBlock(text);
|
|
70
|
+
return /^[ \t]*\[\s*agents\s*[.\]]/m.test(outside);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function stripAgentsBlock(text) {
|
|
74
|
+
const from = text.indexOf(AGENTS_START);
|
|
75
|
+
if (from === -1) return text;
|
|
76
|
+
const to = text.indexOf(AGENTS_END, from);
|
|
77
|
+
if (to === -1) return text;
|
|
78
|
+
// The block is written after one blank separator line, so removing it has to
|
|
79
|
+
// take that line too. Leaving it behind meant `off` returned a file one
|
|
80
|
+
// newline longer than `on` found it - which is not a restore, and the
|
|
81
|
+
// round-trip tests that guard this editor said so.
|
|
82
|
+
let start = from;
|
|
83
|
+
if (text.endsWith('\r\n\r\n', from)) start = from - 2;
|
|
84
|
+
else if (text.endsWith('\n\n', from)) start = from - 1;
|
|
85
|
+
const after = to + AGENTS_END.length;
|
|
86
|
+
// Take the newline that ends the marker line with it.
|
|
87
|
+
const end = text.startsWith('\r\n', after) ? after + 2 : text.startsWith('\n', after) ? after + 1 : after;
|
|
88
|
+
return text.slice(0, start) + text.slice(end);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function agentsBlock(newline) {
|
|
92
|
+
return [
|
|
93
|
+
AGENTS_START,
|
|
94
|
+
'# Written by the usage-limits plugin. "lowpower off --host codex" removes it.',
|
|
95
|
+
'# Astra runs its subagents at xhigh whatever the session is set to, so this',
|
|
96
|
+
'# is the only place that spend is bounded.',
|
|
97
|
+
'[agents]',
|
|
98
|
+
'max_concurrent_threads_per_session = 1',
|
|
99
|
+
'default_subagent_reasoning_effort = "low"',
|
|
100
|
+
AGENTS_END,
|
|
101
|
+
].join(newline) + newline;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function withAgentsBlock(text, wanted, newline) {
|
|
105
|
+
const base = stripAgentsBlock(text);
|
|
106
|
+
if (!wanted) return base;
|
|
107
|
+
if (hasAgentsTable(base)) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
'config.toml already has an [agents] table; the subagent clamp was not written. ' +
|
|
110
|
+
'Set max_concurrent_threads_per_session and default_subagent_reasoning_effort there yourself.'
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
const padded = base.length && !base.endsWith('\n') ? base + newline : base;
|
|
114
|
+
return padded + (padded.length ? newline : '') + agentsBlock(newline);
|
|
115
|
+
}
|
|
116
|
+
|
|
38
117
|
function read(file, fallback) {
|
|
39
118
|
try { return fs.readFileSync(file, 'utf8'); } catch (e) { if (e.code === 'ENOENT') return fallback; throw e; }
|
|
40
119
|
}
|
|
@@ -65,7 +144,13 @@ function plan(text, options, state) {
|
|
|
65
144
|
if (state) for (const [key, line] of Object.entries(state.applied)) {
|
|
66
145
|
if (lineOf(key) !== line) throw new Error(key + ' changed outside lowpower; review config and restore state before continuing.');
|
|
67
146
|
}
|
|
68
|
-
|
|
147
|
+
const newline = text.includes('\r\n') ? '\r\n' : '\n';
|
|
148
|
+
if (options.command === 'off') {
|
|
149
|
+
const restored = state ? rewrite(text, state.previous) : text;
|
|
150
|
+
// The clamp is removed on `off` whether or not this run is the one that
|
|
151
|
+
// wrote it, so a state file lost to a crash cannot strand it in the config.
|
|
152
|
+
return { text: withAgentsBlock(restored, false, newline), state: null };
|
|
153
|
+
}
|
|
69
154
|
const effort = options.effort || 'low';
|
|
70
155
|
if (!EFFORTS.includes(effort)) throw new Error('Unknown Codex effort: ' + effort);
|
|
71
156
|
if (options.model !== null && options.model !== undefined && !/^[a-zA-Z0-9][a-zA-Z0-9_.:/-]*$/.test(options.model))
|
|
@@ -74,12 +159,18 @@ function plan(text, options, state) {
|
|
|
74
159
|
if (options.model) wanted.model = options.model;
|
|
75
160
|
const previous = { ...(state && state.previous) };
|
|
76
161
|
const applied = { ...(state && state.applied) };
|
|
77
|
-
const newline = text.includes('\r\n') ? '\r\n' : '\n';
|
|
78
162
|
for (const [key, value] of Object.entries(wanted)) {
|
|
79
163
|
if (!Object.hasOwn(previous, key)) previous[key] = lineOf(key);
|
|
80
164
|
applied[key] = key + ' = ' + JSON.stringify(value) + newline;
|
|
81
165
|
}
|
|
82
|
-
|
|
166
|
+
// On unless explicitly refused. Astra's subagents run at xhigh regardless of
|
|
167
|
+
// the session's effort, so lowering effort WITHOUT bounding them leaves the
|
|
168
|
+
// most expensive path in the product untouched.
|
|
169
|
+
const clampAgents = options.agents !== false;
|
|
170
|
+
return {
|
|
171
|
+
text: withAgentsBlock(rewrite(text, applied), clampAgents, newline),
|
|
172
|
+
state: { version: 1, previous, applied, agentsClamp: clampAgents },
|
|
173
|
+
};
|
|
83
174
|
}
|
|
84
175
|
function main(args) {
|
|
85
176
|
if (!['status', 'on', 'off'].includes(args.command)) throw new Error('Expected status, on or off');
|
|
@@ -132,4 +223,4 @@ function main(args) {
|
|
|
132
223
|
if (lock !== undefined) { fs.closeSync(lock); fs.unlinkSync(stateFile + '.lock'); }
|
|
133
224
|
}
|
|
134
225
|
}
|
|
135
|
-
module.exports = { scan, rewrite, plan, main, EFFORTS };
|
|
226
|
+
module.exports = { scan, rewrite, plan, main, EFFORTS, AGENTS_START, AGENTS_END, hasAgentsTable, stripAgentsBlock, agentsBlock, withAgentsBlock };
|
|
@@ -606,12 +606,74 @@ function calibrate(events, key, now) {
|
|
|
606
606
|
return { usdPerPercent: cost / moved, turns, percent: Math.round(last.percent) };
|
|
607
607
|
}
|
|
608
608
|
|
|
609
|
+
function limitIdOf(meter) {
|
|
610
|
+
return meter && typeof meter.limit_id === 'string' ? meter.limit_id : '';
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Whether this payload is a reading of a rolling window at all.
|
|
614
|
+
//
|
|
615
|
+
// Percentages count, and so does Codex saying outright that the window is
|
|
616
|
+
// spent: at a limit hit the slots can come back null with
|
|
617
|
+
// `rate_limit_reached_type` set, and that payload is the truest description of
|
|
618
|
+
// the window there is. Skipping it for an older one that still had numbers
|
|
619
|
+
// would report a window as running when it has already stopped.
|
|
620
|
+
function carriesWindows(meter) {
|
|
621
|
+
if (!meter || typeof meter !== 'object') return false;
|
|
622
|
+
if (readingsOf(meter).length) return true;
|
|
623
|
+
return typeof meter.rate_limit_reached_type === 'string' && meter.rate_limit_reached_type !== '';
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Codex writes more than one meter, and they are not successive readings of one
|
|
627
|
+
// thing. A Plus rollout carries `limit_id: "codex"`, which holds the 5-hour and
|
|
628
|
+
// weekly windows, interleaved with `limit_id: "premium"`, which holds the credit
|
|
629
|
+
// balance and has `primary` and `secondary` set to null.
|
|
630
|
+
//
|
|
631
|
+
// Taking whichever was written last therefore threw the windows away whenever a
|
|
632
|
+
// `premium` payload happened to land last, which on this machine was two of
|
|
633
|
+
// every six rollouts - and the report went blind at exactly the moment it was
|
|
634
|
+
// wanted. On 2026-09-07 the final line of a rollout was a `premium` payload two
|
|
635
|
+
// lines after a `codex` payload reading 99 per cent of the 5-hour window, and
|
|
636
|
+
// what reached the agent was a meter with no windows in it at all. That is the
|
|
637
|
+
// whole of "Codex does not slow down when the limit is close": nothing ever
|
|
638
|
+
// told it the limit was close.
|
|
639
|
+
//
|
|
640
|
+
// So the newest reading of EACH meter is kept, and the one that actually
|
|
641
|
+
// describes a window wins. Nothing is merged and nothing is synthesised: the
|
|
642
|
+
// payload returned is one Codex really wrote, and `at` is when it wrote it, so
|
|
643
|
+
// a stale reading still ages honestly.
|
|
644
|
+
function pickMeter(candidates) {
|
|
645
|
+
const newestBy = new Map();
|
|
646
|
+
let newest = null;
|
|
647
|
+
for (const entry of candidates || []) {
|
|
648
|
+
if (!entry || !entry.meter || !Number.isFinite(entry.at)) continue;
|
|
649
|
+
if (!newest || entry.at > newest.at) newest = entry;
|
|
650
|
+
const id = limitIdOf(entry.meter);
|
|
651
|
+
const held = newestBy.get(id);
|
|
652
|
+
if (!held || entry.at > held.at) newestBy.set(id, entry);
|
|
653
|
+
}
|
|
654
|
+
let best = null;
|
|
655
|
+
for (const entry of newestBy.values()) {
|
|
656
|
+
if (!carriesWindows(entry.meter)) continue;
|
|
657
|
+
if (!best || entry.at > best.at) best = entry;
|
|
658
|
+
}
|
|
659
|
+
// No meter describes a window: that is a real answer too, and the newest
|
|
660
|
+
// payload is the one that should say it. utilizationFrom then reports it as
|
|
661
|
+
// unreadable or windowless exactly as before.
|
|
662
|
+
return best || newest;
|
|
663
|
+
}
|
|
664
|
+
|
|
609
665
|
// The newest meter reading in the rollouts, and when it was taken.
|
|
610
666
|
function latestMeter(events) {
|
|
667
|
+
const candidates = [];
|
|
611
668
|
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
612
|
-
|
|
669
|
+
const event = events[index];
|
|
670
|
+
if (!event || !event.meter) continue;
|
|
671
|
+
candidates.push({ meter: event.meter, at: event.at });
|
|
672
|
+
// Newest first, so the first payload that describes a window is the newest
|
|
673
|
+
// one that does and nothing older can beat it.
|
|
674
|
+
if (carriesWindows(event.meter)) break;
|
|
613
675
|
}
|
|
614
|
-
return
|
|
676
|
+
return pickMeter(candidates);
|
|
615
677
|
}
|
|
616
678
|
|
|
617
679
|
// The meter is written next to every request, so the newest one is always near
|
|
@@ -664,6 +726,7 @@ function meterFromLines(text, partial) {
|
|
|
664
726
|
// The first line of a tail is a fragment of whatever it landed in the middle
|
|
665
727
|
// of, so it is never parsed.
|
|
666
728
|
const floor = partial ? 1 : 0;
|
|
729
|
+
const candidates = [];
|
|
667
730
|
for (let index = lines.length - 1; index >= floor; index -= 1) {
|
|
668
731
|
const line = lines[index];
|
|
669
732
|
if (!line || line.indexOf('"token_count"') === -1) continue;
|
|
@@ -675,22 +738,37 @@ function meterFromLines(text, partial) {
|
|
|
675
738
|
}
|
|
676
739
|
const meter = parsed && parsed.payload && parsed.payload.rate_limits;
|
|
677
740
|
const at = Date.parse(parsed && parsed.timestamp);
|
|
678
|
-
if (meter
|
|
741
|
+
if (!meter || !Number.isFinite(at)) continue;
|
|
742
|
+
candidates.push({ meter, at });
|
|
743
|
+
// Walking backwards, so the first payload that describes a window is the
|
|
744
|
+
// newest one that does. Stopping there keeps the common case at a handful
|
|
745
|
+
// of parsed lines rather than the whole tail, which matters on the status
|
|
746
|
+
// line path.
|
|
747
|
+
if (carriesWindows(meter)) break;
|
|
679
748
|
}
|
|
680
|
-
return
|
|
749
|
+
return pickMeter(candidates);
|
|
681
750
|
}
|
|
682
751
|
|
|
683
752
|
// Scanning only the newest few rollouts, for the meter alone. `collect` runs on
|
|
684
753
|
// the status-line path where a full scan would be far too slow.
|
|
754
|
+
//
|
|
755
|
+
// A rollout whose tail holds only the credit meter is not a reason to stop: the
|
|
756
|
+
// windows are in an older one, and reporting nothing when they are three
|
|
757
|
+
// seconds away on disk is the blindness this whole path exists to avoid. The
|
|
758
|
+
// newest windowless reading is still kept, so an account that genuinely has no
|
|
759
|
+
// rolling window says so instead of saying nothing.
|
|
685
760
|
function meterFromDisk() {
|
|
686
761
|
const files = rolloutFiles(NaN).slice(-12).reverse();
|
|
762
|
+
let fallback = null;
|
|
687
763
|
for (const entry of files) {
|
|
688
764
|
const tail = readTail(entry.file, TAIL_BYTES);
|
|
689
765
|
if (!tail) continue;
|
|
690
766
|
const found = meterFromLines(tail.text, tail.partial);
|
|
691
|
-
if (found)
|
|
767
|
+
if (!found) continue;
|
|
768
|
+
if (carriesWindows(found.meter)) return found;
|
|
769
|
+
if (!fallback || found.at > fallback.at) fallback = found;
|
|
692
770
|
}
|
|
693
|
-
return
|
|
771
|
+
return fallback;
|
|
694
772
|
}
|
|
695
773
|
|
|
696
774
|
// Where a live reading taken by refreshIfStale() is kept, so the hooks and the
|
|
@@ -1013,6 +1091,9 @@ module.exports = {
|
|
|
1013
1091
|
utilizationFrom,
|
|
1014
1092
|
labelFor,
|
|
1015
1093
|
planFrom,
|
|
1094
|
+
limitIdOf,
|
|
1095
|
+
carriesWindows,
|
|
1096
|
+
pickMeter,
|
|
1016
1097
|
latestMeter,
|
|
1017
1098
|
meterFromDisk,
|
|
1018
1099
|
readTail,
|