tokentracker-cli 0.80.0 → 0.80.2

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.
Files changed (72) hide show
  1. package/dashboard/dist/assets/{AchievementBadge-D9jOMhlD.js → AchievementBadge-lZzUk8UJ.js} +1 -1
  2. package/dashboard/dist/assets/{AchievementsPage-DuswZa4g.js → AchievementsPage-Ddht3Szx.js} +2 -2
  3. package/dashboard/dist/assets/{AchievementsSection-BJPeE1Uq.js → AchievementsSection-Bz9Enx-j.js} +1 -1
  4. package/dashboard/dist/assets/{BadgeDetailModal-nNPIsy6i.js → BadgeDetailModal-C7QiFDH4.js} +1 -1
  5. package/dashboard/dist/assets/{Card-VENq8eEp.js → Card-Cs_xyoXz.js} +1 -1
  6. package/dashboard/dist/assets/{ClawdAnimated-Cq7TW0vv.js → ClawdAnimated-C1Y6W_Di.js} +1 -1
  7. package/dashboard/dist/assets/{CommunityStatsModal-y2VftQuq.js → CommunityStatsModal-B1VvGpUY.js} +1 -1
  8. package/dashboard/dist/assets/{ConfirmModal-CWgnJ4hm.js → ConfirmModal-Cd-fM8lV.js} +1 -1
  9. package/dashboard/dist/assets/{Controls-BUQ_iZZq.js → Controls-D0ULfwWh.js} +1 -1
  10. package/dashboard/dist/assets/DashboardPage-DXbecrQR.js +19 -0
  11. package/dashboard/dist/assets/{DevicePage-Dh3XDtNx.js → DevicePage-BJsSXmi0.js} +1 -1
  12. package/dashboard/dist/assets/{DialogClose-KTFU_B3F.js → DialogClose-BkG6LVKN.js} +1 -1
  13. package/dashboard/dist/assets/DialogDescription-DDDcU_07.js +1 -0
  14. package/dashboard/dist/assets/{DialogTitle-bza2pIek.js → DialogTitle-Do2zDkSF.js} +1 -1
  15. package/dashboard/dist/assets/{FadeIn-Dm3FVNfr.js → FadeIn-BlxFR4Uo.js} +1 -1
  16. package/dashboard/dist/assets/{HeaderGithubStar-CbSElzLt.js → HeaderGithubStar-CsLBMMBv.js} +1 -1
  17. package/dashboard/dist/assets/{IpCheckPage-BDON7VCU.js → IpCheckPage-Bi1QPsiQ.js} +1 -1
  18. package/dashboard/dist/assets/{LandingPage-Ck3CwZrt.js → LandingPage-Ble4Qxzj.js} +1 -1
  19. package/dashboard/dist/assets/{LeaderboardAvatar-GVUy0Ynq.js → LeaderboardAvatar-D2Iqj9VP.js} +1 -1
  20. package/dashboard/dist/assets/{LeaderboardPage-C19Rkt44.js → LeaderboardPage-DImECEuN.js} +2 -2
  21. package/dashboard/dist/assets/{LeaderboardProfileModal-QcEq_o6E.js → LeaderboardProfileModal-BGkNzlpU.js} +3 -3
  22. package/dashboard/dist/assets/{LeaderboardProfilePage-pFhI6292.js → LeaderboardProfilePage-f-E74S02.js} +1 -1
  23. package/dashboard/dist/assets/{LimitsPage-ChV3Qezd.js → LimitsPage-BgTxwSrO.js} +1 -1
  24. package/dashboard/dist/assets/{LocalOnlyNotice-ALvpjznF.js → LocalOnlyNotice-D91Lp2zA.js} +1 -1
  25. package/dashboard/dist/assets/{LoginPage-CL-QrJxU.js → LoginPage-hbsmEr6V.js} +1 -1
  26. package/dashboard/dist/assets/{PetPage-DSuvdGgd.js → PetPage-CIj7VSZN.js} +1 -1
  27. package/dashboard/dist/assets/{PopoverPopup-jSDQ87Bi.js → PopoverPopup-DMgjzzjH.js} +1 -1
  28. package/dashboard/dist/assets/{ResetPasswordPage-Dk2IDAJq.js → ResetPasswordPage-Cv1mRgno.js} +1 -1
  29. package/dashboard/dist/assets/{Select-Cz6WTj7D.js → Select-DvneEQak.js} +1 -1
  30. package/dashboard/dist/assets/{SelectItemText-BZyRjWCM.js → SelectItemText-BQlJKuRz.js} +1 -1
  31. package/dashboard/dist/assets/{SettingsPage-CANz8HMM.js → SettingsPage-DpnL6Jm6.js} +1 -1
  32. package/dashboard/dist/assets/{SkillsPage-WZkQG_83.js → SkillsPage-Cz7d619N.js} +1 -1
  33. package/dashboard/dist/assets/{WidgetsPage-BEyJ62NV.js → WidgetsPage-BP1zxfmh.js} +1 -1
  34. package/dashboard/dist/assets/{WrappedPage-Ck_dOx_w.js → WrappedPage-2eIams41.js} +1 -1
  35. package/dashboard/dist/assets/{agent-logos-DpjHiAbZ.js → agent-logos-C0_7FQ0s.js} +1 -1
  36. package/dashboard/dist/assets/{arrow-up-right-C4iYPBtK.js → arrow-up-right-DBYYRxgR.js} +1 -1
  37. package/dashboard/dist/assets/check-B1MZEuWp.js +1 -0
  38. package/dashboard/dist/assets/{copy-CHJ-38K6.js → copy-BxhpNvJX.js} +1 -1
  39. package/dashboard/dist/assets/{download-BSeG8u17.js → download-DAvxmQ3t.js} +1 -1
  40. package/dashboard/dist/assets/{flame-DeHwKcE-.js → flame-DX9uRr7X.js} +1 -1
  41. package/dashboard/dist/assets/{format-tokens-B-MJYJyp.js → format-tokens-mMtru3Ww.js} +1 -1
  42. package/dashboard/dist/assets/{info-DA_0xr_o.js → info-BLfeChk0.js} +1 -1
  43. package/dashboard/dist/assets/{link-2-noUH83Lc.js → link-2-JVAqARKl.js} +1 -1
  44. package/dashboard/dist/assets/{loader-circle-wL7KZtm0.js → loader-circle--bezqxUZ.js} +1 -1
  45. package/dashboard/dist/assets/{main-BHfJDFcu.js → main-8AbVGr5W.js} +16 -16
  46. package/dashboard/dist/assets/{provider-display-DxIClOGM.js → provider-display-DUwBv-X6.js} +1 -1
  47. package/dashboard/dist/assets/{trash-2-B8TPUmxZ.js → trash-2-Z1EIC4PI.js} +1 -1
  48. package/dashboard/dist/assets/{use-community-stats-CXD3MRRE.js → use-community-stats-Dr1q98cg.js} +1 -1
  49. package/dashboard/dist/assets/{use-limits-display-prefs-C2pJVYTn.js → use-limits-display-prefs-DnacsQ0f.js} +1 -1
  50. package/dashboard/dist/assets/{use-native-settings-BeOBLEZa.js → use-native-settings-BgBQK2b0.js} +1 -1
  51. package/dashboard/dist/assets/{use-ordered-list-kAtW8pJm.js → use-ordered-list-BBs8mEfc.js} +1 -1
  52. package/dashboard/dist/assets/{use-quality-per-dollar-pref-CFZUZM3S.js → use-quality-per-dollar-pref-BSagz4Mk.js} +1 -1
  53. package/dashboard/dist/assets/{use-transform-B9YwpyKi.js → use-transform-CL6RuAA2.js} +1 -1
  54. package/dashboard/dist/assets/{use-usage-limits-DSX5iKPE.js → use-usage-limits-CzNniz6Z.js} +1 -1
  55. package/dashboard/dist/assets/{useCurrency-CKg5K4Nj.js → useCurrency-ByQkX7uG.js} +1 -1
  56. package/dashboard/dist/assets/{useScrollLock-DyyY_Vyt.js → useScrollLock-D5wf8_sp.js} +1 -1
  57. package/dashboard/dist/assets/useTokenFormat-BpdwKlbn.js +1 -0
  58. package/dashboard/dist/assets/{zap-B69x9NCT.js → zap-v3s0o4Ig.js} +1 -1
  59. package/dashboard/dist/index.html +1 -1
  60. package/dashboard/dist/share.html +1 -1
  61. package/package.json +1 -1
  62. package/src/commands/init.js +596 -6
  63. package/src/commands/sync.js +16 -1
  64. package/src/lib/codex-config.js +135 -40
  65. package/src/lib/codex-context-breakdown.js +123 -43
  66. package/src/lib/codex-rollout-parser.js +329 -32
  67. package/src/lib/pricing/seed-snapshot.json +1 -1
  68. package/src/lib/rollout.js +167 -8
  69. package/dashboard/dist/assets/DashboardPage-WkiygMmq.js +0 -19
  70. package/dashboard/dist/assets/DialogDescription-DIra288n.js +0 -1
  71. package/dashboard/dist/assets/check-BkPUxPZV.js +0 -1
  72. package/dashboard/dist/assets/useTokenFormat-FfTjEH_m.js +0 -1
@@ -55,12 +55,12 @@ async function restoreNotify({ configPath, notifyOriginalPath, expectedNotify })
55
55
  const originalNotify = Array.isArray(original?.notify) ? original.notify : null;
56
56
  const currentNotify = extractNotify(text);
57
57
 
58
- if (expectedNotify && !arraysEqual(currentNotify, expectedNotify)) {
59
- return { restored: false, skippedReason: "current-not-managed" };
58
+ if (!originalNotify && expectedNotify && currentNotify == null) {
59
+ return { restored: false, skippedReason: "no-backup-not-installed" };
60
60
  }
61
61
 
62
- if (!originalNotify && expectedNotify && !arraysEqual(currentNotify, expectedNotify)) {
63
- return { restored: false, skippedReason: "no-backup-not-installed" };
62
+ if (expectedNotify && !arraysEqual(currentNotify, expectedNotify)) {
63
+ return { restored: false, skippedReason: "current-not-managed" };
64
64
  }
65
65
 
66
66
  const updated = originalNotify ? setNotify(text, originalNotify) : removeNotify(text);
@@ -161,15 +161,16 @@ function extractNotify(text) {
161
161
  const lines = text.split(/\r?\n/);
162
162
  for (let i = 0; i < lines.length; i++) {
163
163
  const line = lines[i];
164
+ if (isTomlTableHeader(line)) break;
164
165
  const m = line.match(/^\s*notify\s*=\s*(.*)\s*$/);
165
166
  if (!m) continue;
166
167
 
167
168
  const rhs = (m[1] || "").trim();
168
169
  const literal = readTomlArrayLiteral(lines, i, rhs);
169
- if (!literal) continue;
170
+ if (!literal) return null;
170
171
 
171
172
  const parsed = parseTomlStringArray(literal);
172
- if (parsed) return parsed;
173
+ return parsed;
173
174
  }
174
175
  return null;
175
176
  }
@@ -182,6 +183,14 @@ function setNotify(text, notifyCmd) {
182
183
  let replaced = false;
183
184
  for (let i = 0; i < lines.length; i++) {
184
185
  const line = lines[i];
186
+ if (isTomlTableHeader(line)) {
187
+ if (!replaced) {
188
+ out.push(notifyLine);
189
+ replaced = true;
190
+ }
191
+ out.push(...lines.slice(i));
192
+ break;
193
+ }
185
194
  const m = line.match(/^\s*notify\s*=\s*(.*)\s*$/);
186
195
  if (m) {
187
196
  if (!replaced) {
@@ -190,7 +199,7 @@ function setNotify(text, notifyCmd) {
190
199
  }
191
200
 
192
201
  const rhs = (m[1] || "").trim();
193
- i = findTomlArrayBlockEnd(lines, i, rhs);
202
+ i = findNotifyBlockEnd(lines, i, rhs);
194
203
  continue;
195
204
  }
196
205
  out.push(line);
@@ -211,10 +220,14 @@ function removeNotify(text) {
211
220
  const out = [];
212
221
  for (let i = 0; i < lines.length; i++) {
213
222
  const line = lines[i];
223
+ if (isTomlTableHeader(line)) {
224
+ out.push(...lines.slice(i));
225
+ break;
226
+ }
214
227
  const m = line.match(/^\s*notify\s*=\s*(.*)\s*$/);
215
228
  if (m) {
216
229
  const rhs = (m[1] || "").trim();
217
- i = findTomlArrayBlockEnd(lines, i, rhs);
230
+ i = findNotifyBlockEnd(lines, i, rhs);
218
231
  continue;
219
232
  }
220
233
  out.push(line);
@@ -225,45 +238,78 @@ function removeNotify(text) {
225
238
  function parseTomlStringArray(rhs) {
226
239
  // Minimal parser for TOML basic/literal string arrays.
227
240
  if (!rhs.startsWith("[") || !rhs.endsWith("]")) return null;
228
- const inner = rhs.slice(1, -1).trim();
229
- if (!inner) return [];
230
241
 
231
242
  const parts = [];
232
- let current = "";
233
- let inString = false;
234
- let quote = null;
235
- for (let i = 0; i < inner.length; i++) {
236
- const ch = inner[i];
237
- if (!inString) {
238
- if (ch === '"' || ch === "'") {
239
- inString = true;
240
- quote = ch;
241
- current = "";
243
+ let i = 1;
244
+ let expectValue = true;
245
+
246
+ function skipSpaceAndComments() {
247
+ while (i < rhs.length) {
248
+ const ch = rhs[i];
249
+ if (/\s/.test(ch)) {
250
+ i += 1;
251
+ continue;
242
252
  }
243
- continue;
253
+ if (ch === "#") {
254
+ while (i < rhs.length && rhs[i] !== "\n") i += 1;
255
+ continue;
256
+ }
257
+ break;
244
258
  }
245
- if (ch === quote) {
246
- parts.push(current);
247
- inString = false;
248
- quote = null;
249
- continue;
259
+ }
260
+
261
+ function parseString() {
262
+ const quote = rhs[i];
263
+ if (quote !== '"' && quote !== "'") return null;
264
+ i += 1;
265
+ let value = "";
266
+ while (i < rhs.length) {
267
+ const ch = rhs[i];
268
+ if (ch === quote) {
269
+ i += 1;
270
+ return value;
271
+ }
272
+ if (ch === "\n" || ch === "\r") return null;
273
+ if (quote === '"' && ch === "\\") {
274
+ if (i + 1 >= rhs.length) return null;
275
+ const next = rhs[i + 1];
276
+ if (next === "b") value += "\b";
277
+ else if (next === "t") value += "\t";
278
+ else if (next === "n") value += "\n";
279
+ else if (next === "f") value += "\f";
280
+ else if (next === "r") value += "\r";
281
+ else if (next === '"' || next === "\\" || next === "/") value += next;
282
+ else return null;
283
+ i += 2;
284
+ continue;
285
+ }
286
+ value += ch;
287
+ i += 1;
288
+ }
289
+ return null;
290
+ }
291
+
292
+ while (i < rhs.length) {
293
+ skipSpaceAndComments();
294
+ if (rhs[i] === "]") {
295
+ i += 1;
296
+ skipSpaceAndComments();
297
+ return i === rhs.length ? parts : null;
250
298
  }
251
- if (quote === '"' && ch === "\\") {
252
- if (i + 1 >= inner.length) return null;
253
- const next = inner[++i];
254
- if (next === "b") current += "\b";
255
- else if (next === "t") current += "\t";
256
- else if (next === "n") current += "\n";
257
- else if (next === "f") current += "\f";
258
- else if (next === "r") current += "\r";
259
- else if (next === '"' || next === "\\" || next === "/") current += next;
260
- else return null;
299
+ if (!expectValue) {
300
+ if (rhs[i] !== ",") return null;
301
+ i += 1;
302
+ expectValue = true;
261
303
  continue;
262
304
  }
263
- current += ch;
305
+
306
+ const value = parseString();
307
+ if (value == null) return null;
308
+ parts.push(value);
309
+ expectValue = false;
264
310
  }
265
311
 
266
- return parts.length > 0 ? parts : null;
312
+ return null;
267
313
  }
268
314
 
269
315
  function formatTomlStringArray(arr) {
@@ -278,6 +324,7 @@ function readTomlArrayLiteral(lines, startIndex, rhs) {
278
324
  let quote = null;
279
325
  let depth = 0;
280
326
  let sawOpen = false;
327
+ let invalid = false;
281
328
 
282
329
  function scanChunk(chunk) {
283
330
  for (let i = 0; i < chunk.length; i++) {
@@ -295,7 +342,10 @@ function readTomlArrayLiteral(lines, startIndex, rhs) {
295
342
  }
296
343
  if (ch === "]") {
297
344
  depth -= 1;
298
- if (sawOpen && depth === 0) return i;
345
+ if (sawOpen && depth === 0) {
346
+ if (!isTomlArrayTrailer(chunk.slice(i + 1))) invalid = true;
347
+ return i;
348
+ }
299
349
  }
300
350
  continue;
301
351
  }
@@ -305,6 +355,10 @@ function readTomlArrayLiteral(lines, startIndex, rhs) {
305
355
  continue;
306
356
  }
307
357
  if (quote === '"' && ch === "\\") {
358
+ if (i + 1 >= chunk.length) {
359
+ invalid = true;
360
+ return -1;
361
+ }
308
362
  i += 1;
309
363
  }
310
364
  }
@@ -313,11 +367,13 @@ function readTomlArrayLiteral(lines, startIndex, rhs) {
313
367
 
314
368
  const parts = [first];
315
369
  let endPos = scanChunk(first);
370
+ if (invalid) return null;
316
371
  if (endPos !== -1) return first.slice(0, endPos + 1).trim();
317
372
 
318
373
  for (let j = startIndex + 1; j < lines.length; j++) {
319
374
  const line = lines[j];
320
375
  endPos = scanChunk(line);
376
+ if (invalid) return null;
321
377
  if (endPos !== -1) {
322
378
  parts.push(line.slice(0, endPos + 1));
323
379
  return parts.join("\n").trim();
@@ -336,6 +392,7 @@ function findTomlArrayBlockEnd(lines, startIndex, rhs) {
336
392
  let quote = null;
337
393
  let depth = 0;
338
394
  let sawOpen = false;
395
+ let invalid = false;
339
396
 
340
397
  function scanChunk(chunk) {
341
398
  for (let i = 0; i < chunk.length; i++) {
@@ -353,7 +410,10 @@ function findTomlArrayBlockEnd(lines, startIndex, rhs) {
353
410
  }
354
411
  if (ch === "]") {
355
412
  depth -= 1;
356
- if (sawOpen && depth === 0) return true;
413
+ if (sawOpen && depth === 0) {
414
+ if (!isTomlArrayTrailer(chunk.slice(i + 1))) invalid = true;
415
+ return true;
416
+ }
357
417
  }
358
418
  continue;
359
419
  }
@@ -363,6 +423,10 @@ function findTomlArrayBlockEnd(lines, startIndex, rhs) {
363
423
  continue;
364
424
  }
365
425
  if (quote === '"' && ch === "\\") {
426
+ if (i + 1 >= chunk.length) {
427
+ invalid = true;
428
+ return false;
429
+ }
366
430
  i += 1;
367
431
  }
368
432
  }
@@ -370,12 +434,43 @@ function findTomlArrayBlockEnd(lines, startIndex, rhs) {
370
434
  }
371
435
 
372
436
  if (scanChunk(first)) return startIndex;
437
+ if (invalid) return startIndex;
373
438
  for (let j = startIndex + 1; j < lines.length; j++) {
374
439
  if (scanChunk(lines[j])) return j;
440
+ if (invalid) return startIndex;
375
441
  }
376
442
  return startIndex;
377
443
  }
378
444
 
445
+ function findNotifyBlockEnd(lines, startIndex, rhs) {
446
+ const endIndex = findTomlArrayBlockEnd(lines, startIndex, rhs);
447
+ const literal = readTomlArrayLiteral(lines, startIndex, rhs);
448
+ if (
449
+ endIndex !== startIndex ||
450
+ !rhs.trim().startsWith("[") ||
451
+ (literal && parseTomlStringArray(literal))
452
+ ) {
453
+ return endIndex;
454
+ }
455
+
456
+ for (let j = startIndex + 1; j < lines.length; j++) {
457
+ if (isTopLevelTomlBoundary(lines[j])) return j - 1;
458
+ }
459
+ return lines.length - 1;
460
+ }
461
+
462
+ function isTomlTableHeader(line) {
463
+ return /^\s*\[\[?[^\]]+\]\]?\s*(?:#.*)?$/.test(line);
464
+ }
465
+
466
+ function isTopLevelTomlBoundary(line) {
467
+ return /^\s*(?:\[\[?[^\]]+\]\]?|(?:[A-Za-z0-9_-]+|"[^"]+"|'[^']+')(?:\s*\.\s*(?:[A-Za-z0-9_-]+|"[^"]+"|'[^']+'))*\s*=)/.test(line);
468
+ }
469
+
470
+ function isTomlArrayTrailer(text) {
471
+ return /^\s*(?:#.*)?$/.test(text);
472
+ }
473
+
379
474
  function arraysEqual(a, b) {
380
475
  if (!Array.isArray(a) || !Array.isArray(b)) return false;
381
476
  if (a.length !== b.length) return false;
@@ -10,7 +10,7 @@
10
10
 
11
11
  "use strict";
12
12
 
13
- const fs = require("node:fs");
13
+ const crypto = require("node:crypto");
14
14
  const os = require("node:os");
15
15
  const path = require("node:path");
16
16
 
@@ -25,8 +25,8 @@ const {
25
25
 
26
26
  const {
27
27
  parseCodexRolloutFile,
28
- listCodexSessionFiles,
29
- dayKeyToIsoBounds,
28
+ discoverCodexSessionFiles,
29
+ rolloutSessionIdFromPath,
30
30
  finalizeToolRows,
31
31
  finalizeSkillRows,
32
32
  finalizeExecRows,
@@ -120,25 +120,55 @@ function buildDateRange({ period, date }) {
120
120
  // ---------------------------------------------------------------------------
121
121
 
122
122
  const CACHE = new Map();
123
+ const PARSED_GROUP_CACHE = new Map();
123
124
  const CACHE_TTL_MS = 60_000;
124
- const CACHE_SCHEMA_VERSION = "codex-context-v2";
125
-
126
- function maxMtimeMs(files) {
127
- let max = 0;
128
- for (const filePath of files) {
129
- try {
130
- const st = fs.statSync(filePath);
131
- if (st.mtimeMs > max) max = st.mtimeMs;
132
- } catch {}
133
- }
134
- return max;
135
- }
125
+ const CACHE_SCHEMA_VERSION = "codex-context-v4";
126
+ const MAX_PARSED_GROUP_CACHE_ENTRIES = 4096;
136
127
 
137
128
  function cacheTimeZoneKey(timeZoneContext) {
138
129
  if (!timeZoneContext) return "";
139
130
  return `${timeZoneContext.timeZone || ""}|${Number.isFinite(timeZoneContext.offsetMinutes) ? timeZoneContext.offsetMinutes : ""}`;
140
131
  }
141
132
 
133
+ function statIdentity(filePath, stat) {
134
+ return JSON.stringify([filePath, stat.size, stat.mtimeMs, stat.ctimeMs, stat.ino]);
135
+ }
136
+
137
+ function inventorySignature(entries) {
138
+ const hash = crypto.createHash("sha256");
139
+ for (const { filePath, stat, candidate } of entries) {
140
+ hash.update(statIdentity(filePath, stat));
141
+ hash.update(candidate ? "1" : "0");
142
+ hash.update("\0");
143
+ }
144
+ return hash.digest("hex");
145
+ }
146
+
147
+ function parsedGroupCacheKey(entries, { from, to, timeZoneContext }) {
148
+ const hash = crypto.createHash("sha256");
149
+ hash.update(CACHE_SCHEMA_VERSION);
150
+ hash.update(from || "");
151
+ hash.update("\0");
152
+ hash.update(to || "");
153
+ hash.update("\0");
154
+ hash.update(cacheTimeZoneKey(timeZoneContext));
155
+ for (const { filePath, stat } of entries) {
156
+ hash.update(statIdentity(filePath, stat));
157
+ hash.update("\0");
158
+ }
159
+ return hash.digest("hex");
160
+ }
161
+
162
+ function rememberParsedGroup(key, value) {
163
+ PARSED_GROUP_CACHE.delete(key);
164
+ PARSED_GROUP_CACHE.set(key, value);
165
+ while (PARSED_GROUP_CACHE.size > MAX_PARSED_GROUP_CACHE_ENTRIES) {
166
+ const oldest = PARSED_GROUP_CACHE.keys().next().value;
167
+ if (!oldest) break;
168
+ PARSED_GROUP_CACHE.delete(oldest);
169
+ }
170
+ }
171
+
142
172
  // ---------------------------------------------------------------------------
143
173
  // Main entry
144
174
  // ---------------------------------------------------------------------------
@@ -151,6 +181,9 @@ async function computeCodexContextBreakdown({
151
181
  codexDir = null,
152
182
  top = 20,
153
183
  timeZoneContext = null,
184
+ exhaustive = false,
185
+ nowMs = Date.now(),
186
+ includeDiagnostics = false,
154
187
  } = {}) {
155
188
  let fromKey = from;
156
189
  let toKey = to;
@@ -160,10 +193,33 @@ async function computeCodexContextBreakdown({
160
193
  toKey = range?.to || null;
161
194
  }
162
195
 
163
- const { fromIso, toIso } = dayKeyToIsoBounds(fromKey, toKey);
164
- const baseDir = codexDir || path.join(os.homedir(), ".codex", "sessions");
165
-
166
- const files = await listCodexSessionFiles(baseDir);
196
+ const codexHome = process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
197
+ const roots = codexDir
198
+ ? [codexDir]
199
+ : [path.join(codexHome, "sessions"), path.join(codexHome, "archived_sessions")];
200
+ const baseDir = roots.join(path.delimiter);
201
+ const diagnostics = {
202
+ cache_hit: false,
203
+ full_metadata_audit: false,
204
+ discovered_files: 0,
205
+ candidate_files: 0,
206
+ stat_calls: 0,
207
+ metadata_cache_hits: 0,
208
+ opened_files: 0,
209
+ parsed_files: 0,
210
+ parse_cache_hits: 0,
211
+ json_parse_calls: 0,
212
+ };
213
+ const discovered = discoverCodexSessionFiles(roots, {
214
+ from: fromKey,
215
+ to: toKey,
216
+ timeZoneContext,
217
+ exhaustive,
218
+ nowMs,
219
+ diagnostics,
220
+ });
221
+ const candidates = discovered.filter((entry) => entry.candidate);
222
+ const fileSignature = inventorySignature(discovered);
167
223
  const cacheKey = [
168
224
  CACHE_SCHEMA_VERSION,
169
225
  baseDir,
@@ -171,24 +227,48 @@ async function computeCodexContextBreakdown({
171
227
  toKey || "",
172
228
  cacheTimeZoneKey(timeZoneContext),
173
229
  Number.isFinite(top) ? top : 20,
174
- files.length,
175
- maxMtimeMs(files),
230
+ exhaustive ? "exhaustive" : "bounded",
231
+ fileSignature,
176
232
  ].join("|");
177
233
  const cached = CACHE.get(cacheKey);
178
234
  if (cached && Date.now() - cached.at < CACHE_TTL_MS) {
179
- return cached.value;
235
+ diagnostics.cache_hit = true;
236
+ return includeDiagnostics ? { ...cached.value, diagnostics } : cached.value;
180
237
  }
181
238
 
182
239
  const sessions = [];
240
+ const seenTokenEvents = new Set();
241
+
242
+ const candidateGroups = new Map();
243
+ for (const entry of candidates) {
244
+ const sessionKey = rolloutSessionIdFromPath(entry.filePath) || entry.filePath;
245
+ if (!candidateGroups.has(sessionKey)) candidateGroups.set(sessionKey, []);
246
+ candidateGroups.get(sessionKey).push(entry);
247
+ }
183
248
 
184
- for (const filePath of files) {
185
- const parsed = await parseCodexRolloutFile(filePath, {
186
- fromIso,
187
- toIso,
188
- from: fromKey,
189
- to: toKey,
190
- timeZoneContext,
191
- });
249
+ for (const entries of candidateGroups.values()) {
250
+ const filePaths = entries.map((entry) => entry.filePath);
251
+ const cacheable = Boolean(rolloutSessionIdFromPath(filePaths[0]));
252
+ const parsedCacheKey = cacheable
253
+ ? parsedGroupCacheKey(entries, { from: fromKey, to: toKey, timeZoneContext })
254
+ : null;
255
+ let parsed = parsedCacheKey ? PARSED_GROUP_CACHE.get(parsedCacheKey) : null;
256
+ if (parsed) {
257
+ // Cacheable groups are partitioned by rollout UUID, so their token event
258
+ // keys cannot overlap and cache hits need not backfill seenTokenEvents.
259
+ diagnostics.parse_cache_hits += 1;
260
+ PARSED_GROUP_CACHE.delete(parsedCacheKey);
261
+ PARSED_GROUP_CACHE.set(parsedCacheKey, parsed);
262
+ } else {
263
+ parsed = await parseCodexRolloutFile(filePaths.length === 1 ? filePaths[0] : filePaths, {
264
+ from: fromKey,
265
+ to: toKey,
266
+ timeZoneContext,
267
+ diagnostics,
268
+ seenTokenEvents,
269
+ });
270
+ if (parsedCacheKey) rememberParsedGroup(parsedCacheKey, parsed);
271
+ }
192
272
  if (!parsed || !parsed.totals || !parsed.totals.total_tokens) continue;
193
273
  sessions.push(parsed);
194
274
  }
@@ -261,26 +341,25 @@ async function computeCodexContextBreakdown({
261
341
  }))
262
342
  .sort((a, b) => (b.totals?.total_tokens || 0) - (a.totals?.total_tokens || 0));
263
343
 
264
- const nonTextToolTotal = toolRows.reduce((sum, row) => {
265
- if ((row.raw_name || row.name) === "text_response") return sum;
266
- return sum + Number(row.totals?.total_tokens || 0);
267
- }, 0);
268
- const displayedMessageTotal = Math.max(
269
- 0,
270
- Number(grand.total_tokens || 0) - Number(grand.reasoning_output_tokens || 0) - nonTextToolTotal,
271
- );
272
344
  const textResponse = toolRows.find((row) => (row.raw_name || row.name) === "text_response");
273
345
  const textResponseTotals = textResponse?.totals || emptyTotals();
274
346
  const textResponseHistoryWeight = Math.max(
275
347
  0,
276
348
  Number(textResponseTotals.cached_input_tokens || 0) + Number(textResponseTotals.cache_creation_input_tokens || 0),
277
349
  );
350
+ const textResponseInputWeight = Math.max(0, Number(textResponseTotals.input_tokens || 0));
351
+ const textResponseOutputWeight = Math.max(
352
+ 0,
353
+ Number(textResponseTotals.output_tokens || 0) - Number(textResponseTotals.reasoning_output_tokens || 0),
354
+ );
355
+ const displayedMessageTotal =
356
+ textResponseInputWeight + textResponseHistoryWeight + textResponseOutputWeight;
278
357
  const messageAlloc = allocateByLargestRemainder(
279
358
  displayedMessageTotal,
280
359
  {
281
- user_input: Math.max(0, Number(textResponseTotals.input_tokens || 0)),
360
+ user_input: textResponseInputWeight,
282
361
  conversation_history: textResponseHistoryWeight,
283
- assistant_response: Math.max(0, Number(textResponseTotals.output_tokens || 0)),
362
+ assistant_response: textResponseOutputWeight,
284
363
  },
285
364
  ["user_input", "conversation_history", "assistant_response"],
286
365
  );
@@ -350,7 +429,7 @@ async function computeCodexContextBreakdown({
350
429
  scope: "supported",
351
430
  breakdown_status: "ok",
352
431
  totals: grand,
353
- session_count: sessions.length,
432
+ session_count: new Set(sessions.map((session) => session.sessionId || session.filePath)).size,
354
433
  message_count: sessions.reduce((a, s) => a + (s.turnCount || 0), 0),
355
434
  message_breakdown: {
356
435
  categories: messageBreakdown,
@@ -387,12 +466,13 @@ async function computeCodexContextBreakdown({
387
466
  },
388
467
  };
389
468
 
390
- CACHE.set(cacheKey, { at: Date.now(), value: result });
469
+ const cacheValue = { ...result };
470
+ CACHE.set(cacheKey, { at: Date.now(), value: cacheValue });
391
471
  while (CACHE.size > 32) {
392
472
  const oldest = [...CACHE.entries()].sort((a, b) => a[1].at - b[1].at)[0];
393
473
  if (oldest) CACHE.delete(oldest[0]);
394
474
  }
395
- return result;
475
+ return includeDiagnostics ? { ...result, diagnostics } : result;
396
476
  }
397
477
 
398
478
  module.exports = {