@mauricode/token-derby 2.12.1 → 2.12.3

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/dist/bin.js CHANGED
@@ -26,19 +26,10 @@ import TextInput from "ink-text-input";
26
26
 
27
27
  // ../shared/dist/models.js
28
28
  var MODEL_KEYS = ["claude", "codex", "gemini"];
29
- var SECONDARY_WEIGHT = 0.1;
29
+ var SECONDARY_WEIGHT = 0.5;
30
30
  function isModelKey(v) {
31
31
  return typeof v === "string" && MODEL_KEYS.includes(v);
32
32
  }
33
- function weightFor(primary, key) {
34
- return key === primary ? 1 : SECONDARY_WEIGHT;
35
- }
36
- function weightedTotal(primary, perSource) {
37
- let total = 0;
38
- for (const key of MODEL_KEYS)
39
- total += perSource[key] * weightFor(primary, key);
40
- return total;
41
- }
42
33
 
43
34
  // ../shared/dist/constants.js
44
35
  var CLI_VERSION_HEADER = "x-cli-version";
@@ -208,6 +199,59 @@ var HATS = [
208
199
  // ../shared/dist/series-transform.js
209
200
  var PACE_WINDOW_MS = 15 * 6e4;
210
201
 
202
+ // ../shared/dist/sprite-grid.js
203
+ var ROWS = [
204
+ "................................",
205
+ "................................",
206
+ "..........................MMM...",
207
+ "..........................MMM...",
208
+ ".........................MBBEBB.",
209
+ ".........................MBBEBB.",
210
+ "........................MBBBBBBB",
211
+ "........................MBBBBBBB",
212
+ "..................MMMMMMMBBB....",
213
+ "..................MMMMMMMBBB....",
214
+ "....BBBBBBBBSSSSSSMMBBBBBB......",
215
+ "...BBBBBBBBBSSSSSSMMBBBBBB......",
216
+ ".TTBBBBBBBBBSSSSSSBBBBBBBB......",
217
+ ".TTBBBBBBBBBSSSSSSBBBBBBBB......",
218
+ "TTTBBBBBBBBBBBBBBBBBBBBBBB......",
219
+ "TTTBBBBBBBBBBBBBBBBBBBBB........",
220
+ "...BBB.BBB.....BBB.BBB..........",
221
+ "...BBB.BBB.....BBB.BBB..........",
222
+ "....BB..BB......BB..BB..........",
223
+ "....BB..BB......BB..BB..........",
224
+ "....BB..BB......BB..BB..........",
225
+ "....BB..BB......BB..BB..........",
226
+ "....BB..BB......BB..BB..........",
227
+ "...HHH.HHH.....HHH.HHH.........."
228
+ ];
229
+ var GRID = ROWS.map((row, y) => {
230
+ if (row.length !== 32)
231
+ throw new Error(`sprite row ${y} has length ${row.length}, expected 32`);
232
+ return [...row].map((c) => toTag(c, y));
233
+ });
234
+ function toTag(c, y) {
235
+ switch (c) {
236
+ case "B":
237
+ return "B";
238
+ case "M":
239
+ return "M";
240
+ case "T":
241
+ return "T";
242
+ case "S":
243
+ return "S";
244
+ case "H":
245
+ return "H";
246
+ case "E":
247
+ return "B";
248
+ case ".":
249
+ return null;
250
+ default:
251
+ throw new Error(`unknown sprite char '${c}' at y=${y}`);
252
+ }
253
+ }
254
+
211
255
  // src/ui/HorseSprite.tsx
212
256
  import { Box, Text } from "ink";
213
257
 
@@ -258,10 +302,10 @@ function parse(rows, width, height) {
258
302
  if (row.length !== width) {
259
303
  throw new Error(`sprite row ${y} has length ${row.length}, expected ${width}`);
260
304
  }
261
- return [...row].map((c) => toTag(c));
305
+ return [...row].map((c) => toTag2(c));
262
306
  });
263
307
  }
264
- function toTag(c) {
308
+ function toTag2(c) {
265
309
  switch (c) {
266
310
  case "B":
267
311
  return "B";
@@ -738,13 +782,14 @@ function apiBase() {
738
782
  return process.env.TOKEN_DERBY_API_BASE ?? ENVIRONMENTS[selectedEnv()].apiBase;
739
783
  }
740
784
  var HEARTBEAT_INTERVAL_MS = 6e4;
785
+ var SCAN_TIMEOUT_MS = HEARTBEAT_INTERVAL_MS * 0.75;
741
786
  var HEARTBEAT_RETRY_DELAYS_MS = [1e3, 2e3, 4e3, 8e3, 15e3];
742
787
 
743
788
  // src/version.ts
744
789
  import { createRequire } from "module";
745
790
  function readVersion() {
746
- if ("2.12.1".length > 0) {
747
- return "2.12.1";
791
+ if ("2.12.3".length > 0) {
792
+ return "2.12.3";
748
793
  }
749
794
  try {
750
795
  const req = createRequire(import.meta.url);
@@ -841,8 +886,8 @@ function getIdentity() {
841
886
  function _resetIdentityCacheForTests() {
842
887
  identityCache = null;
843
888
  }
844
- async function request(method, path8, body, horseAuthToken, fetchImpl = fetch) {
845
- const url = path8.startsWith("http") ? path8 : `${apiBase()}${path8}`;
889
+ async function request(method, path9, body, horseAuthToken, fetchImpl = fetch) {
890
+ const url = path9.startsWith("http") ? path9 : `${apiBase()}${path9}`;
846
891
  const headers = {};
847
892
  headers[CLI_VERSION_HEADER] = CLI_VERSION;
848
893
  headers["user-agent"] = `token-derby/${CLI_VERSION}`;
@@ -1343,7 +1388,7 @@ function PrimaryPicker({ onPick }) {
1343
1388
  else if (key.return) onPick(MODEL_KEYS[i]);
1344
1389
  });
1345
1390
  return /* @__PURE__ */ jsxs4(Box6, { flexDirection: "column", children: [
1346
- /* @__PURE__ */ jsx6(Text6, { bold: true, children: "Pick your primary model for this race (counts 1:1; the others count at 10%)." }),
1391
+ /* @__PURE__ */ jsx6(Text6, { bold: true, children: "Pick your primary model for this race (counts 1:1; the others count at 50%)." }),
1347
1392
  /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "This is locked for the whole race \u2014 you can't change it, even by rejoining." }),
1348
1393
  MODEL_KEYS.map((m, idx) => /* @__PURE__ */ jsxs4(Text6, { color: idx === i ? "cyan" : void 0, children: [
1349
1394
  idx === i ? "\u276F " : " ",
@@ -1372,20 +1417,20 @@ import { Box as Box8, Text as Text8, useApp } from "ink";
1372
1417
  import { Box as Box7, Text as Text7 } from "ink";
1373
1418
  import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
1374
1419
  var MODEL_LABELS = { claude: "Claude", codex: "Codex", gemini: "Gemini" };
1375
- function TokenBreakdown(props) {
1376
- const { primaryModel, perSource, raceScore, primaryCapped } = props;
1377
- const primaryTag = primaryCapped ? "(primary \xB7 top 5/beat)" : "(primary)";
1378
- return /* @__PURE__ */ jsxs5(Box7, { flexDirection: "column", marginTop: 1, children: [
1379
- /* @__PURE__ */ jsx7(Text7, { bold: true, children: "Tokens by model (since join)" }),
1380
- MODEL_KEYS.map((m) => /* @__PURE__ */ jsxs5(Text7, { children: [
1381
- ` ${MODEL_LABELS[m].padEnd(10)} ${perSource[m].toLocaleString().padStart(12)} `,
1382
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: m === primaryModel ? primaryTag : "(10%)" })
1383
- ] }, m)),
1384
- /* @__PURE__ */ jsx7(Text7, { children: ` ${"Race score".padEnd(10)} ${Math.round(raceScore).toLocaleString().padStart(12)}` })
1385
- ] });
1420
+ function ModelList(props) {
1421
+ const { primaryModel } = props;
1422
+ const secondaryTag = ` (${Math.round(SECONDARY_WEIGHT * 100)}%)`;
1423
+ return /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsxs5(Text7, { children: [
1424
+ "Models: ",
1425
+ MODEL_KEYS.map((m, i) => /* @__PURE__ */ jsxs5(Text7, { children: [
1426
+ i > 0 ? " \xB7 " : "",
1427
+ MODEL_LABELS[m],
1428
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: m === primaryModel ? " (primary)" : secondaryTag })
1429
+ ] }, m))
1430
+ ] }) });
1386
1431
  }
1387
1432
  function StatusScreen(props) {
1388
- const { race, ownHorseId, ownHorseName, ownColors, ownUserName, lastHeartbeatAgoSec, lastHeartbeatOk, stalled, stallReason, primaryModel, perSource, primaryCapped } = props;
1433
+ const { race, ownHorseId, ownHorseName, ownColors, ownUserName, lastHeartbeatAgoSec, lastHeartbeatOk, stalled, stallReason, primaryModel } = props;
1389
1434
  if (!race) {
1390
1435
  return /* @__PURE__ */ jsx7(Box7, { flexDirection: "column", children: /* @__PURE__ */ jsx7(Text7, { children: "Joining race\u2026" }) });
1391
1436
  }
@@ -1465,15 +1510,7 @@ function StatusScreen(props) {
1465
1510
  ". Your race continues."
1466
1511
  ] })
1467
1512
  ] }),
1468
- primaryModel && perSource && /* @__PURE__ */ jsx7(
1469
- TokenBreakdown,
1470
- {
1471
- primaryModel,
1472
- perSource,
1473
- raceScore: own?.current_tokens ?? weightedTotal(primaryModel, perSource),
1474
- primaryCapped
1475
- }
1476
- ),
1513
+ primaryModel && /* @__PURE__ */ jsx7(ModelList, { primaryModel }),
1477
1514
  /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "Press Ctrl+C to crash out of the race." }) })
1478
1515
  ] });
1479
1516
  }
@@ -1544,27 +1581,168 @@ function runHeartbeatLoop(opts) {
1544
1581
  }
1545
1582
 
1546
1583
  // src/tokens/transcripts.ts
1584
+ import * as fs4 from "fs/promises";
1585
+ import * as path6 from "path";
1586
+
1587
+ // src/tokens/pool.ts
1588
+ var SCAN_CONCURRENCY = 12;
1589
+ async function mapWithConcurrency(items, limit, fn) {
1590
+ const out = new Array(items.length);
1591
+ let next = 0;
1592
+ const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
1593
+ while (true) {
1594
+ const i = next++;
1595
+ if (i >= items.length) return;
1596
+ out[i] = await fn(items[i], i);
1597
+ }
1598
+ });
1599
+ await Promise.all(workers);
1600
+ return out;
1601
+ }
1602
+
1603
+ // src/tokens/scan-cache.ts
1547
1604
  import * as fs3 from "fs/promises";
1548
1605
  import * as path5 from "path";
1606
+ var CACHE_VERSION = 1;
1607
+ function isEntry(v) {
1608
+ const e = v;
1609
+ return !!e && typeof e.mtimeMs === "number" && typeof e.size === "number" && typeof e.offset === "number";
1610
+ }
1611
+ var ScanCache = class _ScanCache {
1612
+ constructor(source, entries) {
1613
+ this.source = source;
1614
+ this.entries = entries;
1615
+ }
1616
+ source;
1617
+ entries;
1618
+ touched = /* @__PURE__ */ new Set();
1619
+ static async open(source) {
1620
+ return new _ScanCache(source, await loadEntries(source));
1621
+ }
1622
+ /**
1623
+ * Total bytes this source was known to hold at its last completed scan, for
1624
+ * diagnosing which source is blowing the beat budget. 0 = never scanned.
1625
+ */
1626
+ static async knownBytes(source) {
1627
+ let total = 0;
1628
+ for (const entry of (await loadEntries(source)).values()) total += entry.size;
1629
+ return total;
1630
+ }
1631
+ has(file) {
1632
+ return this.entries.has(file);
1633
+ }
1634
+ /**
1635
+ * Fold an append-only file, consuming only the bytes added since last time.
1636
+ *
1637
+ * A trailing line with no newline yet is folded into the RETURNED value but
1638
+ * not committed to the cache — matching a plain whole-file read, while still
1639
+ * re-reading that line once the writer completes it.
1640
+ */
1641
+ async readIncremental(file, fold) {
1642
+ const st = await fs3.stat(file);
1643
+ const prev = this.entries.get(file);
1644
+ this.touched.add(file);
1645
+ if (prev && prev.mtimeMs === st.mtimeMs && prev.size === st.size) return prev.value;
1646
+ const grew = prev !== void 0 && st.size > prev.size;
1647
+ const start = grew ? prev.offset : 0;
1648
+ const acc = grew ? prev.value : fold.empty();
1649
+ const { lines, tail, consumedTo } = await readCompleteLines(file, start, st.size);
1650
+ const committed = lines.length > 0 ? fold.append(acc, lines) : acc;
1651
+ this.entries.set(file, { mtimeMs: st.mtimeMs, size: st.size, offset: consumedTo, value: committed });
1652
+ return tail === null ? committed : fold.append(committed, [tail]);
1653
+ }
1654
+ /**
1655
+ * For files that are rewritten whole rather than appended to (Gemini's .json
1656
+ * chats). Gated on mtime+size, recomputed in full whenever either moves.
1657
+ */
1658
+ async readWhenChanged(file, compute) {
1659
+ const st = await fs3.stat(file);
1660
+ const prev = this.entries.get(file);
1661
+ this.touched.add(file);
1662
+ if (prev && prev.mtimeMs === st.mtimeMs && prev.size === st.size) return prev.value;
1663
+ const value = await compute(await fs3.readFile(file, "utf8"));
1664
+ this.entries.set(file, { mtimeMs: st.mtimeMs, size: st.size, offset: st.size, value });
1665
+ return value;
1666
+ }
1667
+ /** Persist, dropping any entry not read since `open` so the file can't grow forever. */
1668
+ async save() {
1669
+ for (const key of [...this.entries.keys()]) {
1670
+ if (!this.touched.has(key)) this.entries.delete(key);
1671
+ }
1672
+ const target = cacheFile(this.source);
1673
+ const tmp = `${target}.tmp`;
1674
+ try {
1675
+ await fs3.mkdir(path5.dirname(target), { recursive: true });
1676
+ await fs3.writeFile(tmp, JSON.stringify({ version: CACHE_VERSION, files: Object.fromEntries(this.entries) }));
1677
+ await fs3.rename(tmp, target);
1678
+ } catch {
1679
+ }
1680
+ }
1681
+ };
1682
+ function cacheFile(source) {
1683
+ return path5.join(homeDir(), "scan-cache", `${source}.json`);
1684
+ }
1685
+ async function loadEntries(source) {
1686
+ let parsed;
1687
+ try {
1688
+ parsed = JSON.parse(await fs3.readFile(cacheFile(source), "utf8"));
1689
+ } catch {
1690
+ return /* @__PURE__ */ new Map();
1691
+ }
1692
+ if (parsed?.version !== CACHE_VERSION || typeof parsed.files !== "object" || parsed.files === null) {
1693
+ return /* @__PURE__ */ new Map();
1694
+ }
1695
+ const out = /* @__PURE__ */ new Map();
1696
+ for (const [file, entry] of Object.entries(parsed.files)) {
1697
+ if (isEntry(entry)) out.set(file, entry);
1698
+ }
1699
+ return out;
1700
+ }
1701
+ async function readCompleteLines(file, start, end) {
1702
+ if (end <= start) return { lines: [], tail: null, consumedTo: start };
1703
+ const fh = await fs3.open(file, "r");
1704
+ try {
1705
+ const buf = Buffer.allocUnsafe(end - start);
1706
+ const { bytesRead } = await fh.read(buf, 0, end - start, start);
1707
+ const chunk = buf.subarray(0, bytesRead);
1708
+ const lastNl = chunk.lastIndexOf(10);
1709
+ if (lastNl === -1) {
1710
+ return { lines: [], tail: chunk.toString("utf8") || null, consumedTo: start };
1711
+ }
1712
+ const tail = chunk.subarray(lastNl + 1).toString("utf8");
1713
+ return {
1714
+ lines: chunk.subarray(0, lastNl).toString("utf8").split("\n"),
1715
+ tail: tail === "" ? null : tail,
1716
+ consumedTo: start + lastNl + 1
1717
+ };
1718
+ } finally {
1719
+ await fh.close();
1720
+ }
1721
+ }
1722
+
1723
+ // src/tokens/transcripts.ts
1549
1724
  var MAX_PROJECT_DEPTH = 8;
1550
1725
  function conversationId(file, root) {
1551
- const rel = path5.relative(root, file);
1552
- const [project, session] = rel.split(path5.sep);
1726
+ const rel = path6.relative(root, file);
1727
+ const [project, session] = rel.split(path6.sep);
1553
1728
  if (project === void 0 || session === void 0) return rel.replace(/\.jsonl$/, "");
1554
1729
  return `${project}/${session.replace(/\.jsonl$/, "")}`;
1555
1730
  }
1556
1731
  async function sumTokensByConversation() {
1557
1732
  const root = claudeProjectsDir();
1558
1733
  const files = await listJsonlFiles(root);
1734
+ const cache = await ScanCache.open("claude");
1735
+ const totals = await mapWithConcurrency(files, SCAN_CONCURRENCY, (f) => cache.readIncremental(f, CLAUDE_FOLD));
1736
+ await cache.save();
1559
1737
  const byConv = /* @__PURE__ */ new Map();
1560
- for (const file of files) {
1561
- const t = await sumFile(file);
1738
+ files.forEach((file, i) => {
1739
+ const t = totals[i];
1562
1740
  const id = conversationId(file, root);
1563
1741
  const acc = byConv.get(id) ?? { input: 0, output: 0 };
1564
1742
  acc.input += t.input;
1565
1743
  acc.output += t.output;
1566
1744
  byConv.set(id, acc);
1567
- }
1745
+ });
1568
1746
  return byConv;
1569
1747
  }
1570
1748
  async function sumTokens() {
@@ -1578,25 +1756,25 @@ async function sumTokens() {
1578
1756
  return { input, output };
1579
1757
  }
1580
1758
  async function listJsonlFiles(root) {
1581
- const projects = await fs3.readdir(root);
1759
+ const projects = await fs4.readdir(root);
1582
1760
  const out = [];
1583
1761
  for (const project of projects) {
1584
- const projectDir = path5.join(root, project);
1585
- const stat3 = await fs3.stat(projectDir);
1586
- if (!stat3.isDirectory()) continue;
1762
+ const projectDir = path6.join(root, project);
1763
+ const stat4 = await fs4.stat(projectDir);
1764
+ if (!stat4.isDirectory()) continue;
1587
1765
  await collectJsonl(projectDir, MAX_PROJECT_DEPTH, out);
1588
1766
  }
1589
1767
  return out;
1590
1768
  }
1591
1769
  async function collectJsonl(dir, depth, out) {
1592
1770
  if (depth <= 0) return;
1593
- const entries = await fs3.readdir(dir);
1771
+ const entries = await fs4.readdir(dir);
1594
1772
  for (const entry of entries) {
1595
1773
  if (entry.endsWith(".jsonl")) {
1596
- out.push(path5.join(dir, entry));
1774
+ out.push(path6.join(dir, entry));
1597
1775
  } else if (depth > 1) {
1598
- const child = path5.join(dir, entry);
1599
- const st = await fs3.stat(child);
1776
+ const child = path6.join(dir, entry);
1777
+ const st = await fs4.stat(child);
1600
1778
  if (st.isDirectory()) await collectJsonl(child, depth - 1, out);
1601
1779
  }
1602
1780
  }
@@ -1604,43 +1782,49 @@ async function collectJsonl(dir, depth, out) {
1604
1782
  function addNum(value) {
1605
1783
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
1606
1784
  }
1607
- async function sumFile(file) {
1608
- const raw = await fs3.readFile(file, "utf8");
1609
- let input = 0;
1610
- let output = 0;
1611
- for (const line of raw.split("\n")) {
1612
- if (!line.trim()) continue;
1613
- let parsed;
1614
- try {
1615
- parsed = JSON.parse(line);
1616
- } catch {
1617
- continue;
1785
+ var CLAUDE_FOLD = {
1786
+ empty: () => ({ input: 0, output: 0 }),
1787
+ append: (acc, lines) => {
1788
+ let { input, output } = acc;
1789
+ for (const line of lines) {
1790
+ if (!line.trim()) continue;
1791
+ let parsed;
1792
+ try {
1793
+ parsed = JSON.parse(line);
1794
+ } catch {
1795
+ continue;
1796
+ }
1797
+ const usage = parsed?.message?.usage;
1798
+ if (!usage) continue;
1799
+ input += addNum(usage.input_tokens) + addNum(usage.cache_creation_input_tokens);
1800
+ output += addNum(usage.output_tokens);
1618
1801
  }
1619
- const usage = parsed?.message?.usage;
1620
- if (!usage) continue;
1621
- input += addNum(usage.input_tokens) + addNum(usage.cache_creation_input_tokens);
1622
- output += addNum(usage.output_tokens);
1802
+ return { input, output };
1623
1803
  }
1624
- return { input, output };
1625
- }
1804
+ };
1626
1805
 
1627
1806
  // src/tokens/codex.ts
1628
- import * as fs4 from "fs/promises";
1629
- import * as path6 from "path";
1807
+ import * as fs5 from "fs/promises";
1808
+ import * as path7 from "path";
1630
1809
  function num(v) {
1631
1810
  return typeof v === "number" && Number.isFinite(v) ? v : 0;
1632
1811
  }
1633
1812
  async function sumCodexByConversation() {
1634
1813
  const root = codexSessionsDir();
1635
- await fs4.stat(root);
1814
+ await fs5.stat(root);
1636
1815
  const files = [
1637
- ...await collectRollouts(path6.join(root, "sessions")),
1638
- ...await collectRollouts(path6.join(root, "archived_sessions"))
1816
+ ...await collectRollouts(path7.join(root, "sessions")),
1817
+ ...await collectRollouts(path7.join(root, "archived_sessions"))
1639
1818
  ];
1819
+ const cache = await ScanCache.open("codex");
1820
+ const totals = await mapWithConcurrency(
1821
+ files,
1822
+ SCAN_CONCURRENCY,
1823
+ (f) => cache.readIncremental(f, CODEX_FOLD).catch(() => ({ input: 0, output: 0 }))
1824
+ );
1825
+ await cache.save();
1640
1826
  const byConv = /* @__PURE__ */ new Map();
1641
- for (const file of files) {
1642
- byConv.set(file, await lastTokenCount(file));
1643
- }
1827
+ files.forEach((file, i) => byConv.set(file, totals[i]));
1644
1828
  return byConv;
1645
1829
  }
1646
1830
  async function sumCodexTokens() {
@@ -1656,58 +1840,60 @@ async function sumCodexTokens() {
1656
1840
  async function collectRollouts(dir) {
1657
1841
  let entries;
1658
1842
  try {
1659
- entries = await fs4.readdir(dir, { withFileTypes: true });
1843
+ entries = await fs5.readdir(dir, { withFileTypes: true });
1660
1844
  } catch (e) {
1661
1845
  if (e?.code === "ENOENT") return [];
1662
1846
  throw e;
1663
1847
  }
1664
1848
  const out = [];
1665
1849
  for (const entry of entries) {
1666
- const full = path6.join(dir, entry.name);
1850
+ const full = path7.join(dir, entry.name);
1667
1851
  if (entry.isDirectory()) out.push(...await collectRollouts(full));
1668
1852
  else if (entry.name.startsWith("rollout-") && entry.name.endsWith(".jsonl")) out.push(full);
1669
1853
  }
1670
1854
  return out;
1671
1855
  }
1672
- async function lastTokenCount(file) {
1673
- let raw;
1674
- try {
1675
- raw = await fs4.readFile(file, "utf8");
1676
- } catch {
1677
- return { input: 0, output: 0 };
1678
- }
1679
- let usage = null;
1680
- for (const line of raw.split("\n")) {
1681
- if (!line.trim()) continue;
1682
- let parsed;
1683
- try {
1684
- parsed = JSON.parse(line);
1685
- } catch {
1686
- continue;
1687
- }
1688
- if (parsed?.payload?.type === "token_count" && parsed.payload.info?.total_token_usage) {
1689
- usage = parsed.payload.info.total_token_usage;
1856
+ var CODEX_FOLD = {
1857
+ empty: () => ({ input: 0, output: 0 }),
1858
+ append: (acc, lines) => {
1859
+ let usage = null;
1860
+ for (const line of lines) {
1861
+ if (!line.trim()) continue;
1862
+ let parsed;
1863
+ try {
1864
+ parsed = JSON.parse(line);
1865
+ } catch {
1866
+ continue;
1867
+ }
1868
+ if (parsed?.payload?.type === "token_count" && parsed.payload.info?.total_token_usage) {
1869
+ usage = parsed.payload.info.total_token_usage;
1870
+ }
1690
1871
  }
1872
+ if (!usage) return acc;
1873
+ return {
1874
+ input: Math.max(0, num(usage.input_tokens) - num(usage.cached_input_tokens)),
1875
+ output: num(usage.output_tokens)
1876
+ };
1691
1877
  }
1692
- if (!usage) return { input: 0, output: 0 };
1693
- return {
1694
- input: Math.max(0, num(usage.input_tokens) - num(usage.cached_input_tokens)),
1695
- output: num(usage.output_tokens)
1696
- };
1697
- }
1878
+ };
1698
1879
 
1699
1880
  // src/tokens/gemini.ts
1700
- import * as fs5 from "fs/promises";
1701
- import * as path7 from "path";
1881
+ import * as fs6 from "fs/promises";
1882
+ import * as path8 from "path";
1702
1883
  function num2(v) {
1703
1884
  return typeof v === "number" && Number.isFinite(v) ? v : 0;
1704
1885
  }
1705
1886
  async function sumGeminiByConversation() {
1706
1887
  const files = await listChatFiles(geminiTmpDir());
1888
+ const cache = await ScanCache.open("gemini");
1889
+ const totals = await mapWithConcurrency(
1890
+ files,
1891
+ SCAN_CONCURRENCY,
1892
+ (f) => cache.readWhenChanged(f, async (raw) => sumGeminiRaw(f, raw)).catch(() => ({ input: 0, output: 0 }))
1893
+ );
1894
+ await cache.save();
1707
1895
  const byConv = /* @__PURE__ */ new Map();
1708
- for (const file of files) {
1709
- byConv.set(file, await sumGeminiFile(file));
1710
- }
1896
+ files.forEach((file, i) => byConv.set(file, totals[i]));
1711
1897
  return byConv;
1712
1898
  }
1713
1899
  async function sumGeminiTokens() {
@@ -1721,29 +1907,23 @@ async function sumGeminiTokens() {
1721
1907
  return { input, output };
1722
1908
  }
1723
1909
  async function listChatFiles(root) {
1724
- const entries = await fs5.readdir(root);
1910
+ const entries = await fs6.readdir(root);
1725
1911
  const out = [];
1726
1912
  for (const entry of entries) {
1727
- const chatsDir = path7.join(root, entry, "chats");
1913
+ const chatsDir = path8.join(root, entry, "chats");
1728
1914
  let files;
1729
1915
  try {
1730
- files = await fs5.readdir(chatsDir);
1916
+ files = await fs6.readdir(chatsDir);
1731
1917
  } catch {
1732
1918
  continue;
1733
1919
  }
1734
1920
  for (const f of files) {
1735
- if (f.endsWith(".json") || f.endsWith(".jsonl")) out.push(path7.join(chatsDir, f));
1921
+ if (f.endsWith(".json") || f.endsWith(".jsonl")) out.push(path8.join(chatsDir, f));
1736
1922
  }
1737
1923
  }
1738
1924
  return out;
1739
1925
  }
1740
- async function sumGeminiFile(file) {
1741
- let raw;
1742
- try {
1743
- raw = await fs5.readFile(file, "utf8");
1744
- } catch {
1745
- return { input: 0, output: 0 };
1746
- }
1926
+ function sumGeminiRaw(file, raw) {
1747
1927
  const messages = file.endsWith(".jsonl") ? parseJsonl(raw) : parseJson(raw);
1748
1928
  let input = 0;
1749
1929
  let output = 0;
@@ -1779,6 +1959,21 @@ function parseJsonl(raw) {
1779
1959
  function isStall(r) {
1780
1960
  return "stall" in r;
1781
1961
  }
1962
+ var TIMED_OUT = /* @__PURE__ */ Symbol("scan-timeout");
1963
+ async function scanWithTimeout(scan, timeoutMs, describeTimeout) {
1964
+ let timer;
1965
+ const budget = new Promise((resolve) => {
1966
+ timer = setTimeout(() => resolve(TIMED_OUT), timeoutMs);
1967
+ });
1968
+ try {
1969
+ const result = await Promise.race([scan(), budget]);
1970
+ if (result !== TIMED_OUT) return result;
1971
+ const detail = describeTimeout ? await describeTimeout() : null;
1972
+ return { stall: detail ?? `Token scan timed out after ${Math.round(timeoutMs / 1e3)}s` };
1973
+ } finally {
1974
+ clearTimeout(timer);
1975
+ }
1976
+ }
1782
1977
  var SCALAR_READERS = {
1783
1978
  claude: sumTokens,
1784
1979
  codex: sumCodexTokens,
@@ -1792,25 +1987,71 @@ var BY_CONVERSATION_READERS = {
1792
1987
  function scoreFor(race, t) {
1793
1988
  return race.counts_input ? t.input + t.output : t.output;
1794
1989
  }
1795
- async function readAllSources(race, primary) {
1990
+ async function readAllSources(race, primary, progress) {
1991
+ progress?.begin(primary);
1992
+ const primaryScan = BY_CONVERSATION_READERS[primary]().then(
1993
+ (map) => ({ ok: true, map }),
1994
+ (err) => ({ ok: false, err })
1995
+ ).finally(() => progress?.end(primary));
1996
+ const secondaryKeys = MODEL_KEYS.filter((k) => k !== primary);
1997
+ const secondaryScans = secondaryKeys.map((k) => {
1998
+ progress?.begin(k);
1999
+ return SCALAR_READERS[k]().then((t) => scoreFor(race, t)).catch(() => 0).finally(() => progress?.end(k));
2000
+ });
2001
+ const [primaryResult, secondaryValues] = await Promise.all([
2002
+ primaryScan,
2003
+ Promise.all(secondaryScans)
2004
+ ]);
1796
2005
  const primaryByConv = /* @__PURE__ */ new Map();
1797
- try {
1798
- const primaryMap = await BY_CONVERSATION_READERS[primary]();
1799
- for (const [id, totals] of primaryMap) primaryByConv.set(id, scoreFor(race, totals));
1800
- } catch (e) {
1801
- if (e?.code !== "ENOENT") {
1802
- return { stall: `Can't read ${primary} token usage: ${e?.message ?? String(e)}` };
1803
- }
2006
+ if (primaryResult.ok) {
2007
+ for (const [id, totals] of primaryResult.map) primaryByConv.set(id, scoreFor(race, totals));
2008
+ } else if (primaryResult.err?.code !== "ENOENT") {
2009
+ const err = primaryResult.err;
2010
+ return { stall: `Can't read ${primary} token usage: ${err?.message ?? String(err)}` };
1804
2011
  }
1805
2012
  const secondary = { claude: 0, codex: 0, gemini: 0 };
1806
- await Promise.all(
1807
- MODEL_KEYS.filter((k) => k !== primary).map(async (k) => {
1808
- secondary[k] = await SCALAR_READERS[k]().then((t) => scoreFor(race, t)).catch(() => 0);
1809
- })
1810
- );
2013
+ secondaryKeys.forEach((k, i) => {
2014
+ secondary[k] = secondaryValues[i] ?? 0;
2015
+ });
1811
2016
  return { secondary, primaryByConv };
1812
2017
  }
1813
2018
 
2019
+ // src/tokens/scan-progress.ts
2020
+ function skipVar(key) {
2021
+ return `TOKEN_DERBY_${key.toUpperCase()}_DIR`;
2022
+ }
2023
+ function formatBytes(bytes) {
2024
+ if (bytes >= 1e9) return `${(bytes / 1e9).toFixed(1)} GB`;
2025
+ return `${Math.round(bytes / 1e6)} MB`;
2026
+ }
2027
+ var ScanProgress = class {
2028
+ started = /* @__PURE__ */ new Map();
2029
+ finished = /* @__PURE__ */ new Set();
2030
+ begin(key) {
2031
+ this.started.set(key, Date.now());
2032
+ }
2033
+ end(key) {
2034
+ this.finished.add(key);
2035
+ }
2036
+ /** Sources begun but never finished, longest-running first. */
2037
+ outstanding() {
2038
+ return [...this.started.entries()].filter(([key]) => !this.finished.has(key)).sort((a, b) => a[1] - b[1]).map(([key]) => key);
2039
+ }
2040
+ };
2041
+ function describeScanTimeout(timeoutMs, outstanding) {
2042
+ const budget = `Token scan timed out after ${Math.round(timeoutMs / 1e3)}s`;
2043
+ if (outstanding.length === 0) return budget;
2044
+ const named = outstanding.map((s) => s.bytes > 0 ? `${s.key} (${formatBytes(s.bytes)})` : s.key).join(", ");
2045
+ const biggest = [...outstanding].sort((a, b) => b.bytes - a.bytes)[0];
2046
+ return `${budget} \u2014 ${named} still scanning; point ${skipVar(biggest.key)} at an empty dir to skip it`;
2047
+ }
2048
+ async function diagnoseScanTimeout(timeoutMs, progress) {
2049
+ const outstanding = await Promise.all(
2050
+ progress.outstanding().map(async (key) => ({ key, bytes: await ScanCache.knownBytes(key) }))
2051
+ );
2052
+ return describeScanTimeout(timeoutMs, outstanding);
2053
+ }
2054
+
1814
2055
  // src/tokens/primary-cap.ts
1815
2056
  var PRIMARY_TOP_CONVERSATIONS = 5;
1816
2057
  function primaryConversationCap(enabled) {
@@ -1958,8 +2199,6 @@ function RunRace({ active, initialState, pendingMode, ownUserName }) {
1958
2199
  const ctrl = useRef(new AbortController());
1959
2200
  const [stalled, setStalled] = useState5(false);
1960
2201
  const [stallReason, setStallReason] = useState5(null);
1961
- const baselineRef = useRef(initialState.acked);
1962
- const [perSource, setPerSource] = useState5({ claude: 0, codex: 0, gemini: 0 });
1963
2202
  useEffect2(() => {
1964
2203
  const t = setInterval(() => setTickNow(/* @__PURE__ */ new Date()), 1e3);
1965
2204
  return () => clearInterval(t);
@@ -1972,29 +2211,25 @@ function RunRace({ active, initialState, pendingMode, ownUserName }) {
1972
2211
  }, [race?.status]);
1973
2212
  useEffect2(() => {
1974
2213
  const tracker = trackerRef.current;
1975
- const scanWithTimeout = async () => {
2214
+ const scanBeat = async () => {
2215
+ const progress = new ScanProgress();
1976
2216
  try {
1977
- return await Promise.race([
1978
- readAllSources(active, active.primary_model),
1979
- new Promise((_, reject) => setTimeout(() => reject(new Error("scan timeout")), 1e4))
1980
- ]);
1981
- } catch {
1982
- return { stall: "Token scan timed out" };
2217
+ return await scanWithTimeout(
2218
+ () => readAllSources(active, active.primary_model, progress),
2219
+ SCAN_TIMEOUT_MS,
2220
+ () => diagnoseScanTimeout(SCAN_TIMEOUT_MS, progress)
2221
+ );
2222
+ } catch (e) {
2223
+ return { stall: `Token scan failed: ${e?.message ?? String(e)}` };
1983
2224
  }
1984
2225
  };
1985
2226
  runHeartbeatLoop({
1986
2227
  prepareBeat: async () => {
1987
- const reading = await scanWithTimeout();
2228
+ const reading = await scanBeat();
1988
2229
  tracker.recordReading(reading);
1989
2230
  if (pendingRef.current && !isStall(reading)) tracker.reprime();
1990
2231
  setStalled(tracker.stalled);
1991
2232
  setStallReason(tracker.stalled ? tracker.stallReason : null);
1992
- const since = tracker.secondarySinceJoin(baselineRef.current);
1993
- const ps = { claude: 0, codex: 0, gemini: 0 };
1994
- for (const k of MODEL_KEYS) {
1995
- ps[k] = k === active.primary_model ? tracker.primaryCounted() : since[k];
1996
- }
1997
- setPerSource(ps);
1998
2233
  return tracker.nextBeat();
1999
2234
  },
2000
2235
  sendBeat: async (snapshot) => {
@@ -2062,9 +2297,7 @@ function RunRace({ active, initialState, pendingMode, ownUserName }) {
2062
2297
  lastHeartbeatOk: lastHbOk,
2063
2298
  stalled,
2064
2299
  stallReason,
2065
- primaryModel: active.primary_model,
2066
- perSource,
2067
- primaryCapped: primaryConversationCap(active.primary_top5 ?? false) !== Infinity
2300
+ primaryModel: active.primary_model
2068
2301
  }
2069
2302
  ),
2070
2303
  achievements.length > 0 && /* @__PURE__ */ jsxs6(Box8, { flexDirection: "column", marginTop: 1, children: [