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
@@ -19,6 +19,11 @@ const {
19
19
  buildExecStatsEntry,
20
20
  } = require("./categorizer-utils");
21
21
 
22
+ const DISCOVERY_FULL_AUDIT_INTERVAL_MS = 60 * 1000;
23
+ const MAX_DISCOVERY_INVENTORIES = 32;
24
+ const DISCOVERY_INVENTORIES = new Map();
25
+ const ZONED_DAY_FORMATTERS = new Map();
26
+
22
27
  // ---------------------------------------------------------------------------
23
28
  // Timezone helpers
24
29
  // ---------------------------------------------------------------------------
@@ -52,13 +57,18 @@ function getZonedParts(date, timeZoneContext = {}) {
52
57
 
53
58
  if (timeZone && typeof Intl !== "undefined" && Intl.DateTimeFormat) {
54
59
  try {
55
- return new Intl.DateTimeFormat("en-CA", {
56
- timeZone,
57
- year: "numeric",
58
- month: "2-digit",
59
- day: "2-digit",
60
- hourCycle: "h23",
61
- }).formatToParts(dt);
60
+ let formatter = ZONED_DAY_FORMATTERS.get(timeZone);
61
+ if (!formatter) {
62
+ formatter = new Intl.DateTimeFormat("en-CA", {
63
+ timeZone,
64
+ year: "numeric",
65
+ month: "2-digit",
66
+ day: "2-digit",
67
+ hourCycle: "h23",
68
+ });
69
+ ZONED_DAY_FORMATTERS.set(timeZone, formatter);
70
+ }
71
+ return formatter.formatToParts(dt);
62
72
  } catch {
63
73
  // Fall through to offset handling.
64
74
  }
@@ -98,8 +108,9 @@ function isTimestampInRequestedDayRange(timestamp, { from, to, timeZoneContext }
98
108
  // File discovery
99
109
  // ---------------------------------------------------------------------------
100
110
 
101
- function safeJsonParse(str) {
111
+ function safeJsonParse(str, diagnostics = null) {
102
112
  try {
113
+ if (diagnostics) diagnostics.json_parse_calls += 1;
103
114
  return JSON.parse(str);
104
115
  } catch {
105
116
  return null;
@@ -133,6 +144,210 @@ function listJsonlFiles(rootDir) {
133
144
  return out;
134
145
  }
135
146
 
147
+ function rolloutPathDay(filePath) {
148
+ const normalized = String(filePath || "").replaceAll("\\", "/");
149
+ const fileMatch = path.basename(normalized).match(/(?:^|rollout-)(\d{4})-(\d{2})-(\d{2})/);
150
+ if (fileMatch) return `${fileMatch[1]}-${fileMatch[2]}-${fileMatch[3]}`;
151
+ const directoryMatch = normalized.match(/.*\/(\d{4})\/(\d{2})\/(\d{2})(?:\/|$)/);
152
+ return directoryMatch
153
+ ? `${directoryMatch[1]}-${directoryMatch[2]}-${directoryMatch[3]}`
154
+ : null;
155
+ }
156
+
157
+ function nextDayKey(dayKey) {
158
+ if (!dayKey) return null;
159
+ const date = new Date(`${dayKey}T00:00:00.000Z`);
160
+ if (!Number.isFinite(date.getTime())) return null;
161
+ date.setUTCDate(date.getUTCDate() + 1);
162
+ return date.toISOString().slice(0, 10);
163
+ }
164
+
165
+ function metadataRangeBounds(from, to, timeZoneContext) {
166
+ const offsetMinutes = timeZoneContext?.offsetMinutes;
167
+ if (Number.isFinite(offsetMinutes)) {
168
+ const dstGuardMs = 2 * 60 * 60 * 1000;
169
+ const fromUtc = from ? Date.parse(`${from}T00:00:00.000Z`) : null;
170
+ const afterTo = to ? nextDayKey(to) : null;
171
+ const toUtcExclusive = afterTo ? Date.parse(`${afterTo}T00:00:00.000Z`) : null;
172
+ return {
173
+ fromMs: Number.isFinite(fromUtc)
174
+ ? fromUtc + offsetMinutes * 60_000 - dstGuardMs
175
+ : null,
176
+ toMs: Number.isFinite(toUtcExclusive)
177
+ ? toUtcExclusive + offsetMinutes * 60_000 + dstGuardMs
178
+ : null,
179
+ };
180
+ }
181
+ const { fromIso, toIso } = dayKeyToIsoBounds(from, to);
182
+ return {
183
+ fromMs: fromIso ? Date.parse(fromIso) : null,
184
+ toMs: toIso ? Date.parse(toIso) : null,
185
+ };
186
+ }
187
+
188
+ function isConservativeRangeCandidate(filePath, stat, {
189
+ from = null,
190
+ to = null,
191
+ timeZoneContext = null,
192
+ metadataBounds = null,
193
+ } = {}) {
194
+ if (!from && !to) return true;
195
+ // With no lower bound, a later-path rewrite can still contain an event at or
196
+ // before `to`. There is no metadata-only exclusion that can prove otherwise.
197
+ if (!from) return true;
198
+ const pathDay = rolloutPathDay(filePath);
199
+ if (!pathDay) return true;
200
+
201
+ const pathOverlaps = (!from || pathDay >= from) && (!to || pathDay <= to);
202
+ if (pathOverlaps) return true;
203
+
204
+ // Old sessions can be appended or rewritten. A post-range mtime or ctime is
205
+ // therefore a reason to parse, never a reason to exclude. ctime also catches
206
+ // cold-process inode replacement/truncation when a writer preserves mtime.
207
+ // Future-dated decoys are kept conservatively; correctness wins over an open.
208
+ const bounds = metadataBounds || metadataRangeBounds(from, to, timeZoneContext);
209
+ const fromMs = bounds.fromMs;
210
+ const toMs = bounds.toMs;
211
+ const mtimeMs = Number(stat?.mtimeMs);
212
+ const ctimeMs = Number(stat?.ctimeMs);
213
+ const changedAfterStart = (value) => (
214
+ Number.isFinite(value) && (!Number.isFinite(fromMs) || value >= fromMs)
215
+ );
216
+ const changedInsideRange = (value) => (
217
+ changedAfterStart(value) && (!Number.isFinite(toMs) || value <= toMs)
218
+ );
219
+ if (pathDay < from) {
220
+ return changedAfterStart(mtimeMs) || changedAfterStart(ctimeMs);
221
+ }
222
+
223
+ // A path one day ahead can still overlap the requested browser day because
224
+ // rollout folders follow the writer's local date. Its current mtime may be
225
+ // later if the same session kept growing, so metadata alone cannot exclude it.
226
+ if (to && pathDay === nextDayKey(to)) return true;
227
+
228
+ // A host-local path day can be later than the requested browser day when
229
+ // their timezones differ. Keep it only when filesystem metadata maps back
230
+ // into the requested day range; do not pull every genuinely future session
231
+ // into a historical query.
232
+ return changedInsideRange(mtimeMs) || changedInsideRange(ctimeMs);
233
+ }
234
+
235
+ function isRolloutPathInRequestedRange(filePath, { from = null, to = null } = {}) {
236
+ const pathDay = rolloutPathDay(filePath);
237
+ if (!pathDay) return true;
238
+ if (from && pathDay < from) return false;
239
+ if (to && pathDay > to) return false;
240
+ return true;
241
+ }
242
+
243
+ function discoveryInventoryKey(roots) {
244
+ return roots.map((root) => path.resolve(root)).sort().join("\0");
245
+ }
246
+
247
+ function getDiscoveryInventory(roots, nowMs) {
248
+ const key = discoveryInventoryKey(roots);
249
+ let inventory = DISCOVERY_INVENTORIES.get(key);
250
+ if (!inventory) {
251
+ inventory = { files: new Map(), lastFullAuditAtMs: 0, lastUsedAtMs: nowMs };
252
+ DISCOVERY_INVENTORIES.set(key, inventory);
253
+ }
254
+ inventory.lastUsedAtMs = nowMs;
255
+ while (DISCOVERY_INVENTORIES.size > MAX_DISCOVERY_INVENTORIES) {
256
+ let oldestKey = null;
257
+ let oldestAt = Infinity;
258
+ for (const [candidateKey, candidate] of DISCOVERY_INVENTORIES) {
259
+ if (candidateKey === key) continue;
260
+ if (candidate.lastUsedAtMs < oldestAt) {
261
+ oldestAt = candidate.lastUsedAtMs;
262
+ oldestKey = candidateKey;
263
+ }
264
+ }
265
+ if (!oldestKey) break;
266
+ DISCOVERY_INVENTORIES.delete(oldestKey);
267
+ }
268
+ return inventory;
269
+ }
270
+
271
+ function discoverCodexSessionFiles(rootDirs, options = {}) {
272
+ const diagnostics = options.diagnostics || null;
273
+ const roots = Array.isArray(rootDirs) ? rootDirs : [rootDirs];
274
+ const nowMs = Number.isFinite(Number(options.nowMs)) ? Number(options.nowMs) : Date.now();
275
+ const inventory = getDiscoveryInventory(roots.filter(Boolean), nowMs);
276
+ const candidateOptions = {
277
+ ...options,
278
+ metadataBounds: metadataRangeBounds(options.from, options.to, options.timeZoneContext),
279
+ };
280
+ const bounded = Boolean(options.from || options.to);
281
+ const auditAgeMs = nowMs - Number(inventory.lastFullAuditAtMs || 0);
282
+ const fullMetadataAudit = Boolean(
283
+ options.exhaustive ||
284
+ !bounded ||
285
+ !Number.isFinite(auditAgeMs) ||
286
+ auditAgeMs < 0 ||
287
+ auditAgeMs >= DISCOVERY_FULL_AUDIT_INTERVAL_MS
288
+ );
289
+ if (diagnostics) diagnostics.full_metadata_audit = fullMetadataAudit;
290
+ const entries = [];
291
+ const visited = new Set();
292
+ const stack = roots.filter(Boolean);
293
+
294
+ while (stack.length > 0) {
295
+ const dir = stack.pop();
296
+ let children;
297
+ try {
298
+ children = fs.readdirSync(dir, { withFileTypes: true });
299
+ } catch {
300
+ continue;
301
+ }
302
+ for (const child of children) {
303
+ const filePath = path.join(dir, child.name);
304
+ if (child.isDirectory()) {
305
+ stack.push(filePath);
306
+ continue;
307
+ }
308
+ if (!child.isFile() || !child.name.endsWith(".jsonl") || visited.has(filePath)) continue;
309
+ visited.add(filePath);
310
+ if (diagnostics) diagnostics.discovered_files += 1;
311
+ const cached = inventory.files.get(filePath) || null;
312
+ const cachedCandidate = cached
313
+ ? isConservativeRangeCandidate(filePath, cached, candidateOptions)
314
+ : false;
315
+ const shouldStat = Boolean(
316
+ fullMetadataAudit ||
317
+ !cached ||
318
+ cachedCandidate ||
319
+ isRolloutPathInRequestedRange(filePath, options)
320
+ );
321
+ let stat = cached;
322
+ if (shouldStat) {
323
+ try {
324
+ if (diagnostics) diagnostics.stat_calls += 1;
325
+ stat = fs.statSync(filePath);
326
+ inventory.files.set(filePath, stat);
327
+ } catch {
328
+ inventory.files.delete(filePath);
329
+ continue;
330
+ }
331
+ } else if (diagnostics) {
332
+ diagnostics.metadata_cache_hits += 1;
333
+ }
334
+ const candidate = Boolean(
335
+ options.exhaustive ||
336
+ isConservativeRangeCandidate(filePath, stat, candidateOptions),
337
+ );
338
+ entries.push({ filePath, stat, candidate });
339
+ if (candidate && diagnostics) diagnostics.candidate_files += 1;
340
+ }
341
+ }
342
+
343
+ for (const filePath of inventory.files.keys()) {
344
+ if (!visited.has(filePath)) inventory.files.delete(filePath);
345
+ }
346
+ if (fullMetadataAudit) inventory.lastFullAuditAtMs = nowMs;
347
+
348
+ return entries.sort((a, b) => a.filePath.localeCompare(b.filePath));
349
+ }
350
+
136
351
  async function listCodexSessionFiles(baseDir) {
137
352
  const rolloutFiles = await listRolloutFiles(baseDir).catch(() => []);
138
353
  const allJsonlFiles = listJsonlFiles(baseDir);
@@ -172,9 +387,9 @@ function normalizeToolName(payload) {
172
387
  return name || "unknown";
173
388
  }
174
389
 
175
- function extractSkillNameFromFunctionCall(payload) {
390
+ function extractSkillNameFromFunctionCall(payload, diagnostics = null) {
176
391
  if (!payload || payload.name !== "exec_command") return null;
177
- const args = safeJsonParse(payload.arguments || "{}") || {};
392
+ const args = safeJsonParse(payload.arguments || "{}", diagnostics) || {};
178
393
  const cmd = String(args.cmd || "");
179
394
  const match = cmd.match(/(?:^|\/)skills\/(?:\.system\/)?([^/\s]+)\/SKILL\.md\b/);
180
395
  return match ? match[1] : null;
@@ -334,9 +549,15 @@ function finalizeExecRows(map) {
334
549
  // Main parser
335
550
  // ---------------------------------------------------------------------------
336
551
 
337
- async function parseCodexRolloutFile(filePath, { fromIso, toIso, from = null, to = null, timeZoneContext = null } = {}) {
338
- const stream = fs.createReadStream(filePath, { encoding: "utf8" });
339
- const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
552
+ async function parseCodexRolloutFile(filePath, {
553
+ from = null,
554
+ to = null,
555
+ timeZoneContext = null,
556
+ diagnostics = null,
557
+ seenTokenEvents = null,
558
+ } = {}) {
559
+ const filePaths = (Array.isArray(filePath) ? filePath : [filePath]).filter(Boolean);
560
+ const primaryFilePath = filePaths[0] || String(filePath || "");
340
561
 
341
562
  let sessionId = null;
342
563
  let cwd = null;
@@ -430,7 +651,13 @@ async function parseCodexRolloutFile(filePath, { fromIso, toIso, from = null, to
430
651
  }
431
652
 
432
653
  function attributeTurn(delta) {
433
- if (!delta || delta.total_tokens <= 0) return;
654
+ if (!delta || delta.total_tokens <= 0) {
655
+ for (const p of pendingExecEnds) absorbExecEnd(p);
656
+ pendingCalls = [];
657
+ pendingSkills = [];
658
+ pendingExecEnds = [];
659
+ return;
660
+ }
434
661
  turnCount += 1;
435
662
 
436
663
  const toolNames = pendingCalls
@@ -517,19 +744,10 @@ async function parseCodexRolloutFile(filePath, { fromIso, toIso, from = null, to
517
744
  pendingExecEnds = [];
518
745
  }
519
746
 
520
- for await (const line of rl) {
521
- if (!line) continue;
522
- let obj;
523
- try {
524
- obj = JSON.parse(line);
525
- } catch {
526
- continue;
527
- }
747
+ for await (const obj of iterateCodexObjects(filePaths, diagnostics)) {
528
748
  const ts = typeof obj?.timestamp === "string" ? obj.timestamp : null;
529
749
  if (!ts) continue;
530
- if (fromIso && ts < fromIso) continue;
531
- if (toIso && ts > toIso) continue;
532
- if (!isTimestampInRequestedDayRange(ts, { from, to, timeZoneContext })) continue;
750
+ const inRequestedRange = isTimestampInRequestedDayRange(ts, { from, to, timeZoneContext });
533
751
 
534
752
  if (obj.type === "session_meta") {
535
753
  const p = obj.payload || {};
@@ -548,7 +766,7 @@ async function parseCodexRolloutFile(filePath, { fromIso, toIso, from = null, to
548
766
 
549
767
  if (obj.type === "response_item" && obj.payload?.type === "function_call") {
550
768
  pendingCalls.push(obj.payload);
551
- const skill = extractSkillNameFromFunctionCall(obj.payload);
769
+ const skill = extractSkillNameFromFunctionCall(obj.payload, diagnostics);
552
770
  if (skill) pendingSkills.push(skill);
553
771
  continue;
554
772
  }
@@ -565,21 +783,31 @@ async function parseCodexRolloutFile(filePath, { fromIso, toIso, from = null, to
565
783
  const totalUsage = info?.total_token_usage;
566
784
  const delta = pickDelta(lastUsage, totalUsage, prevTotals);
567
785
  if (totalUsage && typeof totalUsage === "object") prevTotals = totalUsage;
568
- if (delta) attributeTurn(delta);
786
+ if (inRequestedRange) {
787
+ const eventSessionId = sessionId || rolloutSessionIdFromPath(primaryFilePath) || primaryFilePath;
788
+ const eventKey = `${eventSessionId}:${ts}:${JSON.stringify({ lastUsage, totalUsage })}`;
789
+ if (seenTokenEvents && seenTokenEvents.has(eventKey)) {
790
+ attributeTurn(null);
791
+ } else {
792
+ if (seenTokenEvents && delta?.total_tokens > 0) seenTokenEvents.add(eventKey);
793
+ attributeTurn(delta);
794
+ }
795
+ } else {
796
+ pendingCalls = [];
797
+ pendingSkills = [];
798
+ pendingExecEnds = [];
799
+ }
569
800
  continue;
570
801
  }
571
802
  }
572
803
 
573
- rl.close();
574
- stream.close?.();
575
-
576
804
  return {
577
- sessionId,
805
+ sessionId: sessionId || rolloutSessionIdFromPath(primaryFilePath) || primaryFilePath,
578
806
  cwd,
579
807
  model: model || provider,
580
808
  provider,
581
809
  version: cliVersion,
582
- filePath,
810
+ filePath: primaryFilePath,
583
811
  turnCount,
584
812
  totals,
585
813
  toolBreakdown: {
@@ -599,6 +827,71 @@ async function parseCodexRolloutFile(filePath, { fromIso, toIso, from = null, to
599
827
  };
600
828
  }
601
829
 
830
+ async function* readCodexObjects(filePath, diagnostics, fileIndex) {
831
+ if (diagnostics) diagnostics.opened_files += 1;
832
+ const stream = fs.createReadStream(filePath, { encoding: "utf8" });
833
+ const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
834
+ let lineIndex = 0;
835
+ try {
836
+ for await (const line of rl) {
837
+ if (!line) continue;
838
+ let obj;
839
+ try {
840
+ if (diagnostics) diagnostics.json_parse_calls += 1;
841
+ obj = JSON.parse(line);
842
+ } catch {
843
+ continue;
844
+ }
845
+ yield { obj, fileIndex, lineIndex };
846
+ lineIndex += 1;
847
+ }
848
+ } finally {
849
+ rl.close();
850
+ stream.close?.();
851
+ if (diagnostics) diagnostics.parsed_files += 1;
852
+ }
853
+ }
854
+
855
+ async function* iterateCodexObjects(filePaths, diagnostics) {
856
+ if (filePaths.length <= 1) {
857
+ for await (const record of readCodexObjects(filePaths[0], diagnostics, 0)) {
858
+ yield record.obj;
859
+ }
860
+ return;
861
+ }
862
+
863
+ // A live rollout can overlap its archived copy or be split into prefix/suffix
864
+ // fragments. Merge only duplicate-session groups, order by event time, and
865
+ // suppress byte-semantically identical records from different files so the
866
+ // cumulative baseline is established exactly once.
867
+ const records = [];
868
+ for (let fileIndex = 0; fileIndex < filePaths.length; fileIndex += 1) {
869
+ for await (const record of readCodexObjects(filePaths[fileIndex], diagnostics, fileIndex)) {
870
+ records.push(record);
871
+ }
872
+ }
873
+ records.sort((a, b) => {
874
+ const aTimestamp = typeof a.obj?.timestamp === "string" ? a.obj.timestamp : "";
875
+ const bTimestamp = typeof b.obj?.timestamp === "string" ? b.obj.timestamp : "";
876
+ return aTimestamp.localeCompare(bTimestamp) || a.fileIndex - b.fileIndex || a.lineIndex - b.lineIndex;
877
+ });
878
+ const firstFileByRecord = new Map();
879
+ for (const record of records) {
880
+ const semanticKey = JSON.stringify(record.obj);
881
+ const firstFile = firstFileByRecord.get(semanticKey);
882
+ if (firstFile !== undefined && firstFile !== record.fileIndex) continue;
883
+ if (firstFile === undefined) firstFileByRecord.set(semanticKey, record.fileIndex);
884
+ yield record.obj;
885
+ }
886
+ }
887
+
888
+ function rolloutSessionIdFromPath(filePath) {
889
+ const match = path.basename(String(filePath || "")).match(
890
+ /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i,
891
+ );
892
+ return match ? match[1] : null;
893
+ }
894
+
602
895
  module.exports = {
603
896
  parseCodexRolloutFile,
604
897
  extractTokenCount,
@@ -610,12 +903,16 @@ module.exports = {
610
903
  totalsReset,
611
904
  listJsonlFiles,
612
905
  listCodexSessionFiles,
906
+ discoverCodexSessionFiles,
907
+ rolloutPathDay,
908
+ isConservativeRangeCandidate,
613
909
  safeJsonParse,
614
910
  dayKeyToIsoBounds,
615
911
  formatPartsDayKey,
616
912
  getZonedParts,
617
913
  timestampDayKey,
618
914
  isTimestampInRequestedDayRange,
915
+ rolloutSessionIdFromPath,
619
916
  finalizeToolRows,
620
917
  finalizeSkillRows,
621
918
  finalizeExecRows,