tokentracker-cli 0.84.5 → 0.84.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.de.md +10 -8
- package/README.ja.md +9 -7
- package/README.ko.md +9 -7
- package/README.md +12 -10
- package/README.zh-CN.md +9 -7
- package/dashboard/dist/assets/{AchievementBadge-DJvRfvu1.js → AchievementBadge-DAPsTMsx.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-Cr8WToxS.js → AchievementsPage-B9oXfXvm.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-BCPYB46e.js → AchievementsSection-DI_LTZb3.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-D-12Ek-H.js → BadgeDetailModal--WCrJWVa.js} +1 -1
- package/dashboard/dist/assets/Card-BzbSN3EC.js +1 -0
- package/dashboard/dist/assets/{ClawdAnimated-DoGgAppT.js → ClawdAnimated-Dgxae1At.js} +1 -1
- package/dashboard/dist/assets/{CommandPalette-DIKocEAD.js → CommandPalette-BiZBYV5A.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-BNuMbEWy.js → CommunityStatsModal-C6jZx10_.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-F2gPIIS9.js → ConfirmModal-lD-mHMvO.js} +1 -1
- package/dashboard/dist/assets/{DashboardPage-YvoCEGG9.js → DashboardPage-DCPNoiRG.js} +1 -1
- package/dashboard/dist/assets/{DevicePage-CbC_J22s.js → DevicePage-DiF2IOjJ.js} +1 -1
- package/dashboard/dist/assets/{DialogClose-VEynQCYg.js → DialogClose-Crx2EFnI.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-CHsHo924.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-BbAVHI_v.js → DialogTitle-amHXttsp.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-CZVqSnis.js → FadeIn-CIhF_9PP.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-Cs_h332Y.js → HeaderGithubStar-D6Ph2GuY.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-DvvHTphK.js → HoverTooltip-BS8eO6tj.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-DPVmqSS_.js → IpCheckPage-Cz1Je6WS.js} +1 -1
- package/dashboard/dist/assets/LandingPage-DqeXJfwn.js +2 -0
- package/dashboard/dist/assets/{LeaderboardAvatar-BAPZ2ROX.js → LeaderboardAvatar-D09yLoVA.js} +1 -1
- package/dashboard/dist/assets/LeaderboardPage-BetpFetS.js +2 -0
- package/dashboard/dist/assets/{LeaderboardProfileModal-r03akfe2.js → LeaderboardProfileModal-UW3KnANZ.js} +4 -4
- package/dashboard/dist/assets/{LeaderboardProfilePage-CU63YW2M.js → LeaderboardProfilePage-Chd_2AVY.js} +1 -1
- package/dashboard/dist/assets/LimitsPage-uhpNteXJ.js +4 -0
- package/dashboard/dist/assets/{LocalOnlyNotice-BHlcPCZk.js → LocalOnlyNotice-CpFFX8ro.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-C-dqUumR.js → LoginCard-DkZpGnVu.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-BFzRg4Iy.js → LoginModal-CALv6jkD.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-QtlnGF1S.js → LoginPage-BZuXHYAT.js} +1 -1
- package/dashboard/dist/assets/PetPage-C1LuDyYr.js +1 -0
- package/dashboard/dist/assets/{PopoverPopup-CltmqcrT.js → PopoverPopup-CluVhrMr.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-BqOI9EiD.js → ProviderIcon-D3YKjVKb.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-BsfCBUZD.js → ResetPasswordPage-CzN50nX5.js} +1 -1
- package/dashboard/dist/assets/{SegmentedControl-D4biXw_Q.js → SegmentedControl-DFOAcEKU.js} +1 -1
- package/dashboard/dist/assets/{Select-DTw-14iE.js → Select-B3W_ihs_.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-3tUrnu3f.js → SelectItemText-FXRtXnFV.js} +1 -1
- package/dashboard/dist/assets/ServiceStatusPage-BItZqH7q.js +1 -0
- package/dashboard/dist/assets/{SessionsPage-DYYlNqHg.js → SessionsPage-DVQU-hTH.js} +2 -2
- package/dashboard/dist/assets/SettingsPage-ByvrwOi6.js +1 -0
- package/dashboard/dist/assets/SkillsPage-DrGr5Qij.js +1 -0
- package/dashboard/dist/assets/{TokenGalaxy-DU77e8sM.js → TokenGalaxy-ClF_n9od.js} +1 -1
- package/dashboard/dist/assets/{ToolbarRootContext-DV3KGf-a.js → ToolbarRootContext-B82hGhAf.js} +1 -1
- package/dashboard/dist/assets/{TrendMonitor-D6QYKmbl.js → TrendMonitor-DkWm7_1z.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-DSP4jCsH.js → WidgetsPage-DCY4pyR2.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-B0x6O_DU.js → WrappedPage-yGQ4DLqj.js} +1 -1
- package/dashboard/dist/assets/__vite-browser-external-BdRrPLk6.js +1483 -0
- package/dashboard/dist/assets/__vite-browser-external-hXV_ZYTG.css +1 -0
- package/dashboard/dist/assets/{agent-logos-B2S_cdOL.js → agent-logos-D5SHGOxB.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-DNUVuhx2.js → arrow-up-right-CbMcAcss.js} +1 -1
- package/dashboard/dist/assets/{check-C3ARiiE_.js → check-Dyzh7lTz.js} +1 -1
- package/dashboard/dist/assets/{chevron-right-3bY7H8jO.js → chevron-right-DT06ZIKC.js} +1 -1
- package/dashboard/dist/assets/{copy-C-UIMKoO.js → copy-B3Kt3Wrf.js} +1 -1
- package/dashboard/dist/assets/{download-DtRzJfcr.js → download-B3rOytqI.js} +1 -1
- package/dashboard/dist/assets/external-link-BamTP0Ks.js +1 -0
- package/dashboard/dist/assets/{flame-Bas3ry-g.js → flame-msy2h1ms.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-DFtRUIH8.js → format-tokens-8b1OUM_8.js} +1 -1
- package/dashboard/dist/assets/{icons-CHBt4Tmo.js → icons-C2NR2_Tf.js} +1 -1
- package/dashboard/dist/assets/{index-CZRlrK7w.js → index-C8sDJzUY.js} +1 -1
- package/dashboard/dist/assets/{index-C1ADnvzm.js → index-CvTfpiCF.js} +1 -1
- package/dashboard/dist/assets/{index-4unZlm2w.js → index-x3vFJQFt.js} +1 -1
- package/dashboard/dist/assets/{info-BkrO-fg3.js → info-C9j23aGq.js} +1 -1
- package/dashboard/dist/assets/limits-providers-ySB9Ci5l.js +1 -0
- package/dashboard/dist/assets/{link-2-DTLFz8uG.js → link-2-rrcyTT6S.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-BqQW_Ipq.js → loader-circle-x9x9I8Hv.js} +1 -1
- package/dashboard/dist/assets/{maximize-2-YdpeE4dq.js → maximize-2-7TWFb2k7.js} +1 -1
- package/dashboard/dist/assets/{provider-display-CgBOpjq0.js → provider-display-JbSwpvxU.js} +1 -1
- package/dashboard/dist/assets/{react-DD9jpv--.js → react-DK5clHtY.js} +1 -1
- package/dashboard/dist/assets/{search--TvX-fig.js → search-DukXRvEV.js} +1 -1
- package/dashboard/dist/assets/{skills-api-C5C8a1at.js → skills-api-BUFJuwiR.js} +1 -1
- package/dashboard/dist/assets/{store-CRA8gPnP.js → store-BTh7bv09.js} +1 -1
- package/dashboard/dist/assets/{terminal-BXZyl2ek.js → terminal-CcUqLtn1.js} +1 -1
- package/dashboard/dist/assets/{trash-2-IFdBQ65v.js → trash-2-Bq7hnvNq.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-LyJ5aMpX.js → use-community-stats-qTpx6ugG.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-CKvFS1Nn.js → use-limits-display-prefs-Di_J4eVv.js} +1 -1
- package/dashboard/dist/assets/{use-native-settings-Cg-4j_0z.js → use-native-settings-CIqpv9Z3.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-2pX5PXZO.js → use-ordered-list-OwogSXT7.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-5QGGPoom.js +1 -0
- package/dashboard/dist/assets/{use-session-efficiency-pref-DilJOODA.js → use-session-efficiency-pref-CjAvMh7u.js} +1 -1
- package/dashboard/dist/assets/{use-transform-DiyYTLpI.js → use-transform-KZLdt-NZ.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-DkUmp_nW.js → use-usage-limits-Tbifa037.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-lm7KjGoD.js → useCurrency-D2td-bkn.js} +1 -1
- package/dashboard/dist/assets/{useScrollLock-DVSyJ-GE.js → useScrollLock-Cvyfyezc.js} +1 -1
- package/dashboard/dist/assets/useTokenFormat-DYIUS6IH.js +1 -0
- package/dashboard/dist/assets/{zap-CKZt1_i5.js → zap-B19mEed5.js} +1 -1
- package/dashboard/dist/brand-logos/qoder.svg +5 -0
- package/dashboard/dist/index.html +18 -15
- package/dashboard/dist/ip-check.html +2 -2
- package/dashboard/dist/leaderboard.html +2 -2
- package/dashboard/dist/llms.txt +3 -2
- package/dashboard/dist/share.html +2 -2
- package/package.json +2 -2
- package/src/commands/init.js +1 -0
- package/src/commands/status.js +34 -7
- package/src/commands/sync.js +353 -5
- package/src/lib/codex-rollout-parser.js +17 -39
- package/src/lib/codex-token-usage.js +195 -0
- package/src/lib/local-api.js +4 -1
- package/src/lib/pet-packages.js +44 -2
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/provider-status.js +104 -0
- package/src/lib/qoder-limits.js +985 -0
- package/src/lib/rollout.js +744 -91
- package/src/lib/usage-limits.js +243 -43
- package/dashboard/dist/assets/Card-Ha3x9zIj.js +0 -1
- package/dashboard/dist/assets/DialogDescription-B9bb9Ar0.js +0 -1
- package/dashboard/dist/assets/LandingPage-BCvNh3zE.js +0 -2
- package/dashboard/dist/assets/LeaderboardPage-DZSxazHP.js +0 -2
- package/dashboard/dist/assets/LimitsPage-JWZDa5_Y.js +0 -4
- package/dashboard/dist/assets/PetPage-C6UMY2qD.js +0 -1
- package/dashboard/dist/assets/SettingsPage-CYlEboSl.js +0 -1
- package/dashboard/dist/assets/SkillsPage-Bzq8V5Pd.js +0 -1
- package/dashboard/dist/assets/__vite-browser-external-D3llGq6p.js +0 -1465
- package/dashboard/dist/assets/__vite-browser-external-WKilYZnq.css +0 -1
- package/dashboard/dist/assets/limits-providers-BtwYmYHA.js +0 -1
- package/dashboard/dist/assets/use-reduced-motion-BCR01kRg.js +0 -1
- package/dashboard/dist/assets/useTokenFormat-Li8BANKO.js +0 -1
package/src/lib/rollout.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const fs = require("node:fs/promises");
|
|
2
2
|
const fssync = require("node:fs");
|
|
3
|
+
const os = require("node:os");
|
|
3
4
|
const path = require("node:path");
|
|
4
5
|
const readline = require("node:readline");
|
|
5
6
|
|
|
@@ -8,6 +9,11 @@ const { ensureDir } = require("./fs");
|
|
|
8
9
|
const { readSqliteJsonRows, readSqliteJsonRowsAsync } = require("./sqlite-reader");
|
|
9
10
|
const wsl = require("./wsl-probe");
|
|
10
11
|
const { resolveInstallPaths } = require("./install-resolver");
|
|
12
|
+
const {
|
|
13
|
+
consumeUsageDelta,
|
|
14
|
+
createUsageDeltaState,
|
|
15
|
+
snapshotUsageBaselines,
|
|
16
|
+
} = require("./codex-token-usage");
|
|
11
17
|
|
|
12
18
|
const DEFAULT_SOURCE = "codex";
|
|
13
19
|
const DEFAULT_MODEL = "unknown";
|
|
@@ -360,6 +366,9 @@ async function parseRolloutIncremental({
|
|
|
360
366
|
const lastTotal = sameInode && !truncated && !rebuildingCodexBaseline
|
|
361
367
|
? prev.lastTotal || null
|
|
362
368
|
: null;
|
|
369
|
+
const tokenUsageBaselines = sameInode && !truncated && !rebuildingCodexBaseline
|
|
370
|
+
? prev.tokenUsageBaselines || null
|
|
371
|
+
: null;
|
|
363
372
|
const lastModel = sameInode && !truncated ? prev.lastModel || null : null;
|
|
364
373
|
|
|
365
374
|
const codexProjectFastPath = projectEnabled && fileSource === DEFAULT_SOURCE;
|
|
@@ -430,6 +439,7 @@ async function parseRolloutIncremental({
|
|
|
430
439
|
filePath,
|
|
431
440
|
fileStat: st,
|
|
432
441
|
lastTotal,
|
|
442
|
+
tokenUsageBaselines,
|
|
433
443
|
lastModel,
|
|
434
444
|
projectState,
|
|
435
445
|
projectMetaCache,
|
|
@@ -442,6 +452,7 @@ async function parseRolloutIncremental({
|
|
|
442
452
|
fileStat: st,
|
|
443
453
|
startOffset,
|
|
444
454
|
lastTotal,
|
|
455
|
+
tokenUsageBaselines,
|
|
445
456
|
lastModel,
|
|
446
457
|
hourlyState,
|
|
447
458
|
touchedBuckets,
|
|
@@ -462,6 +473,7 @@ async function parseRolloutIncremental({
|
|
|
462
473
|
inode,
|
|
463
474
|
offset: result.endOffset,
|
|
464
475
|
lastTotal: result.lastTotal,
|
|
476
|
+
tokenUsageBaselines: result.tokenUsageBaselines,
|
|
465
477
|
lastModel: result.lastModel,
|
|
466
478
|
updatedAt: new Date().toISOString(),
|
|
467
479
|
};
|
|
@@ -1731,6 +1743,7 @@ async function parseRolloutFile({
|
|
|
1731
1743
|
fileStat,
|
|
1732
1744
|
startOffset,
|
|
1733
1745
|
lastTotal,
|
|
1746
|
+
tokenUsageBaselines,
|
|
1734
1747
|
lastModel,
|
|
1735
1748
|
hourlyState,
|
|
1736
1749
|
touchedBuckets,
|
|
@@ -1751,14 +1764,25 @@ async function parseRolloutFile({
|
|
|
1751
1764
|
const projectFileContexts = [];
|
|
1752
1765
|
addProjectFileContext(projectFileContexts, projectContext);
|
|
1753
1766
|
if (startOffset >= endOffset) {
|
|
1754
|
-
return {
|
|
1767
|
+
return {
|
|
1768
|
+
endOffset,
|
|
1769
|
+
lastTotal,
|
|
1770
|
+
tokenUsageBaselines,
|
|
1771
|
+
lastModel,
|
|
1772
|
+
eventsAggregated: 0,
|
|
1773
|
+
projectFileContexts,
|
|
1774
|
+
};
|
|
1755
1775
|
}
|
|
1756
1776
|
|
|
1757
1777
|
const stream = fssync.createReadStream(filePath, { encoding: "utf8", start: startOffset });
|
|
1758
1778
|
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
1759
1779
|
|
|
1760
1780
|
let model = typeof lastModel === "string" ? lastModel : null;
|
|
1761
|
-
|
|
1781
|
+
const usageDeltaState = createUsageDeltaState({
|
|
1782
|
+
lastTotal,
|
|
1783
|
+
baselines: tokenUsageBaselines,
|
|
1784
|
+
});
|
|
1785
|
+
let latestTotal = lastTotal && typeof lastTotal === "object" ? lastTotal : null;
|
|
1762
1786
|
let currentCwd = null;
|
|
1763
1787
|
let currentDate = null;
|
|
1764
1788
|
let isForkedRollout = false;
|
|
@@ -1840,15 +1864,13 @@ async function parseRolloutFile({
|
|
|
1840
1864
|
|
|
1841
1865
|
const lastUsage = info.last_token_usage;
|
|
1842
1866
|
const totalUsage = info.total_token_usage;
|
|
1867
|
+
if (totalUsage && typeof totalUsage === "object") latestTotal = totalUsage;
|
|
1843
1868
|
|
|
1844
|
-
const
|
|
1845
|
-
|
|
1869
|
+
const rawDelta = consumeUsageDelta(usageDeltaState, lastUsage, totalUsage);
|
|
1870
|
+
const delta = rawDelta ? normalizeUsage(rawDelta) : null;
|
|
1871
|
+
if (!delta || isAllZeroUsage(delta)) continue;
|
|
1846
1872
|
delta.conversation_count = 1;
|
|
1847
1873
|
|
|
1848
|
-
if (totalUsage && typeof totalUsage === "object") {
|
|
1849
|
-
totals = totalUsage;
|
|
1850
|
-
}
|
|
1851
|
-
|
|
1852
1874
|
// Forked Codex rollouts replay the parent session's entire token history
|
|
1853
1875
|
// into the child file the moment the fork is created. The date guard below
|
|
1854
1876
|
// (current_date < rolloutDate) catches cross-day forks, but same-day forks
|
|
@@ -1863,8 +1885,8 @@ async function parseRolloutFile({
|
|
|
1863
1885
|
// flush spacing and well below genuine turn cadence (≥~4.6s observed). The
|
|
1864
1886
|
// first replayed row cannot be identified without lookahead, so it is still
|
|
1865
1887
|
// counted (a bounded, <1% residual over-count); dropping real usage is the
|
|
1866
|
-
// worse failure, so we bias against it. `
|
|
1867
|
-
// keeping the cumulative
|
|
1888
|
+
// worse failure, so we bias against it. `usageDeltaState` is already advanced above,
|
|
1889
|
+
// keeping the cumulative lineage correct for the live turns we keep.
|
|
1868
1890
|
// Scoped to forked codex rollouts. (issue #169 follow-up.)
|
|
1869
1891
|
let forkedReplaySkip = false;
|
|
1870
1892
|
if (isForkedRollout && source === DEFAULT_SOURCE && replayPrefixActive) {
|
|
@@ -1903,8 +1925,8 @@ async function parseRolloutFile({
|
|
|
1903
1925
|
// buckets. External tools rewrite session files without changing the token
|
|
1904
1926
|
// data — Codex-Manager atomically rewrites them (new inode) to patch the
|
|
1905
1927
|
// 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
|
|
1928
|
+
// double-counts the rewritten sessions. `usageDeltaState` is already advanced above,
|
|
1929
|
+
// so skipping an already-seen event keeps the cumulative lineage intact
|
|
1908
1930
|
// while preventing the re-add; genuinely new turns carry new timestamps and
|
|
1909
1931
|
// are still counted. Key = sessionUUID:eventTimestamp (both stable across the
|
|
1910
1932
|
// rewrite and across a sessions/ -> archived_sessions/ move).
|
|
@@ -1939,13 +1961,21 @@ async function parseRolloutFile({
|
|
|
1939
1961
|
eventsAggregated += 1;
|
|
1940
1962
|
}
|
|
1941
1963
|
|
|
1942
|
-
return {
|
|
1964
|
+
return {
|
|
1965
|
+
endOffset,
|
|
1966
|
+
lastTotal: latestTotal,
|
|
1967
|
+
tokenUsageBaselines: snapshotUsageBaselines(usageDeltaState),
|
|
1968
|
+
lastModel: model,
|
|
1969
|
+
eventsAggregated,
|
|
1970
|
+
projectFileContexts,
|
|
1971
|
+
};
|
|
1943
1972
|
}
|
|
1944
1973
|
|
|
1945
1974
|
async function scanRolloutProjectFileContexts({
|
|
1946
1975
|
filePath,
|
|
1947
1976
|
fileStat,
|
|
1948
1977
|
lastTotal,
|
|
1978
|
+
tokenUsageBaselines,
|
|
1949
1979
|
lastModel,
|
|
1950
1980
|
projectState,
|
|
1951
1981
|
projectMetaCache,
|
|
@@ -1958,7 +1988,14 @@ async function scanRolloutProjectFileContexts({
|
|
|
1958
1988
|
const projectFileContexts = [];
|
|
1959
1989
|
addProjectFileContext(projectFileContexts, projectContext);
|
|
1960
1990
|
if (!projectState || endOffset <= 0) {
|
|
1961
|
-
return {
|
|
1991
|
+
return {
|
|
1992
|
+
endOffset,
|
|
1993
|
+
lastTotal,
|
|
1994
|
+
tokenUsageBaselines,
|
|
1995
|
+
lastModel,
|
|
1996
|
+
eventsAggregated: 0,
|
|
1997
|
+
projectFileContexts,
|
|
1998
|
+
};
|
|
1962
1999
|
}
|
|
1963
2000
|
|
|
1964
2001
|
const stream = fssync.createReadStream(filePath, { encoding: "utf8", start: 0 });
|
|
@@ -2005,7 +2042,14 @@ async function scanRolloutProjectFileContexts({
|
|
|
2005
2042
|
addProjectFileContext(projectFileContexts, context);
|
|
2006
2043
|
}
|
|
2007
2044
|
|
|
2008
|
-
return {
|
|
2045
|
+
return {
|
|
2046
|
+
endOffset,
|
|
2047
|
+
lastTotal,
|
|
2048
|
+
tokenUsageBaselines,
|
|
2049
|
+
lastModel,
|
|
2050
|
+
eventsAggregated: 0,
|
|
2051
|
+
projectFileContexts,
|
|
2052
|
+
};
|
|
2009
2053
|
}
|
|
2010
2054
|
|
|
2011
2055
|
async function parseClaudeFile({
|
|
@@ -2861,6 +2905,15 @@ function normalizeOpencodeState(raw) {
|
|
|
2861
2905
|
};
|
|
2862
2906
|
}
|
|
2863
2907
|
|
|
2908
|
+
function normalizeQoderState(raw) {
|
|
2909
|
+
const state = raw && typeof raw === "object" ? raw : {};
|
|
2910
|
+
const messages = state.messages && typeof state.messages === "object" ? state.messages : {};
|
|
2911
|
+
return {
|
|
2912
|
+
messages,
|
|
2913
|
+
updatedAt: typeof state.updatedAt === "string" ? state.updatedAt : null,
|
|
2914
|
+
};
|
|
2915
|
+
}
|
|
2916
|
+
|
|
2864
2917
|
function normalizeMessageKeyPart(value) {
|
|
2865
2918
|
if (typeof value !== "string") return "";
|
|
2866
2919
|
return value.trim();
|
|
@@ -2954,6 +3007,33 @@ function addTotals(target, delta) {
|
|
|
2954
3007
|
target.conversation_count += delta.conversation_count || 0;
|
|
2955
3008
|
}
|
|
2956
3009
|
|
|
3010
|
+
function subtractTotals(target, totals) {
|
|
3011
|
+
target.input_tokens = Math.max(0, target.input_tokens - (totals.input_tokens || 0));
|
|
3012
|
+
target.cached_input_tokens = Math.max(
|
|
3013
|
+
0,
|
|
3014
|
+
target.cached_input_tokens - (totals.cached_input_tokens || 0),
|
|
3015
|
+
);
|
|
3016
|
+
target.cache_creation_input_tokens = Math.max(
|
|
3017
|
+
0,
|
|
3018
|
+
target.cache_creation_input_tokens - (totals.cache_creation_input_tokens || 0),
|
|
3019
|
+
);
|
|
3020
|
+
target.output_tokens = Math.max(0, target.output_tokens - (totals.output_tokens || 0));
|
|
3021
|
+
target.reasoning_output_tokens = Math.max(
|
|
3022
|
+
0,
|
|
3023
|
+
target.reasoning_output_tokens - (totals.reasoning_output_tokens || 0),
|
|
3024
|
+
);
|
|
3025
|
+
target.total_tokens = Math.max(0, target.total_tokens - (totals.total_tokens || 0));
|
|
3026
|
+
target.billable_total_tokens = Math.max(
|
|
3027
|
+
0,
|
|
3028
|
+
target.billable_total_tokens -
|
|
3029
|
+
(totals.billable_total_tokens ?? totals.total_tokens ?? 0),
|
|
3030
|
+
);
|
|
3031
|
+
target.conversation_count = Math.max(
|
|
3032
|
+
0,
|
|
3033
|
+
target.conversation_count - (totals.conversation_count || 0),
|
|
3034
|
+
);
|
|
3035
|
+
}
|
|
3036
|
+
|
|
2957
3037
|
function totalsKey(totals) {
|
|
2958
3038
|
return [
|
|
2959
3039
|
totals.input_tokens || 0,
|
|
@@ -3570,48 +3650,6 @@ function extractTokenCount(obj) {
|
|
|
3570
3650
|
return null;
|
|
3571
3651
|
}
|
|
3572
3652
|
|
|
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
3653
|
function normalizeUsage(u) {
|
|
3616
3654
|
const out = {};
|
|
3617
3655
|
for (const k of [
|
|
@@ -3694,13 +3732,6 @@ function isAllZeroUsage(u) {
|
|
|
3694
3732
|
return true;
|
|
3695
3733
|
}
|
|
3696
3734
|
|
|
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
3735
|
function isFiniteNumber(v) {
|
|
3705
3736
|
return typeof v === "number" && Number.isFinite(v);
|
|
3706
3737
|
}
|
|
@@ -4046,6 +4077,349 @@ async function parseOpencodeDbIncremental({
|
|
|
4046
4077
|
return { messagesProcessed, eventsAggregated, bucketsQueued, projectBucketsQueued };
|
|
4047
4078
|
}
|
|
4048
4079
|
|
|
4080
|
+
const QODER_USAGE_SQL = `
|
|
4081
|
+
SELECT
|
|
4082
|
+
cm.rowid AS row_id,
|
|
4083
|
+
cm.id,
|
|
4084
|
+
cm.session_id,
|
|
4085
|
+
cm.request_id,
|
|
4086
|
+
cm.token_info,
|
|
4087
|
+
cm.model_info,
|
|
4088
|
+
cm.gmt_create,
|
|
4089
|
+
cr.extra AS record_extra,
|
|
4090
|
+
cs.preferred_model_info,
|
|
4091
|
+
cs.project_uri,
|
|
4092
|
+
cs.project_name
|
|
4093
|
+
FROM chat_message AS cm
|
|
4094
|
+
LEFT JOIN chat_record AS cr ON cr.request_id = cm.request_id
|
|
4095
|
+
LEFT JOIN chat_session AS cs ON cs.session_id = cm.session_id
|
|
4096
|
+
WHERE cm.role = 'assistant'
|
|
4097
|
+
AND cm.token_info IS NOT NULL
|
|
4098
|
+
AND trim(cm.token_info) NOT IN ('', '{}')
|
|
4099
|
+
ORDER BY cm.gmt_create, cm.rowid
|
|
4100
|
+
`;
|
|
4101
|
+
|
|
4102
|
+
function resolveQoderDbPath({
|
|
4103
|
+
home = os.homedir(),
|
|
4104
|
+
env = process.env,
|
|
4105
|
+
platform = process.platform,
|
|
4106
|
+
} = {}) {
|
|
4107
|
+
if (typeof env.QODER_DB_PATH === "string" && env.QODER_DB_PATH.trim()) {
|
|
4108
|
+
return path.resolve(env.QODER_DB_PATH.trim());
|
|
4109
|
+
}
|
|
4110
|
+
let root;
|
|
4111
|
+
if (typeof env.QODER_HOME === "string" && env.QODER_HOME.trim()) {
|
|
4112
|
+
root = path.resolve(env.QODER_HOME.trim());
|
|
4113
|
+
} else if (platform === "darwin") {
|
|
4114
|
+
root = path.join(home, "Library", "Application Support", "Qoder");
|
|
4115
|
+
} else if (platform === "win32") {
|
|
4116
|
+
root = path.join(
|
|
4117
|
+
env.APPDATA || path.join(home, "AppData", "Roaming"),
|
|
4118
|
+
"Qoder",
|
|
4119
|
+
);
|
|
4120
|
+
} else {
|
|
4121
|
+
root = path.join(home, ".config", "Qoder");
|
|
4122
|
+
}
|
|
4123
|
+
return path.join(root, "SharedClientCache", "cache", "db", "local.db");
|
|
4124
|
+
}
|
|
4125
|
+
|
|
4126
|
+
function resolveQoderDbPaths({
|
|
4127
|
+
home = os.homedir(),
|
|
4128
|
+
env = process.env,
|
|
4129
|
+
platform = process.platform,
|
|
4130
|
+
deps = {},
|
|
4131
|
+
} = {}) {
|
|
4132
|
+
const nativeValue = resolveQoderDbPath({ home, env, platform });
|
|
4133
|
+
if (platform !== "win32" || env.QODER_DB_PATH || env.QODER_HOME) {
|
|
4134
|
+
return { native: nativeValue, wsl: null };
|
|
4135
|
+
}
|
|
4136
|
+
const existsSync = deps.existsSync || fssync.existsSync;
|
|
4137
|
+
const native = wsl.shouldProbeNative(env) && existsSync(nativeValue) ? nativeValue : null;
|
|
4138
|
+
const discoverWslHome = deps.discoverWslHome || wsl.discoverWslHome;
|
|
4139
|
+
const wslRoot = wsl.shouldProbeWsl(env)
|
|
4140
|
+
? discoverWslHome(".config/Qoder", { ...deps, env })
|
|
4141
|
+
: null;
|
|
4142
|
+
const wslValue = wslRoot
|
|
4143
|
+
? path.join(wslRoot, "SharedClientCache", "cache", "db", "local.db")
|
|
4144
|
+
: null;
|
|
4145
|
+
const wslDb = wslValue && existsSync(wslValue) ? wslValue : null;
|
|
4146
|
+
return wsl.resolveAllWin32Paths({
|
|
4147
|
+
nativeValue: native,
|
|
4148
|
+
wslValue: wslDb,
|
|
4149
|
+
env,
|
|
4150
|
+
platform: "win32",
|
|
4151
|
+
});
|
|
4152
|
+
}
|
|
4153
|
+
|
|
4154
|
+
async function readQoderDbMessages(dbPath, sqliteOptions = {}) {
|
|
4155
|
+
if (!dbPath || !fssync.existsSync(dbPath)) return [];
|
|
4156
|
+
return readSqliteJsonRowsAsync(dbPath, QODER_USAGE_SQL, {
|
|
4157
|
+
label: "Qoder",
|
|
4158
|
+
...sqliteOptions,
|
|
4159
|
+
});
|
|
4160
|
+
}
|
|
4161
|
+
|
|
4162
|
+
function parseJsonObject(value) {
|
|
4163
|
+
if (!value) return null;
|
|
4164
|
+
if (value && typeof value === "object" && !Buffer.isBuffer(value)) return value;
|
|
4165
|
+
try {
|
|
4166
|
+
const parsed = JSON.parse(String(value));
|
|
4167
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
4168
|
+
} catch (_error) {
|
|
4169
|
+
return null;
|
|
4170
|
+
}
|
|
4171
|
+
}
|
|
4172
|
+
|
|
4173
|
+
function normalizeQoderTokens(tokenInfo) {
|
|
4174
|
+
const tokens = parseJsonObject(tokenInfo);
|
|
4175
|
+
if (!tokens) return null;
|
|
4176
|
+
const prompt = Number(tokens.prompt_tokens);
|
|
4177
|
+
const cached = Number(tokens.cached_tokens || 0);
|
|
4178
|
+
const completion = Number(tokens.completion_tokens);
|
|
4179
|
+
if (
|
|
4180
|
+
!Number.isFinite(prompt) ||
|
|
4181
|
+
!Number.isFinite(cached) ||
|
|
4182
|
+
!Number.isFinite(completion) ||
|
|
4183
|
+
prompt < 0 ||
|
|
4184
|
+
cached < 0 ||
|
|
4185
|
+
completion < 0
|
|
4186
|
+
) {
|
|
4187
|
+
return null;
|
|
4188
|
+
}
|
|
4189
|
+
// Qoder's prompt_tokens already includes cached_tokens. Keep cached input in
|
|
4190
|
+
// its own column and report only the remainder as ordinary input, otherwise
|
|
4191
|
+
// cached context is counted twice in dashboards and cost calculations.
|
|
4192
|
+
const input = Math.max(0, Math.trunc(prompt) - Math.trunc(cached));
|
|
4193
|
+
const cachedInput = Math.min(Math.trunc(prompt), Math.trunc(cached));
|
|
4194
|
+
const output = Math.trunc(completion);
|
|
4195
|
+
return {
|
|
4196
|
+
input_tokens: input,
|
|
4197
|
+
cached_input_tokens: cachedInput,
|
|
4198
|
+
cache_creation_input_tokens: 0,
|
|
4199
|
+
output_tokens: output,
|
|
4200
|
+
reasoning_output_tokens: 0,
|
|
4201
|
+
total_tokens: Math.trunc(prompt) + output,
|
|
4202
|
+
billable_total_tokens: Math.trunc(prompt) + output,
|
|
4203
|
+
};
|
|
4204
|
+
}
|
|
4205
|
+
|
|
4206
|
+
function qoderModelFromRow(row) {
|
|
4207
|
+
const direct = parseJsonObject(row?.model_info);
|
|
4208
|
+
const recordExtra = parseJsonObject(row?.record_extra);
|
|
4209
|
+
const preferred = parseJsonObject(row?.preferred_model_info);
|
|
4210
|
+
return (
|
|
4211
|
+
normalizeModelInput(direct?.model_key || direct?.modelKey) ||
|
|
4212
|
+
normalizeModelInput(recordExtra?.modelConfig?.key || recordExtra?.model_config?.key) ||
|
|
4213
|
+
normalizeModelInput(
|
|
4214
|
+
preferred?.model_key ||
|
|
4215
|
+
preferred?.modelKey ||
|
|
4216
|
+
preferred?.preferred_model ||
|
|
4217
|
+
preferred?.preferredModel,
|
|
4218
|
+
) ||
|
|
4219
|
+
"qoder-agent"
|
|
4220
|
+
);
|
|
4221
|
+
}
|
|
4222
|
+
|
|
4223
|
+
function qoderMessageKey(row) {
|
|
4224
|
+
const id = normalizeMessageKeyPart(row?.id);
|
|
4225
|
+
const sessionId = normalizeMessageKeyPart(row?.session_id);
|
|
4226
|
+
if (sessionId && id) return `${sessionId}|${id}`;
|
|
4227
|
+
if (id) return id;
|
|
4228
|
+
const rowId = row?.row_id;
|
|
4229
|
+
return rowId === null || rowId === undefined ? null : `row:${rowId}`;
|
|
4230
|
+
}
|
|
4231
|
+
|
|
4232
|
+
function qoderProjectPath(row) {
|
|
4233
|
+
const raw = typeof row?.project_uri === "string" ? row.project_uri.trim() : "";
|
|
4234
|
+
if (!raw) return null;
|
|
4235
|
+
if (!raw.startsWith("file://")) return raw;
|
|
4236
|
+
try {
|
|
4237
|
+
return decodeURIComponent(new URL(raw).pathname);
|
|
4238
|
+
} catch (_error) {
|
|
4239
|
+
return null;
|
|
4240
|
+
}
|
|
4241
|
+
}
|
|
4242
|
+
|
|
4243
|
+
async function parseQoderDbIncremental({
|
|
4244
|
+
dbMessages,
|
|
4245
|
+
cursors,
|
|
4246
|
+
queuePath,
|
|
4247
|
+
projectQueuePath,
|
|
4248
|
+
onProgress,
|
|
4249
|
+
publicRepoResolver,
|
|
4250
|
+
} = {}) {
|
|
4251
|
+
await ensureDir(path.dirname(queuePath));
|
|
4252
|
+
const rows = Array.isArray(dbMessages) ? dbMessages : [];
|
|
4253
|
+
const hourlyState = normalizeHourlyState(cursors?.hourly);
|
|
4254
|
+
const qoderState = normalizeQoderState(cursors?.qoder);
|
|
4255
|
+
const touchedBuckets = new Set();
|
|
4256
|
+
const projectEnabled = typeof projectQueuePath === "string" && projectQueuePath.length > 0;
|
|
4257
|
+
const projectState = projectEnabled ? normalizeProjectState(cursors?.projectHourly) : null;
|
|
4258
|
+
const projectTouchedBuckets = projectEnabled ? new Set() : null;
|
|
4259
|
+
const projectMetaCache = projectEnabled ? new Map() : null;
|
|
4260
|
+
const publicRepoCache = projectEnabled ? new Map() : null;
|
|
4261
|
+
const requestOwners = new Map();
|
|
4262
|
+
for (const row of rows) {
|
|
4263
|
+
const messageKey = qoderMessageKey(row);
|
|
4264
|
+
if (
|
|
4265
|
+
!messageKey ||
|
|
4266
|
+
!normalizeQoderTokens(row?.token_info) ||
|
|
4267
|
+
!coerceEpochMs(row?.gmt_create)
|
|
4268
|
+
) {
|
|
4269
|
+
continue;
|
|
4270
|
+
}
|
|
4271
|
+
const requestKey =
|
|
4272
|
+
normalizeMessageKeyPart(row?.request_id) ||
|
|
4273
|
+
normalizeMessageKeyPart(row?.session_id) ||
|
|
4274
|
+
messageKey;
|
|
4275
|
+
if (!requestOwners.has(requestKey)) requestOwners.set(requestKey, messageKey);
|
|
4276
|
+
}
|
|
4277
|
+
const cb = typeof onProgress === "function" ? onProgress : null;
|
|
4278
|
+
let messagesProcessed = 0;
|
|
4279
|
+
let eventsAggregated = 0;
|
|
4280
|
+
|
|
4281
|
+
for (let index = 0; index < rows.length; index++) {
|
|
4282
|
+
const row = rows[index];
|
|
4283
|
+
const messageKey = qoderMessageKey(row);
|
|
4284
|
+
const currentBase = normalizeQoderTokens(row?.token_info);
|
|
4285
|
+
const timestampMs = coerceEpochMs(row?.gmt_create);
|
|
4286
|
+
const bucketStart = timestampMs
|
|
4287
|
+
? toUtcHalfHourStart(new Date(timestampMs).toISOString())
|
|
4288
|
+
: null;
|
|
4289
|
+
if (!messageKey || !currentBase || !bucketStart) {
|
|
4290
|
+
messagesProcessed += 1;
|
|
4291
|
+
continue;
|
|
4292
|
+
}
|
|
4293
|
+
|
|
4294
|
+
const requestKey =
|
|
4295
|
+
normalizeMessageKeyPart(row?.request_id) ||
|
|
4296
|
+
normalizeMessageKeyPart(row?.session_id) ||
|
|
4297
|
+
messageKey;
|
|
4298
|
+
const previous = qoderState.messages[messageKey];
|
|
4299
|
+
// Recompute request ownership from the complete ordered DB snapshot on
|
|
4300
|
+
// every pass. Qoder can attach token_info to an earlier assistant row only
|
|
4301
|
+
// after a later row was already counted; retaining the old per-message
|
|
4302
|
+
// owner in that case inflates one request to two conversations.
|
|
4303
|
+
const conversationCount = requestOwners.get(requestKey) === messageKey ? 1 : 0;
|
|
4304
|
+
|
|
4305
|
+
const currentTotals = {
|
|
4306
|
+
...currentBase,
|
|
4307
|
+
conversation_count: conversationCount,
|
|
4308
|
+
};
|
|
4309
|
+
const model = qoderModelFromRow(row);
|
|
4310
|
+
let projectKey = null;
|
|
4311
|
+
let projectRef = null;
|
|
4312
|
+
if (projectEnabled) {
|
|
4313
|
+
const projectPath = qoderProjectPath(row);
|
|
4314
|
+
if (projectPath) {
|
|
4315
|
+
const context = await resolveProjectContextForPath({
|
|
4316
|
+
startDir: projectPath,
|
|
4317
|
+
projectMetaCache,
|
|
4318
|
+
publicRepoCache,
|
|
4319
|
+
publicRepoResolver,
|
|
4320
|
+
projectState,
|
|
4321
|
+
});
|
|
4322
|
+
projectKey = context?.projectKey || null;
|
|
4323
|
+
projectRef = context?.projectRef || null;
|
|
4324
|
+
}
|
|
4325
|
+
}
|
|
4326
|
+
|
|
4327
|
+
const previousTotals =
|
|
4328
|
+
previous?.totals && typeof previous.totals === "object" ? previous.totals : null;
|
|
4329
|
+
const unchanged =
|
|
4330
|
+
previousTotals &&
|
|
4331
|
+
totalsKey(previousTotals) === totalsKey(currentTotals) &&
|
|
4332
|
+
previous.bucketStart === bucketStart &&
|
|
4333
|
+
previous.model === model &&
|
|
4334
|
+
(previous.projectKey || null) === projectKey;
|
|
4335
|
+
if (!unchanged) {
|
|
4336
|
+
if (previousTotals && previous.bucketStart && previous.model) {
|
|
4337
|
+
const oldBucket = getHourlyBucket(
|
|
4338
|
+
hourlyState,
|
|
4339
|
+
"qoder",
|
|
4340
|
+
previous.model,
|
|
4341
|
+
previous.bucketStart,
|
|
4342
|
+
);
|
|
4343
|
+
subtractTotals(oldBucket.totals, previousTotals);
|
|
4344
|
+
touchedBuckets.add(bucketKey("qoder", previous.model, previous.bucketStart));
|
|
4345
|
+
if (projectEnabled && previous.projectKey) {
|
|
4346
|
+
const oldProjectBucket = getProjectBucket(
|
|
4347
|
+
projectState,
|
|
4348
|
+
previous.projectKey,
|
|
4349
|
+
"qoder",
|
|
4350
|
+
previous.bucketStart,
|
|
4351
|
+
previous.projectRef || null,
|
|
4352
|
+
);
|
|
4353
|
+
subtractTotals(oldProjectBucket.totals, previousTotals);
|
|
4354
|
+
projectTouchedBuckets.add(
|
|
4355
|
+
projectBucketKey(previous.projectKey, "qoder", previous.bucketStart),
|
|
4356
|
+
);
|
|
4357
|
+
}
|
|
4358
|
+
}
|
|
4359
|
+
|
|
4360
|
+
const bucket = getHourlyBucket(hourlyState, "qoder", model, bucketStart);
|
|
4361
|
+
addTotals(bucket.totals, currentTotals);
|
|
4362
|
+
touchedBuckets.add(bucketKey("qoder", model, bucketStart));
|
|
4363
|
+
if (projectEnabled && projectKey) {
|
|
4364
|
+
const projectBucket = getProjectBucket(
|
|
4365
|
+
projectState,
|
|
4366
|
+
projectKey,
|
|
4367
|
+
"qoder",
|
|
4368
|
+
bucketStart,
|
|
4369
|
+
projectRef,
|
|
4370
|
+
);
|
|
4371
|
+
addTotals(projectBucket.totals, currentTotals);
|
|
4372
|
+
projectTouchedBuckets.add(projectBucketKey(projectKey, "qoder", bucketStart));
|
|
4373
|
+
}
|
|
4374
|
+
qoderState.messages[messageKey] = {
|
|
4375
|
+
totals: currentTotals,
|
|
4376
|
+
conversationCount,
|
|
4377
|
+
requestKey,
|
|
4378
|
+
bucketStart,
|
|
4379
|
+
model,
|
|
4380
|
+
projectKey,
|
|
4381
|
+
projectRef,
|
|
4382
|
+
updatedAt: new Date().toISOString(),
|
|
4383
|
+
};
|
|
4384
|
+
eventsAggregated += 1;
|
|
4385
|
+
}
|
|
4386
|
+
|
|
4387
|
+
messagesProcessed += 1;
|
|
4388
|
+
if (cb) {
|
|
4389
|
+
cb({
|
|
4390
|
+
index: index + 1,
|
|
4391
|
+
total: rows.length,
|
|
4392
|
+
messagesProcessed,
|
|
4393
|
+
eventsAggregated,
|
|
4394
|
+
bucketsQueued: touchedBuckets.size,
|
|
4395
|
+
});
|
|
4396
|
+
}
|
|
4397
|
+
}
|
|
4398
|
+
|
|
4399
|
+
const bucketsQueued = await enqueueTouchedBuckets({
|
|
4400
|
+
queuePath,
|
|
4401
|
+
hourlyState,
|
|
4402
|
+
touchedBuckets,
|
|
4403
|
+
});
|
|
4404
|
+
const projectBucketsQueued = projectEnabled
|
|
4405
|
+
? await enqueueTouchedProjectBuckets({
|
|
4406
|
+
projectQueuePath,
|
|
4407
|
+
projectState,
|
|
4408
|
+
projectTouchedBuckets,
|
|
4409
|
+
})
|
|
4410
|
+
: 0;
|
|
4411
|
+
const updatedAt = new Date().toISOString();
|
|
4412
|
+
hourlyState.updatedAt = updatedAt;
|
|
4413
|
+
qoderState.updatedAt = updatedAt;
|
|
4414
|
+
cursors.hourly = hourlyState;
|
|
4415
|
+
cursors.qoder = qoderState;
|
|
4416
|
+
if (projectState) {
|
|
4417
|
+
projectState.updatedAt = updatedAt;
|
|
4418
|
+
cursors.projectHourly = projectState;
|
|
4419
|
+
}
|
|
4420
|
+
return { messagesProcessed, eventsAggregated, bucketsQueued, projectBucketsQueued };
|
|
4421
|
+
}
|
|
4422
|
+
|
|
4049
4423
|
// Resolve project context from DB message (no file path available)
|
|
4050
4424
|
async function resolveProjectContextForDb({
|
|
4051
4425
|
msg,
|
|
@@ -4940,32 +5314,77 @@ function resolveKiroCliDbPath(env = process.env) {
|
|
|
4940
5314
|
// no hyphens. kiro-cli writes proper UUIDs; lock to the canonical shape.
|
|
4941
5315
|
const KIRO_CLI_SESSION_FILE_RE =
|
|
4942
5316
|
/^[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
|
-
|
|
5317
|
+
// The directory prefix and direct messages.jsonl child are the stable
|
|
5318
|
+
// contract. Do not assume the suffix will always remain a UUID: Kiro has
|
|
5319
|
+
// already changed this layout once, and an opaque non-empty session id is
|
|
5320
|
+
// sufficient to keep discovery both bounded and forward-compatible.
|
|
5321
|
+
const KIRO_CLI_V2_SESSION_DIR_RE = /^sess_.+$/;
|
|
5322
|
+
|
|
5323
|
+
// Lists both Kiro CLI session layouts:
|
|
5324
|
+
// legacy: ~/.kiro/sessions/cli/{uuid}.json
|
|
5325
|
+
// 2.13+: ~/.kiro/sessions/{workspaceHash}/sess_{uuid}/messages.jsonl
|
|
5326
|
+
//
|
|
5327
|
+
// Legacy .json files are rewritten atomically per turn. The 2.13+ JSONL is
|
|
5328
|
+
// append-only and may be read while a session is active. Only the direct
|
|
5329
|
+
// messages.jsonl child is accepted; sub-executions and arbitrary nested JSONL
|
|
5330
|
+
// files are intentionally excluded.
|
|
4949
5331
|
//
|
|
4950
5332
|
// TASK-014: env.HOME is honored (symmetric with resolveKiroCliDbPath) so
|
|
4951
5333
|
// callers can redirect to a tmp home for hermetic tests/CI.
|
|
4952
5334
|
function resolveKiroCliSessionFiles(env = process.env) {
|
|
4953
5335
|
const home = env.HOME || require("node:os").homedir();
|
|
4954
5336
|
const kiroHome = env.KIRO_HOME || path.join(home, ".kiro");
|
|
4955
|
-
const
|
|
4956
|
-
if (!fssync.existsSync(
|
|
5337
|
+
const sessionsRoot = path.join(kiroHome, "sessions");
|
|
5338
|
+
if (!fssync.existsSync(sessionsRoot)) return [];
|
|
4957
5339
|
const files = [];
|
|
5340
|
+
|
|
5341
|
+
const legacyDir = path.join(sessionsRoot, "cli");
|
|
4958
5342
|
try {
|
|
4959
|
-
for (const entry of fssync.readdirSync(
|
|
5343
|
+
for (const entry of fssync.readdirSync(legacyDir)) {
|
|
4960
5344
|
// TASK-003: only canonical {uuid}.json files; backups, scratch,
|
|
4961
5345
|
// typos are skipped so they don't feed JSON.parse garbage.
|
|
4962
5346
|
if (!KIRO_CLI_SESSION_FILE_RE.test(entry)) continue;
|
|
4963
|
-
files.push(path.join(
|
|
5347
|
+
files.push(path.join(legacyDir, entry));
|
|
4964
5348
|
}
|
|
4965
5349
|
} catch {
|
|
4966
5350
|
// ignore read errors
|
|
4967
5351
|
}
|
|
4968
|
-
|
|
5352
|
+
|
|
5353
|
+
try {
|
|
5354
|
+
const workspaceDirs = fssync.readdirSync(sessionsRoot, {
|
|
5355
|
+
withFileTypes: true,
|
|
5356
|
+
});
|
|
5357
|
+
for (const workspace of workspaceDirs) {
|
|
5358
|
+
if (!workspace.isDirectory() || workspace.name === "cli") continue;
|
|
5359
|
+
const workspacePath = path.join(sessionsRoot, workspace.name);
|
|
5360
|
+
let sessionDirs;
|
|
5361
|
+
try {
|
|
5362
|
+
sessionDirs = fssync.readdirSync(workspacePath, {
|
|
5363
|
+
withFileTypes: true,
|
|
5364
|
+
});
|
|
5365
|
+
} catch {
|
|
5366
|
+
continue;
|
|
5367
|
+
}
|
|
5368
|
+
for (const session of sessionDirs) {
|
|
5369
|
+
if (
|
|
5370
|
+
!session.isDirectory() ||
|
|
5371
|
+
!KIRO_CLI_V2_SESSION_DIR_RE.test(session.name)
|
|
5372
|
+
) {
|
|
5373
|
+
continue;
|
|
5374
|
+
}
|
|
5375
|
+
const messagesPath = path.join(
|
|
5376
|
+
workspacePath,
|
|
5377
|
+
session.name,
|
|
5378
|
+
"messages.jsonl",
|
|
5379
|
+
);
|
|
5380
|
+
if (fssync.existsSync(messagesPath)) files.push(messagesPath);
|
|
5381
|
+
}
|
|
5382
|
+
}
|
|
5383
|
+
} catch {
|
|
5384
|
+
// ignore read errors
|
|
5385
|
+
}
|
|
5386
|
+
|
|
5387
|
+
return files.sort();
|
|
4969
5388
|
}
|
|
4970
5389
|
|
|
4971
5390
|
// Build char-count maps from a .jsonl sibling file. Lets us approximate
|
|
@@ -5183,6 +5602,199 @@ async function readKiroCliSessionTurns(jsonPath) {
|
|
|
5183
5602
|
return flat;
|
|
5184
5603
|
}
|
|
5185
5604
|
|
|
5605
|
+
function kiroCliV2ContentChars(value) {
|
|
5606
|
+
if (typeof value === "string") return value.length;
|
|
5607
|
+
if (Array.isArray(value)) {
|
|
5608
|
+
return value.reduce(
|
|
5609
|
+
(sum, item) => sum + kiroCliV2ContentChars(item),
|
|
5610
|
+
0,
|
|
5611
|
+
);
|
|
5612
|
+
}
|
|
5613
|
+
if (!value || typeof value !== "object") return 0;
|
|
5614
|
+
for (const key of ["content", "text", "value", "parts", "entries"]) {
|
|
5615
|
+
if (value[key] !== undefined) {
|
|
5616
|
+
return kiroCliV2ContentChars(value[key]);
|
|
5617
|
+
}
|
|
5618
|
+
}
|
|
5619
|
+
return 0;
|
|
5620
|
+
}
|
|
5621
|
+
|
|
5622
|
+
// Kiro CLI 2.13+ writes event-sourced sessions under
|
|
5623
|
+
// ~/.kiro/sessions/<workspaceHash>/sess_<uuid>/messages.jsonl. The records do
|
|
5624
|
+
// not carry token counts, but user/assistant text, turn boundaries, timestamps,
|
|
5625
|
+
// and assistant reasoningModelId are sufficient for the same 4 chars/token
|
|
5626
|
+
// approximation used by the legacy CLI reader.
|
|
5627
|
+
async function readKiroCliV2SessionTurns(messagesPath) {
|
|
5628
|
+
if (
|
|
5629
|
+
!messagesPath ||
|
|
5630
|
+
path.basename(messagesPath) !== "messages.jsonl" ||
|
|
5631
|
+
!fssync.existsSync(messagesPath)
|
|
5632
|
+
) {
|
|
5633
|
+
return [];
|
|
5634
|
+
}
|
|
5635
|
+
|
|
5636
|
+
const sessionDir = path.dirname(messagesPath);
|
|
5637
|
+
let sessionMeta = {};
|
|
5638
|
+
try {
|
|
5639
|
+
sessionMeta = JSON.parse(
|
|
5640
|
+
fssync.readFileSync(path.join(sessionDir, "session.json"), "utf8"),
|
|
5641
|
+
);
|
|
5642
|
+
} catch {
|
|
5643
|
+
// messages.jsonl is self-contained enough to parse without session.json.
|
|
5644
|
+
}
|
|
5645
|
+
|
|
5646
|
+
const sessionId =
|
|
5647
|
+
typeof sessionMeta?.id === "string" && sessionMeta.id
|
|
5648
|
+
? sessionMeta.id
|
|
5649
|
+
: path.basename(sessionDir);
|
|
5650
|
+
const fallbackModel =
|
|
5651
|
+
typeof sessionMeta?.modelId === "string" ? sessionMeta.modelId : null;
|
|
5652
|
+
const fallbackTimestamp = Date.parse(
|
|
5653
|
+
sessionMeta?.createdAt || sessionMeta?.lastModifiedAt || "",
|
|
5654
|
+
);
|
|
5655
|
+
|
|
5656
|
+
const flat = [];
|
|
5657
|
+
let stream;
|
|
5658
|
+
try {
|
|
5659
|
+
stream = fssync.createReadStream(messagesPath, { encoding: "utf8" });
|
|
5660
|
+
} catch {
|
|
5661
|
+
return flat;
|
|
5662
|
+
}
|
|
5663
|
+
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
5664
|
+
|
|
5665
|
+
let pendingUserChars = 0;
|
|
5666
|
+
let pendingUserTimestampMs = NaN;
|
|
5667
|
+
let turn = null;
|
|
5668
|
+
let fallbackTurnIndex = 0;
|
|
5669
|
+
|
|
5670
|
+
const flushTurn = () => {
|
|
5671
|
+
if (!turn) return;
|
|
5672
|
+
const hasUsage =
|
|
5673
|
+
turn.outputChars > 0 ||
|
|
5674
|
+
turn.reasoningChars > 0;
|
|
5675
|
+
const tsMs = Number.isFinite(turn.timestampMs)
|
|
5676
|
+
? turn.timestampMs
|
|
5677
|
+
: Number.isFinite(pendingUserTimestampMs)
|
|
5678
|
+
? pendingUserTimestampMs
|
|
5679
|
+
: fallbackTimestamp;
|
|
5680
|
+
if (hasUsage && Number.isFinite(tsMs) && tsMs > 0) {
|
|
5681
|
+
const requestId =
|
|
5682
|
+
turn.executionId ||
|
|
5683
|
+
`${sessionId}:v2:${fallbackTurnIndex}`;
|
|
5684
|
+
const messageIds = Array.from(turn.messageIds);
|
|
5685
|
+
flat.push({
|
|
5686
|
+
request_id: requestId,
|
|
5687
|
+
message_id: messageIds[0] || turn.executionId || null,
|
|
5688
|
+
all_message_ids: messageIds,
|
|
5689
|
+
session_id: sessionId,
|
|
5690
|
+
session_model_id: fallbackModel,
|
|
5691
|
+
model_id: turn.modelId || fallbackModel,
|
|
5692
|
+
request_start_timestamp_ms: tsMs,
|
|
5693
|
+
user_prompt_length: turn.inputChars,
|
|
5694
|
+
response_size: turn.outputChars,
|
|
5695
|
+
reasoning_size: turn.reasoningChars,
|
|
5696
|
+
});
|
|
5697
|
+
}
|
|
5698
|
+
fallbackTurnIndex++;
|
|
5699
|
+
turn = null;
|
|
5700
|
+
};
|
|
5701
|
+
|
|
5702
|
+
const startTurn = (payload, timestampMs) => {
|
|
5703
|
+
flushTurn();
|
|
5704
|
+
turn = {
|
|
5705
|
+
executionId:
|
|
5706
|
+
typeof payload?.executionId === "string" ? payload.executionId : null,
|
|
5707
|
+
timestampMs:
|
|
5708
|
+
Number.isFinite(timestampMs)
|
|
5709
|
+
? timestampMs
|
|
5710
|
+
: pendingUserTimestampMs,
|
|
5711
|
+
inputChars: pendingUserChars,
|
|
5712
|
+
outputChars: 0,
|
|
5713
|
+
reasoningChars: 0,
|
|
5714
|
+
modelId: null,
|
|
5715
|
+
messageIds: new Set(),
|
|
5716
|
+
};
|
|
5717
|
+
pendingUserChars = 0;
|
|
5718
|
+
pendingUserTimestampMs = NaN;
|
|
5719
|
+
};
|
|
5720
|
+
|
|
5721
|
+
try {
|
|
5722
|
+
for await (const line of rl) {
|
|
5723
|
+
if (!line || !line.trim()) continue;
|
|
5724
|
+
let event;
|
|
5725
|
+
try {
|
|
5726
|
+
event = JSON.parse(line);
|
|
5727
|
+
} catch {
|
|
5728
|
+
continue;
|
|
5729
|
+
}
|
|
5730
|
+
const payload = event?.payload;
|
|
5731
|
+
if (!payload || typeof payload !== "object") continue;
|
|
5732
|
+
const type =
|
|
5733
|
+
typeof payload.type === "string"
|
|
5734
|
+
? payload.type.toLowerCase()
|
|
5735
|
+
: "";
|
|
5736
|
+
const timestampMs = Date.parse(event.timestamp || "");
|
|
5737
|
+
|
|
5738
|
+
if (type === "user") {
|
|
5739
|
+
if (turn) flushTurn();
|
|
5740
|
+
pendingUserChars = kiroCliV2ContentChars(payload.content);
|
|
5741
|
+
pendingUserTimestampMs = timestampMs;
|
|
5742
|
+
continue;
|
|
5743
|
+
}
|
|
5744
|
+
|
|
5745
|
+
if (type === "turn_start") {
|
|
5746
|
+
startTurn(payload, timestampMs);
|
|
5747
|
+
continue;
|
|
5748
|
+
}
|
|
5749
|
+
|
|
5750
|
+
if (
|
|
5751
|
+
["assistant", "tool_call", "tool_result", "usage_summary"].includes(
|
|
5752
|
+
type,
|
|
5753
|
+
) &&
|
|
5754
|
+
!turn
|
|
5755
|
+
) {
|
|
5756
|
+
startTurn(payload, timestampMs);
|
|
5757
|
+
}
|
|
5758
|
+
if (!turn) continue;
|
|
5759
|
+
|
|
5760
|
+
if (
|
|
5761
|
+
typeof payload.executionId === "string" &&
|
|
5762
|
+
payload.executionId &&
|
|
5763
|
+
!turn.executionId
|
|
5764
|
+
) {
|
|
5765
|
+
turn.executionId = payload.executionId;
|
|
5766
|
+
}
|
|
5767
|
+
if (typeof event.id === "string" && event.id) {
|
|
5768
|
+
turn.messageIds.add(event.id);
|
|
5769
|
+
}
|
|
5770
|
+
|
|
5771
|
+
if (type === "assistant") {
|
|
5772
|
+
const chars = kiroCliV2ContentChars(payload.content);
|
|
5773
|
+
if (
|
|
5774
|
+
typeof payload.operationType === "string" &&
|
|
5775
|
+
payload.operationType.toLowerCase() === "reasoning"
|
|
5776
|
+
) {
|
|
5777
|
+
turn.reasoningChars += chars;
|
|
5778
|
+
} else {
|
|
5779
|
+
turn.outputChars += chars;
|
|
5780
|
+
}
|
|
5781
|
+
if (
|
|
5782
|
+
typeof payload.reasoningModelId === "string" &&
|
|
5783
|
+
payload.reasoningModelId
|
|
5784
|
+
) {
|
|
5785
|
+
turn.modelId = payload.reasoningModelId;
|
|
5786
|
+
}
|
|
5787
|
+
} else if (type === "turn_end") {
|
|
5788
|
+
flushTurn();
|
|
5789
|
+
}
|
|
5790
|
+
}
|
|
5791
|
+
} catch {
|
|
5792
|
+
// Return complete turns parsed before a concurrent truncate/delete.
|
|
5793
|
+
}
|
|
5794
|
+
flushTurn();
|
|
5795
|
+
return flat;
|
|
5796
|
+
}
|
|
5797
|
+
|
|
5186
5798
|
// Canonicalize a Kiro-CLI-emitted model id so IDE and CLI rows collapse when
|
|
5187
5799
|
// they refer to the same underlying Bedrock model. Examples:
|
|
5188
5800
|
// anthropic.claude-sonnet-4-20250514-v1:0 -> claude-sonnet-4
|
|
@@ -5202,6 +5814,7 @@ function canonicalizeKiroCliModelId(raw) {
|
|
|
5202
5814
|
if (!name) return null;
|
|
5203
5815
|
name = name.toLowerCase();
|
|
5204
5816
|
if (name === "auto") return null;
|
|
5817
|
+
name = name.replace(/^(?:qdev|kiro)::/, "");
|
|
5205
5818
|
// Strip provider prefix (anthropic., aws., openai., or a full Bedrock ARN).
|
|
5206
5819
|
name = name.replace(
|
|
5207
5820
|
/^(?:arn:aws:bedrock:[^:]*:[^:]*:(?:foundation-model\/)?|anthropic\.|openai\.|aws\.)/,
|
|
@@ -5294,10 +5907,11 @@ async function parseKiroCliIncremental({ sessionFiles, cursors, queuePath, onPro
|
|
|
5294
5907
|
const resolvedEnv = env || process.env;
|
|
5295
5908
|
const dbPath = resolveKiroCliDbPath(resolvedEnv);
|
|
5296
5909
|
|
|
5297
|
-
// Combine
|
|
5298
|
-
// namespace: historical rows from the SQLite DB
|
|
5299
|
-
// from ~/.kiro/sessions/cli/{uuid}.json
|
|
5300
|
-
//
|
|
5910
|
+
// Combine three sources under the same (source='kiro', cursors.kiroCli)
|
|
5911
|
+
// namespace: historical rows from the SQLite DB, legacy live session state
|
|
5912
|
+
// from ~/.kiro/sessions/cli/{uuid}.json, and Kiro CLI 2.13+ event logs at
|
|
5913
|
+
// ~/.kiro/sessions/<workspaceHash>/sess_<uuid>/messages.jsonl. Request ID
|
|
5914
|
+
// shapes differ:
|
|
5301
5915
|
// SQLite carries a persisted request_id UUID; session files synthesize
|
|
5302
5916
|
// `${sessionId}:${loop_id.rand}`. When kiro-cli migrates a live session
|
|
5303
5917
|
// into SQLite the same turn lands under a new request_id — the cross-
|
|
@@ -5309,8 +5923,11 @@ async function parseKiroCliIncremental({ sessionFiles, cursors, queuePath, onPro
|
|
|
5309
5923
|
: [];
|
|
5310
5924
|
const sessionFilesList = resolveKiroCliSessionFiles(resolvedEnv);
|
|
5311
5925
|
let flatSessions = [];
|
|
5312
|
-
for (const
|
|
5313
|
-
const turns =
|
|
5926
|
+
for (const sessionPath of sessionFilesList) {
|
|
5927
|
+
const turns =
|
|
5928
|
+
path.basename(sessionPath) === "messages.jsonl"
|
|
5929
|
+
? await readKiroCliV2SessionTurns(sessionPath)
|
|
5930
|
+
: await readKiroCliSessionTurns(sessionPath);
|
|
5314
5931
|
for (const turn of turns) flatSessions.push(turn);
|
|
5315
5932
|
}
|
|
5316
5933
|
// Per-request state replaces the old seenIds set. Each entry captures
|
|
@@ -5382,7 +5999,14 @@ async function parseKiroCliIncremental({ sessionFiles, cursors, queuePath, onPro
|
|
|
5382
5999
|
toRetract.push([reqId, prev, sid]);
|
|
5383
6000
|
}
|
|
5384
6001
|
for (const [reqId, prev, sid] of toRetract) {
|
|
5385
|
-
if (
|
|
6002
|
+
if (
|
|
6003
|
+
prev.input_tokens ||
|
|
6004
|
+
prev.output_tokens ||
|
|
6005
|
+
prev.reasoning_output_tokens
|
|
6006
|
+
) {
|
|
6007
|
+
const prevReasoning = toNonNegativeInt(
|
|
6008
|
+
prev.reasoning_output_tokens,
|
|
6009
|
+
);
|
|
5386
6010
|
const prevBucket = getHourlyBucket(
|
|
5387
6011
|
hourlyState,
|
|
5388
6012
|
"kiro",
|
|
@@ -5394,8 +6018,12 @@ async function parseKiroCliIncremental({ sessionFiles, cursors, queuePath, onPro
|
|
|
5394
6018
|
cached_input_tokens: 0,
|
|
5395
6019
|
cache_creation_input_tokens: 0,
|
|
5396
6020
|
output_tokens: -prev.output_tokens,
|
|
5397
|
-
reasoning_output_tokens:
|
|
5398
|
-
total_tokens: -(
|
|
6021
|
+
reasoning_output_tokens: -prevReasoning,
|
|
6022
|
+
total_tokens: -(
|
|
6023
|
+
prev.input_tokens +
|
|
6024
|
+
prev.output_tokens +
|
|
6025
|
+
prevReasoning
|
|
6026
|
+
),
|
|
5399
6027
|
conversation_count: -1,
|
|
5400
6028
|
});
|
|
5401
6029
|
touchedBuckets.add(bucketKey("kiro", prev.model, prev.bucketStart));
|
|
@@ -5488,8 +6116,12 @@ async function parseKiroCliIncremental({ sessionFiles, cursors, queuePath, onPro
|
|
|
5488
6116
|
|
|
5489
6117
|
const promptChars = toNonNegativeInt(r.user_prompt_length);
|
|
5490
6118
|
const responseChars = toNonNegativeInt(r.response_size);
|
|
6119
|
+
const reasoningChars = toNonNegativeInt(r.reasoning_size);
|
|
5491
6120
|
const approxInput = Math.floor(promptChars / KIRO_CLI_CHARS_PER_TOKEN);
|
|
5492
6121
|
const approxOutput = Math.floor(responseChars / KIRO_CLI_CHARS_PER_TOKEN);
|
|
6122
|
+
const approxReasoning = Math.floor(
|
|
6123
|
+
reasoningChars / KIRO_CLI_CHARS_PER_TOKEN,
|
|
6124
|
+
);
|
|
5493
6125
|
|
|
5494
6126
|
const tsMs = Number(r.request_start_timestamp_ms);
|
|
5495
6127
|
if (!Number.isFinite(tsMs) || tsMs <= 0) continue;
|
|
@@ -5502,37 +6134,52 @@ async function parseKiroCliIncremental({ sessionFiles, cursors, queuePath, onPro
|
|
|
5502
6134
|
const model = canonical || "kiro-cli-agent";
|
|
5503
6135
|
|
|
5504
6136
|
// Fingerprint captures every field whose change should cause a re-bucket.
|
|
5505
|
-
const fingerprint =
|
|
6137
|
+
const fingerprint =
|
|
6138
|
+
`${promptChars}:${responseChars}:${reasoningChars}:${model}:${tsMs}`;
|
|
5506
6139
|
const prev = requestState[requestId];
|
|
5507
6140
|
if (prev && prev.fingerprint === fingerprint) continue; // unchanged
|
|
5508
6141
|
|
|
5509
6142
|
// Subtract the prior contribution (if any) from its prior bucket so the
|
|
5510
6143
|
// bucket's absolute totals reflect the CURRENT truth, not the historical
|
|
5511
6144
|
// truth. enqueueTouchedBuckets will emit the net delta at flush time.
|
|
5512
|
-
if (
|
|
6145
|
+
if (
|
|
6146
|
+
prev &&
|
|
6147
|
+
(
|
|
6148
|
+
prev.input_tokens ||
|
|
6149
|
+
prev.output_tokens ||
|
|
6150
|
+
prev.reasoning_output_tokens
|
|
6151
|
+
)
|
|
6152
|
+
) {
|
|
6153
|
+
const prevReasoning = toNonNegativeInt(
|
|
6154
|
+
prev.reasoning_output_tokens,
|
|
6155
|
+
);
|
|
5513
6156
|
const prevBucket = getHourlyBucket(hourlyState, "kiro", prev.model, prev.bucketStart);
|
|
5514
6157
|
addTotals(prevBucket.totals, {
|
|
5515
6158
|
input_tokens: -prev.input_tokens,
|
|
5516
6159
|
cached_input_tokens: 0,
|
|
5517
6160
|
cache_creation_input_tokens: 0,
|
|
5518
6161
|
output_tokens: -prev.output_tokens,
|
|
5519
|
-
reasoning_output_tokens:
|
|
5520
|
-
total_tokens: -(
|
|
6162
|
+
reasoning_output_tokens: -prevReasoning,
|
|
6163
|
+
total_tokens: -(
|
|
6164
|
+
prev.input_tokens +
|
|
6165
|
+
prev.output_tokens +
|
|
6166
|
+
prevReasoning
|
|
6167
|
+
),
|
|
5521
6168
|
conversation_count: -1,
|
|
5522
6169
|
});
|
|
5523
6170
|
touchedBuckets.add(bucketKey("kiro", prev.model, prev.bucketStart));
|
|
5524
6171
|
}
|
|
5525
6172
|
|
|
5526
6173
|
// Add the new contribution.
|
|
5527
|
-
if (approxInput > 0 || approxOutput > 0) {
|
|
6174
|
+
if (approxInput > 0 || approxOutput > 0 || approxReasoning > 0) {
|
|
5528
6175
|
const bucket = getHourlyBucket(hourlyState, "kiro", model, bucketStart);
|
|
5529
6176
|
addTotals(bucket.totals, {
|
|
5530
6177
|
input_tokens: approxInput,
|
|
5531
6178
|
cached_input_tokens: 0,
|
|
5532
6179
|
cache_creation_input_tokens: 0,
|
|
5533
6180
|
output_tokens: approxOutput,
|
|
5534
|
-
reasoning_output_tokens:
|
|
5535
|
-
total_tokens: approxInput + approxOutput,
|
|
6181
|
+
reasoning_output_tokens: approxReasoning,
|
|
6182
|
+
total_tokens: approxInput + approxOutput + approxReasoning,
|
|
5536
6183
|
conversation_count: 1,
|
|
5537
6184
|
});
|
|
5538
6185
|
touchedBuckets.add(bucketKey("kiro", model, bucketStart));
|
|
@@ -5551,6 +6198,7 @@ async function parseKiroCliIncremental({ sessionFiles, cursors, queuePath, onPro
|
|
|
5551
6198
|
model,
|
|
5552
6199
|
input_tokens: approxInput,
|
|
5553
6200
|
output_tokens: approxOutput,
|
|
6201
|
+
reasoning_output_tokens: approxReasoning,
|
|
5554
6202
|
...(r.session_id ? { session_id: r.session_id } : {}),
|
|
5555
6203
|
};
|
|
5556
6204
|
|
|
@@ -13586,6 +14234,9 @@ module.exports = {
|
|
|
13586
14234
|
readOpencodeDbMessages,
|
|
13587
14235
|
readMimoDbMessages,
|
|
13588
14236
|
readZcodeDbMessages,
|
|
14237
|
+
resolveQoderDbPath,
|
|
14238
|
+
resolveQoderDbPaths,
|
|
14239
|
+
readQoderDbMessages,
|
|
13589
14240
|
resolveKiroDbPath,
|
|
13590
14241
|
resolveKiroJsonlPath,
|
|
13591
14242
|
resolveHermesPath,
|
|
@@ -13610,6 +14261,7 @@ module.exports = {
|
|
|
13610
14261
|
parseGeminiIncremental,
|
|
13611
14262
|
parseOpencodeIncremental,
|
|
13612
14263
|
parseOpencodeDbIncremental,
|
|
14264
|
+
parseQoderDbIncremental,
|
|
13613
14265
|
openclawCursorKey,
|
|
13614
14266
|
parseOpenclawIncremental,
|
|
13615
14267
|
parseCursorApiIncremental,
|
|
@@ -13697,6 +14349,7 @@ module.exports = {
|
|
|
13697
14349
|
// Exposed for regression tests covering cache-token accounting.
|
|
13698
14350
|
normalizeGeminiTokens,
|
|
13699
14351
|
normalizeOpencodeTokens,
|
|
14352
|
+
normalizeQoderTokens,
|
|
13700
14353
|
sameGeminiTotals,
|
|
13701
14354
|
diffGeminiTotals,
|
|
13702
14355
|
// Exposed so the queue-repair migration can mutate cursors state in the
|