tokentracker-cli 1.5.14 → 1.5.16

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 (119) hide show
  1. package/dashboard/dist/assets/{AchievementBadge-DqJWve1h.js → AchievementBadge-CnnJGct8.js} +1 -1
  2. package/dashboard/dist/assets/{AchievementsPage-CnuBayj3.js → AchievementsPage-CFkUJVxy.js} +2 -2
  3. package/dashboard/dist/assets/{AchievementsSection-WRU5TxAB.js → AchievementsSection-CoLQX04M.js} +1 -1
  4. package/dashboard/dist/assets/{BadgeDetailModal-B5ikEQik.js → BadgeDetailModal-s8rObEHY.js} +1 -1
  5. package/dashboard/dist/assets/{BotAnimated-CaMG6j-6.js → BotAnimated-BmBnL-i8.js} +1 -1
  6. package/dashboard/dist/assets/{Card-BL3vxENk.js → Card-BIqie5mN.js} +1 -1
  7. package/dashboard/dist/assets/{CloudCheckoutPage-CsUb8CJ7.js → CloudCheckoutPage-B_c8Q58A.js} +1 -1
  8. package/dashboard/dist/assets/{CloudPage-5ZiPFzeC.js → CloudPage-CfW_pDf2.js} +2 -2
  9. package/dashboard/dist/assets/{CommandPalette-CYd9nXW-.js → CommandPalette-BRryGPVJ.js} +1 -1
  10. package/dashboard/dist/assets/{CommunityStatsModal-BItox7Sz.js → CommunityStatsModal-Cyh1Q9Fw.js} +1 -1
  11. package/dashboard/dist/assets/{ConfirmModal-BE1ABR4z.js → ConfirmModal-DWBYqpLJ.js} +1 -1
  12. package/dashboard/dist/assets/{Controls-BRqIsaHT.js → Controls-CPSiCzVW.js} +1 -1
  13. package/dashboard/dist/assets/{DashboardPage-Cm0YuBlx.js → DashboardPage-JcS1xwUm.js} +1 -1
  14. package/dashboard/dist/assets/{DateRangePopover-CRwEu66e.js → DateRangePopover-DjuS6888.js} +1 -1
  15. package/dashboard/dist/assets/{DevicePage-DFUPsGb_.js → DevicePage-DZ3qiDbT.js} +1 -1
  16. package/dashboard/dist/assets/{DialogClose-DALl4GmP.js → DialogClose-CfORRnoT.js} +1 -1
  17. package/dashboard/dist/assets/DialogDescription-ChihKLnm.js +1 -0
  18. package/dashboard/dist/assets/{DialogTitle-BSR8ozyn.js → DialogTitle-DZO6-sTI.js} +1 -1
  19. package/dashboard/dist/assets/{ElectricLogo-BtrycBUW.js → ElectricLogo-BzR7mRin.js} +1 -1
  20. package/dashboard/dist/assets/{FadeIn-CfhgvLVd.js → FadeIn-BxHaUvz_.js} +1 -1
  21. package/dashboard/dist/assets/{HeaderGithubStar-N8wqAdOf.js → HeaderGithubStar-KGzKAmuQ.js} +1 -1
  22. package/dashboard/dist/assets/{HoverTooltip-CwdMYIzZ.js → HoverTooltip-C6pASVDN.js} +1 -1
  23. package/dashboard/dist/assets/{IpCheckPage-DCRor9cb.js → IpCheckPage-BJ8TROZC.js} +1 -1
  24. package/dashboard/dist/assets/{LandingPage-s9xvTHSp.js → LandingPage-B15GRFpI.js} +2 -2
  25. package/dashboard/dist/assets/{LeaderboardPage-CUj3WLMo.js → LeaderboardPage-C3hJ2Gh3.js} +2 -2
  26. package/dashboard/dist/assets/{LeaderboardProBadge-N4NOKZWj.js → LeaderboardProBadge-CRfBTxpt.js} +1 -1
  27. package/dashboard/dist/assets/{LeaderboardProfileModal-09HiUyxm.js → LeaderboardProfileModal-DTGmcjWF.js} +3 -3
  28. package/dashboard/dist/assets/{LeaderboardProfilePage-Cw9_rodG.js → LeaderboardProfilePage-BD04D7ML.js} +1 -1
  29. package/dashboard/dist/assets/{LimitsPage-F8b6kZa0.js → LimitsPage-LsDCIUKd.js} +1 -1
  30. package/dashboard/dist/assets/{LocalOnlyNotice-D1eEzDEv.js → LocalOnlyNotice-B3XQKD8k.js} +1 -1
  31. package/dashboard/dist/assets/{LoginCard-0sIovG_J.js → LoginCard-DF1XcskU.js} +1 -1
  32. package/dashboard/dist/assets/{LoginModal-BT3oMVak.js → LoginModal-DuyVT0Tt.js} +1 -1
  33. package/dashboard/dist/assets/{LoginPage-CDsXdaE5.js → LoginPage-kge4kJKa.js} +1 -1
  34. package/dashboard/dist/assets/{PetPage-BEnC1azp.js → PetPage-DuYtsKza.js} +2 -2
  35. package/dashboard/dist/assets/{PopoverPopup-Dwzp-hyf.js → PopoverPopup-BkNNS3VG.js} +1 -1
  36. package/dashboard/dist/assets/{ProviderIcon-Cu_lcuuu.js → ProviderIcon-z2TKXPlS.js} +1 -1
  37. package/dashboard/dist/assets/{ResetPasswordPage-DrpEscm1.js → ResetPasswordPage-CXM9E6d1.js} +1 -1
  38. package/dashboard/dist/assets/{Select-wIR4qvEI.js → Select-CeoAayzi.js} +1 -1
  39. package/dashboard/dist/assets/{SelectItemText-CNuJgYps.js → SelectItemText-CRxvAgUV.js} +1 -1
  40. package/dashboard/dist/assets/{SelfHostPage-CWNLygpL.js → SelfHostPage-Cavstn-d.js} +1 -1
  41. package/dashboard/dist/assets/{ServiceStatusPage-BId9t16Q.js → ServiceStatusPage-CdIeM_DX.js} +1 -1
  42. package/dashboard/dist/assets/{SessionsPage-C_klx7d8.js → SessionsPage-DPihAS26.js} +1 -1
  43. package/dashboard/dist/assets/{SettingsPage-nC73JaZ4.js → SettingsPage-zxOsLeE5.js} +3 -3
  44. package/dashboard/dist/assets/{SkillsPage-B2agou8Y.js → SkillsPage-BRGonzsC.js} +1 -1
  45. package/dashboard/dist/assets/{TokenGalaxy-BNXUWRwa.js → TokenGalaxy-D9-huQJ_.js} +1 -1
  46. package/dashboard/dist/assets/{TooltipPopup--X2kExQ0.js → TooltipPopup-BS99Tf44.js} +1 -1
  47. package/dashboard/dist/assets/{TrendMonitor-BELHsTnz.js → TrendMonitor-DlNT4ObY.js} +1 -1
  48. package/dashboard/dist/assets/{WidgetsPage-DWp0SMXA.js → WidgetsPage-DTFnmTwq.js} +1 -1
  49. package/dashboard/dist/assets/{WrappedPage-DgwczKHe.js → WrappedPage-DwMdnusQ.js} +1 -1
  50. package/dashboard/dist/assets/{__vite-browser-external-BHphZRSC.js → __vite-browser-external-DR_a-OTS.js} +2 -2
  51. package/dashboard/dist/assets/{agent-logos-Cfx5WzU5.js → agent-logos-D6kTooyz.js} +1 -1
  52. package/dashboard/dist/assets/{arrow-left-D83SdANw.js → arrow-left-jpuJhYL4.js} +1 -1
  53. package/dashboard/dist/assets/{arrow-right-D7ye9bmJ.js → arrow-right-n1hLkwiK.js} +1 -1
  54. package/dashboard/dist/assets/{arrow-up-right-CLmwEwwC.js → arrow-up-right-fZmEfh7R.js} +1 -1
  55. package/dashboard/dist/assets/{calendar-days-D1qLlvh4.js → calendar-days-B0LdECvD.js} +1 -1
  56. package/dashboard/dist/assets/{check-BHJvi4ey.js → check-CcnNd8bi.js} +1 -1
  57. package/dashboard/dist/assets/{chevron-down-6RBPKNpt.js → chevron-down-DhbHBjVC.js} +1 -1
  58. package/dashboard/dist/assets/{cloud-checkout-DgWcF-at.js → cloud-checkout-9XOoux-p.js} +1 -1
  59. package/dashboard/dist/assets/{composite-D-0pfyMY.js → composite-DIDAkYhy.js} +1 -1
  60. package/dashboard/dist/assets/{copy-B6mJSQsD.js → copy-BNDBunFG.js} +1 -1
  61. package/dashboard/dist/assets/{download-D31Nn-bI.js → download-wlqdqJmq.js} +1 -1
  62. package/dashboard/dist/assets/{external-link-C_lK9S3p.js → external-link-BvssZ_45.js} +1 -1
  63. package/dashboard/dist/assets/{flame-1ZxhHshT.js → flame-DP8IUT3E.js} +1 -1
  64. package/dashboard/dist/assets/{format-tokens-DJk25SkD.js → format-tokens-DMsx_fWk.js} +1 -1
  65. package/dashboard/dist/assets/{gift-BUZzFh-1.js → gift-zF4D6O4a.js} +1 -1
  66. package/dashboard/dist/assets/{icons-BVPThCa4.js → icons-Batw5aOg.js} +1 -1
  67. package/dashboard/dist/assets/{index-B-mxrdIz.js → index-3qKpTOzz.js} +1 -1
  68. package/dashboard/dist/assets/{index-CjPsyTl7.js → index-Cqla2Zq5.js} +1 -1
  69. package/dashboard/dist/assets/{index-C0zFsOWh.js → index-Dc8PQ8ZR.js} +1 -1
  70. package/dashboard/dist/assets/{index-CzstoU6d.js → index-dCfimErX.js} +1 -1
  71. package/dashboard/dist/assets/{info-DmU-vlJ6.js → info-DcUyOPKG.js} +1 -1
  72. package/dashboard/dist/assets/{link-2-BlBM7uw7.js → link-2-CS-cWLmx.js} +1 -1
  73. package/dashboard/dist/assets/{loader-circle-Dhrb_Ocy.js → loader-circle-DKc-gwgL.js} +1 -1
  74. package/dashboard/dist/assets/{local-usage-transfer-BudmMtkd.js → local-usage-transfer-bHATORmq.js} +1 -1
  75. package/dashboard/dist/assets/{modifiers.esm-kmw1VM8W.js → modifiers.esm-B4QifJzx.js} +1 -1
  76. package/dashboard/dist/assets/{monitor-smartphone-DtjXNC_x.js → monitor-smartphone-D8vpN9Jv.js} +1 -1
  77. package/dashboard/dist/assets/{play-9w46lbjy.js → play-D70P7DVT.js} +1 -1
  78. package/dashboard/dist/assets/{plus-DAf_WGKT.js → plus-YomnSdqY.js} +1 -1
  79. package/dashboard/dist/assets/{provider-display-BZ0XlB2q.js → provider-display-VXu7Hzwq.js} +1 -1
  80. package/dashboard/dist/assets/{react-B01ZJuZY.js → react-CUO9Gs2G.js} +1 -1
  81. package/dashboard/dist/assets/{safePolygon-B6_x12Gb.js → safePolygon-CbVKM3Lv.js} +1 -1
  82. package/dashboard/dist/assets/{search-D3dW9GbV.js → search-BUi1hAmV.js} +1 -1
  83. package/dashboard/dist/assets/{server-C2XZBar7.js → server-B-agbJYI.js} +1 -1
  84. package/dashboard/dist/assets/{shield-check-4FX5vbS7.js → shield-check-yHx3fQDo.js} +1 -1
  85. package/dashboard/dist/assets/{skills-api-BvW_MUyg.js → skills-api-DojEO2dS.js} +1 -1
  86. package/dashboard/dist/assets/{sortable.esm-CAa_BJgh.js → sortable.esm-r4Lz4ugj.js} +1 -1
  87. package/dashboard/dist/assets/{store-2AX2MNkz.js → store-GzQbKyT-.js} +1 -1
  88. package/dashboard/dist/assets/{terminal-CON7n_ir.js → terminal-BLCZgZTm.js} +1 -1
  89. package/dashboard/dist/assets/{trash-C2f88jSi.js → trash-BFGtCBJS.js} +1 -1
  90. package/dashboard/dist/assets/{use-community-stats-CgQu43xV.js → use-community-stats-DGdLAa8L.js} +1 -1
  91. package/dashboard/dist/assets/{use-latest-request-guard-lShM46-3.js → use-latest-request-guard-l39bM4s0.js} +1 -1
  92. package/dashboard/dist/assets/{use-limits-display-prefs-8o7pbEg0.js → use-limits-display-prefs-JcWgMNA0.js} +1 -1
  93. package/dashboard/dist/assets/{use-native-settings-dN8shG3M.js → use-native-settings-0sXFw6mq.js} +1 -1
  94. package/dashboard/dist/assets/{use-ordered-list-beJy2IqQ.js → use-ordered-list-mRdyZ18r.js} +1 -1
  95. package/dashboard/dist/assets/use-reduced-motion-D2g3iGbi.js +1 -0
  96. package/dashboard/dist/assets/{use-transform-cJpAQr2d.js → use-transform-CbJqMa_Y.js} +1 -1
  97. package/dashboard/dist/assets/{use-usage-limits-CuusZA6b.js → use-usage-limits-qQKS4sHR.js} +1 -1
  98. package/dashboard/dist/assets/{useCurrency-Cx9UsKJK.js → useCurrency-BJyZCVtd.js} +1 -1
  99. package/dashboard/dist/assets/{usePositioner-TEClK6mF.js → usePositioner-zsiJqVGU.js} +1 -1
  100. package/dashboard/dist/assets/{useTokenFormat-BNwTLiac.js → useTokenFormat-DxEKSp5H.js} +1 -1
  101. package/dashboard/dist/assets/{zap-CFgbVEhC.js → zap-D40-8VPk.js} +1 -1
  102. package/dashboard/dist/index.html +1 -1
  103. package/dashboard/dist/ip-check.html +1 -1
  104. package/dashboard/dist/leaderboard.html +1 -1
  105. package/dashboard/dist/share.html +1 -1
  106. package/package.json +1 -1
  107. package/src/lib/ark-agent-plan-limits.js +26 -55
  108. package/src/lib/ark-coding-plan-limits.js +31 -62
  109. package/src/lib/cloud-account.js +29 -3
  110. package/src/lib/limits-cache.js +475 -0
  111. package/src/lib/local-api.js +92 -5
  112. package/src/lib/pricing/curated-overrides.json +32 -0
  113. package/src/lib/pricing/matcher.js +4 -1
  114. package/src/lib/pricing/seed-snapshot.json +1 -1
  115. package/src/lib/qoder-limits.js +35 -69
  116. package/src/lib/rollout.js +43 -47
  117. package/src/lib/usage-limits.js +370 -1063
  118. package/dashboard/dist/assets/DialogDescription-CO0hykis.js +0 -1
  119. package/dashboard/dist/assets/use-reduced-motion-S0bR_G1B.js +0 -1
@@ -6,6 +6,7 @@ const net = require("node:net");
6
6
  const os = require("node:os");
7
7
  const path = require("node:path");
8
8
  const { fetchQoderCnCliLimits } = require("./qoder-cn-cli-limits");
9
+ const { createLimitsCache } = require("./limits-cache");
9
10
 
10
11
  const QODER_SITES = {
11
12
  international: {
@@ -406,18 +407,40 @@ function writeQoderActivityCache(window, {
406
407
  } catch (_error) {}
407
408
  }
408
409
 
409
- function qoderLimitsCachePath({ home = os.homedir(), namespace } = {}) {
410
- const file = namespace
410
+ // The active region partitions the cache by filename (cn vs global); the
411
+ // record itself stays account-agnostic. A window without a parseable reset is
412
+ // kept only within the 12h unknown-reset TTL, and the global max-age only
413
+ // bounds snapshots with no future-dated window — a dated survivor outlives it.
414
+ // Provenance must describe the data's age, not the transport (#955): a fresh
415
+ // hit re-serves the winning source it was written with, an older hit honestly
416
+ // reports the disk cache.
417
+ const qoderLimitsCache = createLimitsCache({
418
+ file: ({ namespace }) => (namespace
411
419
  ? `qoder-${namespace}-usage-limits-cache.json`
412
- : QODER_LIMITS_CACHE_FILE;
413
- return path.join(home, ".tokentracker", "tracker", file);
414
- }
420
+ : QODER_LIMITS_CACHE_FILE),
421
+ key: "qoder",
422
+ maxAgeMs: QODER_LIMITS_CACHE_MAX_AGE_MS,
423
+ maxAgeGate: "ifNoDatedSurvivor",
424
+ unknownResetTtlMs: QODER_LIMITS_CACHE_UNKNOWN_RESET_TTL_MS,
425
+ fields: {
426
+ plan_label: "string",
427
+ quota_exceeded: "boolTrue",
428
+ // Remember the winning source so a fresh re-serve keeps honest provenance
429
+ // instead of always claiming "disk-cache" (#955).
430
+ source: { write: (record) => (typeof record?.source === "string" ? record.source : null) },
431
+ },
432
+ windowKeys: ["primary_window", "secondary_window", "tertiary_window"],
433
+ // tertiary_window is written only when present and read back only when it
434
+ // survives — primary/secondary always appear (null when dead).
435
+ sparseWindowKeys: ["tertiary_window"],
436
+ stale: "byAge",
437
+ freshTtlMs: QODER_LIMITS_CACHE_FRESH_TTL_MS,
438
+ cachedFlag: true,
439
+ source: "provenance",
440
+ });
415
441
 
416
- function qoderCachedWindow(window, { cachedAtMs, nowMs } = {}) {
417
- if (!window || typeof window !== "object") return null;
418
- const resetAtMs = Date.parse(window.reset_at || "");
419
- if (Number.isFinite(resetAtMs)) return resetAtMs > nowMs ? window : null;
420
- return nowMs - cachedAtMs <= QODER_LIMITS_CACHE_UNKNOWN_RESET_TTL_MS ? window : null;
442
+ function qoderLimitsCachePath({ home = os.homedir(), namespace } = {}) {
443
+ return qoderLimitsCache.path({ home, namespace });
421
444
  }
422
445
 
423
446
  function readQoderLimitsCache({
@@ -425,44 +448,7 @@ function readQoderLimitsCache({
425
448
  nowMs = Date.now(),
426
449
  namespace,
427
450
  } = {}) {
428
- try {
429
- const raw = JSON.parse(fs.readFileSync(qoderLimitsCachePath({ home, namespace }), "utf8"))?.qoder;
430
- const cachedAtMs = Date.parse(raw?.cached_at || "");
431
- if (!Number.isFinite(cachedAtMs) || cachedAtMs > nowMs + 60_000) return null;
432
- const primaryWindow = qoderCachedWindow(raw.primary_window, { cachedAtMs, nowMs });
433
- const secondaryWindow = qoderCachedWindow(raw.secondary_window, { cachedAtMs, nowMs });
434
- const tertiaryWindow = qoderCachedWindow(raw.tertiary_window, { cachedAtMs, nowMs });
435
- if (!primaryWindow && !secondaryWindow && !tertiaryWindow) return null;
436
- const hasUnexpiredDatedWindow = [primaryWindow, secondaryWindow, tertiaryWindow]
437
- .some((window) => Number.isFinite(Date.parse(window?.reset_at || "")));
438
- if (nowMs - cachedAtMs > QODER_LIMITS_CACHE_MAX_AGE_MS && !hasUnexpiredDatedWindow) {
439
- return null;
440
- }
441
- // Provenance must describe the data's age, not the transport (#955): a
442
- // successful refresh writes this cache and immediately floors later log
443
- // scans, so the next refresh re-serves the same payload. Marking every
444
- // hit stale/disk-cache made the Limits page permanently partial.
445
- const ageMs = nowMs - cachedAtMs;
446
- const fresh = ageMs <= QODER_LIMITS_CACHE_FRESH_TTL_MS;
447
- const writtenSource = typeof raw.source === "string" && raw.source.trim()
448
- ? raw.source.trim()
449
- : null;
450
- return {
451
- configured: true,
452
- error: null,
453
- plan_label: typeof raw.plan_label === "string" ? raw.plan_label : null,
454
- quota_exceeded: raw.quota_exceeded === true,
455
- primary_window: primaryWindow,
456
- secondary_window: secondaryWindow,
457
- ...(tertiaryWindow ? { tertiary_window: tertiaryWindow } : {}),
458
- cached: true,
459
- stale: !fresh,
460
- cached_at: raw.cached_at,
461
- source: fresh && writtenSource ? writtenSource : "disk-cache",
462
- };
463
- } catch (_error) {
464
- return null;
465
- }
451
+ return qoderLimitsCache.read({ home, nowMs, namespace });
466
452
  }
467
453
 
468
454
  function writeQoderLimitsCache(limits, {
@@ -470,27 +456,7 @@ function writeQoderLimitsCache(limits, {
470
456
  nowMs = Date.now(),
471
457
  namespace,
472
458
  } = {}) {
473
- if (!limits?.configured || limits.error || (!limits.primary_window && !limits.secondary_window && !limits.tertiary_window)) return;
474
- const cachePath = qoderLimitsCachePath({ home, namespace });
475
- const payload = {
476
- qoder: {
477
- plan_label: limits.plan_label || null,
478
- quota_exceeded: limits.quota_exceeded === true,
479
- primary_window: limits.primary_window || null,
480
- secondary_window: limits.secondary_window || null,
481
- ...(limits.tertiary_window ? { tertiary_window: limits.tertiary_window } : {}),
482
- // Remember the winning source so a fresh re-serve keeps honest
483
- // provenance instead of always claiming "disk-cache" (#955).
484
- source: typeof limits.source === "string" ? limits.source : null,
485
- cached_at: new Date(nowMs).toISOString(),
486
- },
487
- };
488
- try {
489
- fs.mkdirSync(path.dirname(cachePath), { recursive: true });
490
- const tmpPath = `${cachePath}.${process.pid}.tmp`;
491
- fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 0o600 });
492
- fs.renameSync(tmpPath, cachePath);
493
- } catch (_error) {}
459
+ qoderLimitsCache.write(limits, { home, nowMs, namespace });
494
460
  }
495
461
 
496
462
  function buildQoderActivityHeaders(authStatus, {
@@ -11641,26 +11641,14 @@ async function parseWorkbuddyIncremental({
11641
11641
  const startOffset = stat.size < prevSize || inodeChanged ? 0 : prevSize;
11642
11642
  if (stat.size <= startOffset) continue;
11643
11643
 
11644
- let stream;
11645
- try {
11646
- stream = fssync.createReadStream(filePath, {
11647
- encoding: "utf8",
11648
- start: startOffset,
11649
- });
11650
- } catch { continue; }
11651
- const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
11652
-
11653
- for await (const line of rl) {
11654
- if (!line || !line.trim()) continue;
11655
- let entry;
11656
- try { entry = JSON.parse(line); } catch { continue; }
11657
- if (!entry || typeof entry !== "object") continue;
11644
+ const committedEndOffset = await scanOmpLikeTranscript(filePath, startOffset, OMP_LIKE_MAX_RECORD_BYTES, (entry) => {
11645
+ if (!entry || typeof entry !== "object") return;
11658
11646
 
11659
11647
  // Usage is carried on ANY record with providerData.rawUsage — assistant
11660
11648
  // messages AND function_call records. Aggregate them all; dedup per id.
11661
11649
  const provider = entry.providerData;
11662
11650
  const rawUsage = provider && typeof provider === "object" ? provider.rawUsage : null;
11663
- if (!rawUsage || typeof rawUsage !== "object") continue;
11651
+ if (!rawUsage || typeof rawUsage !== "object") return;
11664
11652
 
11665
11653
  const sessionId =
11666
11654
  typeof entry.sessionId === "string" && entry.sessionId
@@ -11668,10 +11656,10 @@ async function parseWorkbuddyIncremental({
11668
11656
  : path.basename(filePath, ".jsonl");
11669
11657
  const sqliteSession = sqliteSessions[sessionId];
11670
11658
  if (sqliteSession?.detailed || tracedSessionIds.has(sessionId)) {
11671
- continue;
11659
+ return;
11672
11660
  }
11673
11661
  const tsMs =
11674
- Number.isFinite(Number(entry.timestamp)) && Number(entry.timestamp) > 0
11662
+ Number.isFinite(new Date(Number(entry.timestamp)).getTime()) && Number(entry.timestamp) > 0
11675
11663
  ? Number(entry.timestamp)
11676
11664
  : null;
11677
11665
  // One usage record per LLM round-trip; providerData.messageId is the
@@ -11688,24 +11676,24 @@ async function parseWorkbuddyIncremental({
11688
11676
  : tsMs != null
11689
11677
  ? `${sessionId}:${tsMs}`
11690
11678
  : null;
11691
- if (!messageId) continue;
11692
- if (seenIds.has(messageId)) continue;
11679
+ if (!messageId) return;
11680
+ if (seenIds.has(messageId)) return;
11693
11681
 
11694
11682
  recordsProcessed++;
11695
11683
 
11696
11684
  const delta = workbuddyUsageDelta(rawUsage);
11697
11685
  if (delta.total_tokens === 0) {
11698
11686
  seenIds.add(messageId);
11699
- continue;
11687
+ return;
11700
11688
  }
11701
11689
 
11702
11690
  if (tsMs == null) {
11703
11691
  seenIds.add(messageId);
11704
- continue;
11692
+ return;
11705
11693
  }
11706
11694
  const tsIso = new Date(tsMs).toISOString();
11707
11695
  const bucketStart = toUtcHalfHourStart(tsIso);
11708
- if (!bucketStart) continue;
11696
+ if (!bucketStart) return;
11709
11697
 
11710
11698
  const model =
11711
11699
  normalizeModelInput(provider.model) ||
@@ -11735,14 +11723,14 @@ async function parseWorkbuddyIncremental({
11735
11723
  bucketsQueued: touchedBuckets.size,
11736
11724
  });
11737
11725
  }
11738
- }
11726
+ }, { recoverReadErrors: true });
11739
11727
 
11740
- let postStat = stat;
11741
- try { postStat = fssync.statSync(filePath); } catch {}
11728
+ // As in the project pass, acknowledge only records consumed in full. An
11729
+ // unfinished tail or an append after EOF must remain visible next sync.
11742
11730
  fileOffsets[filePath] = {
11743
- size: postStat.size,
11744
- mtimeMs: postStat.mtimeMs,
11745
- ino: postStat.ino,
11731
+ size: committedEndOffset,
11732
+ mtimeMs: stat.mtimeMs,
11733
+ ino: stat.ino,
11746
11734
  };
11747
11735
  }
11748
11736
 
@@ -11850,7 +11838,7 @@ async function parseWorkbuddyIncremental({
11850
11838
  if (sqliteSession?.detailed || tracedSessionIds.has(sessionId)) return;
11851
11839
 
11852
11840
  const tsMs =
11853
- Number.isFinite(Number(entry.timestamp)) && Number(entry.timestamp) > 0
11841
+ Number.isFinite(new Date(Number(entry.timestamp)).getTime()) && Number(entry.timestamp) > 0
11854
11842
  ? Number(entry.timestamp)
11855
11843
  : null;
11856
11844
  if (tsMs == null) return;
@@ -11890,6 +11878,7 @@ async function parseWorkbuddyIncremental({
11890
11878
  addTotals(projectBucket.totals, delta);
11891
11879
  projectTouchedBuckets.add(projectBucketKey(projectKey, source, bucketStart));
11892
11880
  },
11881
+ { recoverReadErrors: true },
11893
11882
  );
11894
11883
 
11895
11884
  // Commit only past records that were read in full: a record appended
@@ -16891,7 +16880,7 @@ function ompEntryTimestampMs(msg, entry) {
16891
16880
  // records are orders of magnitude smaller.
16892
16881
  const OMP_LIKE_MAX_RECORD_BYTES = 64 * 1024 * 1024;
16893
16882
 
16894
- async function scanOmpLikeTranscript(filePath, startOffset, maxRecordBytes, onEntry) {
16883
+ async function scanOmpLikeTranscript(filePath, startOffset, maxRecordBytes, onEntry, { recoverReadErrors = false } = {}) {
16895
16884
  let stream;
16896
16885
  try {
16897
16886
  stream = fssync.createReadStream(filePath, { start: startOffset });
@@ -16900,26 +16889,33 @@ async function scanOmpLikeTranscript(filePath, startOffset, maxRecordBytes, onEn
16900
16889
  }
16901
16890
  let scannedEndOffset = startOffset;
16902
16891
  let committedEndOffset = startOffset;
16903
- for await (const record of physicalJsonlRecords(stream, {
16904
- invalidUtf8: "record",
16905
- maxRecordBytes,
16906
- })) {
16907
- scannedEndOffset += record.physicalBytes;
16908
- if (record.terminated) {
16909
- committedEndOffset = scannedEndOffset;
16910
- if (record.oversized || !record.utf8Valid || !record.line || !record.line.trim()) continue;
16892
+ try {
16893
+ for await (const record of physicalJsonlRecords(stream, {
16894
+ invalidUtf8: "record",
16895
+ maxRecordBytes,
16896
+ })) {
16897
+ scannedEndOffset += record.physicalBytes;
16898
+ if (record.terminated) {
16899
+ committedEndOffset = scannedEndOffset;
16900
+ if (record.oversized || !record.utf8Valid || !record.line || !record.line.trim()) continue;
16901
+ let entry;
16902
+ try { entry = JSON.parse(record.line); } catch { continue; }
16903
+ onEntry(entry);
16904
+ continue;
16905
+ }
16906
+ // A final incomplete record may still be in the process of being written.
16907
+ if (record.oversized || !record.utf8Valid || !record.line || !record.line.trim()) break;
16911
16908
  let entry;
16912
- try { entry = JSON.parse(record.line); } catch { continue; }
16909
+ try { entry = JSON.parse(record.line); } catch { break; }
16913
16910
  onEntry(entry);
16914
- continue;
16911
+ committedEndOffset = scannedEndOffset;
16912
+ break;
16915
16913
  }
16916
- // A final incomplete record may still be in the process of being written.
16917
- if (record.oversized || !record.utf8Valid || !record.line || !record.line.trim()) break;
16918
- let entry;
16919
- try { entry = JSON.parse(record.line); } catch { break; }
16920
- onEntry(entry);
16921
- committedEndOffset = scannedEndOffset;
16922
- break;
16914
+ } catch (error) {
16915
+ // WorkBuddy can publish complete records and continue with other files or
16916
+ // fallbacks after a read failure. Unfinished bytes are never acknowledged.
16917
+ // Do not swallow callback/parser errors, or change other readers' policy.
16918
+ if (!recoverReadErrors || stream.errored !== error) throw error;
16923
16919
  }
16924
16920
  return committedEndOffset;
16925
16921
  }