@mauricode/token-derby 2.12.2 → 2.12.4
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.md +1 -1
- package/dist/bin.js +349 -140
- package/dist/bin.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
package/dist/bin.js
CHANGED
|
@@ -782,13 +782,14 @@ function apiBase() {
|
|
|
782
782
|
return process.env.TOKEN_DERBY_API_BASE ?? ENVIRONMENTS[selectedEnv()].apiBase;
|
|
783
783
|
}
|
|
784
784
|
var HEARTBEAT_INTERVAL_MS = 6e4;
|
|
785
|
+
var SCAN_TIMEOUT_MS = HEARTBEAT_INTERVAL_MS * 0.75;
|
|
785
786
|
var HEARTBEAT_RETRY_DELAYS_MS = [1e3, 2e3, 4e3, 8e3, 15e3];
|
|
786
787
|
|
|
787
788
|
// src/version.ts
|
|
788
789
|
import { createRequire } from "module";
|
|
789
790
|
function readVersion() {
|
|
790
|
-
if ("2.12.
|
|
791
|
-
return "2.12.
|
|
791
|
+
if ("2.12.4".length > 0) {
|
|
792
|
+
return "2.12.4";
|
|
792
793
|
}
|
|
793
794
|
try {
|
|
794
795
|
const req = createRequire(import.meta.url);
|
|
@@ -885,8 +886,8 @@ function getIdentity() {
|
|
|
885
886
|
function _resetIdentityCacheForTests() {
|
|
886
887
|
identityCache = null;
|
|
887
888
|
}
|
|
888
|
-
async function request(method,
|
|
889
|
-
const url =
|
|
889
|
+
async function request(method, path9, body, horseAuthToken, fetchImpl = fetch) {
|
|
890
|
+
const url = path9.startsWith("http") ? path9 : `${apiBase()}${path9}`;
|
|
890
891
|
const headers = {};
|
|
891
892
|
headers[CLI_VERSION_HEADER] = CLI_VERSION;
|
|
892
893
|
headers["user-agent"] = `token-derby/${CLI_VERSION}`;
|
|
@@ -1414,7 +1415,7 @@ import { Box as Box8, Text as Text8, useApp } from "ink";
|
|
|
1414
1415
|
|
|
1415
1416
|
// src/ui/StatusScreen.tsx
|
|
1416
1417
|
import { Box as Box7, Text as Text7 } from "ink";
|
|
1417
|
-
import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1418
|
+
import { Fragment as Fragment2, jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1418
1419
|
var MODEL_LABELS = { claude: "Claude", codex: "Codex", gemini: "Gemini" };
|
|
1419
1420
|
function ModelList(props) {
|
|
1420
1421
|
const { primaryModel } = props;
|
|
@@ -1438,6 +1439,30 @@ function StatusScreen(props) {
|
|
|
1438
1439
|
const elapsedPct = elapsed(race);
|
|
1439
1440
|
const timeLeft = formatDuration(race.time_left_seconds);
|
|
1440
1441
|
const lvl = levelInfo((own?.xp ?? 0) + (own?.live_xp ?? 0));
|
|
1442
|
+
const divisionField = own?.division === void 0 ? [] : race.horses.filter((h) => h.division === own.division);
|
|
1443
|
+
const divisionRank = own ? divisionField.indexOf(own) + 1 : 0;
|
|
1444
|
+
const showDivision = (race.league_division_names?.length ?? 0) > 0 && divisionRank > 0;
|
|
1445
|
+
const rows = [
|
|
1446
|
+
{ label: "Tokens (race):", value: String(own?.current_tokens ?? 0) },
|
|
1447
|
+
{ label: "Position:", value: `${own?.rank ?? "\u2014"} of ${race.horses.length}` },
|
|
1448
|
+
...showDivision ? [{ label: "Position (Division):", value: `${divisionRank} of ${divisionField.length}` }] : [],
|
|
1449
|
+
{ label: "Leader:", value: leaderText(leader) },
|
|
1450
|
+
...showDivision ? [{ label: "Leader (Division):", value: leaderText(divisionField[0]) }] : [],
|
|
1451
|
+
{ label: "Race elapsed:", value: `${(elapsedPct * 100).toFixed(0)}% ${bar(elapsedPct, 20)}` },
|
|
1452
|
+
{ label: "Time left:", value: timeLeft },
|
|
1453
|
+
{
|
|
1454
|
+
label: "XP:",
|
|
1455
|
+
value: lvl.next_level_xp === null ? `${lvl.xp} (max level) ${bar(1, 20)}` : `${lvl.xp_into_level}/${lvl.xp_for_level} \u2192 Lvl. ${lvl.level + 1} ${bar(lvl.progress, 20)}`
|
|
1456
|
+
},
|
|
1457
|
+
{
|
|
1458
|
+
label: "Last heartbeat:",
|
|
1459
|
+
value: /* @__PURE__ */ jsxs5(Fragment2, { children: [
|
|
1460
|
+
lastHeartbeatAgoSec === null ? "\u2014" : `${lastHeartbeatAgoSec}s ago`,
|
|
1461
|
+
" ",
|
|
1462
|
+
/* @__PURE__ */ jsx7(Text7, { color: lastHeartbeatOk ? "green" : "yellow", children: lastHeartbeatOk ? "\u2713" : "\u26A0" })
|
|
1463
|
+
] })
|
|
1464
|
+
}
|
|
1465
|
+
];
|
|
1441
1466
|
return /* @__PURE__ */ jsxs5(Box7, { flexDirection: "column", borderStyle: "round", paddingX: 1, children: [
|
|
1442
1467
|
/* @__PURE__ */ jsxs5(Text7, { children: [
|
|
1443
1468
|
"\u{1F3C7} TOKEN DERBY \u2500\u2500\u2500 ",
|
|
@@ -1469,40 +1494,7 @@ function StatusScreen(props) {
|
|
|
1469
1494
|
] })
|
|
1470
1495
|
] }),
|
|
1471
1496
|
/* @__PURE__ */ jsxs5(Box7, { flexDirection: "column", marginTop: 1, children: [
|
|
1472
|
-
/* @__PURE__ */
|
|
1473
|
-
"Tokens (race): ",
|
|
1474
|
-
own?.current_tokens ?? 0
|
|
1475
|
-
] }),
|
|
1476
|
-
/* @__PURE__ */ jsxs5(Text7, { children: [
|
|
1477
|
-
"Position: ",
|
|
1478
|
-
own?.rank ?? "\u2014",
|
|
1479
|
-
" of ",
|
|
1480
|
-
race.horses.length
|
|
1481
|
-
] }),
|
|
1482
|
-
/* @__PURE__ */ jsxs5(Text7, { children: [
|
|
1483
|
-
"Leader: ",
|
|
1484
|
-
leader ? `${leader.name}${leader.user_name ? ` (${leader.user_name})` : ""} \u2014 ${leader.current_tokens}` : "\u2014"
|
|
1485
|
-
] }),
|
|
1486
|
-
/* @__PURE__ */ jsxs5(Text7, { children: [
|
|
1487
|
-
"Race elapsed: ",
|
|
1488
|
-
(elapsedPct * 100).toFixed(0),
|
|
1489
|
-
"% ",
|
|
1490
|
-
bar(elapsedPct, 20)
|
|
1491
|
-
] }),
|
|
1492
|
-
/* @__PURE__ */ jsxs5(Text7, { children: [
|
|
1493
|
-
"Time left: ",
|
|
1494
|
-
timeLeft
|
|
1495
|
-
] }),
|
|
1496
|
-
/* @__PURE__ */ jsxs5(Text7, { children: [
|
|
1497
|
-
"XP: ",
|
|
1498
|
-
lvl.next_level_xp === null ? `${lvl.xp} (max level) ${bar(1, 20)}` : `${lvl.xp_into_level}/${lvl.xp_for_level} \u2192 Lvl. ${lvl.level + 1} ${bar(lvl.progress, 20)}`
|
|
1499
|
-
] }),
|
|
1500
|
-
/* @__PURE__ */ jsxs5(Text7, { children: [
|
|
1501
|
-
"Last heartbeat: ",
|
|
1502
|
-
lastHeartbeatAgoSec === null ? "\u2014" : `${lastHeartbeatAgoSec}s ago`,
|
|
1503
|
-
" ",
|
|
1504
|
-
/* @__PURE__ */ jsx7(Text7, { color: lastHeartbeatOk ? "green" : "yellow", children: lastHeartbeatOk ? "\u2713" : "\u26A0" })
|
|
1505
|
-
] }),
|
|
1497
|
+
/* @__PURE__ */ jsx7(StatLines, { rows }),
|
|
1506
1498
|
stalled && /* @__PURE__ */ jsxs5(Text7, { color: "yellow", children: [
|
|
1507
1499
|
"\u26A0 ",
|
|
1508
1500
|
stallReason ?? "Can't read token usage",
|
|
@@ -1513,6 +1505,17 @@ function StatusScreen(props) {
|
|
|
1513
1505
|
/* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "Press Ctrl+C to crash out of the race." }) })
|
|
1514
1506
|
] });
|
|
1515
1507
|
}
|
|
1508
|
+
function StatLines(props) {
|
|
1509
|
+
const width = Math.max(...props.rows.map((r) => r.label.length)) + 1;
|
|
1510
|
+
return /* @__PURE__ */ jsx7(Fragment2, { children: props.rows.map((r) => /* @__PURE__ */ jsxs5(Text7, { children: [
|
|
1511
|
+
r.label.padEnd(width),
|
|
1512
|
+
r.value
|
|
1513
|
+
] }, r.label)) });
|
|
1514
|
+
}
|
|
1515
|
+
function leaderText(h) {
|
|
1516
|
+
if (!h) return "\u2014";
|
|
1517
|
+
return `${h.name}${h.user_name ? ` (${h.user_name})` : ""} \u2014 ${h.current_tokens}`;
|
|
1518
|
+
}
|
|
1516
1519
|
function elapsed(race) {
|
|
1517
1520
|
const start = new Date(race.start_time).getTime();
|
|
1518
1521
|
const end = new Date(race.end_time).getTime();
|
|
@@ -1580,27 +1583,168 @@ function runHeartbeatLoop(opts) {
|
|
|
1580
1583
|
}
|
|
1581
1584
|
|
|
1582
1585
|
// src/tokens/transcripts.ts
|
|
1586
|
+
import * as fs4 from "fs/promises";
|
|
1587
|
+
import * as path6 from "path";
|
|
1588
|
+
|
|
1589
|
+
// src/tokens/pool.ts
|
|
1590
|
+
var SCAN_CONCURRENCY = 12;
|
|
1591
|
+
async function mapWithConcurrency(items, limit, fn) {
|
|
1592
|
+
const out = new Array(items.length);
|
|
1593
|
+
let next = 0;
|
|
1594
|
+
const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
|
|
1595
|
+
while (true) {
|
|
1596
|
+
const i = next++;
|
|
1597
|
+
if (i >= items.length) return;
|
|
1598
|
+
out[i] = await fn(items[i], i);
|
|
1599
|
+
}
|
|
1600
|
+
});
|
|
1601
|
+
await Promise.all(workers);
|
|
1602
|
+
return out;
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
// src/tokens/scan-cache.ts
|
|
1583
1606
|
import * as fs3 from "fs/promises";
|
|
1584
1607
|
import * as path5 from "path";
|
|
1608
|
+
var CACHE_VERSION = 1;
|
|
1609
|
+
function isEntry(v) {
|
|
1610
|
+
const e = v;
|
|
1611
|
+
return !!e && typeof e.mtimeMs === "number" && typeof e.size === "number" && typeof e.offset === "number";
|
|
1612
|
+
}
|
|
1613
|
+
var ScanCache = class _ScanCache {
|
|
1614
|
+
constructor(source, entries) {
|
|
1615
|
+
this.source = source;
|
|
1616
|
+
this.entries = entries;
|
|
1617
|
+
}
|
|
1618
|
+
source;
|
|
1619
|
+
entries;
|
|
1620
|
+
touched = /* @__PURE__ */ new Set();
|
|
1621
|
+
static async open(source) {
|
|
1622
|
+
return new _ScanCache(source, await loadEntries(source));
|
|
1623
|
+
}
|
|
1624
|
+
/**
|
|
1625
|
+
* Total bytes this source was known to hold at its last completed scan, for
|
|
1626
|
+
* diagnosing which source is blowing the beat budget. 0 = never scanned.
|
|
1627
|
+
*/
|
|
1628
|
+
static async knownBytes(source) {
|
|
1629
|
+
let total = 0;
|
|
1630
|
+
for (const entry of (await loadEntries(source)).values()) total += entry.size;
|
|
1631
|
+
return total;
|
|
1632
|
+
}
|
|
1633
|
+
has(file) {
|
|
1634
|
+
return this.entries.has(file);
|
|
1635
|
+
}
|
|
1636
|
+
/**
|
|
1637
|
+
* Fold an append-only file, consuming only the bytes added since last time.
|
|
1638
|
+
*
|
|
1639
|
+
* A trailing line with no newline yet is folded into the RETURNED value but
|
|
1640
|
+
* not committed to the cache — matching a plain whole-file read, while still
|
|
1641
|
+
* re-reading that line once the writer completes it.
|
|
1642
|
+
*/
|
|
1643
|
+
async readIncremental(file, fold) {
|
|
1644
|
+
const st = await fs3.stat(file);
|
|
1645
|
+
const prev = this.entries.get(file);
|
|
1646
|
+
this.touched.add(file);
|
|
1647
|
+
if (prev && prev.mtimeMs === st.mtimeMs && prev.size === st.size) return prev.value;
|
|
1648
|
+
const grew = prev !== void 0 && st.size > prev.size;
|
|
1649
|
+
const start = grew ? prev.offset : 0;
|
|
1650
|
+
const acc = grew ? prev.value : fold.empty();
|
|
1651
|
+
const { lines, tail, consumedTo } = await readCompleteLines(file, start, st.size);
|
|
1652
|
+
const committed = lines.length > 0 ? fold.append(acc, lines) : acc;
|
|
1653
|
+
this.entries.set(file, { mtimeMs: st.mtimeMs, size: st.size, offset: consumedTo, value: committed });
|
|
1654
|
+
return tail === null ? committed : fold.append(committed, [tail]);
|
|
1655
|
+
}
|
|
1656
|
+
/**
|
|
1657
|
+
* For files that are rewritten whole rather than appended to (Gemini's .json
|
|
1658
|
+
* chats). Gated on mtime+size, recomputed in full whenever either moves.
|
|
1659
|
+
*/
|
|
1660
|
+
async readWhenChanged(file, compute) {
|
|
1661
|
+
const st = await fs3.stat(file);
|
|
1662
|
+
const prev = this.entries.get(file);
|
|
1663
|
+
this.touched.add(file);
|
|
1664
|
+
if (prev && prev.mtimeMs === st.mtimeMs && prev.size === st.size) return prev.value;
|
|
1665
|
+
const value = await compute(await fs3.readFile(file, "utf8"));
|
|
1666
|
+
this.entries.set(file, { mtimeMs: st.mtimeMs, size: st.size, offset: st.size, value });
|
|
1667
|
+
return value;
|
|
1668
|
+
}
|
|
1669
|
+
/** Persist, dropping any entry not read since `open` so the file can't grow forever. */
|
|
1670
|
+
async save() {
|
|
1671
|
+
for (const key of [...this.entries.keys()]) {
|
|
1672
|
+
if (!this.touched.has(key)) this.entries.delete(key);
|
|
1673
|
+
}
|
|
1674
|
+
const target = cacheFile(this.source);
|
|
1675
|
+
const tmp = `${target}.tmp`;
|
|
1676
|
+
try {
|
|
1677
|
+
await fs3.mkdir(path5.dirname(target), { recursive: true });
|
|
1678
|
+
await fs3.writeFile(tmp, JSON.stringify({ version: CACHE_VERSION, files: Object.fromEntries(this.entries) }));
|
|
1679
|
+
await fs3.rename(tmp, target);
|
|
1680
|
+
} catch {
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
};
|
|
1684
|
+
function cacheFile(source) {
|
|
1685
|
+
return path5.join(homeDir(), "scan-cache", `${source}.json`);
|
|
1686
|
+
}
|
|
1687
|
+
async function loadEntries(source) {
|
|
1688
|
+
let parsed;
|
|
1689
|
+
try {
|
|
1690
|
+
parsed = JSON.parse(await fs3.readFile(cacheFile(source), "utf8"));
|
|
1691
|
+
} catch {
|
|
1692
|
+
return /* @__PURE__ */ new Map();
|
|
1693
|
+
}
|
|
1694
|
+
if (parsed?.version !== CACHE_VERSION || typeof parsed.files !== "object" || parsed.files === null) {
|
|
1695
|
+
return /* @__PURE__ */ new Map();
|
|
1696
|
+
}
|
|
1697
|
+
const out = /* @__PURE__ */ new Map();
|
|
1698
|
+
for (const [file, entry] of Object.entries(parsed.files)) {
|
|
1699
|
+
if (isEntry(entry)) out.set(file, entry);
|
|
1700
|
+
}
|
|
1701
|
+
return out;
|
|
1702
|
+
}
|
|
1703
|
+
async function readCompleteLines(file, start, end) {
|
|
1704
|
+
if (end <= start) return { lines: [], tail: null, consumedTo: start };
|
|
1705
|
+
const fh = await fs3.open(file, "r");
|
|
1706
|
+
try {
|
|
1707
|
+
const buf = Buffer.allocUnsafe(end - start);
|
|
1708
|
+
const { bytesRead } = await fh.read(buf, 0, end - start, start);
|
|
1709
|
+
const chunk = buf.subarray(0, bytesRead);
|
|
1710
|
+
const lastNl = chunk.lastIndexOf(10);
|
|
1711
|
+
if (lastNl === -1) {
|
|
1712
|
+
return { lines: [], tail: chunk.toString("utf8") || null, consumedTo: start };
|
|
1713
|
+
}
|
|
1714
|
+
const tail = chunk.subarray(lastNl + 1).toString("utf8");
|
|
1715
|
+
return {
|
|
1716
|
+
lines: chunk.subarray(0, lastNl).toString("utf8").split("\n"),
|
|
1717
|
+
tail: tail === "" ? null : tail,
|
|
1718
|
+
consumedTo: start + lastNl + 1
|
|
1719
|
+
};
|
|
1720
|
+
} finally {
|
|
1721
|
+
await fh.close();
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
// src/tokens/transcripts.ts
|
|
1585
1726
|
var MAX_PROJECT_DEPTH = 8;
|
|
1586
1727
|
function conversationId(file, root) {
|
|
1587
|
-
const rel =
|
|
1588
|
-
const [project, session] = rel.split(
|
|
1728
|
+
const rel = path6.relative(root, file);
|
|
1729
|
+
const [project, session] = rel.split(path6.sep);
|
|
1589
1730
|
if (project === void 0 || session === void 0) return rel.replace(/\.jsonl$/, "");
|
|
1590
1731
|
return `${project}/${session.replace(/\.jsonl$/, "")}`;
|
|
1591
1732
|
}
|
|
1592
1733
|
async function sumTokensByConversation() {
|
|
1593
1734
|
const root = claudeProjectsDir();
|
|
1594
1735
|
const files = await listJsonlFiles(root);
|
|
1736
|
+
const cache = await ScanCache.open("claude");
|
|
1737
|
+
const totals = await mapWithConcurrency(files, SCAN_CONCURRENCY, (f) => cache.readIncremental(f, CLAUDE_FOLD));
|
|
1738
|
+
await cache.save();
|
|
1595
1739
|
const byConv = /* @__PURE__ */ new Map();
|
|
1596
|
-
|
|
1597
|
-
const t =
|
|
1740
|
+
files.forEach((file, i) => {
|
|
1741
|
+
const t = totals[i];
|
|
1598
1742
|
const id = conversationId(file, root);
|
|
1599
1743
|
const acc = byConv.get(id) ?? { input: 0, output: 0 };
|
|
1600
1744
|
acc.input += t.input;
|
|
1601
1745
|
acc.output += t.output;
|
|
1602
1746
|
byConv.set(id, acc);
|
|
1603
|
-
}
|
|
1747
|
+
});
|
|
1604
1748
|
return byConv;
|
|
1605
1749
|
}
|
|
1606
1750
|
async function sumTokens() {
|
|
@@ -1614,25 +1758,25 @@ async function sumTokens() {
|
|
|
1614
1758
|
return { input, output };
|
|
1615
1759
|
}
|
|
1616
1760
|
async function listJsonlFiles(root) {
|
|
1617
|
-
const projects = await
|
|
1761
|
+
const projects = await fs4.readdir(root);
|
|
1618
1762
|
const out = [];
|
|
1619
1763
|
for (const project of projects) {
|
|
1620
|
-
const projectDir =
|
|
1621
|
-
const
|
|
1622
|
-
if (!
|
|
1764
|
+
const projectDir = path6.join(root, project);
|
|
1765
|
+
const stat4 = await fs4.stat(projectDir);
|
|
1766
|
+
if (!stat4.isDirectory()) continue;
|
|
1623
1767
|
await collectJsonl(projectDir, MAX_PROJECT_DEPTH, out);
|
|
1624
1768
|
}
|
|
1625
1769
|
return out;
|
|
1626
1770
|
}
|
|
1627
1771
|
async function collectJsonl(dir, depth, out) {
|
|
1628
1772
|
if (depth <= 0) return;
|
|
1629
|
-
const entries = await
|
|
1773
|
+
const entries = await fs4.readdir(dir);
|
|
1630
1774
|
for (const entry of entries) {
|
|
1631
1775
|
if (entry.endsWith(".jsonl")) {
|
|
1632
|
-
out.push(
|
|
1776
|
+
out.push(path6.join(dir, entry));
|
|
1633
1777
|
} else if (depth > 1) {
|
|
1634
|
-
const child =
|
|
1635
|
-
const st = await
|
|
1778
|
+
const child = path6.join(dir, entry);
|
|
1779
|
+
const st = await fs4.stat(child);
|
|
1636
1780
|
if (st.isDirectory()) await collectJsonl(child, depth - 1, out);
|
|
1637
1781
|
}
|
|
1638
1782
|
}
|
|
@@ -1640,43 +1784,49 @@ async function collectJsonl(dir, depth, out) {
|
|
|
1640
1784
|
function addNum(value) {
|
|
1641
1785
|
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
1642
1786
|
}
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1787
|
+
var CLAUDE_FOLD = {
|
|
1788
|
+
empty: () => ({ input: 0, output: 0 }),
|
|
1789
|
+
append: (acc, lines) => {
|
|
1790
|
+
let { input, output } = acc;
|
|
1791
|
+
for (const line of lines) {
|
|
1792
|
+
if (!line.trim()) continue;
|
|
1793
|
+
let parsed;
|
|
1794
|
+
try {
|
|
1795
|
+
parsed = JSON.parse(line);
|
|
1796
|
+
} catch {
|
|
1797
|
+
continue;
|
|
1798
|
+
}
|
|
1799
|
+
const usage = parsed?.message?.usage;
|
|
1800
|
+
if (!usage) continue;
|
|
1801
|
+
input += addNum(usage.input_tokens) + addNum(usage.cache_creation_input_tokens);
|
|
1802
|
+
output += addNum(usage.output_tokens);
|
|
1654
1803
|
}
|
|
1655
|
-
|
|
1656
|
-
if (!usage) continue;
|
|
1657
|
-
input += addNum(usage.input_tokens) + addNum(usage.cache_creation_input_tokens);
|
|
1658
|
-
output += addNum(usage.output_tokens);
|
|
1804
|
+
return { input, output };
|
|
1659
1805
|
}
|
|
1660
|
-
|
|
1661
|
-
}
|
|
1806
|
+
};
|
|
1662
1807
|
|
|
1663
1808
|
// src/tokens/codex.ts
|
|
1664
|
-
import * as
|
|
1665
|
-
import * as
|
|
1809
|
+
import * as fs5 from "fs/promises";
|
|
1810
|
+
import * as path7 from "path";
|
|
1666
1811
|
function num(v) {
|
|
1667
1812
|
return typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
1668
1813
|
}
|
|
1669
1814
|
async function sumCodexByConversation() {
|
|
1670
1815
|
const root = codexSessionsDir();
|
|
1671
|
-
await
|
|
1816
|
+
await fs5.stat(root);
|
|
1672
1817
|
const files = [
|
|
1673
|
-
...await collectRollouts(
|
|
1674
|
-
...await collectRollouts(
|
|
1818
|
+
...await collectRollouts(path7.join(root, "sessions")),
|
|
1819
|
+
...await collectRollouts(path7.join(root, "archived_sessions"))
|
|
1675
1820
|
];
|
|
1821
|
+
const cache = await ScanCache.open("codex");
|
|
1822
|
+
const totals = await mapWithConcurrency(
|
|
1823
|
+
files,
|
|
1824
|
+
SCAN_CONCURRENCY,
|
|
1825
|
+
(f) => cache.readIncremental(f, CODEX_FOLD).catch(() => ({ input: 0, output: 0 }))
|
|
1826
|
+
);
|
|
1827
|
+
await cache.save();
|
|
1676
1828
|
const byConv = /* @__PURE__ */ new Map();
|
|
1677
|
-
|
|
1678
|
-
byConv.set(file, await lastTokenCount(file));
|
|
1679
|
-
}
|
|
1829
|
+
files.forEach((file, i) => byConv.set(file, totals[i]));
|
|
1680
1830
|
return byConv;
|
|
1681
1831
|
}
|
|
1682
1832
|
async function sumCodexTokens() {
|
|
@@ -1692,58 +1842,60 @@ async function sumCodexTokens() {
|
|
|
1692
1842
|
async function collectRollouts(dir) {
|
|
1693
1843
|
let entries;
|
|
1694
1844
|
try {
|
|
1695
|
-
entries = await
|
|
1845
|
+
entries = await fs5.readdir(dir, { withFileTypes: true });
|
|
1696
1846
|
} catch (e) {
|
|
1697
1847
|
if (e?.code === "ENOENT") return [];
|
|
1698
1848
|
throw e;
|
|
1699
1849
|
}
|
|
1700
1850
|
const out = [];
|
|
1701
1851
|
for (const entry of entries) {
|
|
1702
|
-
const full =
|
|
1852
|
+
const full = path7.join(dir, entry.name);
|
|
1703
1853
|
if (entry.isDirectory()) out.push(...await collectRollouts(full));
|
|
1704
1854
|
else if (entry.name.startsWith("rollout-") && entry.name.endsWith(".jsonl")) out.push(full);
|
|
1705
1855
|
}
|
|
1706
1856
|
return out;
|
|
1707
1857
|
}
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
parsed
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
}
|
|
1724
|
-
if (parsed?.payload?.type === "token_count" && parsed.payload.info?.total_token_usage) {
|
|
1725
|
-
usage = parsed.payload.info.total_token_usage;
|
|
1858
|
+
var CODEX_FOLD = {
|
|
1859
|
+
empty: () => ({ input: 0, output: 0 }),
|
|
1860
|
+
append: (acc, lines) => {
|
|
1861
|
+
let usage = null;
|
|
1862
|
+
for (const line of lines) {
|
|
1863
|
+
if (!line.trim()) continue;
|
|
1864
|
+
let parsed;
|
|
1865
|
+
try {
|
|
1866
|
+
parsed = JSON.parse(line);
|
|
1867
|
+
} catch {
|
|
1868
|
+
continue;
|
|
1869
|
+
}
|
|
1870
|
+
if (parsed?.payload?.type === "token_count" && parsed.payload.info?.total_token_usage) {
|
|
1871
|
+
usage = parsed.payload.info.total_token_usage;
|
|
1872
|
+
}
|
|
1726
1873
|
}
|
|
1874
|
+
if (!usage) return acc;
|
|
1875
|
+
return {
|
|
1876
|
+
input: Math.max(0, num(usage.input_tokens) - num(usage.cached_input_tokens)),
|
|
1877
|
+
output: num(usage.output_tokens)
|
|
1878
|
+
};
|
|
1727
1879
|
}
|
|
1728
|
-
|
|
1729
|
-
return {
|
|
1730
|
-
input: Math.max(0, num(usage.input_tokens) - num(usage.cached_input_tokens)),
|
|
1731
|
-
output: num(usage.output_tokens)
|
|
1732
|
-
};
|
|
1733
|
-
}
|
|
1880
|
+
};
|
|
1734
1881
|
|
|
1735
1882
|
// src/tokens/gemini.ts
|
|
1736
|
-
import * as
|
|
1737
|
-
import * as
|
|
1883
|
+
import * as fs6 from "fs/promises";
|
|
1884
|
+
import * as path8 from "path";
|
|
1738
1885
|
function num2(v) {
|
|
1739
1886
|
return typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
1740
1887
|
}
|
|
1741
1888
|
async function sumGeminiByConversation() {
|
|
1742
1889
|
const files = await listChatFiles(geminiTmpDir());
|
|
1890
|
+
const cache = await ScanCache.open("gemini");
|
|
1891
|
+
const totals = await mapWithConcurrency(
|
|
1892
|
+
files,
|
|
1893
|
+
SCAN_CONCURRENCY,
|
|
1894
|
+
(f) => cache.readWhenChanged(f, async (raw) => sumGeminiRaw(f, raw)).catch(() => ({ input: 0, output: 0 }))
|
|
1895
|
+
);
|
|
1896
|
+
await cache.save();
|
|
1743
1897
|
const byConv = /* @__PURE__ */ new Map();
|
|
1744
|
-
|
|
1745
|
-
byConv.set(file, await sumGeminiFile(file));
|
|
1746
|
-
}
|
|
1898
|
+
files.forEach((file, i) => byConv.set(file, totals[i]));
|
|
1747
1899
|
return byConv;
|
|
1748
1900
|
}
|
|
1749
1901
|
async function sumGeminiTokens() {
|
|
@@ -1757,29 +1909,23 @@ async function sumGeminiTokens() {
|
|
|
1757
1909
|
return { input, output };
|
|
1758
1910
|
}
|
|
1759
1911
|
async function listChatFiles(root) {
|
|
1760
|
-
const entries = await
|
|
1912
|
+
const entries = await fs6.readdir(root);
|
|
1761
1913
|
const out = [];
|
|
1762
1914
|
for (const entry of entries) {
|
|
1763
|
-
const chatsDir =
|
|
1915
|
+
const chatsDir = path8.join(root, entry, "chats");
|
|
1764
1916
|
let files;
|
|
1765
1917
|
try {
|
|
1766
|
-
files = await
|
|
1918
|
+
files = await fs6.readdir(chatsDir);
|
|
1767
1919
|
} catch {
|
|
1768
1920
|
continue;
|
|
1769
1921
|
}
|
|
1770
1922
|
for (const f of files) {
|
|
1771
|
-
if (f.endsWith(".json") || f.endsWith(".jsonl")) out.push(
|
|
1923
|
+
if (f.endsWith(".json") || f.endsWith(".jsonl")) out.push(path8.join(chatsDir, f));
|
|
1772
1924
|
}
|
|
1773
1925
|
}
|
|
1774
1926
|
return out;
|
|
1775
1927
|
}
|
|
1776
|
-
|
|
1777
|
-
let raw;
|
|
1778
|
-
try {
|
|
1779
|
-
raw = await fs5.readFile(file, "utf8");
|
|
1780
|
-
} catch {
|
|
1781
|
-
return { input: 0, output: 0 };
|
|
1782
|
-
}
|
|
1928
|
+
function sumGeminiRaw(file, raw) {
|
|
1783
1929
|
const messages = file.endsWith(".jsonl") ? parseJsonl(raw) : parseJson(raw);
|
|
1784
1930
|
let input = 0;
|
|
1785
1931
|
let output = 0;
|
|
@@ -1815,6 +1961,21 @@ function parseJsonl(raw) {
|
|
|
1815
1961
|
function isStall(r) {
|
|
1816
1962
|
return "stall" in r;
|
|
1817
1963
|
}
|
|
1964
|
+
var TIMED_OUT = /* @__PURE__ */ Symbol("scan-timeout");
|
|
1965
|
+
async function scanWithTimeout(scan, timeoutMs, describeTimeout) {
|
|
1966
|
+
let timer;
|
|
1967
|
+
const budget = new Promise((resolve) => {
|
|
1968
|
+
timer = setTimeout(() => resolve(TIMED_OUT), timeoutMs);
|
|
1969
|
+
});
|
|
1970
|
+
try {
|
|
1971
|
+
const result = await Promise.race([scan(), budget]);
|
|
1972
|
+
if (result !== TIMED_OUT) return result;
|
|
1973
|
+
const detail = describeTimeout ? await describeTimeout() : null;
|
|
1974
|
+
return { stall: detail ?? `Token scan timed out after ${Math.round(timeoutMs / 1e3)}s` };
|
|
1975
|
+
} finally {
|
|
1976
|
+
clearTimeout(timer);
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1818
1979
|
var SCALAR_READERS = {
|
|
1819
1980
|
claude: sumTokens,
|
|
1820
1981
|
codex: sumCodexTokens,
|
|
@@ -1828,25 +1989,71 @@ var BY_CONVERSATION_READERS = {
|
|
|
1828
1989
|
function scoreFor(race, t) {
|
|
1829
1990
|
return race.counts_input ? t.input + t.output : t.output;
|
|
1830
1991
|
}
|
|
1831
|
-
async function readAllSources(race, primary) {
|
|
1992
|
+
async function readAllSources(race, primary, progress) {
|
|
1993
|
+
progress?.begin(primary);
|
|
1994
|
+
const primaryScan = BY_CONVERSATION_READERS[primary]().then(
|
|
1995
|
+
(map) => ({ ok: true, map }),
|
|
1996
|
+
(err) => ({ ok: false, err })
|
|
1997
|
+
).finally(() => progress?.end(primary));
|
|
1998
|
+
const secondaryKeys = MODEL_KEYS.filter((k) => k !== primary);
|
|
1999
|
+
const secondaryScans = secondaryKeys.map((k) => {
|
|
2000
|
+
progress?.begin(k);
|
|
2001
|
+
return SCALAR_READERS[k]().then((t) => scoreFor(race, t)).catch(() => 0).finally(() => progress?.end(k));
|
|
2002
|
+
});
|
|
2003
|
+
const [primaryResult, secondaryValues] = await Promise.all([
|
|
2004
|
+
primaryScan,
|
|
2005
|
+
Promise.all(secondaryScans)
|
|
2006
|
+
]);
|
|
1832
2007
|
const primaryByConv = /* @__PURE__ */ new Map();
|
|
1833
|
-
|
|
1834
|
-
const
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
return { stall: `Can't read ${primary} token usage: ${e?.message ?? String(e)}` };
|
|
1839
|
-
}
|
|
2008
|
+
if (primaryResult.ok) {
|
|
2009
|
+
for (const [id, totals] of primaryResult.map) primaryByConv.set(id, scoreFor(race, totals));
|
|
2010
|
+
} else if (primaryResult.err?.code !== "ENOENT") {
|
|
2011
|
+
const err = primaryResult.err;
|
|
2012
|
+
return { stall: `Can't read ${primary} token usage: ${err?.message ?? String(err)}` };
|
|
1840
2013
|
}
|
|
1841
2014
|
const secondary = { claude: 0, codex: 0, gemini: 0 };
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
})
|
|
1846
|
-
);
|
|
2015
|
+
secondaryKeys.forEach((k, i) => {
|
|
2016
|
+
secondary[k] = secondaryValues[i] ?? 0;
|
|
2017
|
+
});
|
|
1847
2018
|
return { secondary, primaryByConv };
|
|
1848
2019
|
}
|
|
1849
2020
|
|
|
2021
|
+
// src/tokens/scan-progress.ts
|
|
2022
|
+
function skipVar(key) {
|
|
2023
|
+
return `TOKEN_DERBY_${key.toUpperCase()}_DIR`;
|
|
2024
|
+
}
|
|
2025
|
+
function formatBytes(bytes) {
|
|
2026
|
+
if (bytes >= 1e9) return `${(bytes / 1e9).toFixed(1)} GB`;
|
|
2027
|
+
return `${Math.round(bytes / 1e6)} MB`;
|
|
2028
|
+
}
|
|
2029
|
+
var ScanProgress = class {
|
|
2030
|
+
started = /* @__PURE__ */ new Map();
|
|
2031
|
+
finished = /* @__PURE__ */ new Set();
|
|
2032
|
+
begin(key) {
|
|
2033
|
+
this.started.set(key, Date.now());
|
|
2034
|
+
}
|
|
2035
|
+
end(key) {
|
|
2036
|
+
this.finished.add(key);
|
|
2037
|
+
}
|
|
2038
|
+
/** Sources begun but never finished, longest-running first. */
|
|
2039
|
+
outstanding() {
|
|
2040
|
+
return [...this.started.entries()].filter(([key]) => !this.finished.has(key)).sort((a, b) => a[1] - b[1]).map(([key]) => key);
|
|
2041
|
+
}
|
|
2042
|
+
};
|
|
2043
|
+
function describeScanTimeout(timeoutMs, outstanding) {
|
|
2044
|
+
const budget = `Token scan timed out after ${Math.round(timeoutMs / 1e3)}s`;
|
|
2045
|
+
if (outstanding.length === 0) return budget;
|
|
2046
|
+
const named = outstanding.map((s) => s.bytes > 0 ? `${s.key} (${formatBytes(s.bytes)})` : s.key).join(", ");
|
|
2047
|
+
const biggest = [...outstanding].sort((a, b) => b.bytes - a.bytes)[0];
|
|
2048
|
+
return `${budget} \u2014 ${named} still scanning; point ${skipVar(biggest.key)} at an empty dir to skip it`;
|
|
2049
|
+
}
|
|
2050
|
+
async function diagnoseScanTimeout(timeoutMs, progress) {
|
|
2051
|
+
const outstanding = await Promise.all(
|
|
2052
|
+
progress.outstanding().map(async (key) => ({ key, bytes: await ScanCache.knownBytes(key) }))
|
|
2053
|
+
);
|
|
2054
|
+
return describeScanTimeout(timeoutMs, outstanding);
|
|
2055
|
+
}
|
|
2056
|
+
|
|
1850
2057
|
// src/tokens/primary-cap.ts
|
|
1851
2058
|
var PRIMARY_TOP_CONVERSATIONS = 5;
|
|
1852
2059
|
function primaryConversationCap(enabled) {
|
|
@@ -2006,19 +2213,21 @@ function RunRace({ active, initialState, pendingMode, ownUserName }) {
|
|
|
2006
2213
|
}, [race?.status]);
|
|
2007
2214
|
useEffect2(() => {
|
|
2008
2215
|
const tracker = trackerRef.current;
|
|
2009
|
-
const
|
|
2216
|
+
const scanBeat = async () => {
|
|
2217
|
+
const progress = new ScanProgress();
|
|
2010
2218
|
try {
|
|
2011
|
-
return await
|
|
2012
|
-
readAllSources(active, active.primary_model),
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2219
|
+
return await scanWithTimeout(
|
|
2220
|
+
() => readAllSources(active, active.primary_model, progress),
|
|
2221
|
+
SCAN_TIMEOUT_MS,
|
|
2222
|
+
() => diagnoseScanTimeout(SCAN_TIMEOUT_MS, progress)
|
|
2223
|
+
);
|
|
2224
|
+
} catch (e) {
|
|
2225
|
+
return { stall: `Token scan failed: ${e?.message ?? String(e)}` };
|
|
2017
2226
|
}
|
|
2018
2227
|
};
|
|
2019
2228
|
runHeartbeatLoop({
|
|
2020
2229
|
prepareBeat: async () => {
|
|
2021
|
-
const reading = await
|
|
2230
|
+
const reading = await scanBeat();
|
|
2022
2231
|
tracker.recordReading(reading);
|
|
2023
2232
|
if (pendingRef.current && !isStall(reading)) tracker.reprime();
|
|
2024
2233
|
setStalled(tracker.stalled);
|