claude-usage-limits 1.7.0 → 1.8.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 +76 -12
- package/bin/cli.js +2 -0
- package/commands/session.md +13 -0
- package/hooks/hooks.json +23 -1
- package/package.json +1 -1
- package/skills/usage-limits/SKILL.md +29 -1
- package/skills/usage-limits/references/how-it-works.md +15 -0
- package/skills/usage-limits/references/tactics.md +1 -1
- package/skills/usage-limits/scripts/brief.js +153 -10
- package/skills/usage-limits/scripts/pulse.js +6 -4
- package/skills/usage-limits/scripts/sessionend.js +47 -0
- package/skills/usage-limits/scripts/stop.js +55 -0
- package/skills/usage-limits/scripts/tally.js +378 -0
- package/skills/usage-limits/scripts/usage.js +496 -66
|
@@ -55,7 +55,7 @@ const RATES = {
|
|
|
55
55
|
'claude-opus-4-8': { input: 5, output: 25 },
|
|
56
56
|
'claude-opus-4-7': { input: 5, output: 25 },
|
|
57
57
|
'claude-opus-4-6': { input: 5, output: 25 },
|
|
58
|
-
'claude-sonnet-5': { input:
|
|
58
|
+
'claude-sonnet-5': { input: 2, output: 10 },
|
|
59
59
|
'claude-sonnet-4-6': { input: 3, output: 15 },
|
|
60
60
|
'claude-haiku-4-5': { input: 1, output: 5 },
|
|
61
61
|
};
|
|
@@ -296,6 +296,10 @@ function eventFrom(line, seen, project) {
|
|
|
296
296
|
};
|
|
297
297
|
}
|
|
298
298
|
|
|
299
|
+
// Interrupts and client-side errors are written as assistant messages from
|
|
300
|
+
// a model called <synthetic>, with a usage block of zeros. Not a call.
|
|
301
|
+
if (entry.message.model === '<synthetic>') return null;
|
|
302
|
+
|
|
299
303
|
// A resumed or forked session repeats earlier turns in a new file.
|
|
300
304
|
const id = (entry.message.id || '') + '|' + (entry.requestId || '');
|
|
301
305
|
if (id !== '|' && seen) {
|
|
@@ -303,23 +307,76 @@ function eventFrom(line, seen, project) {
|
|
|
303
307
|
seen.add(id);
|
|
304
308
|
}
|
|
305
309
|
|
|
310
|
+
const parts = tokenParts(entry.message.usage);
|
|
306
311
|
return {
|
|
307
312
|
at,
|
|
308
313
|
model: entry.message.model || '',
|
|
309
314
|
effort: entry.effort || null,
|
|
310
315
|
cost: costOf(entry.message.usage, entry.message.model),
|
|
311
316
|
tokens: tokensOf(entry.message.usage),
|
|
312
|
-
parts
|
|
317
|
+
parts,
|
|
318
|
+
// What the model was shown on this call: everything except what it wrote.
|
|
319
|
+
// It is re-sent on every later call, so it is the recurring cost of the
|
|
320
|
+
// session, and the one number that says how bloated the context has got.
|
|
321
|
+
context: parts.input + parts.cacheRead + parts.cacheWrite,
|
|
313
322
|
project: project || null,
|
|
314
323
|
sessionId: entry.sessionId || null,
|
|
324
|
+
// A subagent's turns are written under the parent session with these set.
|
|
325
|
+
sidechain: Boolean(entry.isSidechain || entry.agentId),
|
|
315
326
|
};
|
|
316
327
|
}
|
|
317
328
|
|
|
329
|
+
// Whether a transcript line is a prompt the user typed. Tool results are also
|
|
330
|
+
// written as user messages, and so are internal notes marked isMeta; neither
|
|
331
|
+
// is something a person asked for.
|
|
332
|
+
function promptFrom(line) {
|
|
333
|
+
if (line.indexOf('"user"') === -1) return false;
|
|
334
|
+
let entry;
|
|
335
|
+
try {
|
|
336
|
+
entry = JSON.parse(line);
|
|
337
|
+
} catch (err) {
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
if (!entry || entry.type !== 'user' || entry.isMeta || !entry.message) return false;
|
|
341
|
+
const content = entry.message.content;
|
|
342
|
+
if (typeof content === 'string') return content.length > 0;
|
|
343
|
+
if (!Array.isArray(content)) return false;
|
|
344
|
+
let typed = false;
|
|
345
|
+
for (const block of content) {
|
|
346
|
+
if (!block) continue;
|
|
347
|
+
if (block.type === 'tool_result') return false;
|
|
348
|
+
if (block.type === 'text') typed = true;
|
|
349
|
+
}
|
|
350
|
+
return typed;
|
|
351
|
+
}
|
|
352
|
+
|
|
318
353
|
async function readEvents(since) {
|
|
319
354
|
if (isCodex()) return codex.readEvents(since);
|
|
320
355
|
return readClaudeEvents(since);
|
|
321
356
|
}
|
|
322
357
|
|
|
358
|
+
// A file last touched before the window opened holds nothing useful.
|
|
359
|
+
function fresh(file, since) {
|
|
360
|
+
try {
|
|
361
|
+
return fs.statSync(file).mtimeMs >= since;
|
|
362
|
+
} catch (err) {
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function freshFiles(dir, since) {
|
|
368
|
+
let names;
|
|
369
|
+
try {
|
|
370
|
+
names = fs.readdirSync(dir);
|
|
371
|
+
} catch (err) {
|
|
372
|
+
return [];
|
|
373
|
+
}
|
|
374
|
+
return names
|
|
375
|
+
.filter((name) => name.endsWith('.jsonl'))
|
|
376
|
+
.map((name) => path.join(dir, name))
|
|
377
|
+
.filter((file) => fresh(file, since));
|
|
378
|
+
}
|
|
379
|
+
|
|
323
380
|
async function readClaudeEvents(since) {
|
|
324
381
|
const root = path.join(configDir(), 'projects');
|
|
325
382
|
let dirs = [];
|
|
@@ -333,21 +390,25 @@ async function readClaudeEvents(since) {
|
|
|
333
390
|
for (const dir of dirs) {
|
|
334
391
|
if (!dir.isDirectory()) continue;
|
|
335
392
|
const full = path.join(root, dir.name);
|
|
336
|
-
let
|
|
393
|
+
let entries = [];
|
|
337
394
|
try {
|
|
338
|
-
|
|
395
|
+
entries = fs.readdirSync(full, { withFileTypes: true });
|
|
339
396
|
} catch (err) {
|
|
340
397
|
continue;
|
|
341
398
|
}
|
|
342
|
-
for (const
|
|
343
|
-
if (
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
//
|
|
347
|
-
|
|
348
|
-
|
|
399
|
+
for (const entry of entries) {
|
|
400
|
+
if (entry.isDirectory()) {
|
|
401
|
+
// A session's subagents write their transcripts under
|
|
402
|
+
// <project>/<session id>/subagents/. Same budget, different file, and
|
|
403
|
+
// for a long time an Explore or Plan agent's whole spend went unseen.
|
|
404
|
+
for (const file of freshFiles(path.join(full, entry.name, 'subagents'), since)) {
|
|
405
|
+
files.push({ file, project: dir.name });
|
|
406
|
+
}
|
|
349
407
|
continue;
|
|
350
408
|
}
|
|
409
|
+
if (!entry.name.endsWith('.jsonl')) continue;
|
|
410
|
+
const file = path.join(full, entry.name);
|
|
411
|
+
if (!fresh(file, since)) continue;
|
|
351
412
|
files.push({ file, project: dir.name });
|
|
352
413
|
}
|
|
353
414
|
}
|
|
@@ -374,13 +435,20 @@ async function readClaudeEvents(since) {
|
|
|
374
435
|
return events;
|
|
375
436
|
}
|
|
376
437
|
|
|
438
|
+
// A turn is one main-thread call, the unit the headroom is planned in. A
|
|
439
|
+
// subagent's calls spend the same budget, so they count in the money and the
|
|
440
|
+
// tokens, and are counted apart so they never inflate the turn figures.
|
|
377
441
|
function totals(events) {
|
|
378
442
|
let cost = 0;
|
|
379
443
|
let tokens = 0;
|
|
444
|
+
let turns = 0;
|
|
445
|
+
let subagentTurns = 0;
|
|
380
446
|
const parts = { input: 0, cacheWrite: 0, cacheRead: 0, output: 0, reasoning: 0 };
|
|
381
447
|
for (const event of events) {
|
|
382
448
|
cost += event.cost;
|
|
383
449
|
tokens += event.tokens;
|
|
450
|
+
if (event.sidechain) subagentTurns += 1;
|
|
451
|
+
else turns += 1;
|
|
384
452
|
if (event.parts) {
|
|
385
453
|
parts.input += event.parts.input;
|
|
386
454
|
parts.cacheWrite += event.parts.cacheWrite;
|
|
@@ -389,7 +457,11 @@ function totals(events) {
|
|
|
389
457
|
parts.reasoning += event.parts.reasoning || 0;
|
|
390
458
|
}
|
|
391
459
|
}
|
|
392
|
-
return { cost, tokens, turns
|
|
460
|
+
return { cost, tokens, turns, subagentTurns, parts };
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function mainThread(events) {
|
|
464
|
+
return (events || []).filter((event) => !event.sidechain);
|
|
393
465
|
}
|
|
394
466
|
|
|
395
467
|
// What each model actually cost, dearest first.
|
|
@@ -437,8 +509,9 @@ function typicalTurnCost(recentEvents, windowEvents, allEvents, minSample) {
|
|
|
437
509
|
|
|
438
510
|
// What a turn costs is a fact about how you work, not about which budget it
|
|
439
511
|
// is being measured against, so a thin window borrows from a wider sample
|
|
440
|
-
// rather than inventing a figure from two turns.
|
|
441
|
-
|
|
512
|
+
// rather than inventing a figure from two turns. Subagent calls are left
|
|
513
|
+
// out: they are small and many, and would make a turn look cheap.
|
|
514
|
+
const tiers = [mainThread(recentEvents), mainThread(windowEvents), mainThread(allEvents)];
|
|
442
515
|
let pool = [];
|
|
443
516
|
for (const tier of tiers) {
|
|
444
517
|
if (tier && tier.length >= floor) {
|
|
@@ -578,7 +651,7 @@ function creditsFrom(utilization) {
|
|
|
578
651
|
// files costs many times one that answers from context. A median alone
|
|
579
652
|
// under-promises on the expensive half, so carry a high end too.
|
|
580
653
|
function costPercentiles(events) {
|
|
581
|
-
const costs = (events
|
|
654
|
+
const costs = mainThread(events)
|
|
582
655
|
.map((event) => event.cost)
|
|
583
656
|
.filter((cost) => Number.isFinite(cost) && cost > 0)
|
|
584
657
|
.sort((a, b) => a - b);
|
|
@@ -640,11 +713,25 @@ function activeSessions(events, now, windowMs) {
|
|
|
640
713
|
|
|
641
714
|
// The slice of the shared budget this session is actually getting. With
|
|
642
715
|
// another session spending half of it, only half those turns are yours.
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
716
|
+
//
|
|
717
|
+
// Past spend says how the budget has been going, not how it will go. A
|
|
718
|
+
// session that has only just started has almost none of it, and dividing its
|
|
719
|
+
// headroom by that share once turned 208 turns into "about 6 turns left" two
|
|
720
|
+
// tool calls into a session at 13 per cent used. So an equal split is the
|
|
721
|
+
// floor: a measured share can raise it, never lower it. `activeCount` is how
|
|
722
|
+
// many sessions are open, which can exceed how many have spent yet; the split
|
|
723
|
+
// is among all of them.
|
|
724
|
+
function shareOf(sessions, sessionId, activeCount) {
|
|
725
|
+
const spent = sessions || [];
|
|
726
|
+
const n = Math.max(spent.length, Number.isFinite(activeCount) ? activeCount : 0);
|
|
727
|
+
if (n < 2) return 1;
|
|
728
|
+
const equal = 1 / n;
|
|
729
|
+
// One session's spend is not a comparison. Until a second one has spent,
|
|
730
|
+
// being the only one on record says nothing about who owns the budget.
|
|
731
|
+
if (spent.length < 2) return equal;
|
|
732
|
+
const mine = spent.find((row) => row.sessionId === sessionId);
|
|
733
|
+
if (!mine || !(mine.share > 0)) return equal;
|
|
734
|
+
return Math.max(mine.share, equal);
|
|
648
735
|
}
|
|
649
736
|
|
|
650
737
|
// What a point of a window costs is a property of the plan, not of the moment,
|
|
@@ -678,6 +765,53 @@ function writeCalibration(all) {
|
|
|
678
765
|
}
|
|
679
766
|
}
|
|
680
767
|
|
|
768
|
+
// Everything learned about a budget belongs to the plan it was learned on.
|
|
769
|
+
//
|
|
770
|
+
// A point of a window is a share of an allowance, so changing the allowance
|
|
771
|
+
// changes what a point is worth, and every figure derived from the old one is
|
|
772
|
+
// then wrong by the ratio between the plans. Upgrading Pro to Max 5x is roughly
|
|
773
|
+
// a fivefold move: a calibration saying a point costs $0.40 keeps being applied
|
|
774
|
+
// to a point now worth several times that, and the turn estimates built on it
|
|
775
|
+
// are wrong in the direction that promises room there is not.
|
|
776
|
+
//
|
|
777
|
+
// There is no timestamp anywhere that says when the plan changed.
|
|
778
|
+
// `subscriptionCreatedAt` is the original signup, not the upgrade. So the plan
|
|
779
|
+
// is stamped onto the calibration instead, and a stamp that no longer matches
|
|
780
|
+
// is itself the proof that it moved.
|
|
781
|
+
function calibrationForPlan(all, planId) {
|
|
782
|
+
const kept = {};
|
|
783
|
+
let dropped = false;
|
|
784
|
+
for (const key of Object.keys(all || {})) {
|
|
785
|
+
const entry = all[key];
|
|
786
|
+
if (!entry || typeof entry !== 'object') continue;
|
|
787
|
+
// A stamp that no longer matches is proof the plan moved, and worth saying
|
|
788
|
+
// so: the reading on disk was measured against the other allowance too.
|
|
789
|
+
if (entry.plan && planId && entry.plan !== planId) {
|
|
790
|
+
dropped = true;
|
|
791
|
+
continue;
|
|
792
|
+
}
|
|
793
|
+
// An entry saved before the stamp existed cannot be shown to belong to this
|
|
794
|
+
// plan. Keeping it would be assuming the answer, and the cost of assuming
|
|
795
|
+
// wrong is the whole bug this guards: a point priced for Pro applied to a
|
|
796
|
+
// Max window, promising several times the turns that exist. It is dropped
|
|
797
|
+
// instead, which costs one relearn and says "unknown" in the meantime.
|
|
798
|
+
// Unknown is not claimed as a plan change, because it is not evidence of
|
|
799
|
+
// one; every install upgrading to this version passes through here once.
|
|
800
|
+
if (!entry.plan && planId) continue;
|
|
801
|
+
kept[key] = entry;
|
|
802
|
+
}
|
|
803
|
+
return { learned: kept, planChanged: dropped };
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function stampPlan(all, planId) {
|
|
807
|
+
if (!planId) return all;
|
|
808
|
+
const stamped = {};
|
|
809
|
+
for (const key of Object.keys(all || {})) {
|
|
810
|
+
stamped[key] = Object.assign({}, all[key], { plan: planId });
|
|
811
|
+
}
|
|
812
|
+
return stamped;
|
|
813
|
+
}
|
|
814
|
+
|
|
681
815
|
// A sample is better when it rests on more turns. Percentages read in whole
|
|
682
816
|
// numbers, so a bigger percentage also divides more precisely.
|
|
683
817
|
function betterCalibration(current, candidate) {
|
|
@@ -721,6 +855,7 @@ function buildWindow(spec, snapshot, events, now, options) {
|
|
|
721
855
|
spentUSD: spent.cost,
|
|
722
856
|
spentTokens: spent.tokens,
|
|
723
857
|
turns: spent.turns,
|
|
858
|
+
subagentTurns: spent.subagentTurns,
|
|
724
859
|
recentTurns: recent.turns,
|
|
725
860
|
recentUSDPerHour: recent.cost / recentHours,
|
|
726
861
|
recentUSDPerTurn: recent.turns ? recent.cost / recent.turns : null,
|
|
@@ -742,6 +877,14 @@ function buildWindow(spec, snapshot, events, now, options) {
|
|
|
742
877
|
correctionUnreliable: false,
|
|
743
878
|
// The price-per-point this window derived from its own baseline.
|
|
744
879
|
calibration: null,
|
|
880
|
+
// True when the bucket quoted its limit in dollars, so the price of a
|
|
881
|
+
// point is known rather than learned.
|
|
882
|
+
metered: false,
|
|
883
|
+
// What the account's own list of limits says about this one.
|
|
884
|
+
severity: null,
|
|
885
|
+
isActive: false,
|
|
886
|
+
scoped: false,
|
|
887
|
+
family: null,
|
|
745
888
|
verdict: 'unknown',
|
|
746
889
|
};
|
|
747
890
|
|
|
@@ -793,16 +936,23 @@ function buildWindow(spec, snapshot, events, now, options) {
|
|
|
793
936
|
rawPercent > 0 && upTo.cost > 0 && upTo.turns >= MIN_BASELINE_TURNS
|
|
794
937
|
? { usdPerPercent: upTo.cost / rawPercent, turns: upTo.turns, percent: rawPercent }
|
|
795
938
|
: null;
|
|
796
|
-
|
|
939
|
+
// A metered window has nothing to learn: its price per point is stated.
|
|
940
|
+
if (selfPriced && !extra.metered) window.calibration = selfPriced;
|
|
797
941
|
|
|
798
942
|
const known = extra.knownCalibration;
|
|
799
943
|
const usable =
|
|
800
944
|
known && Number.isFinite(known.usdPerPercent) && known.usdPerPercent > 0 ? known : null;
|
|
801
|
-
// Trust the better-sampled of the two, whichever that is
|
|
945
|
+
// Trust the better-sampled of the two, whichever that is; a stated price
|
|
946
|
+
// beats both.
|
|
947
|
+
const stated =
|
|
948
|
+
extra.metered && Number.isFinite(extra.usdPerPercent) && extra.usdPerPercent > 0
|
|
949
|
+
? { usdPerPercent: extra.usdPerPercent, turns: Infinity }
|
|
950
|
+
: null;
|
|
802
951
|
const chosen =
|
|
803
|
-
|
|
952
|
+
stated ||
|
|
953
|
+
(selfPriced && usable
|
|
804
954
|
? (usable.turns > selfPriced.turns ? usable : selfPriced)
|
|
805
|
-
: selfPriced || usable;
|
|
955
|
+
: selfPriced || usable);
|
|
806
956
|
|
|
807
957
|
if (after.cost > 0 && chosen) {
|
|
808
958
|
const pricePerPoint = chosen.usdPerPercent;
|
|
@@ -812,6 +962,10 @@ function buildWindow(spec, snapshot, events, now, options) {
|
|
|
812
962
|
// not the window. Better to leave the reading uncorrected and say the
|
|
813
963
|
// snapshot is old than to assert a budget that is gone.
|
|
814
964
|
if (rawPercent + sinceSnapshot > SATURATION_LIMIT) {
|
|
965
|
+
// Keep the size of the overshoot: the brief needs it to say how far
|
|
966
|
+
// past the snapshot the spending has gone, instead of repeating the
|
|
967
|
+
// snapshot figure for hours as if it were current.
|
|
968
|
+
window.pointsBeyondSnapshot = Math.round(sinceSnapshot);
|
|
815
969
|
sinceSnapshot = 0;
|
|
816
970
|
window.correctionUnreliable = true;
|
|
817
971
|
} else if (sinceSnapshot >= 1) {
|
|
@@ -837,14 +991,18 @@ function buildWindow(spec, snapshot, events, now, options) {
|
|
|
837
991
|
const measured = percent !== null && percent > 0 && spent.cost > 0 && !thin;
|
|
838
992
|
const derived = measured ? spent.cost / percent : null;
|
|
839
993
|
const known = extra.knownCalibration;
|
|
840
|
-
const
|
|
841
|
-
|
|
994
|
+
const metered =
|
|
995
|
+
Boolean(extra.metered) && Number.isFinite(extra.usdPerPercent) && extra.usdPerPercent > 0;
|
|
996
|
+
const priced = metered
|
|
997
|
+
? extra.usdPerPercent
|
|
998
|
+
: derived !== null
|
|
842
999
|
? derived
|
|
843
1000
|
: Number.isFinite(extra.usdPerPercent) && extra.usdPerPercent > 0
|
|
844
1001
|
? extra.usdPerPercent
|
|
845
1002
|
: known && Number.isFinite(known.usdPerPercent) && known.usdPerPercent > 0
|
|
846
1003
|
? known.usdPerPercent
|
|
847
1004
|
: null;
|
|
1005
|
+
window.metered = metered;
|
|
848
1006
|
|
|
849
1007
|
if (percent !== null && priced !== null) {
|
|
850
1008
|
window.usdPerPercent = priced;
|
|
@@ -942,6 +1100,10 @@ function bindingWindow(windows) {
|
|
|
942
1100
|
const theirs = soonest(best);
|
|
943
1101
|
if (mine !== theirs) return mine < theirs ? w : best;
|
|
944
1102
|
|
|
1103
|
+
// Equally urgent by our own measure: the account says which limit it is
|
|
1104
|
+
// enforcing, and that is worth more than a rule of thumb.
|
|
1105
|
+
if (Boolean(w.isActive) !== Boolean(best.isActive)) return w.isActive ? w : best;
|
|
1106
|
+
|
|
945
1107
|
// Equally urgent: the shorter window is the one hit first in practice, so
|
|
946
1108
|
// the 5-hour limit wins a tie against the weekly one.
|
|
947
1109
|
const myspan = Number.isFinite(w.spanMs) ? w.spanMs : Infinity;
|
|
@@ -976,6 +1138,14 @@ function formatUSD(value) {
|
|
|
976
1138
|
return '$' + value.toFixed(3);
|
|
977
1139
|
}
|
|
978
1140
|
|
|
1141
|
+
// Money to two places. Three below a dollar is right for a per-turn price and
|
|
1142
|
+
// wrong for a total someone reads after every reply.
|
|
1143
|
+
function formatMoney(value) {
|
|
1144
|
+
if (value === null || !Number.isFinite(value)) return '-';
|
|
1145
|
+
if (value >= 100) return '$' + Math.round(value);
|
|
1146
|
+
return '$' + value.toFixed(2);
|
|
1147
|
+
}
|
|
1148
|
+
|
|
979
1149
|
function formatTokens(value) {
|
|
980
1150
|
if (!Number.isFinite(value)) return '-';
|
|
981
1151
|
if (value >= 1e9) return (value / 1e9).toFixed(1) + 'B';
|
|
@@ -1052,6 +1222,49 @@ function otherLimits(utilization, threshold) {
|
|
|
1052
1222
|
return rows.sort((a, b) => b.percentUsed - a.percentUsed);
|
|
1053
1223
|
}
|
|
1054
1224
|
|
|
1225
|
+
// Beside the per-window buckets, the snapshot carries `limits`: one entry per
|
|
1226
|
+
// limit the account enforces, with a severity, whether it is the active one,
|
|
1227
|
+
// and for the per-model weeklies which model it scopes to. It is the account's
|
|
1228
|
+
// own description of its limits, and it can name a window the bucket table
|
|
1229
|
+
// does not: on a Max plan the Fable weekly sat at 17% while the shared weekly
|
|
1230
|
+
// read 11%, and nothing reported the higher of the two.
|
|
1231
|
+
const LIMIT_KINDS = { session: 'five_hour', weekly_all: 'seven_day' };
|
|
1232
|
+
|
|
1233
|
+
function limitWindows(utilization) {
|
|
1234
|
+
const list = utilization && Array.isArray(utilization.limits) ? utilization.limits : [];
|
|
1235
|
+
const rows = [];
|
|
1236
|
+
for (const limit of list) {
|
|
1237
|
+
if (!limit || typeof limit !== 'object' || typeof limit.percent !== 'number') continue;
|
|
1238
|
+
const resetsAt = limit.resets_at ? Date.parse(limit.resets_at) : null;
|
|
1239
|
+
const base = {
|
|
1240
|
+
kind: limit.kind,
|
|
1241
|
+
percent: limit.percent,
|
|
1242
|
+
severity: typeof limit.severity === 'string' ? limit.severity : null,
|
|
1243
|
+
isActive: Boolean(limit.is_active),
|
|
1244
|
+
resetsAt: Number.isFinite(resetsAt) ? resetsAt : null,
|
|
1245
|
+
family: null,
|
|
1246
|
+
};
|
|
1247
|
+
if (LIMIT_KINDS[limit.kind]) {
|
|
1248
|
+
rows.push(Object.assign(base, { key: LIMIT_KINDS[limit.kind] }));
|
|
1249
|
+
continue;
|
|
1250
|
+
}
|
|
1251
|
+
if (limit.kind === 'weekly_scoped' && limit.scope && limit.scope.model) {
|
|
1252
|
+
const model = limit.scope.model;
|
|
1253
|
+
const name = model.display_name || model.id || 'model';
|
|
1254
|
+
const family = familyOf(name) || familyOf(model.id) || String(name).toLowerCase();
|
|
1255
|
+
rows.push(
|
|
1256
|
+
Object.assign(base, {
|
|
1257
|
+
key: 'seven_day_scoped:' + family,
|
|
1258
|
+
label: 'weekly (' + name + ')',
|
|
1259
|
+
family,
|
|
1260
|
+
spanMs: 7 * DAY,
|
|
1261
|
+
})
|
|
1262
|
+
);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
return rows;
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1055
1268
|
function collect(now) {
|
|
1056
1269
|
if (isCodex()) return codex.collect(now);
|
|
1057
1270
|
return collectClaude(now);
|
|
@@ -1189,11 +1402,10 @@ function buildWindows(utilization, events, now, fetchedAt, learned, specs, rejec
|
|
|
1189
1402
|
// of its two windows in the payload, so it hands its own spans in rather than
|
|
1190
1403
|
// having them assumed.
|
|
1191
1404
|
const table = specs && specs.length ? specs : WINDOWS;
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
// The per-model weekly windows only exist on some plans.
|
|
1195
|
-
if (spec.key !== 'five_hour' && spec.key !== 'seven_day' && !snapshot) return null;
|
|
1405
|
+
const limits = limitWindows(utilization);
|
|
1406
|
+
const limitByKey = new Map(limits.map((limit) => [limit.key, limit]));
|
|
1196
1407
|
|
|
1408
|
+
const one = (spec, snapshot, own, limit) => {
|
|
1197
1409
|
const known = learned ? learned[spec.key] : null;
|
|
1198
1410
|
const refusal = refused.get(spec.key);
|
|
1199
1411
|
|
|
@@ -1214,34 +1426,91 @@ function buildWindows(utilization, events, now, fetchedAt, learned, specs, rejec
|
|
|
1214
1426
|
}
|
|
1215
1427
|
}
|
|
1216
1428
|
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1429
|
+
// A bucket that quotes its limit in dollars needs no calibration: a
|
|
1430
|
+
// hundred dollars is a hundred points. Null on the plans seen so far, but
|
|
1431
|
+
// the field is there, and when it fills in it beats any estimate.
|
|
1432
|
+
const dollars =
|
|
1433
|
+
snapshot && Number.isFinite(snapshot.limit_dollars) && snapshot.limit_dollars > 0
|
|
1434
|
+
? snapshot.limit_dollars / 100
|
|
1435
|
+
: null;
|
|
1436
|
+
|
|
1437
|
+
const window = buildWindow(
|
|
1438
|
+
spec,
|
|
1439
|
+
anchored,
|
|
1440
|
+
own,
|
|
1441
|
+
now,
|
|
1442
|
+
Object.assign(
|
|
1443
|
+
{ fetchedAt, knownCalibration: known },
|
|
1444
|
+
windowStart === undefined ? {} : { windowStart },
|
|
1445
|
+
dollars === null ? {} : { metered: true, usdPerPercent: dollars }
|
|
1446
|
+
)
|
|
1447
|
+
);
|
|
1222
1448
|
if (refusal) {
|
|
1223
1449
|
window.refusedAt = refusal.at;
|
|
1224
1450
|
window.refusedResetsAt = refusal.resetsAt;
|
|
1225
1451
|
}
|
|
1452
|
+
|
|
1453
|
+
let result = window;
|
|
1226
1454
|
if (!window.stale) {
|
|
1227
|
-
|
|
1455
|
+
result = reconstructUnanchored(spec, window, own, now, known) || window;
|
|
1456
|
+
} else {
|
|
1457
|
+
// Rolled over. Rebuild from local history rather than going blind on it.
|
|
1458
|
+
const rebuilt = reconstructWindow(spec, snapshot, own, now);
|
|
1459
|
+
if (rebuilt) {
|
|
1460
|
+
result = buildWindow(
|
|
1461
|
+
spec,
|
|
1462
|
+
{ utilization: rebuilt.percentUsed, resets_at: null },
|
|
1463
|
+
own,
|
|
1464
|
+
now,
|
|
1465
|
+
{
|
|
1466
|
+
estimated: true,
|
|
1467
|
+
windowStart: rebuilt.windowStart,
|
|
1468
|
+
usdPerPercent: rebuilt.usdPerPercent,
|
|
1469
|
+
}
|
|
1470
|
+
);
|
|
1471
|
+
}
|
|
1228
1472
|
}
|
|
1473
|
+
// What the account itself says about this limit rides along.
|
|
1474
|
+
if (limit) {
|
|
1475
|
+
result.severity = limit.severity;
|
|
1476
|
+
result.isActive = limit.isActive;
|
|
1477
|
+
}
|
|
1478
|
+
return result;
|
|
1479
|
+
};
|
|
1229
1480
|
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
windowStart: rebuilt.windowStart,
|
|
1241
|
-
usdPerPercent: rebuilt.usdPerPercent,
|
|
1481
|
+
const windows = table
|
|
1482
|
+
.map((spec) => {
|
|
1483
|
+
let snapshot = utilization[spec.key];
|
|
1484
|
+
const limit = limitByKey.get(spec.key);
|
|
1485
|
+
// The account's own list can carry a window the bucket table does not.
|
|
1486
|
+
if ((!snapshot || typeof snapshot.utilization !== 'number') && limit) {
|
|
1487
|
+
snapshot = {
|
|
1488
|
+
utilization: limit.percent,
|
|
1489
|
+
resets_at: limit.resetsAt ? new Date(limit.resetsAt).toISOString() : null,
|
|
1490
|
+
};
|
|
1242
1491
|
}
|
|
1243
|
-
|
|
1244
|
-
|
|
1492
|
+
// The per-model weekly windows only exist on some plans.
|
|
1493
|
+
if (spec.key !== 'five_hour' && spec.key !== 'seven_day' && !snapshot) return null;
|
|
1494
|
+
return one(spec, snapshot, events, limit);
|
|
1495
|
+
})
|
|
1496
|
+
.filter(Boolean);
|
|
1497
|
+
|
|
1498
|
+
// A per-model weekly is a limit on one model's spend, so it is priced from
|
|
1499
|
+
// that model's calls alone; the shared windows still see everything.
|
|
1500
|
+
for (const limit of limits) {
|
|
1501
|
+
if (!limit.family) continue;
|
|
1502
|
+
const spec = { key: limit.key, label: limit.label, span: limit.spanMs };
|
|
1503
|
+
const own = events.filter((event) => familyOf(event.model) === limit.family);
|
|
1504
|
+
const snapshot = {
|
|
1505
|
+
utilization: limit.percent,
|
|
1506
|
+
resets_at: limit.resetsAt ? new Date(limit.resetsAt).toISOString() : null,
|
|
1507
|
+
};
|
|
1508
|
+
const window = one(spec, snapshot, own, limit);
|
|
1509
|
+
window.scoped = true;
|
|
1510
|
+
window.family = limit.family;
|
|
1511
|
+
windows.push(window);
|
|
1512
|
+
}
|
|
1513
|
+
return windows;
|
|
1245
1514
|
}
|
|
1246
1515
|
|
|
1247
1516
|
// What one session has spent, out of everything on record.
|
|
@@ -1249,12 +1518,14 @@ function sessionSpend(events, sessionId) {
|
|
|
1249
1518
|
if (!sessionId) return null;
|
|
1250
1519
|
let cost = 0;
|
|
1251
1520
|
let turns = 0;
|
|
1521
|
+
let tokens = 0;
|
|
1252
1522
|
for (const event of events) {
|
|
1253
1523
|
if (event.sessionId !== sessionId) continue;
|
|
1254
1524
|
cost += event.cost;
|
|
1255
|
-
|
|
1525
|
+
tokens += event.tokens || 0;
|
|
1526
|
+
if (!event.sidechain) turns += 1;
|
|
1256
1527
|
}
|
|
1257
|
-
return turns ? { turns, cost } : null;
|
|
1528
|
+
return turns ? { turns, cost, tokens } : null;
|
|
1258
1529
|
}
|
|
1259
1530
|
|
|
1260
1531
|
async function report(now, options) {
|
|
@@ -1280,7 +1551,11 @@ async function report(now, options) {
|
|
|
1280
1551
|
}
|
|
1281
1552
|
|
|
1282
1553
|
const onDisk = readCalibration();
|
|
1283
|
-
|
|
1554
|
+
// Anything learned on a different plan is void, and its absence is what makes
|
|
1555
|
+
// the report say so rather than quietly pricing this plan with the last one's
|
|
1556
|
+
// numbers.
|
|
1557
|
+
const calibrated = calibrationForPlan(onDisk, base.planId);
|
|
1558
|
+
const learned = Object.assign({}, calibrated.learned);
|
|
1284
1559
|
|
|
1285
1560
|
// Codex logs the meter next to every request, so the price of a point can be
|
|
1286
1561
|
// measured outright instead of inferred. A measurement from this session
|
|
@@ -1299,7 +1574,11 @@ async function report(now, options) {
|
|
|
1299
1574
|
base.snapshotFetchedAt,
|
|
1300
1575
|
learned,
|
|
1301
1576
|
base.windowSpecs,
|
|
1302
|
-
|
|
1577
|
+
// A refusal describes the allowance that refused it. After a plan change
|
|
1578
|
+
// that allowance is gone, so anchoring the new window to it, or warning
|
|
1579
|
+
// that the room "ran out last time", is describing a budget that no longer
|
|
1580
|
+
// exists.
|
|
1581
|
+
calibrated.planChanged ? new Map() : rejections
|
|
1303
1582
|
);
|
|
1304
1583
|
|
|
1305
1584
|
// Keep the best sample seen so far, so a thin baseline never has to guess.
|
|
@@ -1311,11 +1590,11 @@ async function report(now, options) {
|
|
|
1311
1590
|
}
|
|
1312
1591
|
// Compared against what is actually on disk, so a measurement taken during
|
|
1313
1592
|
// this run is saved too rather than only the ones inferred from a window.
|
|
1314
|
-
let changed =
|
|
1593
|
+
let changed = calibrated.planChanged;
|
|
1315
1594
|
for (const key of Object.keys(updated)) {
|
|
1316
1595
|
if (updated[key] !== onDisk[key]) changed = true;
|
|
1317
1596
|
}
|
|
1318
|
-
if (changed) writeCalibration(updated);
|
|
1597
|
+
if (changed) writeCalibration(stampPlan(updated, base.planId));
|
|
1319
1598
|
|
|
1320
1599
|
const recentEvents = events.filter((event) => event.at >= now - HOUR);
|
|
1321
1600
|
const recent = totals(recentEvents);
|
|
@@ -1332,6 +1611,10 @@ async function report(now, options) {
|
|
|
1332
1611
|
windows,
|
|
1333
1612
|
binding,
|
|
1334
1613
|
otherLimits: otherLimits(base.utilization),
|
|
1614
|
+
// The plan moved since anything was last learned about it, so the cached
|
|
1615
|
+
// percentage was measured against a different allowance and everything
|
|
1616
|
+
// derived from the old one has been dropped.
|
|
1617
|
+
planChanged: calibrated.planChanged,
|
|
1335
1618
|
// The last time the account actually refused work, so a report taken just
|
|
1336
1619
|
// after a cutoff says so rather than describing the fresh window as though
|
|
1337
1620
|
// nothing happened.
|
|
@@ -1354,7 +1637,8 @@ async function report(now, options) {
|
|
|
1354
1637
|
tokens: recent.tokens,
|
|
1355
1638
|
effort: dominantEffort(recentEvents),
|
|
1356
1639
|
},
|
|
1357
|
-
measuredTurns: events.length,
|
|
1640
|
+
measuredTurns: mainThread(events).length,
|
|
1641
|
+
subagentTurns: events.length - mainThread(events).length,
|
|
1358
1642
|
});
|
|
1359
1643
|
}
|
|
1360
1644
|
|
|
@@ -1462,6 +1746,12 @@ function render(data) {
|
|
|
1462
1746
|
(data.snapshotAgeMs === null ? 'none on disk' : formatDuration(data.snapshotAgeMs) + ' old')
|
|
1463
1747
|
);
|
|
1464
1748
|
lines.push(' Settings model=' + data.settings.model + ' effort=' + data.settings.effortLevel);
|
|
1749
|
+
if (data.planChanged) {
|
|
1750
|
+
lines.push(' Plan change this is a different plan from the one the figures below');
|
|
1751
|
+
lines.push(' were learned on, so what a point of a window is worth has');
|
|
1752
|
+
lines.push(' changed with it. The cached reading may predate the change:');
|
|
1753
|
+
lines.push(' run /usage for one measured against this plan.');
|
|
1754
|
+
}
|
|
1465
1755
|
const credits = data.credits;
|
|
1466
1756
|
if (credits && credits.unlimited) {
|
|
1467
1757
|
lines.push(' Credits unlimited');
|
|
@@ -1513,7 +1803,14 @@ function render(data) {
|
|
|
1513
1803
|
(money ? padLeft('Left', 10) : '') + padLeft('Turns left', 12)
|
|
1514
1804
|
);
|
|
1515
1805
|
for (const window of data.windows) {
|
|
1516
|
-
const
|
|
1806
|
+
const bound = data.binding && window.key === data.binding.key;
|
|
1807
|
+
// The account's own severity, when it says critical, is worth a word.
|
|
1808
|
+
const critical = window.severity === 'critical';
|
|
1809
|
+
const marker = bound
|
|
1810
|
+
? ' <- binding' + (critical ? ', critical' : '')
|
|
1811
|
+
: critical
|
|
1812
|
+
? ' critical'
|
|
1813
|
+
: '';
|
|
1517
1814
|
lines.push(
|
|
1518
1815
|
' ' + pad(window.label, 15) +
|
|
1519
1816
|
padLeft(
|
|
@@ -1628,7 +1925,10 @@ function render(data) {
|
|
|
1628
1925
|
} else {
|
|
1629
1926
|
lines.push(' Recent pace no turns in the last hour');
|
|
1630
1927
|
}
|
|
1631
|
-
lines.push(
|
|
1928
|
+
lines.push(
|
|
1929
|
+
' Measured ' + formatCount(data.measuredTurns) + ' turns of local transcript' +
|
|
1930
|
+
(data.subagentTurns > 0 ? ' (+' + formatCount(data.subagentTurns) + ' subagent calls)' : '')
|
|
1931
|
+
);
|
|
1632
1932
|
|
|
1633
1933
|
if (data.windows.some((window) => window.adjusted)) {
|
|
1634
1934
|
lines.push(
|
|
@@ -1796,6 +2096,102 @@ function renderForecast(data, turns) {
|
|
|
1796
2096
|
return lines.join('\n');
|
|
1797
2097
|
}
|
|
1798
2098
|
|
|
2099
|
+
// The session history kept by the Stop hook, one row per session.
|
|
2100
|
+
function sessionTokens(session) {
|
|
2101
|
+
const t = (session && session.tokens) || {};
|
|
2102
|
+
return (t.input || 0) + (t.cacheWrite || 0) + (t.cacheRead || 0) + (t.output || 0);
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
function shortId(sessionId) {
|
|
2106
|
+
return String(sessionId || '').slice(0, 8);
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
function renderSessions(list, now) {
|
|
2110
|
+
const lines = [];
|
|
2111
|
+
lines.push('Sessions on this machine, newest first');
|
|
2112
|
+
lines.push('');
|
|
2113
|
+
if (!list || !list.length) {
|
|
2114
|
+
lines.push(' No sessions on record yet. The Stop hook writes one after each reply, so');
|
|
2115
|
+
lines.push(' this fills in as soon as a session with the hook installed has run.');
|
|
2116
|
+
return lines.join('\n');
|
|
2117
|
+
}
|
|
2118
|
+
lines.push(
|
|
2119
|
+
' ' + pad('Id', 10) + pad('When', 12) + pad('Project', 24) + padLeft('Prompts', 7) +
|
|
2120
|
+
padLeft('Turns', 9) + padLeft('Tokens', 9) + padLeft('Cost', 9)
|
|
2121
|
+
);
|
|
2122
|
+
for (const session of list) {
|
|
2123
|
+
const turns =
|
|
2124
|
+
String(session.turns || 0) + (session.subagentTurns > 0 ? '+' + session.subagentTurns : '');
|
|
2125
|
+
lines.push(
|
|
2126
|
+
' ' + pad(shortId(session.sessionId), 10) +
|
|
2127
|
+
pad(Number.isFinite(session.lastAt) ? formatDuration(now - session.lastAt) + ' ago' : '-', 12) +
|
|
2128
|
+
pad(shortenProject(session.project || '-', 22), 24) +
|
|
2129
|
+
padLeft(session.prompts || 0, 7) +
|
|
2130
|
+
padLeft(turns, 9) +
|
|
2131
|
+
padLeft(formatTokens(sessionTokens(session)), 9) +
|
|
2132
|
+
padLeft(formatMoney(session.cost || 0), 9) +
|
|
2133
|
+
(Number.isFinite(session.endedAt) ? '' : ' open')
|
|
2134
|
+
);
|
|
2135
|
+
}
|
|
2136
|
+
lines.push('');
|
|
2137
|
+
lines.push(' Turns are main-thread calls; +N is what subagents made on top.');
|
|
2138
|
+
return lines.join('\n');
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
function renderSession(session, now) {
|
|
2142
|
+
const lines = [];
|
|
2143
|
+
lines.push('Session ' + shortId(session.sessionId) + (session.project ? ' (' + session.project + ')' : ''));
|
|
2144
|
+
lines.push('');
|
|
2145
|
+
const started = Number.isFinite(session.firstAt)
|
|
2146
|
+
? formatClock(session.firstAt) + ', ' + formatDuration(now - session.firstAt) + ' ago, '
|
|
2147
|
+
: '';
|
|
2148
|
+
const ended = Number.isFinite(session.endedAt)
|
|
2149
|
+
? 'closed ' + formatDuration(now - session.endedAt) + ' ago' + (session.reason ? ' (' + session.reason + ')' : '')
|
|
2150
|
+
: 'still open';
|
|
2151
|
+
lines.push(' Started ' + started + ended);
|
|
2152
|
+
lines.push(' Prompts ' + (session.prompts || 0));
|
|
2153
|
+
lines.push(
|
|
2154
|
+
' Turns ' + (session.turns || 0) +
|
|
2155
|
+
(session.subagentTurns > 0 ? ', plus ' + session.subagentTurns + ' by subagents' : '')
|
|
2156
|
+
);
|
|
2157
|
+
const t = session.tokens || {};
|
|
2158
|
+
lines.push(
|
|
2159
|
+
' Tokens ' + formatTokens(sessionTokens(session)) + ': input ' + formatTokens(t.input || 0) +
|
|
2160
|
+
', cache write ' + formatTokens(t.cacheWrite || 0) + ', cache read ' + formatTokens(t.cacheRead || 0) +
|
|
2161
|
+
', output ' + formatTokens(t.output || 0) +
|
|
2162
|
+
(t.reasoning > 0 ? ' (' + formatTokens(t.reasoning) + ' of it reasoning)' : '')
|
|
2163
|
+
);
|
|
2164
|
+
lines.push(' Cost ' + formatMoney(session.cost || 0));
|
|
2165
|
+
if (Number.isFinite(session.context) && session.context > 0) {
|
|
2166
|
+
lines.push(' Context ' + formatTokens(session.context) + ' tokens at the last call');
|
|
2167
|
+
}
|
|
2168
|
+
const models = Object.keys(session.models || {}).sort(
|
|
2169
|
+
(a, b) => (session.models[b].cost || 0) - (session.models[a].cost || 0)
|
|
2170
|
+
);
|
|
2171
|
+
models.forEach((id, index) => {
|
|
2172
|
+
const row = session.models[id];
|
|
2173
|
+
lines.push(
|
|
2174
|
+
(index === 0 ? ' Models ' : ' ') + pad(id, 24) +
|
|
2175
|
+
padLeft((row.turns || 0) + ' turns', 12) + padLeft(formatMoney(row.cost || 0), 10)
|
|
2176
|
+
);
|
|
2177
|
+
});
|
|
2178
|
+
return lines.join('\n');
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
// "last", an exact id, or an unambiguous prefix of one.
|
|
2182
|
+
function pickSession(list, which) {
|
|
2183
|
+
const rows = list || [];
|
|
2184
|
+
if (!rows.length) return null;
|
|
2185
|
+
const want = String(which || 'last');
|
|
2186
|
+
if (want === 'last') {
|
|
2187
|
+
return rows.slice().sort((a, b) => (b.lastAt || 0) - (a.lastAt || 0))[0];
|
|
2188
|
+
}
|
|
2189
|
+
const exact = rows.find((row) => row.sessionId === want);
|
|
2190
|
+
if (exact) return exact;
|
|
2191
|
+
const prefixed = rows.filter((row) => String(row.sessionId || '').startsWith(want));
|
|
2192
|
+
return prefixed.length === 1 ? prefixed[0] : null;
|
|
2193
|
+
}
|
|
2194
|
+
|
|
1799
2195
|
async function main(argv) {
|
|
1800
2196
|
// Settle the host before anything reads a file, so one run never mixes one
|
|
1801
2197
|
// agent's percentages with the other's turns.
|
|
@@ -1807,6 +2203,29 @@ async function main(argv) {
|
|
|
1807
2203
|
return 0;
|
|
1808
2204
|
}
|
|
1809
2205
|
|
|
2206
|
+
// The session history is a file the Stop hook keeps, so neither of these
|
|
2207
|
+
// opens a transcript. Required lazily: tally.js depends on this module.
|
|
2208
|
+
const sessionAt = argv.indexOf('--session');
|
|
2209
|
+
if (sessionAt !== -1 || argv.indexOf('--sessions') !== -1) {
|
|
2210
|
+
const tally = require('./tally.js');
|
|
2211
|
+
const list = tally.sessions(tally.readState());
|
|
2212
|
+
const now = Date.now();
|
|
2213
|
+
const json = argv.indexOf('--json') !== -1;
|
|
2214
|
+
if (sessionAt !== -1) {
|
|
2215
|
+
const next = argv[sessionAt + 1];
|
|
2216
|
+
const which = next && next.indexOf('--') !== 0 ? next : 'last';
|
|
2217
|
+
const picked = pickSession(list, which);
|
|
2218
|
+
if (!picked) {
|
|
2219
|
+
process.stderr.write('usage: no session matches "' + which + '". Run --sessions to list them.\n');
|
|
2220
|
+
return 2;
|
|
2221
|
+
}
|
|
2222
|
+
process.stdout.write((json ? JSON.stringify(picked, null, 2) : renderSession(picked, now)) + '\n');
|
|
2223
|
+
return 0;
|
|
2224
|
+
}
|
|
2225
|
+
process.stdout.write((json ? JSON.stringify(list, null, 2) : renderSessions(list, now)) + '\n');
|
|
2226
|
+
return 0;
|
|
2227
|
+
}
|
|
2228
|
+
|
|
1810
2229
|
// Codex writes its meter into the session rollouts, so the cached reading is
|
|
1811
2230
|
// only as fresh as the last request it made. Asking Codex itself is a second
|
|
1812
2231
|
// and a child process, which is why it is opt-in rather than the default.
|
|
@@ -1850,13 +2269,8 @@ async function main(argv) {
|
|
|
1850
2269
|
return 0;
|
|
1851
2270
|
}
|
|
1852
2271
|
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
process.stderr.write('usage: ' + (err && err.message ? err.message : String(err)) + '\n');
|
|
1856
|
-
process.exitCode = 1;
|
|
1857
|
-
});
|
|
1858
|
-
}
|
|
1859
|
-
|
|
2272
|
+
// Assigned before main() can run: tally.js requires this module back, and a
|
|
2273
|
+
// lazy require from inside main() would otherwise see an empty exports object.
|
|
1860
2274
|
module.exports = {
|
|
1861
2275
|
main,
|
|
1862
2276
|
setHost,
|
|
@@ -1874,16 +2288,21 @@ module.exports = {
|
|
|
1874
2288
|
costOf,
|
|
1875
2289
|
tokensOf,
|
|
1876
2290
|
eventFrom,
|
|
2291
|
+
promptFrom,
|
|
1877
2292
|
readEvents,
|
|
2293
|
+
readCalibration,
|
|
1878
2294
|
buildWindow,
|
|
1879
2295
|
reconstructWindow,
|
|
1880
2296
|
SATURATION_LIMIT,
|
|
1881
2297
|
MIN_BASELINE_TURNS,
|
|
1882
2298
|
buildWindows,
|
|
2299
|
+
limitWindows,
|
|
1883
2300
|
lastRejections,
|
|
1884
2301
|
bindingWindow,
|
|
1885
2302
|
criticalOthers,
|
|
1886
2303
|
betterCalibration,
|
|
2304
|
+
calibrationForPlan,
|
|
2305
|
+
stampPlan,
|
|
1887
2306
|
calibrationFile,
|
|
1888
2307
|
CRITICAL_PERCENT,
|
|
1889
2308
|
dominantEffort,
|
|
@@ -1895,7 +2314,11 @@ module.exports = {
|
|
|
1895
2314
|
MIN_PACE_SAMPLE,
|
|
1896
2315
|
formatDuration,
|
|
1897
2316
|
formatUSD,
|
|
2317
|
+
formatMoney,
|
|
1898
2318
|
formatCount,
|
|
2319
|
+
renderSessions,
|
|
2320
|
+
renderSession,
|
|
2321
|
+
pickSession,
|
|
1899
2322
|
verdictLine,
|
|
1900
2323
|
render,
|
|
1901
2324
|
report,
|
|
@@ -1916,3 +2339,10 @@ module.exports = {
|
|
|
1916
2339
|
formatTokens,
|
|
1917
2340
|
PLANS,
|
|
1918
2341
|
};
|
|
2342
|
+
|
|
2343
|
+
if (require.main === module) {
|
|
2344
|
+
main(process.argv.slice(2)).catch((err) => {
|
|
2345
|
+
process.stderr.write('usage: ' + (err && err.message ? err.message : String(err)) + '\n');
|
|
2346
|
+
process.exitCode = 1;
|
|
2347
|
+
});
|
|
2348
|
+
}
|