@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 +383 -150
- package/dist/bin.js.map +1 -1
- package/package.json +1 -1
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.
|
|
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) =>
|
|
305
|
+
return [...row].map((c) => toTag2(c));
|
|
262
306
|
});
|
|
263
307
|
}
|
|
264
|
-
function
|
|
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.
|
|
747
|
-
return "2.12.
|
|
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,
|
|
845
|
-
const url =
|
|
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
|
|
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
|
|
1376
|
-
const { primaryModel
|
|
1377
|
-
const
|
|
1378
|
-
return /* @__PURE__ */
|
|
1379
|
-
|
|
1380
|
-
MODEL_KEYS.map((m) => /* @__PURE__ */ jsxs5(Text7, { children: [
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
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
|
|
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 &&
|
|
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 =
|
|
1552
|
-
const [project, session] = rel.split(
|
|
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
|
-
|
|
1561
|
-
const t =
|
|
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
|
|
1759
|
+
const projects = await fs4.readdir(root);
|
|
1582
1760
|
const out = [];
|
|
1583
1761
|
for (const project of projects) {
|
|
1584
|
-
const projectDir =
|
|
1585
|
-
const
|
|
1586
|
-
if (!
|
|
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
|
|
1771
|
+
const entries = await fs4.readdir(dir);
|
|
1594
1772
|
for (const entry of entries) {
|
|
1595
1773
|
if (entry.endsWith(".jsonl")) {
|
|
1596
|
-
out.push(
|
|
1774
|
+
out.push(path6.join(dir, entry));
|
|
1597
1775
|
} else if (depth > 1) {
|
|
1598
|
-
const child =
|
|
1599
|
-
const st = await
|
|
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
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1625
|
-
}
|
|
1804
|
+
};
|
|
1626
1805
|
|
|
1627
1806
|
// src/tokens/codex.ts
|
|
1628
|
-
import * as
|
|
1629
|
-
import * as
|
|
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
|
|
1814
|
+
await fs5.stat(root);
|
|
1636
1815
|
const files = [
|
|
1637
|
-
...await collectRollouts(
|
|
1638
|
-
...await collectRollouts(
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
parsed
|
|
1685
|
-
|
|
1686
|
-
|
|
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
|
-
|
|
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
|
|
1701
|
-
import * as
|
|
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
|
-
|
|
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
|
|
1910
|
+
const entries = await fs6.readdir(root);
|
|
1725
1911
|
const out = [];
|
|
1726
1912
|
for (const entry of entries) {
|
|
1727
|
-
const chatsDir =
|
|
1913
|
+
const chatsDir = path8.join(root, entry, "chats");
|
|
1728
1914
|
let files;
|
|
1729
1915
|
try {
|
|
1730
|
-
files = await
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
1798
|
-
const
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
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
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
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
|
|
2214
|
+
const scanBeat = async () => {
|
|
2215
|
+
const progress = new ScanProgress();
|
|
1976
2216
|
try {
|
|
1977
|
-
return await
|
|
1978
|
-
readAllSources(active, active.primary_model),
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
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
|
|
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: [
|