@mauricode/token-derby 2.12.2 → 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 +312 -105
- package/dist/bin.js.map +1 -1
- package/package.json +1 -1
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.3".length > 0) {
|
|
792
|
+
return "2.12.3";
|
|
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}`;
|
|
@@ -1580,27 +1581,168 @@ function runHeartbeatLoop(opts) {
|
|
|
1580
1581
|
}
|
|
1581
1582
|
|
|
1582
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
|
|
1583
1604
|
import * as fs3 from "fs/promises";
|
|
1584
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
|
|
1585
1724
|
var MAX_PROJECT_DEPTH = 8;
|
|
1586
1725
|
function conversationId(file, root) {
|
|
1587
|
-
const rel =
|
|
1588
|
-
const [project, session] = rel.split(
|
|
1726
|
+
const rel = path6.relative(root, file);
|
|
1727
|
+
const [project, session] = rel.split(path6.sep);
|
|
1589
1728
|
if (project === void 0 || session === void 0) return rel.replace(/\.jsonl$/, "");
|
|
1590
1729
|
return `${project}/${session.replace(/\.jsonl$/, "")}`;
|
|
1591
1730
|
}
|
|
1592
1731
|
async function sumTokensByConversation() {
|
|
1593
1732
|
const root = claudeProjectsDir();
|
|
1594
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();
|
|
1595
1737
|
const byConv = /* @__PURE__ */ new Map();
|
|
1596
|
-
|
|
1597
|
-
const t =
|
|
1738
|
+
files.forEach((file, i) => {
|
|
1739
|
+
const t = totals[i];
|
|
1598
1740
|
const id = conversationId(file, root);
|
|
1599
1741
|
const acc = byConv.get(id) ?? { input: 0, output: 0 };
|
|
1600
1742
|
acc.input += t.input;
|
|
1601
1743
|
acc.output += t.output;
|
|
1602
1744
|
byConv.set(id, acc);
|
|
1603
|
-
}
|
|
1745
|
+
});
|
|
1604
1746
|
return byConv;
|
|
1605
1747
|
}
|
|
1606
1748
|
async function sumTokens() {
|
|
@@ -1614,25 +1756,25 @@ async function sumTokens() {
|
|
|
1614
1756
|
return { input, output };
|
|
1615
1757
|
}
|
|
1616
1758
|
async function listJsonlFiles(root) {
|
|
1617
|
-
const projects = await
|
|
1759
|
+
const projects = await fs4.readdir(root);
|
|
1618
1760
|
const out = [];
|
|
1619
1761
|
for (const project of projects) {
|
|
1620
|
-
const projectDir =
|
|
1621
|
-
const
|
|
1622
|
-
if (!
|
|
1762
|
+
const projectDir = path6.join(root, project);
|
|
1763
|
+
const stat4 = await fs4.stat(projectDir);
|
|
1764
|
+
if (!stat4.isDirectory()) continue;
|
|
1623
1765
|
await collectJsonl(projectDir, MAX_PROJECT_DEPTH, out);
|
|
1624
1766
|
}
|
|
1625
1767
|
return out;
|
|
1626
1768
|
}
|
|
1627
1769
|
async function collectJsonl(dir, depth, out) {
|
|
1628
1770
|
if (depth <= 0) return;
|
|
1629
|
-
const entries = await
|
|
1771
|
+
const entries = await fs4.readdir(dir);
|
|
1630
1772
|
for (const entry of entries) {
|
|
1631
1773
|
if (entry.endsWith(".jsonl")) {
|
|
1632
|
-
out.push(
|
|
1774
|
+
out.push(path6.join(dir, entry));
|
|
1633
1775
|
} else if (depth > 1) {
|
|
1634
|
-
const child =
|
|
1635
|
-
const st = await
|
|
1776
|
+
const child = path6.join(dir, entry);
|
|
1777
|
+
const st = await fs4.stat(child);
|
|
1636
1778
|
if (st.isDirectory()) await collectJsonl(child, depth - 1, out);
|
|
1637
1779
|
}
|
|
1638
1780
|
}
|
|
@@ -1640,43 +1782,49 @@ async function collectJsonl(dir, depth, out) {
|
|
|
1640
1782
|
function addNum(value) {
|
|
1641
1783
|
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
1642
1784
|
}
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
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);
|
|
1654
1801
|
}
|
|
1655
|
-
|
|
1656
|
-
if (!usage) continue;
|
|
1657
|
-
input += addNum(usage.input_tokens) + addNum(usage.cache_creation_input_tokens);
|
|
1658
|
-
output += addNum(usage.output_tokens);
|
|
1802
|
+
return { input, output };
|
|
1659
1803
|
}
|
|
1660
|
-
|
|
1661
|
-
}
|
|
1804
|
+
};
|
|
1662
1805
|
|
|
1663
1806
|
// src/tokens/codex.ts
|
|
1664
|
-
import * as
|
|
1665
|
-
import * as
|
|
1807
|
+
import * as fs5 from "fs/promises";
|
|
1808
|
+
import * as path7 from "path";
|
|
1666
1809
|
function num(v) {
|
|
1667
1810
|
return typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
1668
1811
|
}
|
|
1669
1812
|
async function sumCodexByConversation() {
|
|
1670
1813
|
const root = codexSessionsDir();
|
|
1671
|
-
await
|
|
1814
|
+
await fs5.stat(root);
|
|
1672
1815
|
const files = [
|
|
1673
|
-
...await collectRollouts(
|
|
1674
|
-
...await collectRollouts(
|
|
1816
|
+
...await collectRollouts(path7.join(root, "sessions")),
|
|
1817
|
+
...await collectRollouts(path7.join(root, "archived_sessions"))
|
|
1675
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();
|
|
1676
1826
|
const byConv = /* @__PURE__ */ new Map();
|
|
1677
|
-
|
|
1678
|
-
byConv.set(file, await lastTokenCount(file));
|
|
1679
|
-
}
|
|
1827
|
+
files.forEach((file, i) => byConv.set(file, totals[i]));
|
|
1680
1828
|
return byConv;
|
|
1681
1829
|
}
|
|
1682
1830
|
async function sumCodexTokens() {
|
|
@@ -1692,58 +1840,60 @@ async function sumCodexTokens() {
|
|
|
1692
1840
|
async function collectRollouts(dir) {
|
|
1693
1841
|
let entries;
|
|
1694
1842
|
try {
|
|
1695
|
-
entries = await
|
|
1843
|
+
entries = await fs5.readdir(dir, { withFileTypes: true });
|
|
1696
1844
|
} catch (e) {
|
|
1697
1845
|
if (e?.code === "ENOENT") return [];
|
|
1698
1846
|
throw e;
|
|
1699
1847
|
}
|
|
1700
1848
|
const out = [];
|
|
1701
1849
|
for (const entry of entries) {
|
|
1702
|
-
const full =
|
|
1850
|
+
const full = path7.join(dir, entry.name);
|
|
1703
1851
|
if (entry.isDirectory()) out.push(...await collectRollouts(full));
|
|
1704
1852
|
else if (entry.name.startsWith("rollout-") && entry.name.endsWith(".jsonl")) out.push(full);
|
|
1705
1853
|
}
|
|
1706
1854
|
return out;
|
|
1707
1855
|
}
|
|
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;
|
|
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
|
+
}
|
|
1726
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
|
+
};
|
|
1727
1877
|
}
|
|
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
|
-
}
|
|
1878
|
+
};
|
|
1734
1879
|
|
|
1735
1880
|
// src/tokens/gemini.ts
|
|
1736
|
-
import * as
|
|
1737
|
-
import * as
|
|
1881
|
+
import * as fs6 from "fs/promises";
|
|
1882
|
+
import * as path8 from "path";
|
|
1738
1883
|
function num2(v) {
|
|
1739
1884
|
return typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
1740
1885
|
}
|
|
1741
1886
|
async function sumGeminiByConversation() {
|
|
1742
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();
|
|
1743
1895
|
const byConv = /* @__PURE__ */ new Map();
|
|
1744
|
-
|
|
1745
|
-
byConv.set(file, await sumGeminiFile(file));
|
|
1746
|
-
}
|
|
1896
|
+
files.forEach((file, i) => byConv.set(file, totals[i]));
|
|
1747
1897
|
return byConv;
|
|
1748
1898
|
}
|
|
1749
1899
|
async function sumGeminiTokens() {
|
|
@@ -1757,29 +1907,23 @@ async function sumGeminiTokens() {
|
|
|
1757
1907
|
return { input, output };
|
|
1758
1908
|
}
|
|
1759
1909
|
async function listChatFiles(root) {
|
|
1760
|
-
const entries = await
|
|
1910
|
+
const entries = await fs6.readdir(root);
|
|
1761
1911
|
const out = [];
|
|
1762
1912
|
for (const entry of entries) {
|
|
1763
|
-
const chatsDir =
|
|
1913
|
+
const chatsDir = path8.join(root, entry, "chats");
|
|
1764
1914
|
let files;
|
|
1765
1915
|
try {
|
|
1766
|
-
files = await
|
|
1916
|
+
files = await fs6.readdir(chatsDir);
|
|
1767
1917
|
} catch {
|
|
1768
1918
|
continue;
|
|
1769
1919
|
}
|
|
1770
1920
|
for (const f of files) {
|
|
1771
|
-
if (f.endsWith(".json") || f.endsWith(".jsonl")) out.push(
|
|
1921
|
+
if (f.endsWith(".json") || f.endsWith(".jsonl")) out.push(path8.join(chatsDir, f));
|
|
1772
1922
|
}
|
|
1773
1923
|
}
|
|
1774
1924
|
return out;
|
|
1775
1925
|
}
|
|
1776
|
-
|
|
1777
|
-
let raw;
|
|
1778
|
-
try {
|
|
1779
|
-
raw = await fs5.readFile(file, "utf8");
|
|
1780
|
-
} catch {
|
|
1781
|
-
return { input: 0, output: 0 };
|
|
1782
|
-
}
|
|
1926
|
+
function sumGeminiRaw(file, raw) {
|
|
1783
1927
|
const messages = file.endsWith(".jsonl") ? parseJsonl(raw) : parseJson(raw);
|
|
1784
1928
|
let input = 0;
|
|
1785
1929
|
let output = 0;
|
|
@@ -1815,6 +1959,21 @@ function parseJsonl(raw) {
|
|
|
1815
1959
|
function isStall(r) {
|
|
1816
1960
|
return "stall" in r;
|
|
1817
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
|
+
}
|
|
1818
1977
|
var SCALAR_READERS = {
|
|
1819
1978
|
claude: sumTokens,
|
|
1820
1979
|
codex: sumCodexTokens,
|
|
@@ -1828,25 +1987,71 @@ var BY_CONVERSATION_READERS = {
|
|
|
1828
1987
|
function scoreFor(race, t) {
|
|
1829
1988
|
return race.counts_input ? t.input + t.output : t.output;
|
|
1830
1989
|
}
|
|
1831
|
-
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
|
+
]);
|
|
1832
2005
|
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
|
-
}
|
|
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)}` };
|
|
1840
2011
|
}
|
|
1841
2012
|
const secondary = { claude: 0, codex: 0, gemini: 0 };
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
})
|
|
1846
|
-
);
|
|
2013
|
+
secondaryKeys.forEach((k, i) => {
|
|
2014
|
+
secondary[k] = secondaryValues[i] ?? 0;
|
|
2015
|
+
});
|
|
1847
2016
|
return { secondary, primaryByConv };
|
|
1848
2017
|
}
|
|
1849
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
|
+
|
|
1850
2055
|
// src/tokens/primary-cap.ts
|
|
1851
2056
|
var PRIMARY_TOP_CONVERSATIONS = 5;
|
|
1852
2057
|
function primaryConversationCap(enabled) {
|
|
@@ -2006,19 +2211,21 @@ function RunRace({ active, initialState, pendingMode, ownUserName }) {
|
|
|
2006
2211
|
}, [race?.status]);
|
|
2007
2212
|
useEffect2(() => {
|
|
2008
2213
|
const tracker = trackerRef.current;
|
|
2009
|
-
const
|
|
2214
|
+
const scanBeat = async () => {
|
|
2215
|
+
const progress = new ScanProgress();
|
|
2010
2216
|
try {
|
|
2011
|
-
return await
|
|
2012
|
-
readAllSources(active, active.primary_model),
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
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)}` };
|
|
2017
2224
|
}
|
|
2018
2225
|
};
|
|
2019
2226
|
runHeartbeatLoop({
|
|
2020
2227
|
prepareBeat: async () => {
|
|
2021
|
-
const reading = await
|
|
2228
|
+
const reading = await scanBeat();
|
|
2022
2229
|
tracker.recordReading(reading);
|
|
2023
2230
|
if (pendingRef.current && !isStall(reading)) tracker.reprime();
|
|
2024
2231
|
setStalled(tracker.stalled);
|