tokentracker-cli 1.4.3 → 1.4.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/README.de.md +1 -0
- package/README.ja.md +1 -0
- package/README.ko.md +1 -0
- package/README.md +5 -0
- package/README.zh-CN.md +1 -0
- package/dashboard/dist/assets/{AchievementBadge-3IYidbrI.js → AchievementBadge-BmWRsZMx.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-DmZ_ojyM.js → AchievementsPage-CyIP2nck.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-DIYrrzhw.js → AchievementsSection-BeNyJ0LW.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-DMMExCe4.js → BadgeDetailModal-DIFnFzkv.js} +1 -1
- package/dashboard/dist/assets/{BotAnimated-y6vZNRvC.js → BotAnimated-CKVeJHit.js} +1 -1
- package/dashboard/dist/assets/{Card-LJX3G3zo.js → Card-lQmyKbh4.js} +1 -1
- package/dashboard/dist/assets/{CloudCheckoutPage-BGBgSXNE.js → CloudCheckoutPage-B_8y6dyX.js} +1 -1
- package/dashboard/dist/assets/{CloudPage-Dij2GTXi.js → CloudPage-BOQjUGAt.js} +2 -2
- package/dashboard/dist/assets/{CommandPalette-BIky6Fk0.js → CommandPalette-BV9LqokI.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-DbM99i0s.js → CommunityStatsModal-WELxKEFc.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-C_NH8vgO.js → ConfirmModal-B8H5o8c_.js} +1 -1
- package/dashboard/dist/assets/{Controls-BEAj_kvr.js → Controls-9ynPx9rF.js} +1 -1
- package/dashboard/dist/assets/{DashboardPage-DO7h2dfu.js → DashboardPage-B8z-U63p.js} +1 -1
- package/dashboard/dist/assets/{DateRangePopover-PQXB8v0k.js → DateRangePopover-DEqWsV_8.js} +1 -1
- package/dashboard/dist/assets/{DevicePage-0JZ3OIWZ.js → DevicePage-CUOlUySM.js} +1 -1
- package/dashboard/dist/assets/{DialogClose-BpWoD5h9.js → DialogClose-Dlg52bL6.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-vuQFBlM_.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-DoQgdh5w.js → DialogTitle-CduwXjqH.js} +1 -1
- package/dashboard/dist/assets/{ElectricLogo-0O87VjKv.js → ElectricLogo-DFt7_F2f.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-9ph1h_AG.js → FadeIn-DlyusE6n.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-DCZ92xbH.js → HeaderGithubStar-tUtJoFHh.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-CCKdmuOP.js → HoverTooltip-v-zkCqsX.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-CpyhpmdS.js → IpCheckPage-C5Y1uVEr.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-spvsPkYy.js → LandingPage-DzOJOAj7.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardPage-2nJ08ohw.js → LeaderboardPage-BBZ_6tCw.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProBadge-D6FSL3Qy.js → LeaderboardProBadge-DakoXIQn.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardProfileModal-DD1jX-rD.js → LeaderboardProfileModal-CINpES_x.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-DYqRrntL.js → LeaderboardProfilePage-B6ISf-1l.js} +1 -1
- package/dashboard/dist/assets/{LimitsPage-DXHikunk.js → LimitsPage-UVX730CQ.js} +8 -8
- package/dashboard/dist/assets/{LocalOnlyNotice-BURiQAtP.js → LocalOnlyNotice-BJSsmXF1.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-Cv2ijoJx.js → LoginCard-Dggv2aGv.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-DyOideDA.js → LoginModal-B8qDd2kY.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-BKNvqvYi.js → LoginPage-Dv66U8yJ.js} +1 -1
- package/dashboard/dist/assets/{PetPage-BBmozE0L.js → PetPage-CNwrQdKM.js} +2 -2
- package/dashboard/dist/assets/{PopoverPopup-DDnISLKM.js → PopoverPopup-CqmvpYOi.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-Da90GDcM.js → ProviderIcon-BF2Ch7wi.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-DsETr50j.js → ResetPasswordPage-B0R8VUU3.js} +1 -1
- package/dashboard/dist/assets/{Select-pZDawyxt.js → Select-idAiBnJG.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-D5wuuN7G.js → SelectItemText-DPmmPY_D.js} +1 -1
- package/dashboard/dist/assets/{SelfHostPage-sm-PARYj.js → SelfHostPage-oR9cPK02.js} +1 -1
- package/dashboard/dist/assets/{ServiceStatusPage-D4f8h0sL.js → ServiceStatusPage-CdV9gfGF.js} +1 -1
- package/dashboard/dist/assets/{SessionsPage-xVLsTN4m.js → SessionsPage-BkD34VlL.js} +1 -1
- package/dashboard/dist/assets/{SettingsPage-Dt9iFheY.js → SettingsPage-DsthWCyM.js} +3 -3
- package/dashboard/dist/assets/{SkillsPage-BdWalPpd.js → SkillsPage-CzMk9s3x.js} +1 -1
- package/dashboard/dist/assets/{TokenGalaxy-d2_ml0tE.js → TokenGalaxy-DQ6o8YBp.js} +1 -1
- package/dashboard/dist/assets/{TooltipPopup-B4masxDW.js → TooltipPopup-CMQjA-HE.js} +1 -1
- package/dashboard/dist/assets/{TrendMonitor-CceYpa2O.js → TrendMonitor-DrV1m8E7.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-CSN0829H.js → WidgetsPage-Cc16WM0Y.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-gP4WfO_m.js → WrappedPage-pMwbM4R7.js} +1 -1
- package/dashboard/dist/assets/{__vite-browser-external-BtJVmciH.js → __vite-browser-external-DCy_YLfe.js} +3 -3
- package/dashboard/dist/assets/{agent-logos-wxccxfWz.js → agent-logos-Zw7LNkMO.js} +1 -1
- package/dashboard/dist/assets/{arrow-left-Bd5C77R2.js → arrow-left-DycYjE-k.js} +1 -1
- package/dashboard/dist/assets/{arrow-right-C1RqrK4A.js → arrow-right-uNxLHoJ0.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-B_b2i8hn.js → arrow-up-right-CmdJ7gd7.js} +1 -1
- package/dashboard/dist/assets/{calendar-days-C8eYygme.js → calendar-days-CJ33_t5O.js} +1 -1
- package/dashboard/dist/assets/{check-BlBHefQ7.js → check-Dbubk4Il.js} +1 -1
- package/dashboard/dist/assets/{chevron-down-mjUspqZv.js → chevron-down-BwvBVAOs.js} +1 -1
- package/dashboard/dist/assets/{cloud-checkout-CEkMnWXf.js → cloud-checkout-IG-IOpeP.js} +1 -1
- package/dashboard/dist/assets/{composite-CzXFY9IC.js → composite-IIgJPPIw.js} +1 -1
- package/dashboard/dist/assets/{download-CQvg59w1.js → download-DxtH7w6a.js} +1 -1
- package/dashboard/dist/assets/{external-link-CDYfIZ3Z.js → external-link-evKf3mnD.js} +1 -1
- package/dashboard/dist/assets/{flame-C9NXQRil.js → flame-D2qOE_W4.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-fKy_3qpW.js → format-tokens-CqDmm-sK.js} +1 -1
- package/dashboard/dist/assets/{gift-WmV4SBGR.js → gift-Ge11HK1x.js} +1 -1
- package/dashboard/dist/assets/{host-mode-C5MvZ7SV.js → host-mode-8_TNh7yE.js} +1 -1
- package/dashboard/dist/assets/{icons-B1EtsaZs.js → icons-OmfWvE5K.js} +1 -1
- package/dashboard/dist/assets/{index-9EZ3o2RV.js → index-BYuEzP9p.js} +1 -1
- package/dashboard/dist/assets/{index-BEecBW06.js → index-BsYtyo4U.js} +1 -1
- package/dashboard/dist/assets/{index-DXNwBFNi.js → index-tbe1aERA.js} +1 -1
- package/dashboard/dist/assets/{info-CWDhggIW.js → info-xhrso9aO.js} +1 -1
- package/dashboard/dist/assets/{link-2-DEWNkK7s.js → link-2-BgNpTaO4.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-DotT5gVp.js → loader-circle-DRooZ0e5.js} +1 -1
- package/dashboard/dist/assets/{modifiers.esm-qZnGgJAz.js → modifiers.esm-D-QAEpOZ.js} +1 -1
- package/dashboard/dist/assets/{monitor-smartphone-DOP5eqBD.js → monitor-smartphone-Bwu7__2B.js} +1 -1
- package/dashboard/dist/assets/{play-BUsybi9m.js → play-CBPvTiWM.js} +1 -1
- package/dashboard/dist/assets/{plus-DAjojh_m.js → plus-B4lFIyP8.js} +1 -1
- package/dashboard/dist/assets/{provider-display-BCQQYPWD.js → provider-display-CQlWusXH.js} +1 -1
- package/dashboard/dist/assets/{react-BgpqhoCj.js → react-DDOtS4Bd.js} +1 -1
- package/dashboard/dist/assets/{safePolygon-C-PFpZSA.js → safePolygon-BGfBVKoy.js} +1 -1
- package/dashboard/dist/assets/{search-W1A-6ugo.js → search-DVyNq72J.js} +1 -1
- package/dashboard/dist/assets/{server-CE7pFjDr.js → server-EpUTEsOf.js} +1 -1
- package/dashboard/dist/assets/{shield-check-KL5xc2FR.js → shield-check-DRg8cPKV.js} +1 -1
- package/dashboard/dist/assets/{skills-api-DL_zLjKA.js → skills-api-y42dEF96.js} +1 -1
- package/dashboard/dist/assets/{sortable.esm-CehJQvFH.js → sortable.esm-D2S1gXCK.js} +1 -1
- package/dashboard/dist/assets/{store-BHAXkHPR.js → store-DfGPS2IR.js} +1 -1
- package/dashboard/dist/assets/{terminal-Br5vyD6Q.js → terminal-hXIrrL2b.js} +1 -1
- package/dashboard/dist/assets/{trash-CzaCJP6G.js → trash-cjaASRQl.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-DbJrecQw.js → use-community-stats-B-nipjWt.js} +1 -1
- package/dashboard/dist/assets/{use-latest-request-guard-DGhwmlX8.js → use-latest-request-guard-DIEYrSzn.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-T7KWAWZ6.js → use-limits-display-prefs-BdHDwBBC.js} +1 -1
- package/dashboard/dist/assets/{use-native-settings-MZJhS328.js → use-native-settings-7w71_h44.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-BDN24weA.js → use-ordered-list-CSMpK4ar.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-HUGf223R.js +1 -0
- package/dashboard/dist/assets/{use-session-efficiency-pref-DUu16y8Z.js → use-session-efficiency-pref-BagBVWW6.js} +1 -1
- package/dashboard/dist/assets/{use-transform-BczE46xm.js → use-transform-sjGGXCNn.js} +1 -1
- package/dashboard/dist/assets/use-usage-limits-Nml-wUB-.js +1 -0
- package/dashboard/dist/assets/{useCurrency-BgZmWbkx.js → useCurrency-DAKIY1nR.js} +1 -1
- package/dashboard/dist/assets/{usePositioner-dWENyY8z.js → usePositioner-B7vVQ7th.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-D7s1-Zuk.js → useTokenFormat-gtgw8k2Z.js} +1 -1
- package/dashboard/dist/assets/{zap-Dr__bKlq.js → zap-DFWOiTJ1.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/sync.js +571 -34
- package/src/lib/async-queue-reader.js +37 -16
- package/src/lib/codex-context-breakdown.js +8 -1
- package/src/lib/codex-rollout-parser.js +65 -5
- package/src/lib/grok-limits.js +56 -0
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +690 -60
- package/src/lib/session-analytics.js +1 -1
- package/src/lib/usage-limits.js +33 -1
- package/dashboard/dist/assets/DialogDescription-ne_rYSD7.js +0 -1
- package/dashboard/dist/assets/use-reduced-motion-CfCY-K-a.js +0 -1
- package/dashboard/dist/assets/use-usage-limits-CjyDfH-M.js +0 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
const fs = require("node:fs/promises");
|
|
2
|
+
const { createHash } = require("node:crypto");
|
|
2
3
|
const { StringDecoder } = require("node:string_decoder");
|
|
3
4
|
const { setImmediate: yieldToRequests } = require("node:timers/promises");
|
|
4
5
|
|
|
5
6
|
const CHUNK_BYTES = 64 * 1024;
|
|
6
|
-
const PROBE_BYTES = 256;
|
|
7
7
|
|
|
8
8
|
function signature(stat) {
|
|
9
9
|
return `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}`;
|
|
@@ -17,10 +17,18 @@ function createAsyncQueueReader({ parseRow, label, maxEntries = 4 }) {
|
|
|
17
17
|
const cache = new Map();
|
|
18
18
|
const pending = new Map();
|
|
19
19
|
|
|
20
|
-
async function
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
|
|
20
|
+
async function hashPrefix(file, length) {
|
|
21
|
+
const hash = createHash("sha256");
|
|
22
|
+
const buffer = Buffer.allocUnsafe(CHUNK_BYTES);
|
|
23
|
+
let position = 0;
|
|
24
|
+
while (position < length) {
|
|
25
|
+
const { bytesRead } = await file.read(buffer, 0, Math.min(CHUNK_BYTES, length - position), position);
|
|
26
|
+
if (!bytesRead) throw new Error("Queue truncated during validation");
|
|
27
|
+
hash.update(buffer.subarray(0, bytesRead));
|
|
28
|
+
position += bytesRead;
|
|
29
|
+
await yieldToRequests();
|
|
30
|
+
}
|
|
31
|
+
return hash;
|
|
24
32
|
}
|
|
25
33
|
|
|
26
34
|
async function load(queuePath) {
|
|
@@ -32,19 +40,28 @@ function createAsyncQueueReader({ parseRow, label, maxEntries = 4 }) {
|
|
|
32
40
|
// between stat and open. Its signature always describes the bytes we read.
|
|
33
41
|
const stat = await file.stat({ bigint: true });
|
|
34
42
|
const nextSignature = signature(stat);
|
|
35
|
-
if (cached?.signature === nextSignature) return cached.rows;
|
|
36
43
|
const identity = `${stat.dev}:${stat.ino}`;
|
|
37
44
|
const size = Number(stat.size);
|
|
45
|
+
if (cached?.signature === nextSignature) {
|
|
46
|
+
// Windows can reuse mtime/ctime for same-size writes anywhere in the
|
|
47
|
+
// file. Validate every byte before reusing the parsed snapshot.
|
|
48
|
+
const hash = await hashPrefix(file, size);
|
|
49
|
+
if (hash.digest("hex") === cached.digest) return cached.rows;
|
|
50
|
+
}
|
|
38
51
|
let offset = 0;
|
|
39
52
|
let seen = new Map();
|
|
53
|
+
let hash = createHash("sha256");
|
|
40
54
|
if (cached && cached.identity === identity && size > cached.size && cached.consumedBytes > 0) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
55
|
+
// Growth can include an interior rewrite, so verify the entire old
|
|
56
|
+
// committed prefix before parsing only the new tail.
|
|
57
|
+
const prefixHash = await hashPrefix(file, cached.consumedBytes);
|
|
58
|
+
if (prefixHash.copy().digest("hex") === cached.committedDigest) {
|
|
44
59
|
offset = cached.consumedBytes;
|
|
45
60
|
seen = cached.seen;
|
|
61
|
+
hash = prefixHash;
|
|
46
62
|
}
|
|
47
63
|
}
|
|
64
|
+
let committedHash = hash.copy();
|
|
48
65
|
|
|
49
66
|
const changes = new Map();
|
|
50
67
|
let malformed = 0;
|
|
@@ -66,9 +83,14 @@ function createAsyncQueueReader({ parseRow, label, maxEntries = 4 }) {
|
|
|
66
83
|
const { bytesRead } = await file.read(buffer, 0, length, position);
|
|
67
84
|
if (!bytesRead) throw new Error("Queue truncated during read");
|
|
68
85
|
const newline = buffer.lastIndexOf(0x0a, bytesRead - 1);
|
|
69
|
-
|
|
86
|
+
const chunk = buffer.subarray(0, bytesRead);
|
|
87
|
+
if (newline >= 0) {
|
|
88
|
+
consumedBytes = position + newline + 1;
|
|
89
|
+
committedHash = hash.copy().update(chunk.subarray(0, newline + 1));
|
|
90
|
+
}
|
|
91
|
+
hash.update(chunk);
|
|
70
92
|
position += bytesRead;
|
|
71
|
-
remainder += decoder.write(
|
|
93
|
+
remainder += decoder.write(chunk);
|
|
72
94
|
let start = 0;
|
|
73
95
|
let end;
|
|
74
96
|
while ((end = remainder.indexOf("\n", start)) !== -1) {
|
|
@@ -79,10 +101,6 @@ function createAsyncQueueReader({ parseRow, label, maxEntries = 4 }) {
|
|
|
79
101
|
await yieldToRequests();
|
|
80
102
|
}
|
|
81
103
|
remainder += decoder.end();
|
|
82
|
-
// Capture probes before committing any state: a failed read must not mutate
|
|
83
|
-
// a snapshot still being used by another endpoint.
|
|
84
|
-
const head = await probe(file, 0, Math.min(PROBE_BYTES, consumedBytes));
|
|
85
|
-
const anchor = await probe(file, Math.max(0, consumedBytes - PROBE_BYTES), Math.min(PROBE_BYTES, consumedBytes));
|
|
86
104
|
for (const [key, row] of changes) seen.set(key, row);
|
|
87
105
|
const tail = new Map();
|
|
88
106
|
parse(remainder, tail);
|
|
@@ -90,7 +108,10 @@ function createAsyncQueueReader({ parseRow, label, maxEntries = 4 }) {
|
|
|
90
108
|
// key never enters the committed map (completion may change that key).
|
|
91
109
|
const rows = tail.size ? Array.from(new Map([...seen, ...tail]).values()) : Array.from(seen.values());
|
|
92
110
|
cache.delete(queuePath);
|
|
93
|
-
cache.set(queuePath, {
|
|
111
|
+
cache.set(queuePath, {
|
|
112
|
+
signature: nextSignature, identity, size, consumedBytes,
|
|
113
|
+
digest: hash.digest("hex"), committedDigest: committedHash.digest("hex"), seen, rows,
|
|
114
|
+
});
|
|
94
115
|
while (cache.size > maxEntries) cache.delete(cache.keys().next().value);
|
|
95
116
|
if (malformed) console.error(`[LocalAPI] ${label}: skipped ${malformed} malformed line(s)`);
|
|
96
117
|
return rows;
|
|
@@ -35,6 +35,7 @@ const {
|
|
|
35
35
|
finalizeExecRows,
|
|
36
36
|
buildSkillStatsEntry,
|
|
37
37
|
isCodexResumeInvalidated,
|
|
38
|
+
isRecoverableGroupedReadError,
|
|
38
39
|
} = require("./codex-rollout-parser");
|
|
39
40
|
|
|
40
41
|
// ---------------------------------------------------------------------------
|
|
@@ -473,6 +474,7 @@ async function computeCodexContextBreakdown({
|
|
|
473
474
|
PARSED_GROUP_CACHE.delete(parsedCacheKey);
|
|
474
475
|
PARSED_GROUP_CACHE.set(parsedCacheKey, cachedGroup);
|
|
475
476
|
} else {
|
|
477
|
+
try {
|
|
476
478
|
let captured = null;
|
|
477
479
|
const singleEntry = entries.length === 1 ? entries[0] : null;
|
|
478
480
|
const appendHashEligible = Boolean(
|
|
@@ -538,7 +540,7 @@ async function computeCodexContextBreakdown({
|
|
|
538
540
|
diagnostics.incremental_parse_fallbacks += 1;
|
|
539
541
|
}
|
|
540
542
|
} catch (error) {
|
|
541
|
-
if (!isCodexResumeInvalidated(error)) throw error;
|
|
543
|
+
if (!isCodexResumeInvalidated(error) && !isRecoverableGroupedReadError(error)) throw error;
|
|
542
544
|
diagnostics.incremental_parse_fallbacks += 1;
|
|
543
545
|
} finally {
|
|
544
546
|
await resumeHandle.close();
|
|
@@ -587,6 +589,11 @@ async function computeCodexContextBreakdown({
|
|
|
587
589
|
stat: singleEntry?.stat || null,
|
|
588
590
|
});
|
|
589
591
|
}
|
|
592
|
+
} catch (error) {
|
|
593
|
+
// One unreadable archive/session must not fail the whole breakdown.
|
|
594
|
+
if (!isRecoverableGroupedReadError(error)) throw error;
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
590
597
|
}
|
|
591
598
|
if (!parsed || !parsed.totals || !parsed.totals.total_tokens) continue;
|
|
592
599
|
sessions.push(parsed);
|
|
@@ -42,6 +42,28 @@ const {
|
|
|
42
42
|
} = require("./codex-usage-record");
|
|
43
43
|
|
|
44
44
|
const DISCOVERY_FULL_AUDIT_INTERVAL_MS = 60 * 1000;
|
|
45
|
+
const CODEX_ROLLOUT_TEXT_MAX_BYTES = 256 * 1024 * 1024;
|
|
46
|
+
|
|
47
|
+
async function readCodexRolloutText(filePath, { maxOutputBytes = CODEX_ROLLOUT_TEXT_MAX_BYTES } = {}) {
|
|
48
|
+
if (typeof filePath !== "string" || !/\.jsonl\.zstd?$/i.test(filePath)) {
|
|
49
|
+
return fs.readFile(filePath, "utf8");
|
|
50
|
+
}
|
|
51
|
+
// The snapshot decoder lives in rollout.js (reused by the sync parser);
|
|
52
|
+
// require lazily so this module stays loadable without pulling the whole
|
|
53
|
+
// rollout graph at import time.
|
|
54
|
+
const rollout = require("./rollout");
|
|
55
|
+
const snapshot = await rollout.readCodexRolloutSnapshot(filePath, { maxOutputBytes });
|
|
56
|
+
if (!snapshot) {
|
|
57
|
+
// Treat undecodable / vanished archives as recoverable I/O so grouped
|
|
58
|
+
// session reads (live + archive) can still use a readable sibling copy.
|
|
59
|
+
const error = new Error(`unreadable Codex archive ${filePath}`);
|
|
60
|
+
error.code = "EIO";
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
// Snapshot lines keep their "\n" terminators; the tail element may be an
|
|
64
|
+
// unterminated final line — join preserves exactly that.
|
|
65
|
+
return snapshot.lines.join("\n");
|
|
66
|
+
}
|
|
45
67
|
const MAX_DISCOVERY_INVENTORIES = 32;
|
|
46
68
|
const DISCOVERY_INVENTORIES = new Map();
|
|
47
69
|
const ZONED_DAY_FORMATTERS = new Map();
|
|
@@ -143,6 +165,11 @@ function safeJsonParse(str, diagnostics = null) {
|
|
|
143
165
|
}
|
|
144
166
|
}
|
|
145
167
|
|
|
168
|
+
function isRolloutCandidateFileName(name) {
|
|
169
|
+
if (typeof name !== "string") return false;
|
|
170
|
+
return name.endsWith(".jsonl") || /^rollout-.*\.jsonl\.zstd?$/.test(name);
|
|
171
|
+
}
|
|
172
|
+
|
|
146
173
|
function listJsonlFiles(rootDir) {
|
|
147
174
|
const out = [];
|
|
148
175
|
const stack = [rootDir];
|
|
@@ -160,7 +187,7 @@ function listJsonlFiles(rootDir) {
|
|
|
160
187
|
const filePath = path.join(dir, entry.name);
|
|
161
188
|
if (entry.isDirectory()) {
|
|
162
189
|
stack.push(filePath);
|
|
163
|
-
} else if (entry.isFile() && entry.name
|
|
190
|
+
} else if (entry.isFile() && isRolloutCandidateFileName(entry.name)) {
|
|
164
191
|
out.push(filePath);
|
|
165
192
|
}
|
|
166
193
|
}
|
|
@@ -331,7 +358,7 @@ function discoverCodexSessionFiles(rootDirs, options = {}) {
|
|
|
331
358
|
stack.push(filePath);
|
|
332
359
|
continue;
|
|
333
360
|
}
|
|
334
|
-
if (!child.isFile() || !child.name
|
|
361
|
+
if (!child.isFile() || !isRolloutCandidateFileName(child.name) || visited.has(filePath)) continue;
|
|
335
362
|
visited.add(filePath);
|
|
336
363
|
if (diagnostics) diagnostics.discovered_files += 1;
|
|
337
364
|
const cached = inventory.files.get(filePath) || null;
|
|
@@ -1175,7 +1202,13 @@ async function parseCodexRolloutFile(filePath, {
|
|
|
1175
1202
|
compactionResponseIds: Array.from(compactionResponseIds),
|
|
1176
1203
|
};
|
|
1177
1204
|
result.endOffset = readProgress.endOffset;
|
|
1178
|
-
|
|
1205
|
+
// Compressed snapshots are not append-only logs: endOffset stays at the
|
|
1206
|
+
// pre-decode cursor (often 0) and must never be marked appendable, or the
|
|
1207
|
+
// next growth would "resume" by parsing compressed bytes as JSONL.
|
|
1208
|
+
const hasCompressedArchive = filePaths.some(
|
|
1209
|
+
(candidate) => typeof candidate === "string" && /\.jsonl\.zstd?$/i.test(candidate),
|
|
1210
|
+
);
|
|
1211
|
+
result.appendable = Boolean(contentHasher) && !hasCompressedArchive && (
|
|
1179
1212
|
readProgress.endOffset === 0 || readProgress.lastByte === 0x0a
|
|
1180
1213
|
);
|
|
1181
1214
|
result.contentHashState = contentHasher;
|
|
@@ -1309,6 +1342,27 @@ async function* readCodexObjectsFull(filePath, diagnostics, fileIndex, {
|
|
|
1309
1342
|
readProgress = null,
|
|
1310
1343
|
contentHasher = null,
|
|
1311
1344
|
} = {}) {
|
|
1345
|
+
// Compressed archives are immutable snapshots, not append-only logs: byte
|
|
1346
|
+
// offsets are meaningless inside them, so decode the whole text and split
|
|
1347
|
+
// into lines. `readProgress` stays null so callers never persist a fake
|
|
1348
|
+
// byte cursor for them.
|
|
1349
|
+
if (typeof filePath === "string" && /\.jsonl\.zstd?$/i.test(filePath)) {
|
|
1350
|
+
const text = await readCodexRolloutText(filePath);
|
|
1351
|
+
let lineIndex = 0;
|
|
1352
|
+
const lines = text.split("\n");
|
|
1353
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
1354
|
+
const isLast = index + 1 >= lines.length;
|
|
1355
|
+
if (isLast && lines[index] === "") continue;
|
|
1356
|
+
const lineBuffer = Buffer.from(lines[index], "utf8");
|
|
1357
|
+
const obj = parseCodexLine(lineBuffer, diagnostics);
|
|
1358
|
+
if (obj) {
|
|
1359
|
+
yield { obj, fileIndex, lineIndex };
|
|
1360
|
+
lineIndex += 1;
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
if (diagnostics) diagnostics.parsed_files += 1;
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1312
1366
|
yield* readCodexObjectsIncremental(filePath, diagnostics, fileIndex, {
|
|
1313
1367
|
startOffset,
|
|
1314
1368
|
endOffset,
|
|
@@ -1331,6 +1385,7 @@ function isRecoverableGroupedReadError(error) {
|
|
|
1331
1385
|
|
|
1332
1386
|
async function* iterateCodexObjects(filePaths, diagnostics, options = {}) {
|
|
1333
1387
|
if (filePaths.length <= 1) {
|
|
1388
|
+
if (!filePaths[0]) return;
|
|
1334
1389
|
for await (const record of readCodexObjects(filePaths[0], diagnostics, 0, options)) {
|
|
1335
1390
|
yield record.obj;
|
|
1336
1391
|
}
|
|
@@ -1355,7 +1410,11 @@ async function* iterateCodexObjects(filePaths, diagnostics, options = {}) {
|
|
|
1355
1410
|
if (!isRecoverableGroupedReadError(error)) throw error;
|
|
1356
1411
|
}
|
|
1357
1412
|
}
|
|
1358
|
-
if (!scannedFiles)
|
|
1413
|
+
if (!scannedFiles) {
|
|
1414
|
+
const error = new Error("all grouped Codex rollout files failed during read");
|
|
1415
|
+
error.code = "EIO";
|
|
1416
|
+
throw error;
|
|
1417
|
+
}
|
|
1359
1418
|
records.sort((a, b) => {
|
|
1360
1419
|
const aTimestamp = typeof a.obj?.timestamp === "string" ? a.obj.timestamp : "";
|
|
1361
1420
|
const bTimestamp = typeof b.obj?.timestamp === "string" ? b.obj.timestamp : "";
|
|
@@ -1373,7 +1432,7 @@ async function* iterateCodexObjects(filePaths, diagnostics, options = {}) {
|
|
|
1373
1432
|
|
|
1374
1433
|
function rolloutSessionIdFromPath(filePath) {
|
|
1375
1434
|
const match = path.basename(String(filePath || "")).match(
|
|
1376
|
-
/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl
|
|
1435
|
+
/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl(?:\.zstd?)?$/i,
|
|
1377
1436
|
);
|
|
1378
1437
|
return match ? match[1] : null;
|
|
1379
1438
|
}
|
|
@@ -1404,6 +1463,7 @@ module.exports = {
|
|
|
1404
1463
|
isTimestampInRequestedDayRange,
|
|
1405
1464
|
rolloutSessionIdFromPath,
|
|
1406
1465
|
isCodexResumeInvalidated,
|
|
1466
|
+
isRecoverableGroupedReadError,
|
|
1407
1467
|
finalizeToolRows,
|
|
1408
1468
|
finalizeSkillRows,
|
|
1409
1469
|
finalizeExecRows,
|
package/src/lib/grok-limits.js
CHANGED
|
@@ -529,6 +529,59 @@ function deriveGrokPlanLabel(rawTier) {
|
|
|
529
529
|
return GROK_PLAN_TIERS.get(key) ?? null;
|
|
530
530
|
}
|
|
531
531
|
|
|
532
|
+
// Billing product ids → the name grok.com shows in the usage legend.
|
|
533
|
+
// Unknown ids are humanized below so a new product still gets a row.
|
|
534
|
+
const GROK_PRODUCT_LABELS = new Map([
|
|
535
|
+
["GrokBuild", "Grok Build"],
|
|
536
|
+
["GrokBot", "Grok Bot"],
|
|
537
|
+
["GrokChat", "Chat"],
|
|
538
|
+
["GrokImagine", "Imagine"],
|
|
539
|
+
["Cursor", "Cursor"],
|
|
540
|
+
]);
|
|
541
|
+
|
|
542
|
+
function normalizeGrokProductId(value) {
|
|
543
|
+
if (typeof value !== "string") return null;
|
|
544
|
+
const id = value.trim();
|
|
545
|
+
if (!/^[A-Za-z][A-Za-z0-9]{0,31}$/.test(id)) return null;
|
|
546
|
+
return id;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function humanizeGrokProductId(productId) {
|
|
550
|
+
const spaced = productId
|
|
551
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
552
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2")
|
|
553
|
+
.replace(/[_-]+/g, " ")
|
|
554
|
+
.replace(/\s+/g, " ")
|
|
555
|
+
.trim();
|
|
556
|
+
return spaced.slice(0, 40) || null;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Per-product shares of the same pool as creditUsagePercent.
|
|
561
|
+
* A 12% Grok Bot row is 12 points of the weekly cap, not its own 100% quota.
|
|
562
|
+
* Zero and non-finite rows are omitted (grok.com hides unused products).
|
|
563
|
+
* API order is preserved; a repeated product id keeps the first finite row.
|
|
564
|
+
*/
|
|
565
|
+
function normalizeGrokProductUsage(productUsage, resetAt) {
|
|
566
|
+
if (!Array.isArray(productUsage)) return [];
|
|
567
|
+
const seen = new Set();
|
|
568
|
+
const products = [];
|
|
569
|
+
for (const entry of productUsage) {
|
|
570
|
+
if (!entry || typeof entry !== "object") continue;
|
|
571
|
+
const product = normalizeGrokProductId(entry.product);
|
|
572
|
+
if (!product || seen.has(product)) continue;
|
|
573
|
+
const raw = Number(entry.usagePercent);
|
|
574
|
+
if (!Number.isFinite(raw) || raw <= 0) continue;
|
|
575
|
+
const window = buildWindow({ usedPercent: raw, resetAt });
|
|
576
|
+
if (!window || !(window.used_percent > 0)) continue;
|
|
577
|
+
const label = GROK_PRODUCT_LABELS.get(product) || humanizeGrokProductId(product);
|
|
578
|
+
if (!label) continue;
|
|
579
|
+
seen.add(product);
|
|
580
|
+
products.push({ product, label, window });
|
|
581
|
+
}
|
|
582
|
+
return products;
|
|
583
|
+
}
|
|
584
|
+
|
|
532
585
|
/**
|
|
533
586
|
* Parse either:
|
|
534
587
|
* - Unified billing (`?format=credits`): weekly/monthly period + creditUsagePercent
|
|
@@ -614,6 +667,8 @@ function normalizeGrokBillingResponse(body) {
|
|
|
614
667
|
billing_period_start: periodStart,
|
|
615
668
|
primary_window: primaryWindow,
|
|
616
669
|
secondary_window: secondaryWindow,
|
|
670
|
+
// Shares of primary_window. Do not add these on top of credit_usage_percent.
|
|
671
|
+
products: normalizeGrokProductUsage(config.productUsage, resetAt),
|
|
617
672
|
};
|
|
618
673
|
}
|
|
619
674
|
|
|
@@ -803,6 +858,7 @@ module.exports = {
|
|
|
803
858
|
inferGrokPeriodTypeFromDates,
|
|
804
859
|
sumProductUsagePercent,
|
|
805
860
|
deriveGrokPlanLabel,
|
|
861
|
+
normalizeGrokProductUsage,
|
|
806
862
|
normalizeGrokBillingResponse,
|
|
807
863
|
fetchGrokBilling,
|
|
808
864
|
fetchGrokLimits,
|