tokentracker-cli 1.4.12 → 1.4.13

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-BwrL59nq.js → AchievementBadge-6wnHO-Hp.js} +1 -1
  2. package/dashboard/dist/assets/{AchievementsPage-DG0dvG3d.js → AchievementsPage-BpHA9auG.js} +2 -2
  3. package/dashboard/dist/assets/{AchievementsSection-NrjlZjZK.js → AchievementsSection-ySXWiLC3.js} +1 -1
  4. package/dashboard/dist/assets/{BadgeDetailModal-BjgBAb-0.js → BadgeDetailModal-DuxcntYu.js} +1 -1
  5. package/dashboard/dist/assets/{BotAnimated-D7gMmkaM.js → BotAnimated-Dg1gSql-.js} +1 -1
  6. package/dashboard/dist/assets/{Card-CgvwIS5W.js → Card-73qYdlEv.js} +1 -1
  7. package/dashboard/dist/assets/{CloudCheckoutPage-DKDpT0HI.js → CloudCheckoutPage-C7-9p9zk.js} +1 -1
  8. package/dashboard/dist/assets/{CloudPage-BOfqPAAH.js → CloudPage-CKcDcf6O.js} +2 -2
  9. package/dashboard/dist/assets/{CommandPalette-BfRKy_dn.js → CommandPalette-Cq7TSYUV.js} +1 -1
  10. package/dashboard/dist/assets/{CommunityStatsModal-BcVSszit.js → CommunityStatsModal-C0gj8QaO.js} +1 -1
  11. package/dashboard/dist/assets/{ConfirmModal-Be4EVpPT.js → ConfirmModal-Cuf2JDA9.js} +1 -1
  12. package/dashboard/dist/assets/{Controls-vrG8LLBe.js → Controls-COaS9RLV.js} +1 -1
  13. package/dashboard/dist/assets/DashboardPage-Beg--N9T.js +1 -0
  14. package/dashboard/dist/assets/{DateRangePopover-CZVMdPEq.js → DateRangePopover-rNSRXWkr.js} +1 -1
  15. package/dashboard/dist/assets/{DevicePage-BeNIjTqa.js → DevicePage-jZ9S-taT.js} +1 -1
  16. package/dashboard/dist/assets/{DialogClose-BaNPZ3Bu.js → DialogClose-CEYsCJ6-.js} +1 -1
  17. package/dashboard/dist/assets/DialogDescription-BAaD46Xg.js +1 -0
  18. package/dashboard/dist/assets/{DialogTitle-B3IwNXro.js → DialogTitle-CnaGDa7P.js} +1 -1
  19. package/dashboard/dist/assets/{ElectricLogo-DC-03HL_.js → ElectricLogo-CM0HfX6y.js} +1 -1
  20. package/dashboard/dist/assets/{FadeIn-DhUnSf2V.js → FadeIn-CngXKUqj.js} +1 -1
  21. package/dashboard/dist/assets/{HeaderGithubStar-BiWRLl7V.js → HeaderGithubStar-DQn0rAVr.js} +1 -1
  22. package/dashboard/dist/assets/{HoverTooltip-DLPiBqDu.js → HoverTooltip-By7PDA5D.js} +1 -1
  23. package/dashboard/dist/assets/{IpCheckPage-DKjg0V9L.js → IpCheckPage-BvrRGJNy.js} +1 -1
  24. package/dashboard/dist/assets/{LandingPage-XQuTnBMQ.js → LandingPage-C0CnNfi_.js} +2 -2
  25. package/dashboard/dist/assets/{LeaderboardPage-xAbo5mzo.js → LeaderboardPage-iUXnjGy-.js} +2 -2
  26. package/dashboard/dist/assets/{LeaderboardProBadge-mMiEeaYS.js → LeaderboardProBadge-CrV2bYaH.js} +1 -1
  27. package/dashboard/dist/assets/{LeaderboardProfileModal-B7q0Kk6B.js → LeaderboardProfileModal-iGgVOvl7.js} +3 -3
  28. package/dashboard/dist/assets/{LeaderboardProfilePage-Bru_6wgX.js → LeaderboardProfilePage-DmiiHi5U.js} +1 -1
  29. package/dashboard/dist/assets/LimitsPage-Bpp0LYGk.js +6 -0
  30. package/dashboard/dist/assets/{LocalOnlyNotice-B-lxih4W.js → LocalOnlyNotice-0pQm9-gI.js} +1 -1
  31. package/dashboard/dist/assets/{LoginCard-ChNsRYE2.js → LoginCard-OaDObE8j.js} +1 -1
  32. package/dashboard/dist/assets/{LoginModal-CRw3PgMi.js → LoginModal-C0dRPusa.js} +1 -1
  33. package/dashboard/dist/assets/{LoginPage-CVsbYn-h.js → LoginPage-D6CoX42w.js} +1 -1
  34. package/dashboard/dist/assets/{PetPage-BwEnQLLY.js → PetPage-Ch5t4JT_.js} +2 -2
  35. package/dashboard/dist/assets/{PopoverPopup-CDOtxSL8.js → PopoverPopup-UtHXvmNC.js} +1 -1
  36. package/dashboard/dist/assets/{ProviderIcon-Dsu1p6gG.js → ProviderIcon-CRlMy2AY.js} +1 -1
  37. package/dashboard/dist/assets/{ResetPasswordPage-BzpaSORs.js → ResetPasswordPage-DijAhwkr.js} +1 -1
  38. package/dashboard/dist/assets/{Select-62UH-yLs.js → Select-D1FvgdDV.js} +1 -1
  39. package/dashboard/dist/assets/{SelectItemText-aTexKmQ0.js → SelectItemText-Conq3RQN.js} +1 -1
  40. package/dashboard/dist/assets/{SelfHostPage-CmhV0dms.js → SelfHostPage-CLUgPEFh.js} +1 -1
  41. package/dashboard/dist/assets/{ServiceStatusPage-CLWO33w1.js → ServiceStatusPage-D7KRjgOE.js} +1 -1
  42. package/dashboard/dist/assets/SessionsPage-CWNrbLT4.js +3 -0
  43. package/dashboard/dist/assets/{SettingsPage-DybHncPO.js → SettingsPage-Bh_PuEXU.js} +3 -3
  44. package/dashboard/dist/assets/{SkillsPage-BV5R3VhT.js → SkillsPage-DxZuztPd.js} +1 -1
  45. package/dashboard/dist/assets/{TokenGalaxy-mNNAo6Ai.js → TokenGalaxy-BE-dqlDy.js} +1 -1
  46. package/dashboard/dist/assets/{TooltipPopup-D00SVpLP.js → TooltipPopup-DQrTDsmN.js} +1 -1
  47. package/dashboard/dist/assets/{TrendMonitor-aNIpzAO-.js → TrendMonitor-Cal2kaMh.js} +1 -1
  48. package/dashboard/dist/assets/{WidgetsPage-COjZIMo8.js → WidgetsPage-Cvv2NXRK.js} +1 -1
  49. package/dashboard/dist/assets/{WrappedPage-af-bDIfB.js → WrappedPage-H99MjZkq.js} +1 -1
  50. package/dashboard/dist/assets/__vite-browser-external-Br-KAXh6.css +1 -0
  51. package/dashboard/dist/assets/{__vite-browser-external-KHpR2q-A.js → __vite-browser-external-I1de5Swt.js} +3 -3
  52. package/dashboard/dist/assets/{agent-logos-D9Uq5b6y.js → agent-logos-BUBtw9FF.js} +1 -1
  53. package/dashboard/dist/assets/{arrow-left-Pt53BsA4.js → arrow-left-C99UO2S4.js} +1 -1
  54. package/dashboard/dist/assets/{arrow-right-stBCZPot.js → arrow-right-BMFV-A6L.js} +1 -1
  55. package/dashboard/dist/assets/{arrow-up-right-ltUBOD-o.js → arrow-up-right-CReo0rju.js} +1 -1
  56. package/dashboard/dist/assets/{calendar-days-BMEtkPFp.js → calendar-days-CiEobm13.js} +1 -1
  57. package/dashboard/dist/assets/{check-ax5wQf5Q.js → check-BBOwGLur.js} +1 -1
  58. package/dashboard/dist/assets/{chevron-down-gS62rNQt.js → chevron-down-C08QHnpk.js} +1 -1
  59. package/dashboard/dist/assets/{cloud-checkout-DDtdbz7C.js → cloud-checkout-NOmtAy01.js} +1 -1
  60. package/dashboard/dist/assets/{composite-DVm8byy8.js → composite-C5GvW04X.js} +1 -1
  61. package/dashboard/dist/assets/{copy-VbASSQO1.js → copy-DgY9F9FR.js} +1 -1
  62. package/dashboard/dist/assets/{download-D1g6OCRI.js → download-B7xuZSZ2.js} +1 -1
  63. package/dashboard/dist/assets/{external-link-CGUQrKDL.js → external-link-1nRJjSL1.js} +1 -1
  64. package/dashboard/dist/assets/{flame-Dlvc-qIH.js → flame-CR1qiptA.js} +1 -1
  65. package/dashboard/dist/assets/{format-tokens-CB1QfZsj.js → format-tokens-DNLvupK2.js} +1 -1
  66. package/dashboard/dist/assets/{gift-DydflDeL.js → gift-DUUpsPxb.js} +1 -1
  67. package/dashboard/dist/assets/{icons-B_xYF8_-.js → icons-qY8ZAPUu.js} +1 -1
  68. package/dashboard/dist/assets/{index-ByXXZlPn.js → index-7VLgsnkO.js} +1 -1
  69. package/dashboard/dist/assets/{index-CuLrp2D2.js → index-BvNIpRP0.js} +1 -1
  70. package/dashboard/dist/assets/{index-cidqWI7C.js → index-CfdotR_D.js} +1 -1
  71. package/dashboard/dist/assets/{index-BRNltZbP.js → index-Tl9T3Nm3.js} +1 -1
  72. package/dashboard/dist/assets/{info-D6-LIywq.js → info-DnT3wtaz.js} +1 -1
  73. package/dashboard/dist/assets/{link-2-BbuXAF5y.js → link-2-CjxRD2lW.js} +1 -1
  74. package/dashboard/dist/assets/{loader-circle-DAeCHAyj.js → loader-circle-BYCsfrhr.js} +1 -1
  75. package/dashboard/dist/assets/{local-usage-transfer-BejRwzi4.js → local-usage-transfer-DBfMvvqx.js} +1 -1
  76. package/dashboard/dist/assets/{modifiers.esm-CV2S03sv.js → modifiers.esm-DG_gVjqU.js} +1 -1
  77. package/dashboard/dist/assets/{monitor-smartphone-Dj_u5s6h.js → monitor-smartphone-CJUH6nwc.js} +1 -1
  78. package/dashboard/dist/assets/{play-DrcSicai.js → play-DY4d-C2J.js} +1 -1
  79. package/dashboard/dist/assets/{plus-BwYQW7nf.js → plus-Dydx6cB7.js} +1 -1
  80. package/dashboard/dist/assets/{provider-display-Bgfy-2rn.js → provider-display-DCtG1A8I.js} +1 -1
  81. package/dashboard/dist/assets/{react-BXby2gfP.js → react-D9IQudPE.js} +1 -1
  82. package/dashboard/dist/assets/{safePolygon-B8DY85mD.js → safePolygon-CxfZ_vVn.js} +1 -1
  83. package/dashboard/dist/assets/{search-SCMosATz.js → search-DBso9ppI.js} +1 -1
  84. package/dashboard/dist/assets/{server-DoD96f86.js → server-Bb_KzpvJ.js} +1 -1
  85. package/dashboard/dist/assets/{shield-check-CESJcEp7.js → shield-check-cnqpE0zI.js} +1 -1
  86. package/dashboard/dist/assets/{skills-api-DE71Ylhd.js → skills-api-DMbIT8CJ.js} +1 -1
  87. package/dashboard/dist/assets/{sortable.esm-EgVv2jPt.js → sortable.esm-2Wnda4fq.js} +1 -1
  88. package/dashboard/dist/assets/{store-mMQPBMoh.js → store-CGDaAZE0.js} +1 -1
  89. package/dashboard/dist/assets/{terminal-DxNM-IG2.js → terminal-yZ69kiqV.js} +1 -1
  90. package/dashboard/dist/assets/{trash-rywOpT2_.js → trash-DytHiAPM.js} +1 -1
  91. package/dashboard/dist/assets/{use-community-stats-BQw06Twe.js → use-community-stats-DkR1XWs8.js} +1 -1
  92. package/dashboard/dist/assets/{use-latest-request-guard-BvouGm-c.js → use-latest-request-guard-D_4XuEoM.js} +1 -1
  93. package/dashboard/dist/assets/{use-limits-display-prefs-C5_9TDSy.js → use-limits-display-prefs-BadNsZG8.js} +1 -1
  94. package/dashboard/dist/assets/{use-native-settings-BxuddjkP.js → use-native-settings-BfxfnQAn.js} +1 -1
  95. package/dashboard/dist/assets/use-ordered-list-BHn-XC6u.js +1 -0
  96. package/dashboard/dist/assets/use-reduced-motion-BsG6Qxa-.js +1 -0
  97. package/dashboard/dist/assets/{use-transform-Dl-SvALB.js → use-transform-C0qKlh_s.js} +1 -1
  98. package/dashboard/dist/assets/{use-usage-limits-BqmHYYc3.js → use-usage-limits-CQugaku6.js} +1 -1
  99. package/dashboard/dist/assets/{useCurrency-B1XK_Z0N.js → useCurrency-ChAc4UHj.js} +1 -1
  100. package/dashboard/dist/assets/{usePositioner-BvqDMGZo.js → usePositioner-CGtwrNEt.js} +1 -1
  101. package/dashboard/dist/assets/{useTokenFormat-pPWJeWRe.js → useTokenFormat-CjvWdxnV.js} +1 -1
  102. package/dashboard/dist/assets/{zap-DKYjQi1B.js → zap-WTlrDMoe.js} +1 -1
  103. package/dashboard/dist/index.html +2 -2
  104. package/dashboard/dist/ip-check.html +2 -2
  105. package/dashboard/dist/leaderboard.html +2 -2
  106. package/dashboard/dist/share.html +2 -2
  107. package/package.json +1 -1
  108. package/src/lib/local-usage-transfer.js +1 -1
  109. package/src/lib/pricing/curated-overrides.json +12 -0
  110. package/src/lib/pricing/index.js +2 -1
  111. package/src/lib/pricing/seed-snapshot.json +1 -1
  112. package/src/lib/rollout.js +131 -37
  113. package/dashboard/dist/assets/DashboardPage-DvhF72XX.js +0 -1
  114. package/dashboard/dist/assets/DialogDescription-LHQ-SC4e.js +0 -1
  115. package/dashboard/dist/assets/LimitsPage-DQH6ziz4.js +0 -6
  116. package/dashboard/dist/assets/SessionsPage-BNCDKb58.js +0 -3
  117. package/dashboard/dist/assets/__vite-browser-external-C9a2vCBN.css +0 -1
  118. package/dashboard/dist/assets/use-ordered-list-Cwqc5XA8.js +0 -1
  119. package/dashboard/dist/assets/use-reduced-motion-DU3FvZwM.js +0 -1
@@ -6770,13 +6770,35 @@ async function parseQoderDbIncremental({
6770
6770
  //
6771
6771
  // Qoder 2026-08 (app 0.1.2+) migrated from SharedClientCache/cache/db/local.db
6772
6772
  // to Electron main.sqlite + ~/.qoder/projects/<slug>/<sessionId>.jsonl.
6773
- // The new transcript's message.usage no longer carries prompt_tokens — it is a
6774
- // credit-billed SDK: {input_tokens:0, output_tokens:0, credits:3.2, billable:true}.
6775
- // Only rows with authoritative token fields are counted: credit-only usage
6776
- // without tokens is intentionally not counted (usage stays unsupported, no
6777
- // token delta) because there is no first-party evidence for a credit→token
6778
- // rate. Cost was never estimated — no authoritative credit→USD rate is
6779
- // published, so total_cost_usd stays 0.
6773
+ //
6774
+ // The transcript's message.usage shape has drifted at least once, and BOTH
6775
+ // shapes occur in real user history:
6776
+ // - app 0.1.8 / agent 1.1.44: Anthropic-style precise token fields, on the
6777
+ // main transcript AND on subagent transcripts
6778
+ // (see test/qoder-new-parser.test.js "Real CN record shape").
6779
+ // - app 0.4.x, model "qfmodel": every row is
6780
+ // {input_tokens:0, output_tokens:0, cache_*:0, credits:>0, billable:false}.
6781
+ // The old comment here claimed the credit-billed shape was `{credits, billable:
6782
+ // true}`; that assumption is what made the gate below drop 100% of modern Qoder
6783
+ // rows, so Qoder vanished from the dashboard instead of showing up as
6784
+ // "0 tokens / N requests". `billable` means "charged to the account", NOT
6785
+ // "a call happened" — free-tier models report false while still reporting
6786
+ // credits. Admission therefore needs all three signals, and dropping any of
6787
+ // them regresses a shape that used to count.
6788
+ //
6789
+ // Non-goals: credit→token conversion stays out. There is no first-party credit
6790
+ // → token (or → USD) rate, so fabricating one would push an invented number
6791
+ // into totals, trends, achievements and the cloud leaderboard. Rows without
6792
+ // authoritative tokens contribute conversation activity only, and
6793
+ // total_cost_usd stays 0. Transcript `usage.credits` is deliberately NOT copied
6794
+ // into bucket totals — there is no first-party credit→USD rate to book cost
6795
+ // from, and carrying it forward invites someone to divide by it later.
6796
+ //
6797
+ // conversation_count here follows this provider's existing per-response
6798
+ // convention (`: 1` per admitted main-transcript row). That field is NOT
6799
+ // comparable across providers — same rule as the Claude branch's isMainSession
6800
+ // (#307) and Claude Science's `isRoot ? 1 : 0` (see the Claude Science parser).
6801
+ //
6780
6802
  // Old local.db is kept as a legacy fallback; both sources now use distinct
6781
6803
  // cursor namespaces (qoder vs qoderNew) via disjoint messageKey prefixes
6782
6804
  // (row: vs jsonl:) but upload under the same source="qoder".
@@ -6870,17 +6892,47 @@ function qoderNewMessageKey(record, filePath, lineIndex = 0) {
6870
6892
  return `jsonl:${fallbackSuffix}|${record?.uuid || ""}`;
6871
6893
  }
6872
6894
 
6895
+ // 8.64e15 is the ECMAScript TimeClip limit. A larger finite number (nanosecond
6896
+ // epochs, clock garbage) passes coerceEpochMs untouched, and `new Date(ms)`
6897
+ // yields Invalid Date whose .toISOString() THROWS RangeError — outside the
6898
+ // per-line JSON.parse try/catch, aborting the whole source on one bad row and
6899
+ // silently removing Qoder from every later sync (warnProviderParseFailure is
6900
+ // quiet in auto syncs). Clamp to a drop instead.
6901
+ const MAX_QODER_TIMESTAMP_MS = 8.64e15;
6902
+
6873
6903
  function qoderNewTimestampMs(record) {
6874
- return coerceEpochMs(record?.timestamp)
6904
+ const ms = coerceEpochMs(record?.timestamp)
6875
6905
  || coerceEpochMs(record?.message?.timestamp)
6876
6906
  || parseIsoTimestampMs(record?.timestamp)
6877
6907
  || parseIsoTimestampMs(record?.message?.timestamp)
6878
6908
  || 0;
6909
+ return ms > MAX_QODER_TIMESTAMP_MS ? 0 : ms;
6910
+ }
6911
+
6912
+ // Nominal credits for one transcript row. `credits` and `original_credits`
6913
+ // coexist (the latter is the pre-discount value), and either alone can be 0,
6914
+ // so take the larger instead of `credits ?? original_credits` — `??` does not
6915
+ // skip a present-but-zero `credits`, which would drop real activity.
6916
+ //
6917
+ // The epsilon floor is load-bearing: the activity gate below only needs "some
6918
+ // credits were reported", and upstream float noise like 1e-9 would otherwise
6919
+ // admit rows that carry no usage at all. Real Qoder credits observed in the
6920
+ // wild start around 0.2 (2026-10 CN corpus), so 1e-6 never suppresses genuine
6921
+ // activity.
6922
+ const MIN_QODER_CREDIT_ACTIVITY = 1e-6;
6923
+
6924
+ function readQoderNominalCredits(usage) {
6925
+ let max = 0;
6926
+ for (const raw of [usage?.credits, usage?.original_credits]) {
6927
+ if (typeof raw !== "number" || !Number.isFinite(raw)) continue;
6928
+ if (raw > max) max = raw;
6929
+ }
6930
+ return max >= MIN_QODER_CREDIT_ACTIVITY ? max : 0;
6879
6931
  }
6880
6932
 
6881
6933
  function normalizeQoderNewTokens(usage) {
6882
6934
  if (!usage || typeof usage !== "object") return null;
6883
- const credits = Number(usage.credits ?? usage.original_credits ?? 0);
6935
+ const credits = readQoderNominalCredits(usage);
6884
6936
  let input = Number(usage.input_tokens ?? 0);
6885
6937
  let cached = Number(usage.cache_read_input_tokens ?? usage.cached_tokens ?? 0);
6886
6938
  let cacheCreation = Number(usage.cache_creation_input_tokens ?? 0);
@@ -6907,16 +6959,31 @@ function normalizeQoderNewTokens(usage) {
6907
6959
  total_tokens: inp + cach + cc + out,
6908
6960
  billable_total_tokens: inp + cach + cc + out,
6909
6961
  credits: Number.isFinite(credits) && credits > 0 ? credits : 0,
6962
+ // `credits` dies at the totals boundary — parseQoderNewIncremental never
6963
+ // copies it into bucket totals or the cursor, and must not start: there
6964
+ // is no first-party credit→token or credit→USD rate to divide it by.
6910
6965
  usage_precision: null,
6911
6966
  };
6912
6967
  }
6913
- // Credit-only usage without authoritative token fields is intentionally
6914
- // not counted (no token delta): there is no first-party evidence for a
6915
- // credit→token rate. The caller still counts billable messages as
6916
- // conversation activity.
6968
+ // Credit-only usage without authoritative token fields yields null: no token
6969
+ // delta, because there is no first-party credit→token rate. The caller still
6970
+ // counts it as conversation activity when nominal credits prove the call
6971
+ // really happened.
6917
6972
  return null;
6918
6973
  }
6919
6974
 
6975
+ // Subagent transcripts are internal turns of the same conversation, so they
6976
+ // must not add to conversation activity — same rule the Claude branch enforces
6977
+ // with isMainSession (#307) and Claude Science encodes with `isRoot ? 1 : 0`.
6978
+ // A row that carries neither tokens nor activity is still tracked in the cursor:
6979
+ // a streamed rewrite can hand it real tokens later, and the cursor entry is what
6980
+ // makes that transition subtract/add correctly. It must not touch an hourly or
6981
+ // project bucket though, because getHourlyBucket() creates buckets on demand and
6982
+ // would publish an all-zero row.
6983
+ function qoderRowCountsAsUsage(totals) {
6984
+ return (totals?.total_tokens || 0) > 0 || (totals?.conversation_count || 0) > 0;
6985
+ }
6986
+
6920
6987
  async function parseQoderNewIncremental({
6921
6988
  sessionFiles,
6922
6989
  cursors,
@@ -6977,6 +7044,9 @@ async function parseQoderNewIncremental({
6977
7044
  } catch (_e) {
6978
7045
  continue;
6979
7046
  }
7047
+ // Separator-agnostic: WSL installs are walked over \\wsl$ UNC paths where
7048
+ // path.join emits backslashes (#307).
7049
+ const isMainSession = !/[\\/]subagents[\\/]/.test(filePath);
6980
7050
  const lines = raw.split("\n");
6981
7051
  for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
6982
7052
  const line = lines[lineIdx];
@@ -6994,9 +7064,19 @@ async function parseQoderNewIncremental({
6994
7064
  const usage = msg.usage;
6995
7065
  if (!usage || typeof usage !== "object") continue;
6996
7066
  const base = normalizeQoderNewTokens(usage);
6997
- // Allow billable zero-token messages to still count conversation (no token delta)
7067
+ // Three independent admission signals, kept as a superset of the previous
7068
+ // behaviour — dropping any one regresses rows that used to count:
7069
+ // base authoritative tokens (app 0.1.8 / agent 1.1.44 shape)
7070
+ // isBillable billable is absent or true (the older credit-billed shape)
7071
+ // nominalCredits credits > 0 even though billable is false: Qoder 0.4.x
7072
+ // reports billable=false for its free model while still
7073
+ // reporting real credits, and gate-on-billable alone
7074
+ // discarded every row so the whole source vanished.
7075
+ // Rows that are not billable AND carry no credits stay dropped: that is
7076
+ // the synthetic / cancelled / empty-call case we must not count.
6998
7077
  const isBillable = usage.billable !== false;
6999
- if (!base && !isBillable) continue;
7078
+ const nominalCredits = readQoderNominalCredits(usage);
7079
+ if (!base && !isBillable && nominalCredits <= 0) continue;
7000
7080
  const timestampMs = qoderNewTimestampMs(record);
7001
7081
  if (!timestampMs) continue;
7002
7082
  const bucketStart = toUtcHalfHourStart(new Date(timestampMs).toISOString());
@@ -7014,7 +7094,7 @@ async function parseQoderNewIncremental({
7014
7094
  billable_total_tokens: base.billable_total_tokens,
7015
7095
  total_cost_usd: 0,
7016
7096
  usage_precision: base.usage_precision || undefined,
7017
- conversation_count: 1,
7097
+ conversation_count: isMainSession ? 1 : 0,
7018
7098
  } : {
7019
7099
  input_tokens: 0,
7020
7100
  cached_input_tokens: 0,
@@ -7024,7 +7104,7 @@ async function parseQoderNewIncremental({
7024
7104
  total_tokens: 0,
7025
7105
  billable_total_tokens: 0,
7026
7106
  total_cost_usd: 0,
7027
- conversation_count: 1,
7107
+ conversation_count: isMainSession ? 1 : 0,
7028
7108
  };
7029
7109
  let projectKey = null;
7030
7110
  let projectRef = null;
@@ -7043,6 +7123,16 @@ async function parseQoderNewIncremental({
7043
7123
  projectRef = context?.projectRef || null;
7044
7124
  }
7045
7125
  }
7126
+ // Same sessionId+msgId can appear in both the main transcript and a
7127
+ // subagent transcript (mirrored id). currentByKey is last-wins and
7128
+ // listQoderNewSessionFiles orders by localeCompare, so an unguarded
7129
+ // subagent copy (conversation_count 0) could silently replace the main
7130
+ // row's activity depending on file order. Precedence: a main-session
7131
+ // record always wins a subagent copy; within the same class, last-wins
7132
+ // stands so a streamed rewrite (the later, final token counts) is what
7133
+ // gets booked — first-wins here would under-count subagent tokens.
7134
+ const existing = currentByKey.get(messageKey);
7135
+ if (existing?.isMainSession && !isMainSession) continue;
7046
7136
  currentByKey.set(messageKey, {
7047
7137
  totals,
7048
7138
  bucketStart,
@@ -7050,6 +7140,7 @@ async function parseQoderNewIncremental({
7050
7140
  projectKey,
7051
7141
  projectRef,
7052
7142
  filePath,
7143
+ isMainSession,
7053
7144
  });
7054
7145
  }
7055
7146
  if (cb && (fileIdx % 50 === 0 || fileIdx === files.length - 1)) {
@@ -7077,7 +7168,7 @@ async function parseQoderNewIncremental({
7077
7168
  && (prev.projectKey || null) === (cur.projectKey || null)
7078
7169
  && (prev.totals?.total_cost_usd || 0) === (cur.totals.total_cost_usd || 0);
7079
7170
  if (unchanged) continue;
7080
- if (prev.totals && prev.bucketStart && prev.model) {
7171
+ if (prev.totals && prev.bucketStart && prev.model && qoderRowCountsAsUsage(prev.totals)) {
7081
7172
  const oldBucket = getHourlyBucket(hourlyState, sourceKey, prev.model, prev.bucketStart);
7082
7173
  subtractTotals(oldBucket.totals, prev.totals);
7083
7174
  touchedBuckets.add(bucketKey(sourceKey, prev.model, prev.bucketStart));
@@ -7088,16 +7179,17 @@ async function parseQoderNewIncremental({
7088
7179
  }
7089
7180
  }
7090
7181
  if (cur) {
7091
- const bucket = getHourlyBucket(hourlyState, sourceKey, cur.model, cur.bucketStart);
7092
- addTotals(bucket.totals, cur.totals);
7093
- if (cur.totals.usage_precision) bucket.usage_precision = cur.totals.usage_precision;
7094
- // addTotals handles total_cost_usd via USD_TICKS, but credits cost is small; ensure it accumulates
7095
- touchedBuckets.add(bucketKey(sourceKey, cur.model, cur.bucketStart));
7096
- if (projectEnabled && cur.projectKey) {
7097
- const projectBucket = getProjectBucket(projectState, cur.projectKey, sourceKey, cur.bucketStart, cur.projectRef);
7098
- addTotals(projectBucket.totals, cur.totals);
7099
- if (cur.totals.usage_precision) projectBucket.usage_precision = cur.totals.usage_precision;
7100
- projectTouchedBuckets.add(projectBucketKey(cur.projectKey, sourceKey, cur.bucketStart));
7182
+ if (qoderRowCountsAsUsage(cur.totals)) {
7183
+ const bucket = getHourlyBucket(hourlyState, sourceKey, cur.model, cur.bucketStart);
7184
+ addTotals(bucket.totals, cur.totals);
7185
+ if (cur.totals.usage_precision) bucket.usage_precision = cur.totals.usage_precision;
7186
+ touchedBuckets.add(bucketKey(sourceKey, cur.model, cur.bucketStart));
7187
+ if (projectEnabled && cur.projectKey) {
7188
+ const projectBucket = getProjectBucket(projectState, cur.projectKey, sourceKey, cur.bucketStart, cur.projectRef);
7189
+ addTotals(projectBucket.totals, cur.totals);
7190
+ if (cur.totals.usage_precision) projectBucket.usage_precision = cur.totals.usage_precision;
7191
+ projectTouchedBuckets.add(projectBucketKey(cur.projectKey, sourceKey, cur.bucketStart));
7192
+ }
7101
7193
  }
7102
7194
  qoderState.messages[key] = {
7103
7195
  totals: cur.totals,
@@ -7118,15 +7210,17 @@ async function parseQoderNewIncremental({
7118
7210
  // Add brand-new keys
7119
7211
  for (const [key, cur] of currentByKey.entries()) {
7120
7212
  if (qoderState.messages[key]) continue;
7121
- const bucket = getHourlyBucket(hourlyState, sourceKey, cur.model, cur.bucketStart);
7122
- addTotals(bucket.totals, cur.totals);
7123
- if (cur.totals.usage_precision) bucket.usage_precision = cur.totals.usage_precision;
7124
- touchedBuckets.add(bucketKey(sourceKey, cur.model, cur.bucketStart));
7125
- if (projectEnabled && cur.projectKey) {
7126
- const projectBucket = getProjectBucket(projectState, cur.projectKey, sourceKey, cur.bucketStart, cur.projectRef);
7127
- addTotals(projectBucket.totals, cur.totals);
7128
- if (cur.totals.usage_precision) projectBucket.usage_precision = cur.totals.usage_precision;
7129
- projectTouchedBuckets.add(projectBucketKey(cur.projectKey, sourceKey, cur.bucketStart));
7213
+ if (qoderRowCountsAsUsage(cur.totals)) {
7214
+ const bucket = getHourlyBucket(hourlyState, sourceKey, cur.model, cur.bucketStart);
7215
+ addTotals(bucket.totals, cur.totals);
7216
+ if (cur.totals.usage_precision) bucket.usage_precision = cur.totals.usage_precision;
7217
+ touchedBuckets.add(bucketKey(sourceKey, cur.model, cur.bucketStart));
7218
+ if (projectEnabled && cur.projectKey) {
7219
+ const projectBucket = getProjectBucket(projectState, cur.projectKey, sourceKey, cur.bucketStart, cur.projectRef);
7220
+ addTotals(projectBucket.totals, cur.totals);
7221
+ if (cur.totals.usage_precision) projectBucket.usage_precision = cur.totals.usage_precision;
7222
+ projectTouchedBuckets.add(projectBucketKey(cur.projectKey, sourceKey, cur.bucketStart));
7223
+ }
7130
7224
  }
7131
7225
  qoderState.messages[key] = {
7132
7226
  totals: cur.totals,