tokentracker-cli 0.84.4 → 0.84.6
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/dashboard/dist/assets/{AchievementBadge-C29PwsK_.js → AchievementBadge-CKIvD3lG.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-BlZODLp8.js → AchievementsPage-CZWCUiS5.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-Bc6NMnIa.js → AchievementsSection-BSk_fR8P.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-BUy4636q.js → BadgeDetailModal-CjHkcmuq.js} +1 -1
- package/dashboard/dist/assets/{Card-CqxuFMAN.js → Card-D6hdzwvI.js} +1 -1
- package/dashboard/dist/assets/{ClawdAnimated-BG3m8BgW.js → ClawdAnimated-Cl6gmF7u.js} +1 -1
- package/dashboard/dist/assets/{CommandPalette-Dcwtom2b.js → CommandPalette-B0rcodcF.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-dBjHrgjE.js → CommunityStatsModal-BtlTJbf4.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-DncQkptq.js → ConfirmModal-tJCWVZQH.js} +1 -1
- package/dashboard/dist/assets/{DashboardPage-CdF5WNHN.js → DashboardPage-Dtxq8dzI.js} +1 -1
- package/dashboard/dist/assets/{DevicePage-B-PO4s-r.js → DevicePage-9USuEsRu.js} +1 -1
- package/dashboard/dist/assets/{DialogClose-DGtj17VY.js → DialogClose-DxHdqMcl.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-DgUSe6A1.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-DLkuu1Iw.js → DialogTitle-C1ZeQgf2.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-Dd5RBmXd.js → FadeIn-Cx4jLl9G.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-BBCOpHqO.js → HeaderGithubStar-BOyuGkoA.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-BbigYxdC.js → HoverTooltip-BDhqMR3L.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-BBpZYjSC.js → IpCheckPage-DTvA9Wmx.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-ttXnt_s9.js → LandingPage-k1271lWV.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardAvatar-DOl_cOO9.js → LeaderboardAvatar-3ibNg6If.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardPage-BYIyoLkB.js → LeaderboardPage-Byww5UpT.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProfileModal-BO2n4LSQ.js → LeaderboardProfileModal-DeGbJtvi.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-CqwW5yMw.js → LeaderboardProfilePage-DEPzO6hW.js} +1 -1
- package/dashboard/dist/assets/{LimitsPage-B-w771Tl.js → LimitsPage-DPtqL0PS.js} +1 -1
- package/dashboard/dist/assets/{LocalOnlyNotice-Cqc_diwF.js → LocalOnlyNotice-C76LAT_3.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-D6zc1JKY.js → LoginCard-DEyCJT67.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-udN3TqoF.js → LoginModal-CueMQgAZ.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-BucPr3_X.js → LoginPage-CwqBb5Fd.js} +1 -1
- package/dashboard/dist/assets/{PetPage-6_nmMXWp.js → PetPage--xQAjW4_.js} +1 -1
- package/dashboard/dist/assets/{PopoverPopup-DntczBmH.js → PopoverPopup-Dt9VY2VQ.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-Z6qlwSj1.js → ProviderIcon-CHx4ohfx.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-WqQwsQC1.js → ResetPasswordPage-DB6R7fjf.js} +1 -1
- package/dashboard/dist/assets/{SegmentedControl-D3NM3dOv.js → SegmentedControl-BINOpXIY.js} +1 -1
- package/dashboard/dist/assets/{Select-DCpwSFK7.js → Select-DcYXcsKv.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-DICaclqa.js → SelectItemText-BDv2Kb3x.js} +1 -1
- package/dashboard/dist/assets/{SessionsPage-COHoSjPP.js → SessionsPage-cL_w0ytB.js} +1 -1
- package/dashboard/dist/assets/{SettingsPage-nzbrBo_U.js → SettingsPage-CHGA0bQS.js} +1 -1
- package/dashboard/dist/assets/{SkillsPage-DE8mK1lS.js → SkillsPage-I29SqkXO.js} +1 -1
- package/dashboard/dist/assets/{TokenGalaxy-BSWGbByr.js → TokenGalaxy-BHq4YSv2.js} +1 -1
- package/dashboard/dist/assets/{ToolbarRootContext-CxxXTuTT.js → ToolbarRootContext-akXaaKb6.js} +1 -1
- package/dashboard/dist/assets/{TrendMonitor-DillSTMV.js → TrendMonitor-DPhFXzQc.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-LqyxKYAt.js → WidgetsPage-Nf0UEz7s.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-DNDuRKjf.js → WrappedPage-D4zOSsqD.js} +1 -1
- package/dashboard/dist/assets/{__vite-browser-external-DEhUaWQY.js → __vite-browser-external-UqlSqI-1.js} +2 -2
- package/dashboard/dist/assets/{agent-logos-D34B25op.js → agent-logos-Bja3VCAa.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-BYWbL4WV.js → arrow-up-right-CzcCNJJ2.js} +1 -1
- package/dashboard/dist/assets/{check-DkfaR6JX.js → check-DUHSZCYn.js} +1 -1
- package/dashboard/dist/assets/{chevron-right-D2cpt_J3.js → chevron-right-jLvhaNUy.js} +1 -1
- package/dashboard/dist/assets/{copy-Cv5Ix0Pn.js → copy-ChyH-ynx.js} +1 -1
- package/dashboard/dist/assets/{download-5nr9Tl80.js → download-CgwvXrUe.js} +1 -1
- package/dashboard/dist/assets/{flame-B6M-msYH.js → flame-ysf1F8uB.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-DiRRwgY8.js → format-tokens-DAa2cubl.js} +1 -1
- package/dashboard/dist/assets/{icons-DQHoG48j.js → icons-DccoWQq9.js} +1 -1
- package/dashboard/dist/assets/{index-hSxFduER.js → index-CAycNPQf.js} +1 -1
- package/dashboard/dist/assets/{index-BoH_HaYH.js → index-CxExExXh.js} +1 -1
- package/dashboard/dist/assets/{index-vFee2-sp.js → index-kkQrDRat.js} +1 -1
- package/dashboard/dist/assets/{info-CM5MR8lk.js → info-DWQQIq9M.js} +1 -1
- package/dashboard/dist/assets/{limits-providers-BV4Y9nI4.js → limits-providers-B_18X9Rp.js} +1 -1
- package/dashboard/dist/assets/{link-2-Bd5jF09Z.js → link-2-CKeVVq4P.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-B5oZJSYV.js → loader-circle-Dw0tu6IN.js} +1 -1
- package/dashboard/dist/assets/{maximize-2-CiYy9Kri.js → maximize-2-BMU0YzEF.js} +1 -1
- package/dashboard/dist/assets/{provider-display-lzLynfde.js → provider-display-Cf9I-VLS.js} +1 -1
- package/dashboard/dist/assets/{react-CllryaXi.js → react-BhbcISKy.js} +1 -1
- package/dashboard/dist/assets/{search-DDdsotHp.js → search-BoY8EW1z.js} +1 -1
- package/dashboard/dist/assets/{skills-api-487vZmwI.js → skills-api-KVH21GJw.js} +1 -1
- package/dashboard/dist/assets/{store-CYaTRd_k.js → store-BL7rSajG.js} +1 -1
- package/dashboard/dist/assets/{terminal-OyeOY0tb.js → terminal-CeMuITdm.js} +1 -1
- package/dashboard/dist/assets/{trash-2-COI9stXJ.js → trash-2-jVUCKqrR.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-DkzinngC.js → use-community-stats-uWK0fE6l.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-CiMETU5Y.js → use-limits-display-prefs-DWsF4ShM.js} +1 -1
- package/dashboard/dist/assets/{use-native-settings-qzpxm9Bh.js → use-native-settings-SEbqWDvc.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-BI-Qh9xR.js → use-ordered-list-D4UhrPvw.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-C3UrOh55.js +1 -0
- package/dashboard/dist/assets/{use-session-efficiency-pref-1WyhIjeG.js → use-session-efficiency-pref-DbysyUTJ.js} +1 -1
- package/dashboard/dist/assets/{use-transform-D5i_ArzG.js → use-transform-DaEssGaZ.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-Drcnt7__.js → use-usage-limits-CmmQ34Ov.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-KWJo1GIr.js → useCurrency-DvBEn4rx.js} +1 -1
- package/dashboard/dist/assets/{useScrollLock-K0FPky1Y.js → useScrollLock-QozsdFng.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-CwG4ck8v.js → useTokenFormat-BTLrL7Mi.js} +1 -1
- package/dashboard/dist/assets/{zap-wZ6IQAyU.js → zap-Dp_aJ1q6.js} +1 -1
- package/dashboard/dist/index.html +1 -1
- package/dashboard/dist/ip-check.html +1 -1
- package/dashboard/dist/leaderboard.html +1 -1
- package/dashboard/dist/share.html +1 -1
- package/package.json +1 -1
- package/src/commands/status.js +17 -7
- package/src/commands/sync.js +333 -5
- package/src/lib/codex-rollout-parser.js +17 -39
- package/src/lib/codex-token-usage.js +195 -0
- package/src/lib/pricing/curated-overrides.json +536 -101
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +732 -163
- package/src/lib/usage-limits.js +211 -41
- package/dashboard/dist/assets/DialogDescription-wStLk8Br.js +0 -1
- package/dashboard/dist/assets/use-reduced-motion-DrsPn1ux.js +0 -1
package/src/lib/rollout.js
CHANGED
|
@@ -8,6 +8,11 @@ const { ensureDir } = require("./fs");
|
|
|
8
8
|
const { readSqliteJsonRows, readSqliteJsonRowsAsync } = require("./sqlite-reader");
|
|
9
9
|
const wsl = require("./wsl-probe");
|
|
10
10
|
const { resolveInstallPaths } = require("./install-resolver");
|
|
11
|
+
const {
|
|
12
|
+
consumeUsageDelta,
|
|
13
|
+
createUsageDeltaState,
|
|
14
|
+
snapshotUsageBaselines,
|
|
15
|
+
} = require("./codex-token-usage");
|
|
11
16
|
|
|
12
17
|
const DEFAULT_SOURCE = "codex";
|
|
13
18
|
const DEFAULT_MODEL = "unknown";
|
|
@@ -360,6 +365,9 @@ async function parseRolloutIncremental({
|
|
|
360
365
|
const lastTotal = sameInode && !truncated && !rebuildingCodexBaseline
|
|
361
366
|
? prev.lastTotal || null
|
|
362
367
|
: null;
|
|
368
|
+
const tokenUsageBaselines = sameInode && !truncated && !rebuildingCodexBaseline
|
|
369
|
+
? prev.tokenUsageBaselines || null
|
|
370
|
+
: null;
|
|
363
371
|
const lastModel = sameInode && !truncated ? prev.lastModel || null : null;
|
|
364
372
|
|
|
365
373
|
const codexProjectFastPath = projectEnabled && fileSource === DEFAULT_SOURCE;
|
|
@@ -430,6 +438,7 @@ async function parseRolloutIncremental({
|
|
|
430
438
|
filePath,
|
|
431
439
|
fileStat: st,
|
|
432
440
|
lastTotal,
|
|
441
|
+
tokenUsageBaselines,
|
|
433
442
|
lastModel,
|
|
434
443
|
projectState,
|
|
435
444
|
projectMetaCache,
|
|
@@ -442,6 +451,7 @@ async function parseRolloutIncremental({
|
|
|
442
451
|
fileStat: st,
|
|
443
452
|
startOffset,
|
|
444
453
|
lastTotal,
|
|
454
|
+
tokenUsageBaselines,
|
|
445
455
|
lastModel,
|
|
446
456
|
hourlyState,
|
|
447
457
|
touchedBuckets,
|
|
@@ -462,6 +472,7 @@ async function parseRolloutIncremental({
|
|
|
462
472
|
inode,
|
|
463
473
|
offset: result.endOffset,
|
|
464
474
|
lastTotal: result.lastTotal,
|
|
475
|
+
tokenUsageBaselines: result.tokenUsageBaselines,
|
|
465
476
|
lastModel: result.lastModel,
|
|
466
477
|
updatedAt: new Date().toISOString(),
|
|
467
478
|
};
|
|
@@ -1731,6 +1742,7 @@ async function parseRolloutFile({
|
|
|
1731
1742
|
fileStat,
|
|
1732
1743
|
startOffset,
|
|
1733
1744
|
lastTotal,
|
|
1745
|
+
tokenUsageBaselines,
|
|
1734
1746
|
lastModel,
|
|
1735
1747
|
hourlyState,
|
|
1736
1748
|
touchedBuckets,
|
|
@@ -1751,14 +1763,25 @@ async function parseRolloutFile({
|
|
|
1751
1763
|
const projectFileContexts = [];
|
|
1752
1764
|
addProjectFileContext(projectFileContexts, projectContext);
|
|
1753
1765
|
if (startOffset >= endOffset) {
|
|
1754
|
-
return {
|
|
1766
|
+
return {
|
|
1767
|
+
endOffset,
|
|
1768
|
+
lastTotal,
|
|
1769
|
+
tokenUsageBaselines,
|
|
1770
|
+
lastModel,
|
|
1771
|
+
eventsAggregated: 0,
|
|
1772
|
+
projectFileContexts,
|
|
1773
|
+
};
|
|
1755
1774
|
}
|
|
1756
1775
|
|
|
1757
1776
|
const stream = fssync.createReadStream(filePath, { encoding: "utf8", start: startOffset });
|
|
1758
1777
|
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
1759
1778
|
|
|
1760
1779
|
let model = typeof lastModel === "string" ? lastModel : null;
|
|
1761
|
-
|
|
1780
|
+
const usageDeltaState = createUsageDeltaState({
|
|
1781
|
+
lastTotal,
|
|
1782
|
+
baselines: tokenUsageBaselines,
|
|
1783
|
+
});
|
|
1784
|
+
let latestTotal = lastTotal && typeof lastTotal === "object" ? lastTotal : null;
|
|
1762
1785
|
let currentCwd = null;
|
|
1763
1786
|
let currentDate = null;
|
|
1764
1787
|
let isForkedRollout = false;
|
|
@@ -1840,15 +1863,13 @@ async function parseRolloutFile({
|
|
|
1840
1863
|
|
|
1841
1864
|
const lastUsage = info.last_token_usage;
|
|
1842
1865
|
const totalUsage = info.total_token_usage;
|
|
1866
|
+
if (totalUsage && typeof totalUsage === "object") latestTotal = totalUsage;
|
|
1843
1867
|
|
|
1844
|
-
const
|
|
1845
|
-
|
|
1868
|
+
const rawDelta = consumeUsageDelta(usageDeltaState, lastUsage, totalUsage);
|
|
1869
|
+
const delta = rawDelta ? normalizeUsage(rawDelta) : null;
|
|
1870
|
+
if (!delta || isAllZeroUsage(delta)) continue;
|
|
1846
1871
|
delta.conversation_count = 1;
|
|
1847
1872
|
|
|
1848
|
-
if (totalUsage && typeof totalUsage === "object") {
|
|
1849
|
-
totals = totalUsage;
|
|
1850
|
-
}
|
|
1851
|
-
|
|
1852
1873
|
// Forked Codex rollouts replay the parent session's entire token history
|
|
1853
1874
|
// into the child file the moment the fork is created. The date guard below
|
|
1854
1875
|
// (current_date < rolloutDate) catches cross-day forks, but same-day forks
|
|
@@ -1863,8 +1884,8 @@ async function parseRolloutFile({
|
|
|
1863
1884
|
// flush spacing and well below genuine turn cadence (≥~4.6s observed). The
|
|
1864
1885
|
// first replayed row cannot be identified without lookahead, so it is still
|
|
1865
1886
|
// counted (a bounded, <1% residual over-count); dropping real usage is the
|
|
1866
|
-
// worse failure, so we bias against it. `
|
|
1867
|
-
// keeping the cumulative
|
|
1887
|
+
// worse failure, so we bias against it. `usageDeltaState` is already advanced above,
|
|
1888
|
+
// keeping the cumulative lineage correct for the live turns we keep.
|
|
1868
1889
|
// Scoped to forked codex rollouts. (issue #169 follow-up.)
|
|
1869
1890
|
let forkedReplaySkip = false;
|
|
1870
1891
|
if (isForkedRollout && source === DEFAULT_SOURCE && replayPrefixActive) {
|
|
@@ -1903,8 +1924,8 @@ async function parseRolloutFile({
|
|
|
1903
1924
|
// buckets. External tools rewrite session files without changing the token
|
|
1904
1925
|
// data — Codex-Manager atomically rewrites them (new inode) to patch the
|
|
1905
1926
|
// provider on every account/channel switch — so without dedup each switch
|
|
1906
|
-
// double-counts the rewritten sessions. `
|
|
1907
|
-
// so skipping an already-seen event keeps the cumulative
|
|
1927
|
+
// double-counts the rewritten sessions. `usageDeltaState` is already advanced above,
|
|
1928
|
+
// so skipping an already-seen event keeps the cumulative lineage intact
|
|
1908
1929
|
// while preventing the re-add; genuinely new turns carry new timestamps and
|
|
1909
1930
|
// are still counted. Key = sessionUUID:eventTimestamp (both stable across the
|
|
1910
1931
|
// rewrite and across a sessions/ -> archived_sessions/ move).
|
|
@@ -1939,13 +1960,21 @@ async function parseRolloutFile({
|
|
|
1939
1960
|
eventsAggregated += 1;
|
|
1940
1961
|
}
|
|
1941
1962
|
|
|
1942
|
-
return {
|
|
1963
|
+
return {
|
|
1964
|
+
endOffset,
|
|
1965
|
+
lastTotal: latestTotal,
|
|
1966
|
+
tokenUsageBaselines: snapshotUsageBaselines(usageDeltaState),
|
|
1967
|
+
lastModel: model,
|
|
1968
|
+
eventsAggregated,
|
|
1969
|
+
projectFileContexts,
|
|
1970
|
+
};
|
|
1943
1971
|
}
|
|
1944
1972
|
|
|
1945
1973
|
async function scanRolloutProjectFileContexts({
|
|
1946
1974
|
filePath,
|
|
1947
1975
|
fileStat,
|
|
1948
1976
|
lastTotal,
|
|
1977
|
+
tokenUsageBaselines,
|
|
1949
1978
|
lastModel,
|
|
1950
1979
|
projectState,
|
|
1951
1980
|
projectMetaCache,
|
|
@@ -1958,7 +1987,14 @@ async function scanRolloutProjectFileContexts({
|
|
|
1958
1987
|
const projectFileContexts = [];
|
|
1959
1988
|
addProjectFileContext(projectFileContexts, projectContext);
|
|
1960
1989
|
if (!projectState || endOffset <= 0) {
|
|
1961
|
-
return {
|
|
1990
|
+
return {
|
|
1991
|
+
endOffset,
|
|
1992
|
+
lastTotal,
|
|
1993
|
+
tokenUsageBaselines,
|
|
1994
|
+
lastModel,
|
|
1995
|
+
eventsAggregated: 0,
|
|
1996
|
+
projectFileContexts,
|
|
1997
|
+
};
|
|
1962
1998
|
}
|
|
1963
1999
|
|
|
1964
2000
|
const stream = fssync.createReadStream(filePath, { encoding: "utf8", start: 0 });
|
|
@@ -2005,7 +2041,14 @@ async function scanRolloutProjectFileContexts({
|
|
|
2005
2041
|
addProjectFileContext(projectFileContexts, context);
|
|
2006
2042
|
}
|
|
2007
2043
|
|
|
2008
|
-
return {
|
|
2044
|
+
return {
|
|
2045
|
+
endOffset,
|
|
2046
|
+
lastTotal,
|
|
2047
|
+
tokenUsageBaselines,
|
|
2048
|
+
lastModel,
|
|
2049
|
+
eventsAggregated: 0,
|
|
2050
|
+
projectFileContexts,
|
|
2051
|
+
};
|
|
2009
2052
|
}
|
|
2010
2053
|
|
|
2011
2054
|
async function parseClaudeFile({
|
|
@@ -3570,48 +3613,6 @@ function extractTokenCount(obj) {
|
|
|
3570
3613
|
return null;
|
|
3571
3614
|
}
|
|
3572
3615
|
|
|
3573
|
-
function pickDelta(lastUsage, totalUsage, prevTotals) {
|
|
3574
|
-
const hasLast = isNonEmptyObject(lastUsage);
|
|
3575
|
-
const hasTotal = isNonEmptyObject(totalUsage);
|
|
3576
|
-
const hasPrevTotals = isNonEmptyObject(prevTotals);
|
|
3577
|
-
|
|
3578
|
-
if (hasTotal && hasPrevTotals) {
|
|
3579
|
-
if (totalsReset(totalUsage, prevTotals)) {
|
|
3580
|
-
const resetUsage = hasLast ? lastUsage : totalUsage;
|
|
3581
|
-
const normalized = normalizeUsage(resetUsage);
|
|
3582
|
-
return isAllZeroUsage(normalized) ? null : normalized;
|
|
3583
|
-
}
|
|
3584
|
-
|
|
3585
|
-
const delta = {};
|
|
3586
|
-
for (const k of [
|
|
3587
|
-
"input_tokens",
|
|
3588
|
-
"cached_input_tokens",
|
|
3589
|
-
"cache_creation_input_tokens",
|
|
3590
|
-
"output_tokens",
|
|
3591
|
-
"reasoning_output_tokens",
|
|
3592
|
-
"total_tokens",
|
|
3593
|
-
]) {
|
|
3594
|
-
const a = Number(totalUsage[k]);
|
|
3595
|
-
const b = Number(prevTotals[k]);
|
|
3596
|
-
if (Number.isFinite(a) && Number.isFinite(b)) delta[k] = Math.max(0, a - b);
|
|
3597
|
-
}
|
|
3598
|
-
const normalized = normalizeUsage(delta);
|
|
3599
|
-
return isAllZeroUsage(normalized) ? null : normalized;
|
|
3600
|
-
}
|
|
3601
|
-
|
|
3602
|
-
if (hasLast) {
|
|
3603
|
-
const normalized = normalizeUsage(lastUsage);
|
|
3604
|
-
return isAllZeroUsage(normalized) ? null : normalized;
|
|
3605
|
-
}
|
|
3606
|
-
|
|
3607
|
-
if (hasTotal) {
|
|
3608
|
-
const normalized = normalizeUsage(totalUsage);
|
|
3609
|
-
return isAllZeroUsage(normalized) ? null : normalized;
|
|
3610
|
-
}
|
|
3611
|
-
|
|
3612
|
-
return null;
|
|
3613
|
-
}
|
|
3614
|
-
|
|
3615
3616
|
function normalizeUsage(u) {
|
|
3616
3617
|
const out = {};
|
|
3617
3618
|
for (const k of [
|
|
@@ -3694,13 +3695,6 @@ function isAllZeroUsage(u) {
|
|
|
3694
3695
|
return true;
|
|
3695
3696
|
}
|
|
3696
3697
|
|
|
3697
|
-
function totalsReset(curr, prev) {
|
|
3698
|
-
const currTotal = curr?.total_tokens;
|
|
3699
|
-
const prevTotal = prev?.total_tokens;
|
|
3700
|
-
if (!isFiniteNumber(currTotal) || !isFiniteNumber(prevTotal)) return false;
|
|
3701
|
-
return currTotal < prevTotal;
|
|
3702
|
-
}
|
|
3703
|
-
|
|
3704
3698
|
function isFiniteNumber(v) {
|
|
3705
3699
|
return typeof v === "number" && Number.isFinite(v);
|
|
3706
3700
|
}
|
|
@@ -4940,32 +4934,77 @@ function resolveKiroCliDbPath(env = process.env) {
|
|
|
4940
4934
|
// no hyphens. kiro-cli writes proper UUIDs; lock to the canonical shape.
|
|
4941
4935
|
const KIRO_CLI_SESSION_FILE_RE =
|
|
4942
4936
|
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.json$/i;
|
|
4943
|
-
|
|
4944
|
-
//
|
|
4945
|
-
//
|
|
4946
|
-
//
|
|
4947
|
-
|
|
4948
|
-
|
|
4937
|
+
// The directory prefix and direct messages.jsonl child are the stable
|
|
4938
|
+
// contract. Do not assume the suffix will always remain a UUID: Kiro has
|
|
4939
|
+
// already changed this layout once, and an opaque non-empty session id is
|
|
4940
|
+
// sufficient to keep discovery both bounded and forward-compatible.
|
|
4941
|
+
const KIRO_CLI_V2_SESSION_DIR_RE = /^sess_.+$/;
|
|
4942
|
+
|
|
4943
|
+
// Lists both Kiro CLI session layouts:
|
|
4944
|
+
// legacy: ~/.kiro/sessions/cli/{uuid}.json
|
|
4945
|
+
// 2.13+: ~/.kiro/sessions/{workspaceHash}/sess_{uuid}/messages.jsonl
|
|
4946
|
+
//
|
|
4947
|
+
// Legacy .json files are rewritten atomically per turn. The 2.13+ JSONL is
|
|
4948
|
+
// append-only and may be read while a session is active. Only the direct
|
|
4949
|
+
// messages.jsonl child is accepted; sub-executions and arbitrary nested JSONL
|
|
4950
|
+
// files are intentionally excluded.
|
|
4949
4951
|
//
|
|
4950
4952
|
// TASK-014: env.HOME is honored (symmetric with resolveKiroCliDbPath) so
|
|
4951
4953
|
// callers can redirect to a tmp home for hermetic tests/CI.
|
|
4952
4954
|
function resolveKiroCliSessionFiles(env = process.env) {
|
|
4953
4955
|
const home = env.HOME || require("node:os").homedir();
|
|
4954
4956
|
const kiroHome = env.KIRO_HOME || path.join(home, ".kiro");
|
|
4955
|
-
const
|
|
4956
|
-
if (!fssync.existsSync(
|
|
4957
|
+
const sessionsRoot = path.join(kiroHome, "sessions");
|
|
4958
|
+
if (!fssync.existsSync(sessionsRoot)) return [];
|
|
4957
4959
|
const files = [];
|
|
4960
|
+
|
|
4961
|
+
const legacyDir = path.join(sessionsRoot, "cli");
|
|
4958
4962
|
try {
|
|
4959
|
-
for (const entry of fssync.readdirSync(
|
|
4963
|
+
for (const entry of fssync.readdirSync(legacyDir)) {
|
|
4960
4964
|
// TASK-003: only canonical {uuid}.json files; backups, scratch,
|
|
4961
4965
|
// typos are skipped so they don't feed JSON.parse garbage.
|
|
4962
4966
|
if (!KIRO_CLI_SESSION_FILE_RE.test(entry)) continue;
|
|
4963
|
-
files.push(path.join(
|
|
4967
|
+
files.push(path.join(legacyDir, entry));
|
|
4964
4968
|
}
|
|
4965
4969
|
} catch {
|
|
4966
4970
|
// ignore read errors
|
|
4967
4971
|
}
|
|
4968
|
-
|
|
4972
|
+
|
|
4973
|
+
try {
|
|
4974
|
+
const workspaceDirs = fssync.readdirSync(sessionsRoot, {
|
|
4975
|
+
withFileTypes: true,
|
|
4976
|
+
});
|
|
4977
|
+
for (const workspace of workspaceDirs) {
|
|
4978
|
+
if (!workspace.isDirectory() || workspace.name === "cli") continue;
|
|
4979
|
+
const workspacePath = path.join(sessionsRoot, workspace.name);
|
|
4980
|
+
let sessionDirs;
|
|
4981
|
+
try {
|
|
4982
|
+
sessionDirs = fssync.readdirSync(workspacePath, {
|
|
4983
|
+
withFileTypes: true,
|
|
4984
|
+
});
|
|
4985
|
+
} catch {
|
|
4986
|
+
continue;
|
|
4987
|
+
}
|
|
4988
|
+
for (const session of sessionDirs) {
|
|
4989
|
+
if (
|
|
4990
|
+
!session.isDirectory() ||
|
|
4991
|
+
!KIRO_CLI_V2_SESSION_DIR_RE.test(session.name)
|
|
4992
|
+
) {
|
|
4993
|
+
continue;
|
|
4994
|
+
}
|
|
4995
|
+
const messagesPath = path.join(
|
|
4996
|
+
workspacePath,
|
|
4997
|
+
session.name,
|
|
4998
|
+
"messages.jsonl",
|
|
4999
|
+
);
|
|
5000
|
+
if (fssync.existsSync(messagesPath)) files.push(messagesPath);
|
|
5001
|
+
}
|
|
5002
|
+
}
|
|
5003
|
+
} catch {
|
|
5004
|
+
// ignore read errors
|
|
5005
|
+
}
|
|
5006
|
+
|
|
5007
|
+
return files.sort();
|
|
4969
5008
|
}
|
|
4970
5009
|
|
|
4971
5010
|
// Build char-count maps from a .jsonl sibling file. Lets us approximate
|
|
@@ -5183,6 +5222,199 @@ async function readKiroCliSessionTurns(jsonPath) {
|
|
|
5183
5222
|
return flat;
|
|
5184
5223
|
}
|
|
5185
5224
|
|
|
5225
|
+
function kiroCliV2ContentChars(value) {
|
|
5226
|
+
if (typeof value === "string") return value.length;
|
|
5227
|
+
if (Array.isArray(value)) {
|
|
5228
|
+
return value.reduce(
|
|
5229
|
+
(sum, item) => sum + kiroCliV2ContentChars(item),
|
|
5230
|
+
0,
|
|
5231
|
+
);
|
|
5232
|
+
}
|
|
5233
|
+
if (!value || typeof value !== "object") return 0;
|
|
5234
|
+
for (const key of ["content", "text", "value", "parts", "entries"]) {
|
|
5235
|
+
if (value[key] !== undefined) {
|
|
5236
|
+
return kiroCliV2ContentChars(value[key]);
|
|
5237
|
+
}
|
|
5238
|
+
}
|
|
5239
|
+
return 0;
|
|
5240
|
+
}
|
|
5241
|
+
|
|
5242
|
+
// Kiro CLI 2.13+ writes event-sourced sessions under
|
|
5243
|
+
// ~/.kiro/sessions/<workspaceHash>/sess_<uuid>/messages.jsonl. The records do
|
|
5244
|
+
// not carry token counts, but user/assistant text, turn boundaries, timestamps,
|
|
5245
|
+
// and assistant reasoningModelId are sufficient for the same 4 chars/token
|
|
5246
|
+
// approximation used by the legacy CLI reader.
|
|
5247
|
+
async function readKiroCliV2SessionTurns(messagesPath) {
|
|
5248
|
+
if (
|
|
5249
|
+
!messagesPath ||
|
|
5250
|
+
path.basename(messagesPath) !== "messages.jsonl" ||
|
|
5251
|
+
!fssync.existsSync(messagesPath)
|
|
5252
|
+
) {
|
|
5253
|
+
return [];
|
|
5254
|
+
}
|
|
5255
|
+
|
|
5256
|
+
const sessionDir = path.dirname(messagesPath);
|
|
5257
|
+
let sessionMeta = {};
|
|
5258
|
+
try {
|
|
5259
|
+
sessionMeta = JSON.parse(
|
|
5260
|
+
fssync.readFileSync(path.join(sessionDir, "session.json"), "utf8"),
|
|
5261
|
+
);
|
|
5262
|
+
} catch {
|
|
5263
|
+
// messages.jsonl is self-contained enough to parse without session.json.
|
|
5264
|
+
}
|
|
5265
|
+
|
|
5266
|
+
const sessionId =
|
|
5267
|
+
typeof sessionMeta?.id === "string" && sessionMeta.id
|
|
5268
|
+
? sessionMeta.id
|
|
5269
|
+
: path.basename(sessionDir);
|
|
5270
|
+
const fallbackModel =
|
|
5271
|
+
typeof sessionMeta?.modelId === "string" ? sessionMeta.modelId : null;
|
|
5272
|
+
const fallbackTimestamp = Date.parse(
|
|
5273
|
+
sessionMeta?.createdAt || sessionMeta?.lastModifiedAt || "",
|
|
5274
|
+
);
|
|
5275
|
+
|
|
5276
|
+
const flat = [];
|
|
5277
|
+
let stream;
|
|
5278
|
+
try {
|
|
5279
|
+
stream = fssync.createReadStream(messagesPath, { encoding: "utf8" });
|
|
5280
|
+
} catch {
|
|
5281
|
+
return flat;
|
|
5282
|
+
}
|
|
5283
|
+
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
5284
|
+
|
|
5285
|
+
let pendingUserChars = 0;
|
|
5286
|
+
let pendingUserTimestampMs = NaN;
|
|
5287
|
+
let turn = null;
|
|
5288
|
+
let fallbackTurnIndex = 0;
|
|
5289
|
+
|
|
5290
|
+
const flushTurn = () => {
|
|
5291
|
+
if (!turn) return;
|
|
5292
|
+
const hasUsage =
|
|
5293
|
+
turn.outputChars > 0 ||
|
|
5294
|
+
turn.reasoningChars > 0;
|
|
5295
|
+
const tsMs = Number.isFinite(turn.timestampMs)
|
|
5296
|
+
? turn.timestampMs
|
|
5297
|
+
: Number.isFinite(pendingUserTimestampMs)
|
|
5298
|
+
? pendingUserTimestampMs
|
|
5299
|
+
: fallbackTimestamp;
|
|
5300
|
+
if (hasUsage && Number.isFinite(tsMs) && tsMs > 0) {
|
|
5301
|
+
const requestId =
|
|
5302
|
+
turn.executionId ||
|
|
5303
|
+
`${sessionId}:v2:${fallbackTurnIndex}`;
|
|
5304
|
+
const messageIds = Array.from(turn.messageIds);
|
|
5305
|
+
flat.push({
|
|
5306
|
+
request_id: requestId,
|
|
5307
|
+
message_id: messageIds[0] || turn.executionId || null,
|
|
5308
|
+
all_message_ids: messageIds,
|
|
5309
|
+
session_id: sessionId,
|
|
5310
|
+
session_model_id: fallbackModel,
|
|
5311
|
+
model_id: turn.modelId || fallbackModel,
|
|
5312
|
+
request_start_timestamp_ms: tsMs,
|
|
5313
|
+
user_prompt_length: turn.inputChars,
|
|
5314
|
+
response_size: turn.outputChars,
|
|
5315
|
+
reasoning_size: turn.reasoningChars,
|
|
5316
|
+
});
|
|
5317
|
+
}
|
|
5318
|
+
fallbackTurnIndex++;
|
|
5319
|
+
turn = null;
|
|
5320
|
+
};
|
|
5321
|
+
|
|
5322
|
+
const startTurn = (payload, timestampMs) => {
|
|
5323
|
+
flushTurn();
|
|
5324
|
+
turn = {
|
|
5325
|
+
executionId:
|
|
5326
|
+
typeof payload?.executionId === "string" ? payload.executionId : null,
|
|
5327
|
+
timestampMs:
|
|
5328
|
+
Number.isFinite(timestampMs)
|
|
5329
|
+
? timestampMs
|
|
5330
|
+
: pendingUserTimestampMs,
|
|
5331
|
+
inputChars: pendingUserChars,
|
|
5332
|
+
outputChars: 0,
|
|
5333
|
+
reasoningChars: 0,
|
|
5334
|
+
modelId: null,
|
|
5335
|
+
messageIds: new Set(),
|
|
5336
|
+
};
|
|
5337
|
+
pendingUserChars = 0;
|
|
5338
|
+
pendingUserTimestampMs = NaN;
|
|
5339
|
+
};
|
|
5340
|
+
|
|
5341
|
+
try {
|
|
5342
|
+
for await (const line of rl) {
|
|
5343
|
+
if (!line || !line.trim()) continue;
|
|
5344
|
+
let event;
|
|
5345
|
+
try {
|
|
5346
|
+
event = JSON.parse(line);
|
|
5347
|
+
} catch {
|
|
5348
|
+
continue;
|
|
5349
|
+
}
|
|
5350
|
+
const payload = event?.payload;
|
|
5351
|
+
if (!payload || typeof payload !== "object") continue;
|
|
5352
|
+
const type =
|
|
5353
|
+
typeof payload.type === "string"
|
|
5354
|
+
? payload.type.toLowerCase()
|
|
5355
|
+
: "";
|
|
5356
|
+
const timestampMs = Date.parse(event.timestamp || "");
|
|
5357
|
+
|
|
5358
|
+
if (type === "user") {
|
|
5359
|
+
if (turn) flushTurn();
|
|
5360
|
+
pendingUserChars = kiroCliV2ContentChars(payload.content);
|
|
5361
|
+
pendingUserTimestampMs = timestampMs;
|
|
5362
|
+
continue;
|
|
5363
|
+
}
|
|
5364
|
+
|
|
5365
|
+
if (type === "turn_start") {
|
|
5366
|
+
startTurn(payload, timestampMs);
|
|
5367
|
+
continue;
|
|
5368
|
+
}
|
|
5369
|
+
|
|
5370
|
+
if (
|
|
5371
|
+
["assistant", "tool_call", "tool_result", "usage_summary"].includes(
|
|
5372
|
+
type,
|
|
5373
|
+
) &&
|
|
5374
|
+
!turn
|
|
5375
|
+
) {
|
|
5376
|
+
startTurn(payload, timestampMs);
|
|
5377
|
+
}
|
|
5378
|
+
if (!turn) continue;
|
|
5379
|
+
|
|
5380
|
+
if (
|
|
5381
|
+
typeof payload.executionId === "string" &&
|
|
5382
|
+
payload.executionId &&
|
|
5383
|
+
!turn.executionId
|
|
5384
|
+
) {
|
|
5385
|
+
turn.executionId = payload.executionId;
|
|
5386
|
+
}
|
|
5387
|
+
if (typeof event.id === "string" && event.id) {
|
|
5388
|
+
turn.messageIds.add(event.id);
|
|
5389
|
+
}
|
|
5390
|
+
|
|
5391
|
+
if (type === "assistant") {
|
|
5392
|
+
const chars = kiroCliV2ContentChars(payload.content);
|
|
5393
|
+
if (
|
|
5394
|
+
typeof payload.operationType === "string" &&
|
|
5395
|
+
payload.operationType.toLowerCase() === "reasoning"
|
|
5396
|
+
) {
|
|
5397
|
+
turn.reasoningChars += chars;
|
|
5398
|
+
} else {
|
|
5399
|
+
turn.outputChars += chars;
|
|
5400
|
+
}
|
|
5401
|
+
if (
|
|
5402
|
+
typeof payload.reasoningModelId === "string" &&
|
|
5403
|
+
payload.reasoningModelId
|
|
5404
|
+
) {
|
|
5405
|
+
turn.modelId = payload.reasoningModelId;
|
|
5406
|
+
}
|
|
5407
|
+
} else if (type === "turn_end") {
|
|
5408
|
+
flushTurn();
|
|
5409
|
+
}
|
|
5410
|
+
}
|
|
5411
|
+
} catch {
|
|
5412
|
+
// Return complete turns parsed before a concurrent truncate/delete.
|
|
5413
|
+
}
|
|
5414
|
+
flushTurn();
|
|
5415
|
+
return flat;
|
|
5416
|
+
}
|
|
5417
|
+
|
|
5186
5418
|
// Canonicalize a Kiro-CLI-emitted model id so IDE and CLI rows collapse when
|
|
5187
5419
|
// they refer to the same underlying Bedrock model. Examples:
|
|
5188
5420
|
// anthropic.claude-sonnet-4-20250514-v1:0 -> claude-sonnet-4
|
|
@@ -5202,6 +5434,7 @@ function canonicalizeKiroCliModelId(raw) {
|
|
|
5202
5434
|
if (!name) return null;
|
|
5203
5435
|
name = name.toLowerCase();
|
|
5204
5436
|
if (name === "auto") return null;
|
|
5437
|
+
name = name.replace(/^(?:qdev|kiro)::/, "");
|
|
5205
5438
|
// Strip provider prefix (anthropic., aws., openai., or a full Bedrock ARN).
|
|
5206
5439
|
name = name.replace(
|
|
5207
5440
|
/^(?:arn:aws:bedrock:[^:]*:[^:]*:(?:foundation-model\/)?|anthropic\.|openai\.|aws\.)/,
|
|
@@ -5294,10 +5527,11 @@ async function parseKiroCliIncremental({ sessionFiles, cursors, queuePath, onPro
|
|
|
5294
5527
|
const resolvedEnv = env || process.env;
|
|
5295
5528
|
const dbPath = resolveKiroCliDbPath(resolvedEnv);
|
|
5296
5529
|
|
|
5297
|
-
// Combine
|
|
5298
|
-
// namespace: historical rows from the SQLite DB
|
|
5299
|
-
// from ~/.kiro/sessions/cli/{uuid}.json
|
|
5300
|
-
//
|
|
5530
|
+
// Combine three sources under the same (source='kiro', cursors.kiroCli)
|
|
5531
|
+
// namespace: historical rows from the SQLite DB, legacy live session state
|
|
5532
|
+
// from ~/.kiro/sessions/cli/{uuid}.json, and Kiro CLI 2.13+ event logs at
|
|
5533
|
+
// ~/.kiro/sessions/<workspaceHash>/sess_<uuid>/messages.jsonl. Request ID
|
|
5534
|
+
// shapes differ:
|
|
5301
5535
|
// SQLite carries a persisted request_id UUID; session files synthesize
|
|
5302
5536
|
// `${sessionId}:${loop_id.rand}`. When kiro-cli migrates a live session
|
|
5303
5537
|
// into SQLite the same turn lands under a new request_id — the cross-
|
|
@@ -5309,8 +5543,11 @@ async function parseKiroCliIncremental({ sessionFiles, cursors, queuePath, onPro
|
|
|
5309
5543
|
: [];
|
|
5310
5544
|
const sessionFilesList = resolveKiroCliSessionFiles(resolvedEnv);
|
|
5311
5545
|
let flatSessions = [];
|
|
5312
|
-
for (const
|
|
5313
|
-
const turns =
|
|
5546
|
+
for (const sessionPath of sessionFilesList) {
|
|
5547
|
+
const turns =
|
|
5548
|
+
path.basename(sessionPath) === "messages.jsonl"
|
|
5549
|
+
? await readKiroCliV2SessionTurns(sessionPath)
|
|
5550
|
+
: await readKiroCliSessionTurns(sessionPath);
|
|
5314
5551
|
for (const turn of turns) flatSessions.push(turn);
|
|
5315
5552
|
}
|
|
5316
5553
|
// Per-request state replaces the old seenIds set. Each entry captures
|
|
@@ -5382,7 +5619,14 @@ async function parseKiroCliIncremental({ sessionFiles, cursors, queuePath, onPro
|
|
|
5382
5619
|
toRetract.push([reqId, prev, sid]);
|
|
5383
5620
|
}
|
|
5384
5621
|
for (const [reqId, prev, sid] of toRetract) {
|
|
5385
|
-
if (
|
|
5622
|
+
if (
|
|
5623
|
+
prev.input_tokens ||
|
|
5624
|
+
prev.output_tokens ||
|
|
5625
|
+
prev.reasoning_output_tokens
|
|
5626
|
+
) {
|
|
5627
|
+
const prevReasoning = toNonNegativeInt(
|
|
5628
|
+
prev.reasoning_output_tokens,
|
|
5629
|
+
);
|
|
5386
5630
|
const prevBucket = getHourlyBucket(
|
|
5387
5631
|
hourlyState,
|
|
5388
5632
|
"kiro",
|
|
@@ -5394,8 +5638,12 @@ async function parseKiroCliIncremental({ sessionFiles, cursors, queuePath, onPro
|
|
|
5394
5638
|
cached_input_tokens: 0,
|
|
5395
5639
|
cache_creation_input_tokens: 0,
|
|
5396
5640
|
output_tokens: -prev.output_tokens,
|
|
5397
|
-
reasoning_output_tokens:
|
|
5398
|
-
total_tokens: -(
|
|
5641
|
+
reasoning_output_tokens: -prevReasoning,
|
|
5642
|
+
total_tokens: -(
|
|
5643
|
+
prev.input_tokens +
|
|
5644
|
+
prev.output_tokens +
|
|
5645
|
+
prevReasoning
|
|
5646
|
+
),
|
|
5399
5647
|
conversation_count: -1,
|
|
5400
5648
|
});
|
|
5401
5649
|
touchedBuckets.add(bucketKey("kiro", prev.model, prev.bucketStart));
|
|
@@ -5488,8 +5736,12 @@ async function parseKiroCliIncremental({ sessionFiles, cursors, queuePath, onPro
|
|
|
5488
5736
|
|
|
5489
5737
|
const promptChars = toNonNegativeInt(r.user_prompt_length);
|
|
5490
5738
|
const responseChars = toNonNegativeInt(r.response_size);
|
|
5739
|
+
const reasoningChars = toNonNegativeInt(r.reasoning_size);
|
|
5491
5740
|
const approxInput = Math.floor(promptChars / KIRO_CLI_CHARS_PER_TOKEN);
|
|
5492
5741
|
const approxOutput = Math.floor(responseChars / KIRO_CLI_CHARS_PER_TOKEN);
|
|
5742
|
+
const approxReasoning = Math.floor(
|
|
5743
|
+
reasoningChars / KIRO_CLI_CHARS_PER_TOKEN,
|
|
5744
|
+
);
|
|
5493
5745
|
|
|
5494
5746
|
const tsMs = Number(r.request_start_timestamp_ms);
|
|
5495
5747
|
if (!Number.isFinite(tsMs) || tsMs <= 0) continue;
|
|
@@ -5502,37 +5754,52 @@ async function parseKiroCliIncremental({ sessionFiles, cursors, queuePath, onPro
|
|
|
5502
5754
|
const model = canonical || "kiro-cli-agent";
|
|
5503
5755
|
|
|
5504
5756
|
// Fingerprint captures every field whose change should cause a re-bucket.
|
|
5505
|
-
const fingerprint =
|
|
5757
|
+
const fingerprint =
|
|
5758
|
+
`${promptChars}:${responseChars}:${reasoningChars}:${model}:${tsMs}`;
|
|
5506
5759
|
const prev = requestState[requestId];
|
|
5507
5760
|
if (prev && prev.fingerprint === fingerprint) continue; // unchanged
|
|
5508
5761
|
|
|
5509
5762
|
// Subtract the prior contribution (if any) from its prior bucket so the
|
|
5510
5763
|
// bucket's absolute totals reflect the CURRENT truth, not the historical
|
|
5511
5764
|
// truth. enqueueTouchedBuckets will emit the net delta at flush time.
|
|
5512
|
-
if (
|
|
5765
|
+
if (
|
|
5766
|
+
prev &&
|
|
5767
|
+
(
|
|
5768
|
+
prev.input_tokens ||
|
|
5769
|
+
prev.output_tokens ||
|
|
5770
|
+
prev.reasoning_output_tokens
|
|
5771
|
+
)
|
|
5772
|
+
) {
|
|
5773
|
+
const prevReasoning = toNonNegativeInt(
|
|
5774
|
+
prev.reasoning_output_tokens,
|
|
5775
|
+
);
|
|
5513
5776
|
const prevBucket = getHourlyBucket(hourlyState, "kiro", prev.model, prev.bucketStart);
|
|
5514
5777
|
addTotals(prevBucket.totals, {
|
|
5515
5778
|
input_tokens: -prev.input_tokens,
|
|
5516
5779
|
cached_input_tokens: 0,
|
|
5517
5780
|
cache_creation_input_tokens: 0,
|
|
5518
5781
|
output_tokens: -prev.output_tokens,
|
|
5519
|
-
reasoning_output_tokens:
|
|
5520
|
-
total_tokens: -(
|
|
5782
|
+
reasoning_output_tokens: -prevReasoning,
|
|
5783
|
+
total_tokens: -(
|
|
5784
|
+
prev.input_tokens +
|
|
5785
|
+
prev.output_tokens +
|
|
5786
|
+
prevReasoning
|
|
5787
|
+
),
|
|
5521
5788
|
conversation_count: -1,
|
|
5522
5789
|
});
|
|
5523
5790
|
touchedBuckets.add(bucketKey("kiro", prev.model, prev.bucketStart));
|
|
5524
5791
|
}
|
|
5525
5792
|
|
|
5526
5793
|
// Add the new contribution.
|
|
5527
|
-
if (approxInput > 0 || approxOutput > 0) {
|
|
5794
|
+
if (approxInput > 0 || approxOutput > 0 || approxReasoning > 0) {
|
|
5528
5795
|
const bucket = getHourlyBucket(hourlyState, "kiro", model, bucketStart);
|
|
5529
5796
|
addTotals(bucket.totals, {
|
|
5530
5797
|
input_tokens: approxInput,
|
|
5531
5798
|
cached_input_tokens: 0,
|
|
5532
5799
|
cache_creation_input_tokens: 0,
|
|
5533
5800
|
output_tokens: approxOutput,
|
|
5534
|
-
reasoning_output_tokens:
|
|
5535
|
-
total_tokens: approxInput + approxOutput,
|
|
5801
|
+
reasoning_output_tokens: approxReasoning,
|
|
5802
|
+
total_tokens: approxInput + approxOutput + approxReasoning,
|
|
5536
5803
|
conversation_count: 1,
|
|
5537
5804
|
});
|
|
5538
5805
|
touchedBuckets.add(bucketKey("kiro", model, bucketStart));
|
|
@@ -5551,6 +5818,7 @@ async function parseKiroCliIncremental({ sessionFiles, cursors, queuePath, onPro
|
|
|
5551
5818
|
model,
|
|
5552
5819
|
input_tokens: approxInput,
|
|
5553
5820
|
output_tokens: approxOutput,
|
|
5821
|
+
reasoning_output_tokens: approxReasoning,
|
|
5554
5822
|
...(r.session_id ? { session_id: r.session_id } : {}),
|
|
5555
5823
|
};
|
|
5556
5824
|
|
|
@@ -6204,7 +6472,7 @@ async function parseKimiCodeIncremental({ wireFiles, cursors, queuePath, onProgr
|
|
|
6204
6472
|
// each sync (passive scan only, same shape as Kimi's wire.jsonl reader).
|
|
6205
6473
|
//
|
|
6206
6474
|
// Per-line record types: message, reasoning, topic, file-history-snapshot.
|
|
6207
|
-
// Only `type=="message"
|
|
6475
|
+
// Only `type=="message" and role=="assistant"` carry token usage. The shape:
|
|
6208
6476
|
//
|
|
6209
6477
|
// providerData.rawUsage = {
|
|
6210
6478
|
// prompt_tokens: 22223, // OpenAI-style — INCLUDES cached
|
|
@@ -6734,7 +7002,7 @@ async function parseCodebuddyIncremental({
|
|
|
6734
7002
|
// (each verified against real ~/.workbuddy logs, NOT assumed from CodeBuddy):
|
|
6735
7003
|
//
|
|
6736
7004
|
// 1. Usage lives on `function_call` records too — not only on
|
|
6737
|
-
// `type=="message"
|
|
7005
|
+
// `type=="message" and role=="assistant"`. Each LLM round-trip (whether it
|
|
6738
7006
|
// ends in a tool call or a text reply) carries its own providerData.rawUsage.
|
|
6739
7007
|
// We therefore aggregate EVERY record that has providerData.rawUsage and
|
|
6740
7008
|
// dedup per response id, instead of filtering by record type.
|
|
@@ -12387,7 +12655,8 @@ async function parseCopilotAppDbIncremental({
|
|
|
12387
12655
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
12388
12656
|
|
|
12389
12657
|
const GROK_ESTIMATED_INPUT_RATIO = 0.8;
|
|
12390
|
-
|
|
12658
|
+
// v4: bill from turn_completed.usage (true cumulative API usage), not context-window totalTokens.
|
|
12659
|
+
const GROK_CURSOR_VERSION = 4;
|
|
12391
12660
|
|
|
12392
12661
|
function resolveGrokBuildHome(env = process.env) {
|
|
12393
12662
|
if (env.TOKENTRACKER_GROK_HOME) return env.TOKENTRACKER_GROK_HOME;
|
|
@@ -12540,6 +12809,9 @@ function grokMessageCountFromSignals(signals) {
|
|
|
12540
12809
|
);
|
|
12541
12810
|
}
|
|
12542
12811
|
|
|
12812
|
+
// Context-window telemetry only. Prefer turn_completed.usage when available —
|
|
12813
|
+
// signals.totalTokens / contextTokensUsed track the live window, not billable
|
|
12814
|
+
// cumulative spend across a session (especially after compaction).
|
|
12543
12815
|
function grokEffectiveTotalFromSignals(signals) {
|
|
12544
12816
|
if (!signals || typeof signals !== "object") return 0;
|
|
12545
12817
|
const beforeCompaction = normalizeNonNegativeNumber(signals.totalTokensBeforeCompaction);
|
|
@@ -12612,28 +12884,135 @@ function grokFileEndsWithNewline(filePath, size) {
|
|
|
12612
12884
|
}
|
|
12613
12885
|
}
|
|
12614
12886
|
|
|
12615
|
-
|
|
12616
|
-
|
|
12887
|
+
|
|
12888
|
+
function canonicalizeGrokUsageModel(model) {
|
|
12889
|
+
const raw = normalizeModelInput(model) || "grok-build";
|
|
12890
|
+
const lower = raw.toLowerCase();
|
|
12891
|
+
// Free Build SKU must not fuzzy-match paid grok-4.5 rates in native clients.
|
|
12892
|
+
if (lower.includes("build-free") || lower.endsWith("-free") || lower.includes("free-tier")) {
|
|
12893
|
+
return "grok-build-free";
|
|
12894
|
+
}
|
|
12895
|
+
if (lower === "grok-4.5-build" || lower === "grok-4-5-build") {
|
|
12896
|
+
return "grok-4.5-build";
|
|
12897
|
+
}
|
|
12898
|
+
return raw;
|
|
12899
|
+
}
|
|
12900
|
+
|
|
12901
|
+
function normalizeGrokTurnUsage(usage, model, timestamp, eventId) {
|
|
12902
|
+
if (!usage || typeof usage !== "object") return null;
|
|
12903
|
+
const inputRaw = normalizeNonNegativeNumber(
|
|
12904
|
+
usage.inputTokens ?? usage.input_tokens,
|
|
12905
|
+
);
|
|
12906
|
+
const output = normalizeNonNegativeNumber(
|
|
12907
|
+
usage.outputTokens ?? usage.output_tokens,
|
|
12908
|
+
);
|
|
12909
|
+
const cached = normalizeNonNegativeNumber(
|
|
12910
|
+
usage.cachedReadTokens ??
|
|
12911
|
+
usage.cache_read_input_tokens ??
|
|
12912
|
+
usage.cached_input_tokens,
|
|
12913
|
+
);
|
|
12914
|
+
const reasoning = normalizeNonNegativeNumber(
|
|
12915
|
+
usage.reasoningTokens ?? usage.reasoning_output_tokens,
|
|
12916
|
+
);
|
|
12917
|
+
// Grok reports inputTokens as the full prompt (including cache hits). Split
|
|
12918
|
+
// so pricing can apply cache_read rates correctly.
|
|
12919
|
+
const nonCachedInput = Math.max(0, inputRaw - cached);
|
|
12920
|
+
let total = normalizeNonNegativeNumber(usage.totalTokens ?? usage.total_tokens);
|
|
12921
|
+
if (total <= 0) {
|
|
12922
|
+
total = inputRaw + output + reasoning;
|
|
12923
|
+
}
|
|
12924
|
+
if (total <= 0 && nonCachedInput <= 0 && cached <= 0 && output <= 0) return null;
|
|
12925
|
+
return {
|
|
12926
|
+
input_tokens: nonCachedInput,
|
|
12927
|
+
cached_input_tokens: cached,
|
|
12928
|
+
cache_creation_input_tokens: 0,
|
|
12929
|
+
output_tokens: output,
|
|
12930
|
+
reasoning_output_tokens: reasoning,
|
|
12931
|
+
total_tokens: total > 0 ? total : nonCachedInput + cached + output + reasoning,
|
|
12932
|
+
billable_total_tokens: total > 0 ? total : nonCachedInput + cached + output + reasoning,
|
|
12933
|
+
conversation_count: 1,
|
|
12934
|
+
model: canonicalizeGrokUsageModel(model),
|
|
12935
|
+
timestamp,
|
|
12936
|
+
eventId,
|
|
12937
|
+
};
|
|
12938
|
+
}
|
|
12939
|
+
|
|
12940
|
+
function extractGrokTurnUsageEvents(record, fallbackTimestamp, fallbackModel, lineIndex) {
|
|
12941
|
+
const update = record?.params?.update;
|
|
12942
|
+
if (!update || typeof update !== "object") return [];
|
|
12943
|
+
if (update.sessionUpdate !== "turn_completed") return [];
|
|
12944
|
+
const usage = update.usage;
|
|
12945
|
+
if (!usage || typeof usage !== "object") return [];
|
|
12946
|
+
|
|
12947
|
+
const meta = record?.params?._meta || record?._meta || {};
|
|
12948
|
+
const timestamp = grokTimestampFromUpdate(meta, record, fallbackTimestamp);
|
|
12949
|
+
const baseEventId = grokEventId(
|
|
12950
|
+
meta.eventId ?? record?.eventId ?? record?.id ?? update.prompt_id,
|
|
12951
|
+
String(lineIndex),
|
|
12952
|
+
);
|
|
12953
|
+
|
|
12954
|
+
const modelUsage =
|
|
12955
|
+
usage.modelUsage && typeof usage.modelUsage === "object" ? usage.modelUsage : null;
|
|
12956
|
+
const events = [];
|
|
12957
|
+
if (modelUsage && Object.keys(modelUsage).length > 0) {
|
|
12958
|
+
for (const [modelName, modelUsageEntry] of Object.entries(modelUsage)) {
|
|
12959
|
+
if (!modelUsageEntry || typeof modelUsageEntry !== "object") continue;
|
|
12960
|
+
const event = normalizeGrokTurnUsage(
|
|
12961
|
+
modelUsageEntry,
|
|
12962
|
+
modelName,
|
|
12963
|
+
timestamp,
|
|
12964
|
+
`${baseEventId}|${modelName}`,
|
|
12965
|
+
);
|
|
12966
|
+
if (event) events.push(event);
|
|
12967
|
+
}
|
|
12968
|
+
}
|
|
12969
|
+
if (events.length === 0) {
|
|
12970
|
+
const event = normalizeGrokTurnUsage(usage, fallbackModel, timestamp, baseEventId);
|
|
12971
|
+
if (event) events.push(event);
|
|
12972
|
+
}
|
|
12973
|
+
return events;
|
|
12974
|
+
}
|
|
12975
|
+
|
|
12976
|
+
// Context-window watermark events (legacy / fallback only).
|
|
12977
|
+
function extractGrokContextTokenEvent(record, fallbackTimestamp, lineIndex) {
|
|
12978
|
+
const meta = record?.params?._meta || record?._meta;
|
|
12979
|
+
if (!meta || typeof meta !== "object") return null;
|
|
12980
|
+
const totalTokens = normalizeNonNegativeNumber(meta.totalTokens);
|
|
12981
|
+
if (totalTokens <= 0) return null;
|
|
12982
|
+
return {
|
|
12983
|
+
totalTokens,
|
|
12984
|
+
timestamp: grokTimestampFromUpdate(meta, record, fallbackTimestamp),
|
|
12985
|
+
eventId: grokEventId(meta.eventId ?? record?.eventId ?? record?.id, String(lineIndex)),
|
|
12986
|
+
};
|
|
12987
|
+
}
|
|
12988
|
+
|
|
12989
|
+
async function readGrokUpdateTokenEvents(updatesPath, fallbackTimestamp, prevOffsetEntry, options = {}) {
|
|
12990
|
+
const fallbackModel = options.fallbackModel || "grok-build";
|
|
12991
|
+
if (!updatesPath) {
|
|
12992
|
+
return { turnEvents: [], contextEvents: [], offsetEntry: null };
|
|
12993
|
+
}
|
|
12617
12994
|
let stat;
|
|
12618
12995
|
try {
|
|
12619
12996
|
stat = fssync.statSync(updatesPath);
|
|
12620
|
-
if (!stat.isFile()) return {
|
|
12997
|
+
if (!stat.isFile()) return { turnEvents: [], contextEvents: [], offsetEntry: null };
|
|
12621
12998
|
} catch {
|
|
12622
|
-
return {
|
|
12999
|
+
return { turnEvents: [], contextEvents: [], offsetEntry: null };
|
|
12623
13000
|
}
|
|
12624
13001
|
|
|
12625
|
-
// updates.jsonl is append-only
|
|
12626
|
-
//
|
|
12627
|
-
//
|
|
12628
|
-
// bytes a write race leaves unparsed are covered by the next event's
|
|
12629
|
-
// cumulative total. Re-read from 0 on truncation or inode change.
|
|
13002
|
+
// updates.jsonl is append-only. Turn usage is additive per turn_completed, so
|
|
13003
|
+
// resuming from the last consumed byte is safe. Re-read from 0 on truncation
|
|
13004
|
+
// or inode change.
|
|
12630
13005
|
const prevSize = Number(prevOffsetEntry?.size) || 0;
|
|
12631
13006
|
const prevIno = prevOffsetEntry?.ino;
|
|
12632
13007
|
const inodeChanged = typeof prevIno === "number" && prevIno !== stat.ino;
|
|
12633
13008
|
const startOffset = stat.size < prevSize || inodeChanged ? 0 : prevSize;
|
|
12634
13009
|
const baseOffset = { mtimeMs: stat.mtimeMs, ino: stat.ino };
|
|
12635
13010
|
if (stat.size <= startOffset) {
|
|
12636
|
-
return {
|
|
13011
|
+
return {
|
|
13012
|
+
turnEvents: [],
|
|
13013
|
+
contextEvents: [],
|
|
13014
|
+
offsetEntry: { size: startOffset, ...baseOffset },
|
|
13015
|
+
};
|
|
12637
13016
|
}
|
|
12638
13017
|
|
|
12639
13018
|
// Only advance the stored offset to the end of the last newline-terminated
|
|
@@ -12642,7 +13021,8 @@ async function readGrokUpdateTokenEvents(updatesPath, fallbackTimestamp, prevOff
|
|
|
12642
13021
|
// complete instead of skipping it forever (which would undercount tokens).
|
|
12643
13022
|
const endsWithNewline = grokFileEndsWithNewline(updatesPath, stat.size);
|
|
12644
13023
|
|
|
12645
|
-
const
|
|
13024
|
+
const turnEvents = [];
|
|
13025
|
+
const contextEvents = [];
|
|
12646
13026
|
let lineIndex = 0;
|
|
12647
13027
|
let lastLine = "";
|
|
12648
13028
|
const input = fssync.createReadStream(updatesPath, {
|
|
@@ -12662,21 +13042,24 @@ async function readGrokUpdateTokenEvents(updatesPath, fallbackTimestamp, prevOff
|
|
|
12662
13042
|
} catch {
|
|
12663
13043
|
continue;
|
|
12664
13044
|
}
|
|
12665
|
-
const
|
|
12666
|
-
|
|
12667
|
-
|
|
12668
|
-
|
|
12669
|
-
|
|
12670
|
-
|
|
12671
|
-
|
|
12672
|
-
|
|
12673
|
-
|
|
12674
|
-
}
|
|
13045
|
+
const turns = extractGrokTurnUsageEvents(
|
|
13046
|
+
record,
|
|
13047
|
+
fallbackTimestamp,
|
|
13048
|
+
fallbackModel,
|
|
13049
|
+
lineIndex,
|
|
13050
|
+
);
|
|
13051
|
+
if (turns.length > 0) {
|
|
13052
|
+
turnEvents.push(...turns);
|
|
13053
|
+
continue;
|
|
13054
|
+
}
|
|
13055
|
+
const contextEvent = extractGrokContextTokenEvent(record, fallbackTimestamp, lineIndex);
|
|
13056
|
+
if (contextEvent) contextEvents.push(contextEvent);
|
|
12675
13057
|
}
|
|
12676
13058
|
} catch {
|
|
12677
|
-
// Stream error mid-read:
|
|
12678
|
-
// offset so the next sync
|
|
12679
|
-
|
|
13059
|
+
// Stream error mid-read: discard partial events and do not advance the
|
|
13060
|
+
// offset, so the next sync re-extracts from the same range exactly once
|
|
13061
|
+
// instead of double-counting already-parsed turn events.
|
|
13062
|
+
return { turnEvents: [], contextEvents: [], offsetEntry: prevOffsetEntry || null };
|
|
12680
13063
|
}
|
|
12681
13064
|
|
|
12682
13065
|
// When the file does not end on a newline, the final emitted line is a
|
|
@@ -12684,7 +13067,11 @@ async function readGrokUpdateTokenEvents(updatesPath, fallbackTimestamp, prevOff
|
|
|
12684
13067
|
// stays on a complete-line boundary and the line is re-read once finished.
|
|
12685
13068
|
const trailingPartialBytes = endsWithNewline ? 0 : Buffer.byteLength(lastLine, "utf8");
|
|
12686
13069
|
const committedSize = Math.max(startOffset, stat.size - trailingPartialBytes);
|
|
12687
|
-
return {
|
|
13070
|
+
return {
|
|
13071
|
+
turnEvents,
|
|
13072
|
+
contextEvents,
|
|
13073
|
+
offsetEntry: { size: committedSize, ...baseOffset },
|
|
13074
|
+
};
|
|
12688
13075
|
}
|
|
12689
13076
|
|
|
12690
13077
|
function estimateGrokTokenDelta(totalTokens, conversationCount, options = {}) {
|
|
@@ -12706,6 +13093,70 @@ function estimateGrokTokenDelta(totalTokens, conversationCount, options = {}) {
|
|
|
12706
13093
|
};
|
|
12707
13094
|
}
|
|
12708
13095
|
|
|
13096
|
+
function clearGrokHourlyBuckets(hourlyState) {
|
|
13097
|
+
if (!hourlyState || typeof hourlyState !== "object") return;
|
|
13098
|
+
const buckets = hourlyState.buckets && typeof hourlyState.buckets === "object" ? hourlyState.buckets : null;
|
|
13099
|
+
if (buckets) {
|
|
13100
|
+
for (const key of Object.keys(buckets)) {
|
|
13101
|
+
if (key.startsWith("grok|")) delete buckets[key];
|
|
13102
|
+
}
|
|
13103
|
+
}
|
|
13104
|
+
const groupQueued =
|
|
13105
|
+
hourlyState.groupQueued && typeof hourlyState.groupQueued === "object"
|
|
13106
|
+
? hourlyState.groupQueued
|
|
13107
|
+
: null;
|
|
13108
|
+
if (groupQueued) {
|
|
13109
|
+
for (const key of Object.keys(groupQueued)) {
|
|
13110
|
+
if (key.startsWith("grok|")) delete groupQueued[key];
|
|
13111
|
+
}
|
|
13112
|
+
}
|
|
13113
|
+
}
|
|
13114
|
+
|
|
13115
|
+
async function retractStaleGrokQueueRows(queuePath, keepKeys) {
|
|
13116
|
+
if (!queuePath) return 0;
|
|
13117
|
+
let raw = "";
|
|
13118
|
+
try {
|
|
13119
|
+
raw = fssync.readFileSync(queuePath, "utf8");
|
|
13120
|
+
} catch (error) {
|
|
13121
|
+
if (error?.code === "ENOENT") return 0;
|
|
13122
|
+
throw error;
|
|
13123
|
+
}
|
|
13124
|
+
|
|
13125
|
+
const latestGrok = new Map();
|
|
13126
|
+
for (const line of raw.split("\n")) {
|
|
13127
|
+
if (!line.trim()) continue;
|
|
13128
|
+
let row;
|
|
13129
|
+
try {
|
|
13130
|
+
row = JSON.parse(line);
|
|
13131
|
+
} catch {
|
|
13132
|
+
continue;
|
|
13133
|
+
}
|
|
13134
|
+
if ((row?.source || "") !== "grok") continue;
|
|
13135
|
+
const model = normalizeModelInput(row.model) || DEFAULT_MODEL;
|
|
13136
|
+
const hourStart = typeof row.hour_start === "string" ? row.hour_start : null;
|
|
13137
|
+
if (!hourStart) continue;
|
|
13138
|
+
latestGrok.set(bucketKey("grok", model, hourStart), { model, hour_start: hourStart, row });
|
|
13139
|
+
}
|
|
13140
|
+
|
|
13141
|
+
const zero = initTotals();
|
|
13142
|
+
const lines = [];
|
|
13143
|
+
for (const [key, entry] of latestGrok.entries()) {
|
|
13144
|
+
if (keepKeys.has(key)) continue;
|
|
13145
|
+
if (totalsKey(entry.row) === totalsKey(zero)) continue;
|
|
13146
|
+
lines.push(
|
|
13147
|
+
JSON.stringify({
|
|
13148
|
+
source: "grok",
|
|
13149
|
+
model: entry.model,
|
|
13150
|
+
hour_start: entry.hour_start,
|
|
13151
|
+
...zero,
|
|
13152
|
+
}),
|
|
13153
|
+
);
|
|
13154
|
+
}
|
|
13155
|
+
if (lines.length === 0) return 0;
|
|
13156
|
+
await fs.appendFile(queuePath, `${lines.join("\n")}\n`, "utf8");
|
|
13157
|
+
return lines.length;
|
|
13158
|
+
}
|
|
13159
|
+
|
|
12709
13160
|
async function parseGrokBuildIncremental({
|
|
12710
13161
|
sessions,
|
|
12711
13162
|
cursors = {},
|
|
@@ -12716,11 +13167,44 @@ async function parseGrokBuildIncremental({
|
|
|
12716
13167
|
if (queuePath) await ensureDir(path.dirname(queuePath));
|
|
12717
13168
|
const hourlyState = normalizeHourlyState(cursors?.hourly);
|
|
12718
13169
|
const grokState = cursors.grok && typeof cursors.grok === "object" ? { ...cursors.grok } : {};
|
|
12719
|
-
|
|
13170
|
+
const prevVersion = Number(grokState.version) || 0;
|
|
13171
|
+
const needsTurnUsageMigration = prevVersion < GROK_CURSOR_VERSION;
|
|
13172
|
+
|
|
13173
|
+
// v3 and earlier treated context-window totalTokens as cumulative spend, which
|
|
13174
|
+
// undercounts heavily (often 10-50x) and mis-splits input/output. Rebuild from
|
|
13175
|
+
// turn_completed.usage when migrating to v4.
|
|
13176
|
+
//
|
|
13177
|
+
// Drop prior watermark totals / updateOffsets so files are re-read from byte 0,
|
|
13178
|
+
// but keep legacySeen markers from seenSessions so the one-shot baseline
|
|
13179
|
+
// (sessions already counted under the old scanner) still applies.
|
|
13180
|
+
let sessionSnapshots;
|
|
13181
|
+
if (needsTurnUsageMigration) {
|
|
13182
|
+
const normalized = normalizeGrokSessionSnapshots(grokState);
|
|
13183
|
+
sessionSnapshots = {};
|
|
13184
|
+
for (const [sessionId, snapshot] of Object.entries(normalized)) {
|
|
13185
|
+
if (!snapshot?.legacySeen) continue;
|
|
13186
|
+
sessionSnapshots[sessionId] = {
|
|
13187
|
+
totalTokens: 0,
|
|
13188
|
+
messageCount: 0,
|
|
13189
|
+
model: null,
|
|
13190
|
+
source: null,
|
|
13191
|
+
lastEventId: null,
|
|
13192
|
+
lastEventTimestamp: null,
|
|
13193
|
+
updatedAt: snapshot.updatedAt || null,
|
|
13194
|
+
legacySeen: true,
|
|
13195
|
+
};
|
|
13196
|
+
}
|
|
13197
|
+
clearGrokHourlyBuckets(hourlyState);
|
|
13198
|
+
} else {
|
|
13199
|
+
sessionSnapshots = normalizeGrokSessionSnapshots(grokState);
|
|
13200
|
+
}
|
|
12720
13201
|
const prevUpdateOffsets =
|
|
12721
|
-
|
|
13202
|
+
!needsTurnUsageMigration &&
|
|
13203
|
+
grokState.updateOffsets &&
|
|
13204
|
+
typeof grokState.updateOffsets === "object"
|
|
12722
13205
|
? grokState.updateOffsets
|
|
12723
13206
|
: {};
|
|
13207
|
+
|
|
12724
13208
|
// Rebuilt from the sessions seen this scan, so entries for deleted session
|
|
12725
13209
|
// dirs are pruned and the cursor stays bounded by the on-disk session count.
|
|
12726
13210
|
const updateOffsets = {};
|
|
@@ -12755,83 +13239,142 @@ async function parseGrokBuildIncremental({
|
|
|
12755
13239
|
const model = grokModelFromSignals(safeSignals);
|
|
12756
13240
|
const lastActive = grokLastActiveFromSignals(safeSignals, summary);
|
|
12757
13241
|
|
|
12758
|
-
let
|
|
12759
|
-
let observedTotal = previousTotal;
|
|
13242
|
+
let cumulativeTotal = previousTotal;
|
|
12760
13243
|
let tokenDeltaForSession = 0;
|
|
12761
13244
|
let finalTouchedHourStart = null;
|
|
12762
13245
|
let source = previous.source || null;
|
|
12763
13246
|
let lastEventId = previous.lastEventId || null;
|
|
12764
13247
|
let lastEventTimestamp = previous.lastEventTimestamp || null;
|
|
12765
|
-
|
|
12766
|
-
|
|
12767
|
-
|
|
12768
|
-
|
|
12769
|
-
|
|
12770
|
-
pendingTokenDeltas.push({ deltaTokens, hourStartStr });
|
|
12771
|
-
tokenDeltaForSession += deltaTokens;
|
|
12772
|
-
finalTouchedHourStart = hourStartStr;
|
|
12773
|
-
source = deltaSource;
|
|
12774
|
-
lastEventTimestamp = timestamp || lastEventTimestamp;
|
|
12775
|
-
return true;
|
|
12776
|
-
};
|
|
13248
|
+
let lastModel = previous.model || model;
|
|
13249
|
+
let sawTurnUsage = source === "turn_usage" || previous.source === "turn_usage";
|
|
13250
|
+
// Defer bucket writes until after we know whether this is a legacy baseline
|
|
13251
|
+
// pass (first sighting of a session already counted under an older scanner).
|
|
13252
|
+
const pendingBucketDeltas = [];
|
|
12777
13253
|
|
|
12778
13254
|
const updatesPath = grokUpdatesPathForSession(sess);
|
|
12779
13255
|
const updates = await readGrokUpdateTokenEvents(
|
|
12780
13256
|
updatesPath,
|
|
12781
13257
|
lastActive,
|
|
12782
13258
|
updatesPath ? prevUpdateOffsets[updatesPath] : null,
|
|
13259
|
+
{ fallbackModel: model },
|
|
12783
13260
|
);
|
|
12784
13261
|
if (updatesPath && updates.offsetEntry) {
|
|
12785
13262
|
updateOffsets[updatesPath] = updates.offsetEntry;
|
|
12786
13263
|
}
|
|
12787
|
-
|
|
12788
|
-
|
|
13264
|
+
|
|
13265
|
+
// Preferred path: each turn_completed carries true cumulative API usage for
|
|
13266
|
+
// that turn (input/output/cache/reasoning). Sum them.
|
|
13267
|
+
for (const event of updates.turnEvents) {
|
|
13268
|
+
sawTurnUsage = true;
|
|
13269
|
+
const hourStartStr = toUtcHalfHourStart(event.timestamp) || toUtcHalfHourStart(lastActive) || toUtcHalfHourStart(Date.now());
|
|
13270
|
+
if (!hourStartStr) continue;
|
|
13271
|
+
const eventModel = event.model || model;
|
|
13272
|
+
const delta = {
|
|
13273
|
+
input_tokens: event.input_tokens,
|
|
13274
|
+
cached_input_tokens: event.cached_input_tokens,
|
|
13275
|
+
cache_creation_input_tokens: event.cache_creation_input_tokens,
|
|
13276
|
+
output_tokens: event.output_tokens,
|
|
13277
|
+
reasoning_output_tokens: event.reasoning_output_tokens,
|
|
13278
|
+
total_tokens: event.total_tokens,
|
|
13279
|
+
billable_total_tokens: event.billable_total_tokens,
|
|
13280
|
+
conversation_count: event.conversation_count || 1,
|
|
13281
|
+
};
|
|
13282
|
+
pendingBucketDeltas.push({ model: eventModel, hourStartStr, delta });
|
|
13283
|
+
cumulativeTotal += event.total_tokens;
|
|
13284
|
+
tokenDeltaForSession += event.total_tokens;
|
|
13285
|
+
finalTouchedHourStart = hourStartStr;
|
|
13286
|
+
source = "turn_usage";
|
|
12789
13287
|
lastEventId = event.eventId || lastEventId;
|
|
12790
13288
|
lastEventTimestamp = event.timestamp || lastEventTimestamp;
|
|
12791
|
-
|
|
12792
|
-
|
|
12793
|
-
|
|
12794
|
-
|
|
12795
|
-
|
|
13289
|
+
lastModel = eventModel;
|
|
13290
|
+
}
|
|
13291
|
+
|
|
13292
|
+
// Fallback only when this session never emitted turn_completed usage
|
|
13293
|
+
// (older logs / partial sessions). Context watermark is a lower-bound
|
|
13294
|
+
// estimate and must not run on top of turn usage.
|
|
13295
|
+
if (!sawTurnUsage) {
|
|
13296
|
+
let highWatermark = previousTotal;
|
|
13297
|
+
for (const event of updates.contextEvents) {
|
|
13298
|
+
lastEventId = event.eventId || lastEventId;
|
|
13299
|
+
lastEventTimestamp = event.timestamp || lastEventTimestamp;
|
|
13300
|
+
if (event.totalTokens <= highWatermark) continue;
|
|
13301
|
+
const deltaTokens = event.totalTokens - highWatermark;
|
|
13302
|
+
highWatermark = event.totalTokens;
|
|
13303
|
+
const hourStartStr =
|
|
13304
|
+
toUtcHalfHourStart(event.timestamp) ||
|
|
13305
|
+
toUtcHalfHourStart(lastActive) ||
|
|
13306
|
+
toUtcHalfHourStart(Date.now());
|
|
13307
|
+
if (!hourStartStr) continue;
|
|
13308
|
+
const delta = estimateGrokTokenDelta(deltaTokens, 0, { allowZeroConversationCount: true });
|
|
13309
|
+
pendingBucketDeltas.push({ model, hourStartStr, delta });
|
|
13310
|
+
tokenDeltaForSession += deltaTokens;
|
|
13311
|
+
finalTouchedHourStart = hourStartStr;
|
|
13312
|
+
source = "updates";
|
|
13313
|
+
}
|
|
12796
13314
|
|
|
12797
|
-
|
|
12798
|
-
|
|
12799
|
-
|
|
12800
|
-
|
|
12801
|
-
|
|
12802
|
-
|
|
13315
|
+
const effectiveSignalTotal = grokEffectiveTotalFromSignals(safeSignals);
|
|
13316
|
+
if (effectiveSignalTotal > highWatermark) {
|
|
13317
|
+
const deltaTokens = effectiveSignalTotal - highWatermark;
|
|
13318
|
+
highWatermark = effectiveSignalTotal;
|
|
13319
|
+
const hourStartStr = toUtcHalfHourStart(lastActive) || toUtcHalfHourStart(Date.now());
|
|
13320
|
+
if (hourStartStr) {
|
|
13321
|
+
const delta = estimateGrokTokenDelta(deltaTokens, 0, { allowZeroConversationCount: true });
|
|
13322
|
+
pendingBucketDeltas.push({ model, hourStartStr, delta });
|
|
13323
|
+
tokenDeltaForSession += deltaTokens;
|
|
13324
|
+
finalTouchedHourStart = hourStartStr;
|
|
13325
|
+
source = "signals";
|
|
13326
|
+
}
|
|
13327
|
+
}
|
|
13328
|
+
cumulativeTotal = Math.max(previousTotal, highWatermark);
|
|
12803
13329
|
}
|
|
12804
13330
|
|
|
12805
|
-
const finalTotal = Math.max(previousTotal,
|
|
13331
|
+
const finalTotal = Math.max(previousTotal, cumulativeTotal);
|
|
13332
|
+
// Sessions already observed under an older scanner must establish a watermark
|
|
13333
|
+
// without backfilling historical tokens as brand-new usage.
|
|
12806
13334
|
const legacyBaselineOnly = previous.legacySeen && previousTotal === 0 && finalTotal > 0;
|
|
13335
|
+
|
|
12807
13336
|
if (!legacyBaselineOnly) {
|
|
12808
|
-
for (const pending of
|
|
12809
|
-
const
|
|
12810
|
-
|
|
12811
|
-
|
|
12812
|
-
touchedBuckets.add(bucketKey("grok", model, pending.hourStartStr));
|
|
13337
|
+
for (const pending of pendingBucketDeltas) {
|
|
13338
|
+
const bucket = getHourlyBucket(hourlyState, "grok", pending.model, pending.hourStartStr);
|
|
13339
|
+
addTotals(bucket.totals, pending.delta);
|
|
13340
|
+
touchedBuckets.add(bucketKey("grok", pending.model, pending.hourStartStr));
|
|
12813
13341
|
eventsAggregated++;
|
|
12814
13342
|
}
|
|
12815
|
-
}
|
|
12816
13343
|
|
|
12817
|
-
|
|
12818
|
-
|
|
12819
|
-
|
|
12820
|
-
|
|
12821
|
-
|
|
12822
|
-
|
|
13344
|
+
// Message/conversation count for fallback-only sessions (turn path already
|
|
13345
|
+
// counts each turn_completed as one conversation).
|
|
13346
|
+
if (!sawTurnUsage && tokenDeltaForSession > 0 && finalTouchedHourStart) {
|
|
13347
|
+
const deltaMessageCount =
|
|
13348
|
+
messageCount > previousMessageCount ? messageCount - previousMessageCount : 1;
|
|
13349
|
+
const bucket = getHourlyBucket(hourlyState, "grok", lastModel || model, finalTouchedHourStart);
|
|
13350
|
+
addTotals(bucket.totals, { conversation_count: deltaMessageCount });
|
|
13351
|
+
touchedBuckets.add(bucketKey("grok", lastModel || model, finalTouchedHourStart));
|
|
13352
|
+
}
|
|
12823
13353
|
}
|
|
12824
13354
|
|
|
12825
13355
|
if (finalTotal > 0 && (tokenDeltaForSession > 0 || previousTotal > 0 || legacyBaselineOnly)) {
|
|
12826
13356
|
sessionSnapshots[sessionId] = {
|
|
12827
13357
|
totalTokens: finalTotal,
|
|
12828
13358
|
messageCount: Math.max(previousMessageCount, messageCount),
|
|
12829
|
-
model,
|
|
13359
|
+
model: lastModel || model,
|
|
12830
13360
|
source: source || previous.source || null,
|
|
12831
13361
|
lastEventId,
|
|
12832
13362
|
lastEventTimestamp,
|
|
12833
13363
|
updatedAt: new Date().toISOString(),
|
|
12834
13364
|
};
|
|
13365
|
+
} else if (previous.legacySeen && finalTotal === 0) {
|
|
13366
|
+
// Keep the baseline marker across empty syncs so later growth is still
|
|
13367
|
+
// baselined once instead of backfilled as brand-new usage.
|
|
13368
|
+
sessionSnapshots[sessionId] = {
|
|
13369
|
+
totalTokens: 0,
|
|
13370
|
+
messageCount: Math.max(previousMessageCount, messageCount),
|
|
13371
|
+
model: lastModel || model,
|
|
13372
|
+
source: previous.source || null,
|
|
13373
|
+
lastEventId: lastEventId || previous.lastEventId || null,
|
|
13374
|
+
lastEventTimestamp: lastEventTimestamp || previous.lastEventTimestamp || null,
|
|
13375
|
+
updatedAt: new Date().toISOString(),
|
|
13376
|
+
legacySeen: true,
|
|
13377
|
+
};
|
|
12835
13378
|
}
|
|
12836
13379
|
|
|
12837
13380
|
if (onProgress) {
|
|
@@ -12839,19 +13382,45 @@ async function parseGrokBuildIncremental({
|
|
|
12839
13382
|
}
|
|
12840
13383
|
}
|
|
12841
13384
|
|
|
12842
|
-
|
|
13385
|
+
let bucketsQueued = queuePath
|
|
12843
13386
|
? await enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets })
|
|
12844
13387
|
: 0;
|
|
13388
|
+
|
|
13389
|
+
// After a semantics migration, retract stale grok queue keys that the full
|
|
13390
|
+
// rescan no longer produces so dashboard "latest per key" no longer keeps
|
|
13391
|
+
// the old undercounted rows.
|
|
13392
|
+
if (needsTurnUsageMigration && queuePath) {
|
|
13393
|
+
const keepKeys = new Set();
|
|
13394
|
+
for (const [key, bucket] of Object.entries(hourlyState.buckets || {})) {
|
|
13395
|
+
if (!key.startsWith("grok|") || !bucket?.totals) continue;
|
|
13396
|
+
keepKeys.add(key);
|
|
13397
|
+
}
|
|
13398
|
+
const retracted = await retractStaleGrokQueueRows(queuePath, keepKeys);
|
|
13399
|
+
bucketsQueued += retracted;
|
|
13400
|
+
}
|
|
13401
|
+
|
|
12845
13402
|
hourlyState.updatedAt = new Date().toISOString();
|
|
12846
13403
|
cursors.hourly = hourlyState;
|
|
12847
13404
|
sessionSnapshots = capGrokSessionSnapshots(sessionSnapshots);
|
|
12848
13405
|
|
|
13406
|
+
const migrations = grokState.migrations && typeof grokState.migrations === "object"
|
|
13407
|
+
? { ...grokState.migrations }
|
|
13408
|
+
: {};
|
|
13409
|
+
if (needsTurnUsageMigration) {
|
|
13410
|
+
migrations.turnUsageV4 = {
|
|
13411
|
+
appliedAt: new Date().toISOString(),
|
|
13412
|
+
fromVersion: prevVersion,
|
|
13413
|
+
toVersion: GROK_CURSOR_VERSION,
|
|
13414
|
+
};
|
|
13415
|
+
}
|
|
13416
|
+
|
|
12849
13417
|
cursors.grok = {
|
|
12850
13418
|
...grokState,
|
|
12851
13419
|
version: GROK_CURSOR_VERSION,
|
|
12852
13420
|
sessionSnapshots,
|
|
12853
13421
|
seenSessions: Object.keys(sessionSnapshots),
|
|
12854
13422
|
updateOffsets,
|
|
13423
|
+
migrations,
|
|
12855
13424
|
updatedAt: new Date().toISOString()
|
|
12856
13425
|
};
|
|
12857
13426
|
|