claude-usage-limits 1.13.0 → 1.13.2
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/commands/relay.md +5 -0
- package/package.json +1 -1
- package/skills/usage-limits/scripts/brief.js +9 -1
- package/skills/usage-limits/scripts/reading.js +7 -1
- package/skills/usage-limits/scripts/relay.js +114 -16
- package/skills/usage-limits/scripts/statusline.js +8 -7
- package/skills/usage-limits/scripts/usage.js +14 -2
- package/skills/usage-limits/scripts/wake.js +6 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "usage-limits",
|
|
3
3
|
"displayName": "Usage Limits",
|
|
4
|
-
"version": "1.13.
|
|
4
|
+
"version": "1.13.2",
|
|
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.13.
|
|
3
|
+
"version": "1.13.2",
|
|
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/commands/relay.md
CHANGED
|
@@ -31,6 +31,11 @@ The rest:
|
|
|
31
31
|
- `thinking off|resume|always` - `resume` puts the word ultrathink into the
|
|
32
32
|
prompt the relay delivers. `always` sets `alwaysThinkingEnabled` in your
|
|
33
33
|
settings, backs the file up first, and applies to new sessions.
|
|
34
|
+
- `arm [--session <id>] ["<text>"]` - arm by hand, against the binding
|
|
35
|
+
window's reset, without waiting for the hook to see a todo list. This is the
|
|
36
|
+
way under Codex, which writes no plan tool into its rollouts for the hook to
|
|
37
|
+
read, and for any project that lives in your head rather than a list. Any
|
|
38
|
+
text given is stored as the continuation.
|
|
34
39
|
- `note "<text>"` (or `note --file <path>`) - store the continuation. This is
|
|
35
40
|
the text that gets delivered, so write it to be acted on: what is done, what
|
|
36
41
|
is next in order, which files are mid-change, what to verify first.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-usage-limits",
|
|
3
|
-
"version": "1.13.
|
|
3
|
+
"version": "1.13.2",
|
|
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",
|
|
@@ -815,6 +815,9 @@ function relayState(now, hookInput, binding, sessionId) {
|
|
|
815
815
|
const armed = relay.arm({
|
|
816
816
|
now,
|
|
817
817
|
config,
|
|
818
|
+
// Registration must be finished, not merely started, inside the hook's
|
|
819
|
+
// ten seconds; the scheduler trims its own waits to this.
|
|
820
|
+
deadline: now + ARM_DEADLINE_MS + 4000,
|
|
818
821
|
sessionId,
|
|
819
822
|
binding,
|
|
820
823
|
work,
|
|
@@ -914,6 +917,11 @@ async function run(now, hookInput) {
|
|
|
914
917
|
turnsLeft: binding && Number.isFinite(binding.turnsLeft) ? binding.turnsLeft : null,
|
|
915
918
|
session: data.session,
|
|
916
919
|
othersSummary: summariseOthers(data.windows, binding && binding.key),
|
|
920
|
+
// Every window, trimmed to the cacheable fields, so the corrected reading
|
|
921
|
+
// can be recorded for all three columns of the status line on a cache
|
|
922
|
+
// hit too. Recording the binding window alone left the other two at
|
|
923
|
+
// their raw snapshots on every short turn, where the pulse never runs.
|
|
924
|
+
windows: (data.windows || []).map(cacheableBinding),
|
|
917
925
|
sessions: data.sessions,
|
|
918
926
|
staleWindows: data.staleWindows,
|
|
919
927
|
planChanged: data.planChanged,
|
|
@@ -954,7 +962,7 @@ async function run(now, hookInput) {
|
|
|
954
962
|
// too often to scan for itself, and without this it shows the raw snapshot,
|
|
955
963
|
// which during a heavy session is wrong by tens of points in the flattering
|
|
956
964
|
// direction.
|
|
957
|
-
reading.
|
|
965
|
+
reading.recordAll(view.windows && view.windows.length ? view.windows : [binding], now, usage.isCodex() ? require('./codex.js').homeDir() : null);
|
|
958
966
|
const carry = relayState(now, hookInput, binding, sessionId);
|
|
959
967
|
// An instruction the user typed at /usage-limits:voice set. The learned
|
|
960
968
|
// traits are for writing AS them and stay out of the way; this is them
|
|
@@ -89,7 +89,13 @@ function record(binding, now, codexHome) {
|
|
|
89
89
|
else if (Number.isFinite(entry.resetsAt) && entry.resetsAt <= at) delete all[key];
|
|
90
90
|
}
|
|
91
91
|
fs.mkdirSync(path.dirname(readingFile(codexHome)), { recursive: true });
|
|
92
|
-
|
|
92
|
+
// Write beside and rename: the prompt hook and the pulse can both land
|
|
93
|
+
// here in the same second, and a reader between a truncate and a write
|
|
94
|
+
// would see half a file.
|
|
95
|
+
const file = readingFile(codexHome);
|
|
96
|
+
const tmp = file + '.' + process.pid + '.tmp';
|
|
97
|
+
fs.writeFileSync(tmp, JSON.stringify(all));
|
|
98
|
+
fs.renameSync(tmp, file);
|
|
93
99
|
return true;
|
|
94
100
|
} catch (err) {
|
|
95
101
|
return false;
|
|
@@ -114,10 +114,19 @@ function read() {
|
|
|
114
114
|
};
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
// Written to a sibling and renamed into place. Two hooks can run at once, and a
|
|
118
|
+
// reader that lands between the truncate and the write of a plain writeFileSync
|
|
119
|
+
// sees half a file; the rename is the only step another process can observe.
|
|
120
|
+
function writeAtomic(file, text) {
|
|
121
|
+
const tmp = file + '.' + process.pid + '.tmp';
|
|
122
|
+
fs.writeFileSync(tmp, text);
|
|
123
|
+
fs.renameSync(tmp, file);
|
|
124
|
+
}
|
|
125
|
+
|
|
117
126
|
function write(state) {
|
|
118
127
|
try {
|
|
119
128
|
fs.mkdirSync(configDir(), { recursive: true });
|
|
120
|
-
|
|
129
|
+
writeAtomic(relayFile(), JSON.stringify(state, null, 2) + '\n');
|
|
121
130
|
return true;
|
|
122
131
|
} catch (err) {
|
|
123
132
|
return false;
|
|
@@ -432,6 +441,31 @@ function psQuote(value) {
|
|
|
432
441
|
return "'" + String(value).replace(/'/g, "''") + "'";
|
|
433
442
|
}
|
|
434
443
|
|
|
444
|
+
// One argument, quoted the way CommandLineToArgvW will unquote it. The rule
|
|
445
|
+
// that matters and that a naive version misses: a backslash is only an escape
|
|
446
|
+
// when it precedes a quote, so a run of backslashes before the closing quote
|
|
447
|
+
// has to be doubled or the quote is eaten - which is exactly what a config
|
|
448
|
+
// directory ending in a backslash did to the wake's command line.
|
|
449
|
+
function winArg(value) {
|
|
450
|
+
const text = String(value);
|
|
451
|
+
if (!/[\s"]/.test(text)) return text;
|
|
452
|
+
let out = '"';
|
|
453
|
+
let slashes = 0;
|
|
454
|
+
for (const ch of text) {
|
|
455
|
+
if (ch === '\\') {
|
|
456
|
+
slashes += 1;
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
if (ch === '"') {
|
|
460
|
+
out += '\\'.repeat(slashes * 2 + 1) + '"';
|
|
461
|
+
} else {
|
|
462
|
+
out += '\\'.repeat(slashes) + ch;
|
|
463
|
+
}
|
|
464
|
+
slashes = 0;
|
|
465
|
+
}
|
|
466
|
+
return out + '\\'.repeat(slashes * 2) + '"';
|
|
467
|
+
}
|
|
468
|
+
|
|
435
469
|
function two(value) {
|
|
436
470
|
return String(value).padStart(2, '0');
|
|
437
471
|
}
|
|
@@ -442,12 +476,22 @@ function two(value) {
|
|
|
442
476
|
// silently and forever. -StartWhenAvailable makes it fire on wake instead,
|
|
443
477
|
// which is the whole difference between a relay and a coin toss. schtasks is
|
|
444
478
|
// still the fallback for a box where the module is missing.
|
|
445
|
-
|
|
479
|
+
// The two registrations below are bounded by the caller's deadline, not by a
|
|
480
|
+
// fixed figure: from inside a hook there may be a few seconds left, from the
|
|
481
|
+
// command line there are as many as it takes. Measured once at 38 seconds
|
|
482
|
+
// worst case when both spawns waited their full fixed timeouts, inside a hook
|
|
483
|
+
// that is killed at ten.
|
|
484
|
+
function remainingMs(deadline, ceiling) {
|
|
485
|
+
const left = Number.isFinite(deadline) ? deadline - Date.now() : ceiling;
|
|
486
|
+
return Math.max(0, Math.min(ceiling, left));
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function scheduleWindows(when, argv, name, cwd, deadline) {
|
|
446
490
|
const date = new Date(when);
|
|
447
491
|
const stamp =
|
|
448
492
|
date.getFullYear() + '-' + two(date.getMonth() + 1) + '-' + two(date.getDate()) + ' ' +
|
|
449
493
|
two(date.getHours()) + ':' + two(date.getMinutes()) + ':' + two(date.getSeconds());
|
|
450
|
-
const argument = argv.map(
|
|
494
|
+
const argument = argv.map(winArg).join(' ');
|
|
451
495
|
const script = [
|
|
452
496
|
'$ErrorActionPreference = "Stop"',
|
|
453
497
|
'$action = New-ScheduledTaskAction -Execute ' + psQuote(process.execPath) +
|
|
@@ -470,9 +514,11 @@ function scheduleWindows(when, argv, name, cwd) {
|
|
|
470
514
|
const run = spawnSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', file], {
|
|
471
515
|
encoding: 'utf8',
|
|
472
516
|
windowsHide: true,
|
|
473
|
-
timeout: 8000,
|
|
517
|
+
timeout: Math.max(500, remainingMs(deadline, 8000)),
|
|
474
518
|
});
|
|
475
519
|
if (run.status === 0 && /registered/.test(run.stdout || '')) return { ok: true, how: 'ScheduledTasks' };
|
|
520
|
+
// No time left for a second attempt is a plain refusal, not a hung hook.
|
|
521
|
+
if (remainingMs(deadline, 8000) < 1500) return { ok: false, error: 'no time left in this hook to register the wake; it will arm on the next prompt' };
|
|
476
522
|
// schtasks cannot express StartWhenAvailable, so this path is a worse
|
|
477
523
|
// guarantee and says so rather than pretending the two are the same.
|
|
478
524
|
const fallback = spawnSync(
|
|
@@ -481,7 +527,7 @@ function scheduleWindows(when, argv, name, cwd) {
|
|
|
481
527
|
'/ST', two(date.getHours()) + ':' + two(date.getMinutes()),
|
|
482
528
|
'/SD', two(date.getMonth() + 1) + '/' + two(date.getDate()) + '/' + date.getFullYear(),
|
|
483
529
|
'/IT', '/Z', '/F'],
|
|
484
|
-
{ encoding: 'utf8', windowsHide: true, timeout:
|
|
530
|
+
{ encoding: 'utf8', windowsHide: true, timeout: Math.max(500, remainingMs(deadline, 8000)) }
|
|
485
531
|
);
|
|
486
532
|
if (fallback.status === 0) return { ok: true, how: 'schtasks', warning: 'a sleeping machine will miss this wake' };
|
|
487
533
|
return { ok: false, error: (run.stderr || fallback.stderr || 'could not register a scheduled task').trim().split('\n')[0] };
|
|
@@ -524,8 +570,8 @@ function schedulePosix(when, argv, name, cwd) {
|
|
|
524
570
|
}
|
|
525
571
|
}
|
|
526
572
|
|
|
527
|
-
function schedule(when, argv, name, cwd) {
|
|
528
|
-
if (process.platform === 'win32') return scheduleWindows(when, argv, name, cwd);
|
|
573
|
+
function schedule(when, argv, name, cwd, deadline) {
|
|
574
|
+
if (process.platform === 'win32') return scheduleWindows(when, argv, name, cwd, deadline);
|
|
529
575
|
return schedulePosix(when, argv, name, cwd);
|
|
530
576
|
}
|
|
531
577
|
|
|
@@ -579,7 +625,7 @@ function arm(input) {
|
|
|
579
625
|
// pointed at the same one or it reads somebody else's relay - which in
|
|
580
626
|
// testing meant it read the real one and found nothing armed.
|
|
581
627
|
if (process.env.CLAUDE_CONFIG_DIR) argv.push('--config-dir', process.env.CLAUDE_CONFIG_DIR);
|
|
582
|
-
const scheduled = options.schedule === false ? { ok: true, how: 'none' } : schedule(when, argv, name, options.cwd);
|
|
628
|
+
const scheduled = options.schedule === false ? { ok: true, how: 'none' } : schedule(when, argv, name, options.cwd, options.deadline);
|
|
583
629
|
if (!scheduled.ok) {
|
|
584
630
|
note('arm failed for ' + id + ': ' + scheduled.error, now);
|
|
585
631
|
return { ok: false, error: scheduled.error };
|
|
@@ -698,7 +744,10 @@ function applyAlwaysThinking(on) {
|
|
|
698
744
|
try {
|
|
699
745
|
const raw = fs.readFileSync(file, 'utf8');
|
|
700
746
|
existed = true;
|
|
701
|
-
|
|
747
|
+
// Editors on Windows save JSON with a byte order mark often enough that
|
|
748
|
+
// refusing it would read as "your settings file is broken" to somebody
|
|
749
|
+
// whose settings file is fine.
|
|
750
|
+
parsed = JSON.parse(raw.replace(/^/, ''));
|
|
702
751
|
fs.writeFileSync(file + '.bak-usage-limits', raw);
|
|
703
752
|
} catch (err) {
|
|
704
753
|
if (existed) return { ok: false, error: 'settings.json is not readable JSON; left untouched' };
|
|
@@ -715,6 +764,42 @@ function applyAlwaysThinking(on) {
|
|
|
715
764
|
return { ok: true, file, backup: existed ? file + '.bak-usage-limits' : null };
|
|
716
765
|
}
|
|
717
766
|
|
|
767
|
+
// `relay arm [--session <id>] [note text]`: take the current reading, book the
|
|
768
|
+
// wake against the binding window's reset, and store any text given as the
|
|
769
|
+
// continuation. Uses the report rather than the hook's cache, so it costs a
|
|
770
|
+
// scan; that is fine from a command line.
|
|
771
|
+
async function armByHand(rest) {
|
|
772
|
+
const usage = require('./usage.js');
|
|
773
|
+
usage.setHost(host.detect(process.argv.slice(2), process.env));
|
|
774
|
+
const config = configure({ enabled: true });
|
|
775
|
+
const now = Date.now();
|
|
776
|
+
const data = await usage.report(now, {});
|
|
777
|
+
const binding = data && data.binding;
|
|
778
|
+
if (!binding || !Number.isFinite(binding.resetsAt)) return 'No window with a known reset time to arm against; run /usage and try again.';
|
|
779
|
+
const at = rest.indexOf('--session');
|
|
780
|
+
const sessionId = at !== -1 && rest[at + 1] ? rest[at + 1] : (data.session && data.session.id) || null;
|
|
781
|
+
if (!sessionId) return 'No session id: pass --session <id> (from usage.js --sessions).';
|
|
782
|
+
const text = rest.filter((item, i) => item !== '--session' && !(at !== -1 && i === at + 1)).join(' ').trim();
|
|
783
|
+
const result = arm({
|
|
784
|
+
now,
|
|
785
|
+
config,
|
|
786
|
+
sessionId,
|
|
787
|
+
binding,
|
|
788
|
+
resetsAt: binding.resetsAt,
|
|
789
|
+
cwd: process.cwd(),
|
|
790
|
+
project: path.basename(process.cwd()),
|
|
791
|
+
hostName: usage.currentHost(),
|
|
792
|
+
work: { hasWork: true, pending: 1, source: 'manual', todos: [], plan: text || null },
|
|
793
|
+
});
|
|
794
|
+
if (!result.ok) return 'Could not arm: ' + result.error;
|
|
795
|
+
if (text) saveContinuation(sessionId, text);
|
|
796
|
+
return (
|
|
797
|
+
'Armed by hand for session ' + sessionId.slice(0, 8) + ': wake at ' +
|
|
798
|
+
new Date(result.record.wakeAt).toLocaleString() + ' via ' + result.record.how + '.' +
|
|
799
|
+
(text ? ' Continuation saved.' : ' No continuation yet - add one with: relay note "<text>"')
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
|
|
718
803
|
/* ----------------------------------------------------------------- cli ---- */
|
|
719
804
|
|
|
720
805
|
function main(argv) {
|
|
@@ -796,6 +881,10 @@ function main(argv) {
|
|
|
796
881
|
const result = disarm('cancelled by hand', Date.now());
|
|
797
882
|
return result.changed ? 'Relay cancelled and the scheduled wake removed.' : 'Nothing was armed.';
|
|
798
883
|
}
|
|
884
|
+
// Arming by hand, for the cases the hook cannot see: Codex writes no plan
|
|
885
|
+
// tool into its rollouts, so nothing there ever reads as work to carry; and
|
|
886
|
+
// a person can have a project in their head that is in no todo list.
|
|
887
|
+
if (command === 'arm') return armByHand(rest);
|
|
799
888
|
if (command === 'log') {
|
|
800
889
|
try {
|
|
801
890
|
return fs.readFileSync(logFile(), 'utf8').split('\n').slice(-20).join('\n');
|
|
@@ -804,19 +893,25 @@ function main(argv) {
|
|
|
804
893
|
}
|
|
805
894
|
}
|
|
806
895
|
return [
|
|
807
|
-
'usage: relay.js [status|on|off|at N|grace N|mode notify|resume|permission MODE|model NAME|thinking off|resume|always|note TEXT|cancel|log]',
|
|
896
|
+
'usage: relay.js [status|on|off|at N|grace N|mode notify|resume|permission MODE|model NAME|thinking off|resume|always|arm [--session ID] [TEXT]|note TEXT|cancel|log]',
|
|
808
897
|
'',
|
|
809
898
|
status(Date.now()),
|
|
810
899
|
].join('\n');
|
|
811
900
|
}
|
|
812
901
|
|
|
813
902
|
if (require.main === module) {
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
903
|
+
Promise.resolve()
|
|
904
|
+
.then(() => main(process.argv.slice(2)))
|
|
905
|
+
.then(
|
|
906
|
+
(text) => {
|
|
907
|
+
process.stdout.write(text + '\n');
|
|
908
|
+
process.exit(0);
|
|
909
|
+
},
|
|
910
|
+
(err) => {
|
|
911
|
+
process.stdout.write('relay: ' + err.message + '\n');
|
|
912
|
+
process.exit(0);
|
|
913
|
+
}
|
|
914
|
+
);
|
|
820
915
|
}
|
|
821
916
|
|
|
822
917
|
module.exports = {
|
|
@@ -847,6 +942,9 @@ module.exports = {
|
|
|
847
942
|
wakeAt,
|
|
848
943
|
taskName,
|
|
849
944
|
psQuote,
|
|
945
|
+
winArg,
|
|
946
|
+
remainingMs,
|
|
947
|
+
armByHand,
|
|
850
948
|
schedule,
|
|
851
949
|
scheduleWindows,
|
|
852
950
|
schedulePosix,
|
|
@@ -144,13 +144,14 @@ function planOn(settings, state, options) {
|
|
|
144
144
|
const opts = options || {};
|
|
145
145
|
const launcher = opts.launcher || launcherFile();
|
|
146
146
|
const existing = current.statusLine === undefined ? null : current.statusLine;
|
|
147
|
-
//
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
147
|
+
// What "off" must put back is whatever is in the settings right now that is
|
|
148
|
+
// not ours. A second "on" must not record our own launcher as that thing -
|
|
149
|
+
// but it must not keep an old memory either: a status line the user set
|
|
150
|
+
// between two "on" runs was being overwritten and lost, because the first
|
|
151
|
+
// run's record was trusted over the file in front of us.
|
|
152
|
+
const remembered =
|
|
153
|
+
state && Object.prototype.hasOwnProperty.call(state, 'previous') ? state.previous : null;
|
|
154
|
+
const previous = isOurs(existing, launcher) ? remembered : existing;
|
|
154
155
|
const command = 'node ' + JSON.stringify(launcher);
|
|
155
156
|
const statusLine = { type: 'command', command };
|
|
156
157
|
if (Number.isFinite(opts.refresh) && opts.refresh >= 1) statusLine.refreshInterval = Math.floor(opts.refresh);
|
|
@@ -1272,7 +1272,12 @@ function codexBlock(now) {
|
|
|
1272
1272
|
// The warning that would have caught that incident: what the window holds at
|
|
1273
1273
|
// the effort actually set, rather than at the average of everything ever run.
|
|
1274
1274
|
function effortWarning(events, current, window) {
|
|
1275
|
-
|
|
1275
|
+
// Once spending has outrun the snapshot, percentLeft and usdPerPercent are
|
|
1276
|
+
// a floor and a stale price. buildWindow nulls the turn count for that
|
|
1277
|
+
// reason, and this function was quietly recomputing one from the two fields
|
|
1278
|
+
// it left behind - printing "at ultra effort this window holds about 104
|
|
1279
|
+
// turns" beside the warning that the window may already be exhausted.
|
|
1280
|
+
if (!current || !window || window.stale || window.correctionUnreliable) return null;
|
|
1276
1281
|
const rates = effortRates(events);
|
|
1277
1282
|
const here = rates.find((row) => row.effort === current);
|
|
1278
1283
|
if (!here || here.turns < MIN_EFFORT_SAMPLE || !here.perTurn || here.perTurn <= 0) return null;
|
|
@@ -2080,7 +2085,9 @@ function criticalOthers(windows, bindingKey, threshold) {
|
|
|
2080
2085
|
// do about it is less work, against a limit its work never touches.
|
|
2081
2086
|
w.applies !== false &&
|
|
2082
2087
|
w.percentUsed !== null &&
|
|
2083
|
-
|
|
2088
|
+
// A window whose spend has outrun its snapshot reads as its old floor;
|
|
2089
|
+
// it belongs in this list whatever that floor says.
|
|
2090
|
+
(w.percentUsed >= limit || w.correctionUnreliable)
|
|
2084
2091
|
);
|
|
2085
2092
|
}
|
|
2086
2093
|
|
|
@@ -2107,6 +2114,11 @@ function bindingWindow(windows) {
|
|
|
2107
2114
|
// ranked by how full it is instead, because a nearly full window must never
|
|
2108
2115
|
// be passed over merely because nothing has been spent in it lately.
|
|
2109
2116
|
const soonest = (w) => {
|
|
2117
|
+
// Spend that has outrun the snapshot leaves percentUsed at the old floor
|
|
2118
|
+
// and headroomMs empty, so on those two numbers alone the fullest window
|
|
2119
|
+
// on the account ranked as the least urgent one. It is the most urgent:
|
|
2120
|
+
// the budget it describes may already be gone.
|
|
2121
|
+
if (w.correctionUnreliable) return 0;
|
|
2110
2122
|
if (Number.isFinite(w.headroomMs)) return w.headroomMs;
|
|
2111
2123
|
return w.percentUsed >= 90 ? 0 : Infinity;
|
|
2112
2124
|
};
|
|
@@ -89,8 +89,12 @@ async function windowReopened(record, now) {
|
|
|
89
89
|
const codex = require('./codex.js');
|
|
90
90
|
await codex.refresh({ now, timeoutMs: 8000 }).catch(() => null);
|
|
91
91
|
const collected = codex.collect(now);
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
// Same shape as the Claude side: keyed by window, each { utilization }.
|
|
93
|
+
// Reading a flat "primary" that codex.collect never writes answered
|
|
94
|
+
// "cannot tell" every time and disabled this check under Codex.
|
|
95
|
+
const bucket = collected && collected.utilization ? collected.utilization[record.windowKey || 'five_hour'] : null;
|
|
96
|
+
const value = bucket && typeof bucket.utilization === 'number' ? bucket.utilization : null;
|
|
97
|
+
return { known: value !== null, percent: value };
|
|
94
98
|
}
|
|
95
99
|
const live = require('./live.js');
|
|
96
100
|
const usage = require('./usage.js');
|