tokentracker-cli 0.79.9 → 0.80.1

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 (74) hide show
  1. package/dashboard/dist/assets/{AchievementBadge-B1WOPlu0.js → AchievementBadge-D617bvS9.js} +1 -1
  2. package/dashboard/dist/assets/{AchievementsPage-9kHEQicS.js → AchievementsPage-mzE76Ah9.js} +2 -2
  3. package/dashboard/dist/assets/{AchievementsSection-Dw05PYPr.js → AchievementsSection-8CYGBoPJ.js} +1 -1
  4. package/dashboard/dist/assets/{BadgeDetailModal-DaWgiijJ.js → BadgeDetailModal-DM3veVmW.js} +1 -1
  5. package/dashboard/dist/assets/{Card-DYhrLLM4.js → Card-DWONuizy.js} +1 -1
  6. package/dashboard/dist/assets/{ClawdAnimated-BLy2LUKz.js → ClawdAnimated-CGQXyWuH.js} +1 -1
  7. package/dashboard/dist/assets/{CommunityStatsModal-CWYqfht3.js → CommunityStatsModal-B5yDmJ0o.js} +1 -1
  8. package/dashboard/dist/assets/{ConfirmModal-DcE2C24Y.js → ConfirmModal-d4wpsMPW.js} +1 -1
  9. package/dashboard/dist/assets/{Controls-CI7qw0-b.js → Controls-CMSqknaB.js} +1 -1
  10. package/dashboard/dist/assets/{DashboardPage-CAlgaBa7.js → DashboardPage-Cw6lmVSg.js} +1 -1
  11. package/dashboard/dist/assets/{DevicePage-DinOyaeN.js → DevicePage-CsPSjSLH.js} +1 -1
  12. package/dashboard/dist/assets/{DialogClose-CjfSDrLo.js → DialogClose-Cbs-z783.js} +1 -1
  13. package/dashboard/dist/assets/DialogDescription-B0fmBJxQ.js +1 -0
  14. package/dashboard/dist/assets/{DialogTitle-f7gNmCca.js → DialogTitle-C4mJiIQW.js} +1 -1
  15. package/dashboard/dist/assets/{FadeIn-D_5w0Qh0.js → FadeIn-xOJwCmZ8.js} +1 -1
  16. package/dashboard/dist/assets/{HeaderGithubStar-DTZyNKyL.js → HeaderGithubStar-oRz4rfC8.js} +1 -1
  17. package/dashboard/dist/assets/{IpCheckPage-D8CViF98.js → IpCheckPage-CTwL-Zmz.js} +1 -1
  18. package/dashboard/dist/assets/{LandingPage-CqN5KEjV.js → LandingPage-CqYJ3sZh.js} +1 -1
  19. package/dashboard/dist/assets/{LeaderboardAvatar-CwUVO4wn.js → LeaderboardAvatar-CuF6Kzkj.js} +1 -1
  20. package/dashboard/dist/assets/{LeaderboardPage-B12HJ9LA.js → LeaderboardPage-BvOPIj-b.js} +2 -2
  21. package/dashboard/dist/assets/{LeaderboardProfileModal-DFzPpsnV.js → LeaderboardProfileModal-DVXuKkRN.js} +3 -3
  22. package/dashboard/dist/assets/{LeaderboardProfilePage-Z0ZYpNFd.js → LeaderboardProfilePage-RE1CPqzM.js} +1 -1
  23. package/dashboard/dist/assets/{LimitsPage-BpwioIa-.js → LimitsPage-BovkRgac.js} +1 -1
  24. package/dashboard/dist/assets/{LocalOnlyNotice-Covi6zdt.js → LocalOnlyNotice-CTFFU_ip.js} +1 -1
  25. package/dashboard/dist/assets/{LoginPage-CRqbhwCx.js → LoginPage-mguBCTBZ.js} +1 -1
  26. package/dashboard/dist/assets/{PetPage-BY_yObVj.js → PetPage-C5KLH6Fm.js} +1 -1
  27. package/dashboard/dist/assets/{PopoverPopup-B2dE3Ary.js → PopoverPopup-CLDh5yIk.js} +1 -1
  28. package/dashboard/dist/assets/{ResetPasswordPage-C8IEJbVc.js → ResetPasswordPage-DnhgHQrV.js} +1 -1
  29. package/dashboard/dist/assets/{Select-BmRLYSqA.js → Select-DPSaPIlz.js} +1 -1
  30. package/dashboard/dist/assets/{SelectItemText-FC46bk8b.js → SelectItemText-jIBbXpqi.js} +1 -1
  31. package/dashboard/dist/assets/{SettingsPage-DipzOQVg.js → SettingsPage-BK0IqSjg.js} +1 -1
  32. package/dashboard/dist/assets/{SkillsPage-CziypUTf.js → SkillsPage-BQNoMpL2.js} +1 -1
  33. package/dashboard/dist/assets/{WidgetsPage-CJegJhVR.js → WidgetsPage-308d8icu.js} +1 -1
  34. package/dashboard/dist/assets/{WrappedPage-DcMiKvzf.js → WrappedPage-T720sQ9S.js} +1 -1
  35. package/dashboard/dist/assets/{agent-logos-S7bF1LwW.js → agent-logos-Blo5C96d.js} +1 -1
  36. package/dashboard/dist/assets/{arrow-up-right-D0yeL56y.js → arrow-up-right-DKz2Ua6U.js} +1 -1
  37. package/dashboard/dist/assets/check-fF6lQGN-.js +1 -0
  38. package/dashboard/dist/assets/{copy-D-z9ZtIu.js → copy-DcUFG0PD.js} +1 -1
  39. package/dashboard/dist/assets/{download-Dwxnd3QG.js → download-CNK1wtxV.js} +1 -1
  40. package/dashboard/dist/assets/{flame-Bdqk9A75.js → flame-BEhJZRaZ.js} +1 -1
  41. package/dashboard/dist/assets/{format-tokens-DNVcCRwm.js → format-tokens-Bsfbd8nj.js} +1 -1
  42. package/dashboard/dist/assets/{info-CMLT6oUI.js → info-DRJLv-aV.js} +1 -1
  43. package/dashboard/dist/assets/{link-2-BmiHu726.js → link-2-C0GdaANY.js} +1 -1
  44. package/dashboard/dist/assets/{loader-circle-CXVUQtzm.js → loader-circle-F9ymYeor.js} +1 -1
  45. package/dashboard/dist/assets/{main-B9m79io-.js → main-CThKxvKx.js} +3 -3
  46. package/dashboard/dist/assets/{provider-display-BoAesdke.js → provider-display-D4NdFjHN.js} +1 -1
  47. package/dashboard/dist/assets/{trash-2-C9Rh2DAL.js → trash-2-D9vulGMW.js} +1 -1
  48. package/dashboard/dist/assets/{use-community-stats-aZwYxrXR.js → use-community-stats-BU4dXEOM.js} +1 -1
  49. package/dashboard/dist/assets/{use-limits-display-prefs-tJKR83RT.js → use-limits-display-prefs-Bx2VJhwl.js} +1 -1
  50. package/dashboard/dist/assets/{use-native-settings-PjfJPK5v.js → use-native-settings-BSXaxaTC.js} +1 -1
  51. package/dashboard/dist/assets/{use-ordered-list-D1QFmZUY.js → use-ordered-list-OA0HwwEL.js} +1 -1
  52. package/dashboard/dist/assets/{use-quality-per-dollar-pref-4N3Su4wC.js → use-quality-per-dollar-pref-Bqooc4FA.js} +1 -1
  53. package/dashboard/dist/assets/{use-transform-D9nurInL.js → use-transform-CZ9FdRGw.js} +1 -1
  54. package/dashboard/dist/assets/{use-usage-limits-DapTpi_6.js → use-usage-limits-Ct_A4wTr.js} +1 -1
  55. package/dashboard/dist/assets/{useCurrency-B0DH9ZZA.js → useCurrency-D4bvdaFB.js} +1 -1
  56. package/dashboard/dist/assets/{useScrollLock-DijV6DYS.js → useScrollLock-ZsWO-GoL.js} +1 -1
  57. package/dashboard/dist/assets/useTokenFormat-B3huMEws.js +1 -0
  58. package/dashboard/dist/assets/{zap-BVfhR3Ck.js → zap-C9Mp-GNU.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 +607 -6
  63. package/src/commands/sync.js +176 -7
  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/local-api.js +11 -5
  68. package/src/lib/openclaw-session-plugin.js +13 -0
  69. package/src/lib/pricing/seed-snapshot.json +1 -1
  70. package/src/lib/rollout.js +230 -10
  71. package/src/lib/usage-limits.js +9 -5
  72. package/dashboard/dist/assets/DialogDescription-DWs-KIUA.js +0 -1
  73. package/dashboard/dist/assets/check-Dn9BLYEf.js +0 -1
  74. package/dashboard/dist/assets/useTokenFormat-nxdG6gi9.js +0 -1
@@ -172,6 +172,7 @@ async function parseRolloutIncremental({
172
172
  onProgress,
173
173
  source,
174
174
  publicRepoResolver,
175
+ diagnostics,
175
176
  }) {
176
177
  await ensureDir(path.dirname(queuePath));
177
178
  let filesProcessed = 0;
@@ -189,6 +190,7 @@ async function parseRolloutIncremental({
189
190
  const projectFreshnessNowMs = Date.now();
190
191
  const touchedBuckets = new Set();
191
192
  const defaultSource = normalizeSourceInput(source) || DEFAULT_SOURCE;
193
+ const syncDiagnostics = diagnostics && typeof diagnostics === "object" ? diagnostics : null;
192
194
 
193
195
  if (!cursors.files || typeof cursors.files !== "object") {
194
196
  cursors.files = {};
@@ -199,7 +201,108 @@ async function parseRolloutIncremental({
199
201
  // external rewrite of a session file (Codex-Manager's atomic provider-patch on
200
202
  // account switch, issue #187) cannot re-count already-counted events.
201
203
  const prevCodexHashes = Array.isArray(cursors?.codexHashes) ? cursors.codexHashes : [];
202
- const seenCodexEvents = new Set(prevCodexHashes);
204
+ if (!Array.isArray(cursors.codexHashes)) cursors.codexHashes = prevCodexHashes;
205
+ let seenCodexEvents = null;
206
+ let newCodexEventKeys = null;
207
+ let newCodexEventKeySet = null;
208
+ let appendOnlyCodexEvents = null;
209
+ let historicalCodexEvents = null;
210
+ let cursorSessionPaths = null;
211
+ if (syncDiagnostics) {
212
+ const codexParseCandidates = (Array.isArray(rolloutFiles) ? rolloutFiles : []).reduce((count, entry) => {
213
+ const entrySource = typeof entry === "string"
214
+ ? defaultSource
215
+ : normalizeSourceInput(entry?.source) || defaultSource;
216
+ return count + (entrySource === DEFAULT_SOURCE ? 1 : 0);
217
+ }, 0);
218
+ const discoveredRollouts = Number(syncDiagnostics.discovered_rollouts);
219
+ const cursorKeys = Number(syncDiagnostics.cursor_keys);
220
+ const coldSkipped = Number(syncDiagnostics.cold_skipped);
221
+ const parseCandidates = Number(syncDiagnostics.parse_candidates);
222
+ Object.assign(syncDiagnostics, {
223
+ discovered_rollouts: Number.isFinite(discoveredRollouts) ? discoveredRollouts : codexParseCandidates,
224
+ cursor_keys: Number.isFinite(cursorKeys) ? cursorKeys : Object.keys(cursors.files).length,
225
+ parse_candidates: Number.isFinite(parseCandidates) ? parseCandidates : codexParseCandidates,
226
+ stat_candidates: 0,
227
+ cold_skipped: Number.isFinite(coldSkipped) ? coldSkipped : 0,
228
+ content_files_read: 0,
229
+ hash_set_constructions: 0,
230
+ hash_array_materializations: 0,
231
+ hash_array_materialized_items: 0,
232
+ codex_hash_count: prevCodexHashes.length,
233
+ });
234
+ }
235
+ const ensureNewCodexEventKeys = () => {
236
+ if (!newCodexEventKeys) newCodexEventKeys = [];
237
+ if (!newCodexEventKeySet) newCodexEventKeySet = new Set();
238
+ };
239
+ const recordNewCodexEvent = (key) => {
240
+ ensureNewCodexEventKeys();
241
+ if (newCodexEventKeySet.has(key)) return;
242
+ newCodexEventKeySet.add(key);
243
+ newCodexEventKeys.push(key);
244
+ if (seenCodexEvents) seenCodexEvents.add(key);
245
+ };
246
+ const getAppendOnlyCodexEvents = () => {
247
+ if (!appendOnlyCodexEvents) {
248
+ appendOnlyCodexEvents = {
249
+ has(key) {
250
+ return Boolean(
251
+ newCodexEventKeySet?.has(key) ||
252
+ seenCodexEvents?.has(key)
253
+ );
254
+ },
255
+ add: recordNewCodexEvent,
256
+ };
257
+ }
258
+ return appendOnlyCodexEvents;
259
+ };
260
+ const getHistoricalCodexEvents = () => {
261
+ if (!seenCodexEvents) {
262
+ seenCodexEvents = new Set(prevCodexHashes);
263
+ for (const key of newCodexEventKeys || []) seenCodexEvents.add(key);
264
+ if (syncDiagnostics) syncDiagnostics.hash_set_constructions += 1;
265
+ }
266
+ if (!historicalCodexEvents) {
267
+ historicalCodexEvents = {
268
+ has: (key) => seenCodexEvents.has(key),
269
+ add: recordNewCodexEvent,
270
+ };
271
+ }
272
+ return historicalCodexEvents;
273
+ };
274
+ const getCursorSessionPaths = () => {
275
+ if (cursorSessionPaths) return cursorSessionPaths;
276
+ cursorSessionPaths = new Map();
277
+ for (const existingPath of Object.keys(cursors.files)) {
278
+ const sessionId = codexSessionIdFromPath(existingPath);
279
+ if (!sessionId) continue;
280
+ if (!cursorSessionPaths.has(sessionId)) cursorSessionPaths.set(sessionId, new Set());
281
+ cursorSessionPaths.get(sessionId).add(existingPath);
282
+ }
283
+ return cursorSessionPaths;
284
+ };
285
+ const needsHistoricalCodexDedup = ({
286
+ filePath,
287
+ prev,
288
+ sameInode,
289
+ truncated,
290
+ startOffset,
291
+ rebuildingBaseline,
292
+ }) => {
293
+ if (rebuildingBaseline) return true;
294
+ if (prev && (!sameInode || truncated)) return true;
295
+ if (sameInode && !truncated && startOffset > 0) return false;
296
+ if (prev?.lastTotal) return true;
297
+ const sessionId = codexSessionIdFromPath(filePath);
298
+ if (!sessionId) return true;
299
+ const knownPaths = getCursorSessionPaths().get(sessionId);
300
+ if (!knownPaths) return false;
301
+ for (const knownPath of knownPaths) {
302
+ if (knownPath !== filePath) return true;
303
+ }
304
+ return false;
305
+ };
203
306
 
204
307
  for (let idx = 0; idx < rolloutFiles.length; idx++) {
205
308
  const entry = rolloutFiles[idx];
@@ -209,6 +312,7 @@ async function parseRolloutIncremental({
209
312
  typeof entry === "string"
210
313
  ? defaultSource
211
314
  : normalizeSourceInput(entry?.source) || defaultSource;
315
+ if (syncDiagnostics && fileSource === DEFAULT_SOURCE) syncDiagnostics.stat_candidates += 1;
212
316
  const st = await fs.stat(filePath).catch(() => null);
213
317
  if (!st || !st.isFile()) continue;
214
318
 
@@ -218,8 +322,18 @@ async function parseRolloutIncremental({
218
322
  const sameInode = prev && prev.inode === inode;
219
323
  const prevOffset = sameInode ? prev.offset || 0 : 0;
220
324
  const truncated = sameInode && prevOffset > st.size;
221
- const startOffset = sameInode && !truncated ? prevOffset : 0;
222
- const lastTotal = sameInode && !truncated ? prev.lastTotal || null : null;
325
+ const rebuildingCodexBaseline = Boolean(
326
+ fileSource === DEFAULT_SOURCE &&
327
+ sameInode &&
328
+ !truncated &&
329
+ prevOffset > 0 &&
330
+ prevOffset < st.size &&
331
+ (!prev.lastTotal || typeof prev.lastTotal !== "object")
332
+ );
333
+ const startOffset = sameInode && !truncated && !rebuildingCodexBaseline ? prevOffset : 0;
334
+ const lastTotal = sameInode && !truncated && !rebuildingCodexBaseline
335
+ ? prev.lastTotal || null
336
+ : null;
223
337
  const lastModel = sameInode && !truncated ? prev.lastModel || null : null;
224
338
 
225
339
  const codexProjectFastPath = projectEnabled && fileSource === DEFAULT_SOURCE;
@@ -270,6 +384,21 @@ async function parseRolloutIncremental({
270
384
  const projectRef = projectContext?.projectRef || null;
271
385
  const projectKey = projectContext?.projectKey || null;
272
386
 
387
+ if (syncDiagnostics && !projectContextOnlyScan && fileSource === DEFAULT_SOURCE) {
388
+ syncDiagnostics.content_files_read += 1;
389
+ }
390
+ const codexEventTracker = fileSource === DEFAULT_SOURCE
391
+ ? (needsHistoricalCodexDedup({
392
+ filePath,
393
+ prev,
394
+ sameInode,
395
+ truncated,
396
+ startOffset,
397
+ rebuildingBaseline: rebuildingCodexBaseline,
398
+ })
399
+ ? getHistoricalCodexEvents
400
+ : getAppendOnlyCodexEvents)
401
+ : null;
273
402
  const result = projectContextOnlyScan
274
403
  ? await scanRolloutProjectFileContexts({
275
404
  filePath,
@@ -299,7 +428,7 @@ async function parseRolloutIncremental({
299
428
  publicRepoCache,
300
429
  publicRepoResolver,
301
430
  projectContext,
302
- seenCodexEvents,
431
+ seenCodexEvents: codexEventTracker,
303
432
  sessionId: codexSessionIdFromPath(filePath),
304
433
  });
305
434
 
@@ -341,7 +470,12 @@ async function parseRolloutIncremental({
341
470
  const projectBucketsQueued = projectEnabled
342
471
  ? await enqueueTouchedProjectBuckets({ projectQueuePath, projectState, projectTouchedBuckets })
343
472
  : 0;
344
- cursors.codexHashes = Array.from(seenCodexEvents);
473
+ for (const key of newCodexEventKeys || []) prevCodexHashes.push(key);
474
+ if (syncDiagnostics) {
475
+ syncDiagnostics.codex_hash_count = Array.isArray(cursors.codexHashes)
476
+ ? cursors.codexHashes.length
477
+ : prevCodexHashes.length;
478
+ }
345
479
  hourlyState.updatedAt = new Date().toISOString();
346
480
  cursors.hourly = hourlyState;
347
481
  if (projectState) {
@@ -359,8 +493,23 @@ async function filterColdCodexRolloutFiles({
359
493
  auditDue = false,
360
494
  nowMs = Date.now(),
361
495
  recentDays = DEFAULT_CODEX_COLD_SKIP_RECENT_DAYS,
496
+ diagnostics = null,
362
497
  } = {}) {
363
498
  const files = Array.isArray(rolloutFiles) ? rolloutFiles : [];
499
+ const syncDiagnostics = diagnostics && typeof diagnostics === "object" ? diagnostics : null;
500
+ const isCodexEntry = (entry) => (
501
+ typeof entry === "string" || (normalizeSourceInput(entry?.source) || DEFAULT_SOURCE) === DEFAULT_SOURCE
502
+ );
503
+ if (syncDiagnostics) {
504
+ const discoveredRollouts = files.reduce(
505
+ (count, entry) => count + (isCodexEntry(entry) ? 1 : 0),
506
+ 0,
507
+ );
508
+ syncDiagnostics.discovered_rollouts = discoveredRollouts;
509
+ syncDiagnostics.cursor_keys = Object.keys(cursors?.files || {}).length;
510
+ syncDiagnostics.cold_skipped = 0;
511
+ syncDiagnostics.parse_candidates = discoveredRollouts;
512
+ }
364
513
  if (auditDue || !cursors?.files || typeof cursors.files !== "object") {
365
514
  return { rolloutFiles: files, skipped: 0 };
366
515
  }
@@ -414,6 +563,13 @@ async function filterColdCodexRolloutFiles({
414
563
  skipped += 1;
415
564
  }
416
565
 
566
+ if (syncDiagnostics) {
567
+ syncDiagnostics.cold_skipped = skipped;
568
+ syncDiagnostics.parse_candidates = out.reduce(
569
+ (count, entry) => count + (isCodexEntry(entry) ? 1 : 0),
570
+ 0,
571
+ );
572
+ }
417
573
  return { rolloutFiles: out, skipped };
418
574
  }
419
575
 
@@ -1514,6 +1670,16 @@ async function parseRolloutFile({
1514
1670
  let currentCwd = null;
1515
1671
  let currentDate = null;
1516
1672
  let isForkedRollout = false;
1673
+ // The replay prefix only ever exists at the head of a freshly-forked file. A
1674
+ // resumed scan (startOffset > 0) can never see it — and never re-reads the
1675
+ // session_meta line, so isForkedRollout stays false there anyway — but gate
1676
+ // on the offset explicitly so correctness does not hinge on that distant
1677
+ // invariant. Known accepted limitation: if a sync's read races the fork's
1678
+ // single-flush write and persists a cursor INSIDE the burst, the resumed
1679
+ // scan counts the remaining burst tail (bounded over-count, ms-wide window;
1680
+ // self-heals on any inode-changing rescan and never drops genuine usage).
1681
+ let replayPrefixActive = startOffset === 0;
1682
+ let prevForkedTokenMs = null;
1517
1683
  const rolloutDate = rolloutDateFromPath(filePath);
1518
1684
  let currentProjectRef = projectRef || null;
1519
1685
  let currentProjectKey = projectKey || null;
@@ -1591,8 +1757,50 @@ async function parseRolloutFile({
1591
1757
  totals = totalUsage;
1592
1758
  }
1593
1759
 
1594
- // date matching is conservative; same-day fork replays are still counted.
1595
- if (isForkedReplayToken({ isForkedRollout, rolloutDate, currentDate })) continue;
1760
+ // Forked Codex rollouts replay the parent session's entire token history
1761
+ // into the child file the moment the fork is created. The date guard below
1762
+ // (current_date < rolloutDate) catches cross-day forks, but same-day forks
1763
+ // share the parent's current_date and slip through it. The replay is written
1764
+ // in a single flush, so its rows carry near-identical timestamps (sub-ms to a
1765
+ // few ms apart across observed 0.129–0.137 samples), while the first genuine
1766
+ // live turn lands seconds later (≥11s in every sampled fork). We therefore
1767
+ // skip the *leading* run of densely-spaced token_count rows and latch off
1768
+ // permanently at the first multi-second gap: a lone fast turn (always
1769
+ // preceded by a large gap) is never dropped, and an arbitrarily large replay
1770
+ // is fully skipped regardless of length. The threshold sits far above the
1771
+ // flush spacing and well below genuine turn cadence (≥~4.6s observed). The
1772
+ // first replayed row cannot be identified without lookahead, so it is still
1773
+ // counted (a bounded, <1% residual over-count); dropping real usage is the
1774
+ // worse failure, so we bias against it. `totals` is already advanced above,
1775
+ // keeping the cumulative-delta baseline correct for the live turns we keep.
1776
+ // Scoped to forked codex rollouts. (issue #169 follow-up.)
1777
+ let forkedReplaySkip = false;
1778
+ if (isForkedRollout && source === DEFAULT_SOURCE && replayPrefixActive) {
1779
+ const tokenMs = Date.parse(tokenTimestamp);
1780
+ // Fail open on anything the burst heuristic was not measured against:
1781
+ // an unparseable timestamp or a backwards clock step permanently ends
1782
+ // the prefix, so no later row can be skipped off a stale baseline.
1783
+ // Replay flushes are monotonic in every sampled rollout (2,116 gaps,
1784
+ // min 0ms), so this never fires on genuine replays.
1785
+ if (!Number.isFinite(tokenMs) || (prevForkedTokenMs !== null && tokenMs < prevForkedTokenMs)) {
1786
+ replayPrefixActive = false;
1787
+ } else {
1788
+ if (prevForkedTokenMs !== null && tokenMs - prevForkedTokenMs >= CODEX_FORK_REPLAY_GAP_MS) {
1789
+ replayPrefixActive = false;
1790
+ }
1791
+ forkedReplaySkip =
1792
+ replayPrefixActive &&
1793
+ prevForkedTokenMs !== null &&
1794
+ tokenMs - prevForkedTokenMs < CODEX_FORK_REPLAY_GAP_MS;
1795
+ prevForkedTokenMs = tokenMs;
1796
+ }
1797
+ }
1798
+
1799
+ // date matching is conservative; same-day fork replays are caught by the
1800
+ // burst detector above rather than this cross-day date guard.
1801
+ if (forkedReplaySkip || isForkedReplayToken({ isForkedRollout, rolloutDate, currentDate })) {
1802
+ continue;
1803
+ }
1596
1804
 
1597
1805
  const bucketStart = toUtcHalfHourStart(tokenTimestamp);
1598
1806
  if (!bucketStart) continue;
@@ -1613,10 +1821,13 @@ async function parseRolloutFile({
1613
1821
  // rewrite) manages Codex. Other rollout-format sources (e.g. every-code) have
1614
1822
  // their own model re-alignment that legitimately re-reads prior events, which
1615
1823
  // this dedup would otherwise suppress.
1616
- if (seenCodexEvents && source === "codex") {
1824
+ const codexEvents = typeof seenCodexEvents === "function"
1825
+ ? seenCodexEvents()
1826
+ : seenCodexEvents;
1827
+ if (codexEvents && source === "codex") {
1617
1828
  const dedupKey = `${sessionId || filePath}:${tokenTimestamp}`;
1618
- if (seenCodexEvents.has(dedupKey)) continue;
1619
- seenCodexEvents.add(dedupKey);
1829
+ if (codexEvents.has(dedupKey)) continue;
1830
+ codexEvents.add(dedupKey);
1620
1831
  }
1621
1832
 
1622
1833
  const bucket = getHourlyBucket(hourlyState, source, model, bucketStart);
@@ -2694,6 +2905,15 @@ function normalizeIsoDate(value) {
2694
2905
  return match ? match[1] : null;
2695
2906
  }
2696
2907
 
2908
+ // Max inter-row gap (ms) still treated as part of a forked rollout's replay
2909
+ // burst. Empirically the replay flush spaces rows sub-ms to a few ms apart while
2910
+ // genuine live turns arrive ≥~4.6s apart and the replay→live break is ≥11s, so
2911
+ // any value in the ~0.25–2s band separates them with a wide margin; 500ms biases
2912
+ // toward the low end because merging a real fast turn (under-count) is worse than
2913
+ // leaving a slow replay counted (bounded over-count). See the skip site in
2914
+ // parseRolloutFile. (issue #169 follow-up.)
2915
+ const CODEX_FORK_REPLAY_GAP_MS = 500;
2916
+
2697
2917
  function isForkedReplayToken({ isForkedRollout, rolloutDate, currentDate }) {
2698
2918
  return Boolean(isForkedRollout && rolloutDate && currentDate && currentDate < rolloutDate);
2699
2919
  }
@@ -2823,6 +2823,7 @@ async function fetchUsageLimitsUncached({
2823
2823
  requestFn,
2824
2824
  now = new Date(),
2825
2825
  providerTimeoutMs = DEFAULT_PROVIDER_TIMEOUT_MS,
2826
+ forceRefresh = false,
2826
2827
  } = {}) {
2827
2828
  const nowMs = Date.now();
2828
2829
 
@@ -2870,11 +2871,14 @@ async function fetchUsageLimitsUncached({
2870
2871
  // Skip the upstream Claude call entirely while a 429 cooldown is active — calling again
2871
2872
  // just renews the penalty. The result handling below serves cache or a cooldown message.
2872
2873
  const claudeRetryAtMs = claudeToken ? readClaudeRateLimitRetryAtMs({ home, nowMs }) : null;
2873
- // Also avoid cross-process hammering after a recent successful read. The macOS app can
2874
- // restart its embedded Node server or force-refresh the limits page, both of which clear
2875
- // the in-memory cache; the disk cache keeps those paths from immediately spending
2876
- // another Claude OAuth usage request.
2877
- const freshClaudeCache = claudeToken ? readFreshClaudeLimitsCache({ home, nowMs }) : null;
2874
+ // Also avoid cross-process hammering after a recent successful read: embedded-server
2875
+ // restarts and background polls read the disk cache instead of spending another Claude
2876
+ // OAuth usage request. An explicit user refresh (refresh=1 forceRefresh) punches
2877
+ // through this cache but never through the 429 cooldown above, which is exactly the
2878
+ // hammering the cooldown exists to prevent.
2879
+ const freshClaudeCache = claudeToken && !forceRefresh
2880
+ ? readFreshClaudeLimitsCache({ home, nowMs })
2881
+ : null;
2878
2882
 
2879
2883
  const providerFetch = withFetchTimeout(fetchImpl, providerTimeoutMs);
2880
2884
  const [claudeResult, codexResult, cursor, kimi, gemini, kiro, antigravity, copilot, grok, zcode, opencodeGo] = await Promise.all([
@@ -1 +0,0 @@
1
- import{r as n,bb as a}from"./main-B9m79io-.js";import{u as p}from"./DialogTitle-f7gNmCca.js";import{u as c}from"./useScrollLock-DijV6DYS.js";const x=n.forwardRef(function(e,r){const{render:l,className:u,style:m,id:s,...t}=e,{store:i}=p(),o=c(s);return i.useSyncedValueWithCleanup("descriptionElementId",o),a("p",e,{ref:r,props:[{id:o},t]})});export{x as D};
@@ -1 +0,0 @@
1
- import{c}from"./main-B9m79io-.js";const e=[["path",{d:"M20 6 9 17l-5-5",key:"1gmf2c"}]],t=c("check",e);export{t as C};
@@ -1 +0,0 @@
1
- import{r as e,b3 as r,ai as a,b4 as n,b5 as s}from"./main-B9m79io-.js";const T=Object.freeze({mode:a.COMPACT,setMode:()=>{},formatTokens:(o,t={})=>s(o,t),formatTokensTooltip:(o,t={})=>n(o,t)});function f(){return e.useContext(r)??T}export{f as u};