claude-token-saver 3.10.0 โ 3.11.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/README.en.md +5 -0
- package/README.md +5 -0
- package/bin/cli.js +12 -0
- package/package.json +1 -1
- package/src/formatters/statusline.js +18 -1
- package/src/model-alias.js +86 -13
- package/src/model-rules.js +21 -0
package/README.en.md
CHANGED
|
@@ -77,6 +77,7 @@ The statusline appears at the bottom of Claude Code right away. If auto-registra
|
|
|
77
77
|
| `โฆ current` / `๐
weekly` | 5-hour / 7-day rate-limit window usage + reset time |
|
|
78
78
|
| `๐ฆ` | Context usage (e.g. `Ctx 68% of 1M`) โ colored by fill. Current models default to 1M with no premium, but token volume itself drives per-turn cost and 5H/7D burn |
|
|
79
79
|
| `๐ฐ` | Cumulative savings from prompt caching |
|
|
80
|
+
| `๐` | Cumulative savings from delegation โ what running work on a cheaper tier saved, a different number from `๐ฐ`. Hidden until a delegation has actually been measured |
|
|
80
81
|
|
|
81
82
|
When something is wrong, a **warning chip leads the line**:
|
|
82
83
|
|
|
@@ -300,6 +301,10 @@ Also update `statusLine.command` in `~/.claude/settings.json` to `claude-token-s
|
|
|
300
301
|
|
|
301
302
|
## Release notes
|
|
302
303
|
|
|
304
|
+
### v3.11.0 (2026-08-21)
|
|
305
|
+
- **Delegation savings chip in the statusline** โ `๐ Delegated $3.2`. The existing `๐ฐ Cache saved` covers the prompt cache; this one is what running work on a cheaper tier saved. Zero or missing data hides the chip entirely, so nothing changes for direct-API users. The statusline only reads `model-rules.json` โ it never triggers a scan (it re-renders every few seconds). Segment name: `delegated`.
|
|
306
|
+
- **Role-learning mis-classification fixed** โ subagent records sometimes land in the parent transcript without an `isSidechain` flag, and that circumstantial evidence could outvote stated evidence, confirming a haiku profile as the session model (measured: 16 against 1, agreement exactly at the 80% line). Stated evidence (`Task(model:)` parameters, agent-definition frontmatter) and inferred evidence are now tallied separately, and an inference is not adopted when stated evidence contradicts it. Undecided ids stay `unknown` and leave the aggregate.
|
|
307
|
+
|
|
303
308
|
### v3.10.0 (2026-08-20)
|
|
304
309
|
- **Model tiers are detected again behind a Bedrock / LiteLLM gateway** โ when the transcript's model id is an inference-profile ARN there is no `opus` or `haiku` in the string, so it fell back to Sonnet. Since `worthDelegating()` requires `rank > target`, **every T1 rule was rejected**, savings aggregated to zero, and cost was under-counted by roughly 1.67x. The profile id is now learned as a role (parent `Task` call joined to the subagent run by `toolUseId`) and mapped back to the alias your environment declares. The pricing table, the ranks, and the tiering logic are untouched.
|
|
305
310
|
- **No confident mapping means no guess** โ under three observations, or below 80% agreement, the id stays `unknown` and leaves the delegation aggregate. Quietly calling it Sonnet was the worse failure.
|
package/README.md
CHANGED
|
@@ -76,6 +76,7 @@ npm i -g claude-token-saver
|
|
|
76
76
|
| `โฆ current` / `๐
weekly` | 5์๊ฐ / 7์ผ rate-limit ์๋ ์ฌ์ฉ๋ฅ + ๋ฆฌ์
์๊ฐ |
|
|
77
77
|
| `๐ฆ` | ์ปจํ
์คํธ ์ฌ์ฉ๋ฅ (์: `Ctx 68% of 1M`) โ ์ฌ์ฉ๋ฅ ๊ธฐ์ค ๋
น/ํฉ/์ . ํ์ฌ ๋ชจ๋ธ์ 1M์ด ๊ธฐ๋ณธยทํ๋ฆฌ๋ฏธ์ ์์์ด์ง๋ง, ํ ํฐ๋ ์์ฒด๊ฐ ํด๋น ๋น์ฉ๊ณผ 5H/7D ํ๋๋ฅผ ํ์๋๋ค |
|
|
78
78
|
| `๐ฐ` | ์บ์๊ฐ ์ ์ฝํด์ค ๋์ ๊ธ์ก |
|
|
79
|
+
| `๐` | ์์์ด ์ ์ฝํด์ค ๋์ ๊ธ์ก โ ๋ ์ผ ํฐ์ด๋ก ๋ด๋ ค์ ์๋ ๋ชซ์ด๋ฉฐ `๐ฐ`์๋ ๋ค๋ฅธ ์์น์
๋๋ค. ์ค์ธก ์์์ด ์์ผ๋ฉด ํ์๋์ง ์์ต๋๋ค |
|
|
79
80
|
|
|
80
81
|
๋ฌธ์ ๊ฐ ๊ฐ์ง๋๋ฉด **๊ฒฝ๊ณ ์นฉ์ด ๋งจ ์์** ๋ถ์ต๋๋ค:
|
|
81
82
|
|
|
@@ -256,6 +257,10 @@ npm uninstall -g claude-cache-monitor && npm i -g claude-token-saver
|
|
|
256
257
|
|
|
257
258
|
## ๋ฆด๋ฆฌ์ค ๋
ธํธ
|
|
258
259
|
|
|
260
|
+
### v3.11.0 (2026-08-21)
|
|
261
|
+
- **statusline์ ์์ ์ ๊ฐ ์นฉ ์ถ๊ฐ** โ `๐ Delegated $3.2`. ๊ธฐ์กด `๐ฐ Cache saved`๋ ํ๋กฌํํธ ์บ์ ์ ๊ฐ์ด๊ณ , ์ ์นฉ์ **๋ ์ผ ํฐ์ด๋ก ๋ด๋ ค์ ์๋ ๊ธ์ก**์
๋๋ค. ๊ฐ์ด 0์ด๊ฑฐ๋ ๋ฐ์ดํฐ๊ฐ ์์ผ๋ฉด ์นฉ์ ์์ ๊ทธ๋ฆฌ์ง ์์ ์ง์ API ์ฌ์ฉ์์๊ฒ๋ ์๋ฌด๊ฒ๋ ๋์ง ์์ต๋๋ค. statusline์ `model-rules.json`์ ์ฝ๊ธฐ๋ง ํ๋ฉฐ ์ค์บ์ ๋๋ฆฌ์ง ์์ต๋๋ค(5์ด๋ง๋ค ํธ์ถ๋๋ ์๋ฆฌ์
๋๋ค). ์ธ๊ทธ๋จผํธ ์ด๋ฆ์ `delegated`์
๋๋ค.
|
|
262
|
+
- **์ญํ ํ์ต์ ์ค๋ถ๋ฅ ์์ ** โ ์๋ธ์์ด์ ํธ ๋ ์ฝ๋๊ฐ ๋ถ๋ชจ ํธ๋์คํฌ๋ฆฝํธ์ `isSidechain` ์์ด ์์ฌ ๋ค์ด์ค๋ ๊ฒฝ์ฐ๊ฐ ์์ด, ๊ทธ ์ ํฉ ์ฆ๊ฑฐ๊ฐ ๋ช
์ ์ฆ๊ฑฐ๋ฅผ ์ด๊ฒจ haiku ํ๋กํ์ผ์ด ์ธ์
๋ชจ๋ธ๋ก ํ์ ๋๊ณค ํ์ต๋๋ค(์ค์ธก 16๊ฑด ๋ 1๊ฑด, ํฉ์์จ ์ ํํ 80%). ์ด์ **๋ช
์ ์ฆ๊ฑฐ(`Task(model:)` ํ๋ผ๋ฏธํฐยท์์ด์ ํธ ์ ์ frontmatter)์ ์ ํฉ ์ฆ๊ฑฐ(sidechain ํ๋๊ทธ ๋ถ์ฌ)๋ฅผ ๋ถ๋ฆฌํด ์ง๊ณ**ํ๊ณ , ๋ช
์ ์ฆ๊ฑฐ๊ฐ ์ด๋ฅผ ๋ฐ๋ฐํ๋ฉด ์ ํฉ ์ถ๋ก ์ ์ฑํํ์ง ์์ต๋๋ค. ํ์ ์ด ์ ๋๋ฉด `unknown`์ผ๋ก ๋จ์ ์ง๊ณ์์ ๋น ์ง๋๋ค.
|
|
263
|
+
|
|
259
264
|
### v3.10.0 (2026-08-20)
|
|
260
265
|
- **๊ฒ์ดํธ์จ์ด(BedrockยทLiteLLM) ํ๊ฒฝ์์ ๋ชจ๋ธ ํฐ์ด๋ฅผ ๋ค์ ์ธ์ํฉ๋๋ค** โ ๋ก๊ทธ์ ๋ชจ๋ธ๋ช
์ด ์ถ๋ก ํ๋กํ์ผ ARN์ด๋ฉด `opus`ยท`haiku` ๋จ์๊ฐ ์์ด Sonnet์ผ๋ก ํด๋ฐฑํ๊ณ , `worthDelegating()`์ด `rank > target`์ ์๊ตฌํ๋ฏ๋ก **T1 ์์์ด ์ ๋ถ ๊ธฐ๊ฐ**๋์ต๋๋ค. ์ ๊ฐ ์ง๊ณ๋ 0, ๋น์ฉ์ ์ฝ 1.67๋ฐฐ ๊ณผ์ ๊ณ์์ด์์ต๋๋ค. ์ด์ ํ๋กํ์ผ ID๋ฅผ ์ญํ ๋ก ํ์ตํด(๋ถ๋ชจ `Task` ํธ์ถ โ ์๋ธ์์ด์ ํธ `toolUseId` ์ ํ ์กฐ์ธ) ํ๊ฒฝ๋ณ์๊ฐ ์ ์ธํ ๋ณ์นญ์ผ๋ก ๋๋๋ฆฝ๋๋ค. ๊ฐ๊ฒฉํยท๋ญํฌยทํ์ ๋ก์ง์ ๊ทธ๋๋ก์
๋๋ค.
|
|
261
266
|
- **ํ์ ์ด ์์ผ๋ฉด ์จ๊ธฐ์ง ์๊ณ ๋๋ฌ๋
๋๋ค** โ ๊ด์ธก 3๊ฑด ๋ฏธ๋ง์ด๊ฑฐ๋ ์ญํ ๋์์จ 80% ๋ฏธ๋ง์ด๋ฉด `unknown`์ผ๋ก ๋๊ณ ์์ ์ง๊ณ์์ ์ ์ธํฉ๋๋ค. Sonnet์ผ๋ก ์กฐ์ฉํ ํ๋ฆฌ๋ ๊ธฐ์กด ๋์์ด ๋ ๋๋นด์ต๋๋ค.
|
package/bin/cli.js
CHANGED
|
@@ -433,6 +433,17 @@ async function main() {
|
|
|
433
433
|
}
|
|
434
434
|
const lastActivity = Math.max(otherLastActivity, currentSessionLastUser);
|
|
435
435
|
|
|
436
|
+
// What delegation has measurably saved, read straight from the registry
|
|
437
|
+
// route-scan maintains. A lookup, never a scan: the statusline re-renders
|
|
438
|
+
// every few seconds and a scan parses tens of MB of transcripts.
|
|
439
|
+
let delegationSaved = 0;
|
|
440
|
+
try {
|
|
441
|
+
const { delegationSavedUsd } = await import('../src/model-rules.js');
|
|
442
|
+
delegationSaved = delegationSavedUsd();
|
|
443
|
+
} catch (e) {
|
|
444
|
+
debug('model-rules:saved', e); // an unreadable registry just hides the chip
|
|
445
|
+
}
|
|
446
|
+
|
|
436
447
|
const data = {
|
|
437
448
|
summary: sum,
|
|
438
449
|
trend,
|
|
@@ -447,6 +458,7 @@ async function main() {
|
|
|
447
458
|
spikeChip,
|
|
448
459
|
caps,
|
|
449
460
|
model,
|
|
461
|
+
delegationSaved,
|
|
450
462
|
};
|
|
451
463
|
|
|
452
464
|
let output;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-token-saver",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.11.0",
|
|
4
4
|
"description": "Route the easy work your expensive Claude model keeps repeating down to haiku/sonnet โ post-hoc session analysis, no realtime router, no extra LLM calls.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -211,7 +211,7 @@ export function formatNoSession({ caps = null, model = null, windowLabel = '' }
|
|
|
211
211
|
* @param {boolean} [opts.verbose=false] - longer layout with labels
|
|
212
212
|
* @param {boolean} [opts.timer=true] - show TTL countdown segment
|
|
213
213
|
* @param {'text'|'icon'} [opts.mode='text'] - label style. 'icon' uses ๐ง โณ ๐ฐ instead of word labels.
|
|
214
|
-
* @param {string[]|null} [opts.segments] - whitelist of segments to render. Names: cap-warn, spike, harness, model, hit, ttl, saved, ctx, period, plus per-window keys (`five_hour`, `seven_day`, โฆ). `5h`/`7d` are kept as aliases for back-compat. Null/undefined = all.
|
|
214
|
+
* @param {string[]|null} [opts.segments] - whitelist of segments to render. Names: cap-warn, spike, harness, model, hit, ttl, saved, delegated, ctx, period, plus per-window keys (`five_hour`, `seven_day`, โฆ). `5h`/`7d` are kept as aliases for back-compat. Null/undefined = all.
|
|
215
215
|
*/
|
|
216
216
|
export function formatReport(data, { color = true, verbose = false, timer = true, mode = 'text', segments = null } = {}) {
|
|
217
217
|
const { summary, ttl, cost, options, lastActivity, contextWindow, ctxLive, spikeChip, caps, model } = data;
|
|
@@ -255,6 +255,21 @@ export function formatReport(data, { color = true, verbose = false, timer = true
|
|
|
255
255
|
: 'Cache saved';
|
|
256
256
|
const saveSeg = `${c(CYAN)}${saveLabel}${c(RESET)} ${formatMoney(savings)}`;
|
|
257
257
|
|
|
258
|
+
// Delegation savings โ a DIFFERENT number from "Cache saved" above, which
|
|
259
|
+
// covers the prompt cache only. This one is what running work on a cheaper
|
|
260
|
+
// tier saved, summed from the rule registry route-scan maintains. Hidden
|
|
261
|
+
// when zero or absent: a permanent "$0" is noise for direct-API users and
|
|
262
|
+
// for anyone who has not delegated yet.
|
|
263
|
+
// text: "Delegated $3.2" | same in verbose
|
|
264
|
+
// icon: "๐ $3.2" | verbose: "๐ Delegated $3.2"
|
|
265
|
+
const delegationSaved = Number(data.delegationSaved) || 0;
|
|
266
|
+
const delegateLabel = isIcon
|
|
267
|
+
? (verbose ? '๐ Delegated' : '๐')
|
|
268
|
+
: 'Delegated';
|
|
269
|
+
const delegateSeg = delegationSaved > 0
|
|
270
|
+
? `${c(GREEN)}${delegateLabel}${c(RESET)} ${formatMoney(delegationSaved)}`
|
|
271
|
+
: null;
|
|
272
|
+
|
|
258
273
|
// Period label honors hour-precision configs (`mode 6h` โ "6h", `mode 1d` โ "1d").
|
|
259
274
|
// Fall back to legacy `${days}d` when callers haven't supplied a label.
|
|
260
275
|
const periodLabel = options.windowLabel || `${options.days}d`;
|
|
@@ -482,6 +497,8 @@ export function formatReport(data, { color = true, verbose = false, timer = true
|
|
|
482
497
|
// it sits near the tail. The period label closes the line as a quiet
|
|
483
498
|
// timeframe footer.
|
|
484
499
|
if (want('saved')) segs.push(saveSeg);
|
|
500
|
+
// Both savings numbers sit side by side, then the period label closes the line.
|
|
501
|
+
if (delegateSeg && want('delegated')) segs.push(delegateSeg);
|
|
485
502
|
if (want('period')) segs.push(periodSeg);
|
|
486
503
|
// Trailing erase-to-end-of-line so any leftover characters from a previous
|
|
487
504
|
// (longer) statusline render don't bleed into ours. \x1b[K is the standard
|
package/src/model-alias.js
CHANGED
|
@@ -200,10 +200,21 @@ function roleFromAgentType(agentType, cache) {
|
|
|
200
200
|
return role;
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
-
|
|
203
|
+
/**
|
|
204
|
+
* Record one observation.
|
|
205
|
+
*
|
|
206
|
+
* `kind` matters more than the count. An 'explicit' vote comes from a stated
|
|
207
|
+
* model โ a `Task(model: "haiku")` parameter or an agent definition's
|
|
208
|
+
* frontmatter. An 'inferred' vote is circumstantial: the record carried no
|
|
209
|
+
* sidechain flag, so it is *probably* the parent session's own model. Mixing
|
|
210
|
+
* the two by volume let 4 inferred votes outrank 1 explicit one and stamped a
|
|
211
|
+
* haiku profile as 'main' โ see tallyVotes.
|
|
212
|
+
*/
|
|
213
|
+
function addVote(votes, pid, role, kind) {
|
|
204
214
|
if (!pid || !role) return;
|
|
205
|
-
const v = (votes[pid] ||= {});
|
|
206
|
-
v[
|
|
215
|
+
const v = (votes[pid] ||= { explicit: {}, inferred: {} });
|
|
216
|
+
const bucket = v[kind] || (v[kind] = {});
|
|
217
|
+
bucket[role] = (bucket[role] || 0) + 1;
|
|
207
218
|
}
|
|
208
219
|
|
|
209
220
|
/**
|
|
@@ -235,7 +246,9 @@ async function scanMainTranscript(path, votes, requestedByToolUse, agentTypeCach
|
|
|
235
246
|
const pid = profileIdFrom(msg.model);
|
|
236
247
|
if (pid) {
|
|
237
248
|
sawGateway = true;
|
|
238
|
-
|
|
249
|
+
// Circumstantial: a subagent's records also land in the parent file
|
|
250
|
+
// without the flag often enough to outvote real evidence.
|
|
251
|
+
addVote(votes, pid, 'main', 'inferred');
|
|
239
252
|
}
|
|
240
253
|
}
|
|
241
254
|
|
|
@@ -289,18 +302,76 @@ async function subagentFiles(sessionPath) {
|
|
|
289
302
|
}
|
|
290
303
|
}
|
|
291
304
|
|
|
292
|
-
/**
|
|
305
|
+
/**
|
|
306
|
+
* Whether two roles describe the same model. 'main' and 'opus' routinely do:
|
|
307
|
+
* the session model is the opus alias on a default setup, so a Task that asked
|
|
308
|
+
* for opus does not contradict "this is the parent's own profile".
|
|
309
|
+
*/
|
|
310
|
+
function rolesAgree(a, b) {
|
|
311
|
+
if (!a || !b) return false;
|
|
312
|
+
if (a === b) return true;
|
|
313
|
+
const pair = new Set([a, b]);
|
|
314
|
+
return pair.has('main') && pair.has('opus');
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** Winner of one bucket, with the counts needed to judge confidence. */
|
|
318
|
+
function topRole(tally) {
|
|
319
|
+
let role = null;
|
|
320
|
+
let top = 0;
|
|
321
|
+
let total = 0;
|
|
322
|
+
for (const [r, n] of Object.entries(tally || {})) {
|
|
323
|
+
total += n;
|
|
324
|
+
if (n > top) { top = n; role = r; }
|
|
325
|
+
}
|
|
326
|
+
return { role, top, total };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Decide a role per profile id.
|
|
331
|
+
*
|
|
332
|
+
* Explicit evidence decides alone whenever there is enough of it; inferred
|
|
333
|
+
* evidence is only consulted when the explicit bucket is too thin. Counting
|
|
334
|
+
* both together is what produced the original mis-classification: a haiku
|
|
335
|
+
* profile appeared 16 times in parent transcripts without a sidechain flag
|
|
336
|
+
* against 1,702 times as a subagent, and those 16 inferred 'main' votes beat
|
|
337
|
+
* the single explicit 'haiku' one at exactly the 80% line.
|
|
338
|
+
*
|
|
339
|
+
* When neither bucket is decisive the id stays unresolved. An 'unknown' that
|
|
340
|
+
* drops out of the aggregate beats a confident wrong answer that silently
|
|
341
|
+
* re-tiers every run on that profile.
|
|
342
|
+
*/
|
|
293
343
|
export function tallyVotes(votes, { minVotes = MIN_VOTES, minAgreement = MIN_AGREEMENT } = {}) {
|
|
294
344
|
const learned = {};
|
|
295
|
-
for (const [pid,
|
|
296
|
-
|
|
345
|
+
for (const [pid, buckets] of Object.entries(votes)) {
|
|
346
|
+
// Legacy flat shape (`{ haiku: 3 }`) is read as explicit evidence.
|
|
347
|
+
const split = buckets && (buckets.explicit || buckets.inferred)
|
|
348
|
+
? { explicit: buckets.explicit || {}, inferred: buckets.inferred || {} }
|
|
349
|
+
: { explicit: buckets || {}, inferred: {} };
|
|
350
|
+
|
|
351
|
+
const explicit = topRole(split.explicit);
|
|
352
|
+
const inferred = topRole(split.inferred);
|
|
353
|
+
|
|
297
354
|
let role = null;
|
|
298
|
-
let
|
|
299
|
-
|
|
300
|
-
|
|
355
|
+
let source = null;
|
|
356
|
+
if (explicit.total >= minVotes && explicit.top / explicit.total >= minAgreement) {
|
|
357
|
+
role = explicit.role;
|
|
358
|
+
source = 'explicit';
|
|
359
|
+
} else if (inferred.total >= minVotes
|
|
360
|
+
&& inferred.top / inferred.total >= minAgreement
|
|
361
|
+
&& (explicit.total === 0 || rolesAgree(explicit.role, inferred.role))) {
|
|
362
|
+
// Thin explicit evidence still vetoes a contradicting inference: one
|
|
363
|
+
// stated `Task(model: "haiku")` outweighs any number of "no sidechain
|
|
364
|
+
// flag, so probably the session model" observations.
|
|
365
|
+
role = inferred.role;
|
|
366
|
+
source = 'inferred';
|
|
301
367
|
}
|
|
302
|
-
|
|
303
|
-
learned[pid] = {
|
|
368
|
+
|
|
369
|
+
learned[pid] = {
|
|
370
|
+
role,
|
|
371
|
+
source,
|
|
372
|
+
votes: split,
|
|
373
|
+
total: explicit.total + inferred.total,
|
|
374
|
+
};
|
|
304
375
|
}
|
|
305
376
|
return learned;
|
|
306
377
|
}
|
|
@@ -345,7 +416,9 @@ export async function learnProfileMapping({ sessionPaths = [], maxSessions = 40
|
|
|
345
416
|
const pid = await subagentProfileId(jsonl);
|
|
346
417
|
if (!pid) continue;
|
|
347
418
|
sawGateway = true;
|
|
348
|
-
|
|
419
|
+
// Stated evidence: the Task call named this tier, or the agent
|
|
420
|
+
// definition it used did.
|
|
421
|
+
addVote(votes, pid, role, 'explicit');
|
|
349
422
|
}
|
|
350
423
|
if (sawGateway) gateway = true;
|
|
351
424
|
}
|
package/src/model-rules.js
CHANGED
|
@@ -100,6 +100,27 @@ export function loadModelRules() {
|
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
/**
|
|
104
|
+
* USD that measured delegations have saved, summed across registered rules.
|
|
105
|
+
* Reads the file route-scan already wrote โ the statusline refreshes every few
|
|
106
|
+
* seconds and must never start a scan of its own (a scan parses tens of MB of
|
|
107
|
+
* transcripts).
|
|
108
|
+
*
|
|
109
|
+
* Rules with no measured delegation contribute nothing, and an unreadable
|
|
110
|
+
* registry returns 0 rather than throwing: the statusline reads 0 as "draw no
|
|
111
|
+
* chip", which is the right outcome for anyone who never delegates.
|
|
112
|
+
*/
|
|
113
|
+
export function delegationSavedUsd() {
|
|
114
|
+
try {
|
|
115
|
+
return loadModelRules().rules.reduce(
|
|
116
|
+
(sum, r) => sum + (r.delegatedRuns ? (Number(r.savedUsd) || 0) : 0),
|
|
117
|
+
0,
|
|
118
|
+
);
|
|
119
|
+
} catch {
|
|
120
|
+
return 0;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
103
124
|
export function saveModelRules(data) {
|
|
104
125
|
const dir = stateDir();
|
|
105
126
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|