@ipv9/tokentracker-cli 0.39.53 → 0.39.55

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 (22) hide show
  1. package/dashboard/dist/assets/{Card-BZkG7ExP.js → Card-CtvDXZw0.js} +1 -1
  2. package/dashboard/dist/assets/{DashboardPage-BXs8ZVPT.js → DashboardPage-Y1wWgbMc.js} +3 -3
  3. package/dashboard/dist/assets/{IpCheckPage-Hm0zUwdf.js → IpCheckPage-Bor8g4lv.js} +1 -1
  4. package/dashboard/dist/assets/{LimitsPage-BxsnbmDo.js → LimitsPage-CIac9eQA.js} +1 -1
  5. package/dashboard/dist/assets/{LocalOnlyNotice-BvwC1vCO.js → LocalOnlyNotice-Dr74ws0K.js} +1 -1
  6. package/dashboard/dist/assets/{PopoverPopup-HsE_7fXK.js → PopoverPopup-DhIs9KLg.js} +1 -1
  7. package/dashboard/dist/assets/{Select-7_WyWhaS.js → Select-B-hdqLRw.js} +1 -1
  8. package/dashboard/dist/assets/{SettingsPage-DbJxd3N7.js → SettingsPage-CGmMNqQ4.js} +1 -1
  9. package/dashboard/dist/assets/{SkillsPage-Dae3HfP-.js → SkillsPage-D70T7nYt.js} +1 -1
  10. package/dashboard/dist/assets/{WrappedPage-DZU9PYF5.js → WrappedPage-CXCjVq_h.js} +1 -1
  11. package/dashboard/dist/assets/{format-DtQGOz67.js → format-Cf1THGDG.js} +1 -1
  12. package/dashboard/dist/assets/{limitDisplay-B1ZUkTHM.js → limitDisplay-HHiXLwsI.js} +1 -1
  13. package/dashboard/dist/assets/{main-BBQHiOju.js → main-B5OWbrt1.js} +4 -3
  14. package/dashboard/dist/assets/{mock-data-Ct1Wvcew.js → mock-data-D7OvhHFq.js} +1 -1
  15. package/dashboard/dist/assets/{use-limits-display-prefs-CYoiCfEE.js → use-limits-display-prefs-CJObZZbn.js} +1 -1
  16. package/dashboard/dist/assets/{useCurrency-DGkZG989.js → useCurrency-AuPUpwUc.js} +1 -1
  17. package/dashboard/dist/index.html +1 -1
  18. package/package.json +1 -1
  19. package/src/lib/pricing/curated-overrides.json +2 -2
  20. package/src/lib/pricing/matcher.js +36 -1
  21. package/src/lib/pricing/seed-snapshot.json +1 -1
  22. package/src/lib/rollout.js +92 -34
@@ -3446,6 +3446,7 @@ const HERMES_TOKEN_TOTAL_FIELDS = [
3446
3446
  "reasoning_output_tokens",
3447
3447
  "total_tokens",
3448
3448
  "billable_total_tokens",
3449
+ "conversation_count",
3449
3450
  ];
3450
3451
 
3451
3452
  function hermesRowTotals(row) {
@@ -3570,12 +3571,16 @@ async function appendHermesReconciliationRows({ queuePath, queueStatePath, canon
3570
3571
  }
3571
3572
  }
3572
3573
 
3573
- // One-time, fail-closed correction for the v0.39.48 adoption gate. That gate
3574
+ // One-time, per-bucket correction for the v0.39.48 adoption gate. That gate
3574
3575
  // intentionally avoided replaying legacy aggregate snapshots, which preserved
3575
3576
  // the grand total but left already-counted mixed-model usage on sessions.model.
3576
- // Rebuild only when the authoritative Hermes model rows have exactly the same
3577
- // token total as the existing Hermes accumulator; otherwise leave data intact
3578
- // and let the normal incremental path catch up before retrying next sync.
3577
+ // Compares each canonical (model, half-hour) bucket against the existing
3578
+ // accumulator instead of requiring global grand-total equality: production
3579
+ // installs cannot satisfy exact equality (source retention deletes ingested
3580
+ // sessions, live sessions grow between reads). Differing canonical buckets are
3581
+ // appended as latest-wins correction rows after the consumed upload offset;
3582
+ // existing buckets with no canonical counterpart that predate retained source
3583
+ // coverage are zeroed out explicitly.
3579
3584
  async function reconcileHermesMixedModelUsage({ dbPaths, cursors, queuePath, queueStatePath, hermesState, updatedAt, sqliteOptions }) {
3580
3585
  if (!cursors || typeof cursors !== "object") return { status: "invalid_cursor" };
3581
3586
  const migrations = cursors.migrations && typeof cursors.migrations === "object" ? cursors.migrations : {};
@@ -3604,13 +3609,22 @@ async function reconcileHermesMixedModelUsage({ dbPaths, cursors, queuePath, que
3604
3609
  }
3605
3610
 
3606
3611
  const hourlyState = normalizeHourlyState(cursors.hourly);
3607
- const existingTotals = initTotals();
3608
- for (const [key, bucket] of Object.entries(hourlyState.buckets || {})) {
3609
- if (parseBucketKey(key).source === "hermes") addTotals(existingTotals, bucket?.totals);
3612
+ // Retained source coverage boundary: the oldest session start still present in
3613
+ // any authoritative database. Existing buckets whose hour starts before this
3614
+ // boundary cannot have a surviving source row, so they are provably orphans.
3615
+ let retentionBoundaryEpochSec = null;
3616
+ for (const { rows } of rowsBySource) {
3617
+ for (const row of rows) {
3618
+ const startedAt = Number(row.started_at);
3619
+ if (Number.isFinite(startedAt) && startedAt > 0) {
3620
+ retentionBoundaryEpochSec = retentionBoundaryEpochSec == null
3621
+ ? startedAt
3622
+ : Math.min(retentionBoundaryEpochSec, startedAt);
3623
+ }
3624
+ }
3610
3625
  }
3611
3626
 
3612
3627
  const canonicalBuckets = {};
3613
- const authoritativeTotals = initTotals();
3614
3628
  for (const { rows } of rowsBySource) {
3615
3629
  for (const row of rows) {
3616
3630
  const totals = hermesRowTotals(row);
@@ -3621,46 +3635,82 @@ async function reconcileHermesMixedModelUsage({ dbPaths, cursors, queuePath, que
3621
3635
  const key = bucketKey("hermes", model, hourStart);
3622
3636
  const bucket = canonicalBuckets[key] ||= { totals: initTotals(), queuedKey: null };
3623
3637
  addTotals(bucket.totals, totals);
3624
- addTotals(authoritativeTotals, totals);
3625
3638
  }
3626
3639
  }
3627
3640
 
3628
- if (!sameHermesTokenTotals(existingTotals, authoritativeTotals)) {
3629
- cursors.migrations ||= {};
3630
- cursors.migrations[HERMES_MIXED_MODEL_RECONCILIATION_MIGRATION_KEY] = {
3631
- status: "blocked_total_mismatch",
3632
- updatedAt,
3633
- existingTotalTokens: existingTotals.total_tokens,
3634
- authoritativeTotalTokens: authoritativeTotals.total_tokens,
3635
- };
3636
- return cursors.migrations[HERMES_MIXED_MODEL_RECONCILIATION_MIGRATION_KEY];
3641
+ function bucketHourStartToEpochSec(hourStart) {
3642
+ const parsedMs = Date.parse(`${hourStart}Z`.replace(/Z+$/, "Z"));
3643
+ return Number.isFinite(parsedMs) ? parsedMs / 1000 : null;
3637
3644
  }
3638
3645
 
3639
- for (const key of Object.keys(hourlyState.buckets || {})) {
3640
- if (parseBucketKey(key).source === "hermes") delete hourlyState.buckets[key];
3641
- }
3642
- for (const key of Object.keys(hourlyState.groupQueued || {})) {
3643
- if (key.startsWith("hermes|")) delete hourlyState.groupQueued[key];
3644
- }
3645
- const canonicalRows = [];
3646
+ const existingHermesKeys = new Set(Object.keys(hourlyState.buckets || {})
3647
+ .filter((key) => parseBucketKey(key).source === "hermes"));
3648
+
3649
+ const correctionRows = [];
3650
+ let bucketsCorrected = 0;
3651
+ let bucketsSkipped = 0;
3652
+ let orphansZeroed = 0;
3653
+ let stalePlacementsZeroed = 0;
3654
+
3655
+ // Corrections for canonical buckets that differ from the existing accumulator.
3646
3656
  for (const [key, bucket] of Object.entries(canonicalBuckets)) {
3647
3657
  const { model, hourStart } = parseBucketKey(key);
3658
+ const existingBucket = hourlyState.buckets[key];
3659
+ if (existingBucket && sameHermesTokenTotals(existingBucket?.totals, bucket.totals)) {
3660
+ bucketsSkipped += 1;
3661
+ continue;
3662
+ }
3648
3663
  bucket.queuedKey = totalsKey(bucket.totals);
3649
3664
  hourlyState.buckets[key] = bucket;
3650
- canonicalRows.push(JSON.stringify({
3665
+ correctionRows.push(JSON.stringify({
3651
3666
  source: "hermes",
3652
3667
  model,
3653
3668
  hour_start: hourStart,
3654
3669
  ...bucket.totals,
3655
3670
  }));
3671
+ bucketsCorrected += 1;
3672
+ }
3673
+
3674
+ // Zero out existing buckets with no canonical counterpart. Outside retained
3675
+ // coverage they are provably orphans (source rows deleted after ingestion).
3676
+ // Inside coverage they are stale placements: the incremental path spread a
3677
+ // session's cumulative totals across observation-time buckets while the
3678
+ // canonical view places them at the session's current last_seen bucket.
3679
+ // Latest-wins upserts make the zero row retire the stale placement without
3680
+ // rewriting queue history; unfinished sessions re-emit growth at their new
3681
+ // bucket on the next incremental sync.
3682
+ for (const key of existingHermesKeys) {
3683
+ if (canonicalBuckets[key]) continue;
3684
+ const { model, hourStart } = parseBucketKey(key);
3685
+ const existingBucketTotal = Number(hourlyState.buckets[key]?.totals?.total_tokens || 0);
3686
+ if (existingBucketTotal === 0) continue;
3687
+ const hourStartSec = bucketHourStartToEpochSec(hourStart);
3688
+ const provablyOrphaned = hourStartSec != null
3689
+ && retentionBoundaryEpochSec != null
3690
+ && hourStartSec < retentionBoundaryEpochSec;
3691
+ const zeroTotals = initTotals();
3692
+ hourlyState.buckets[key] = { totals: zeroTotals, queuedKey: totalsKey(zeroTotals) };
3693
+ correctionRows.push(JSON.stringify({
3694
+ source: "hermes",
3695
+ model,
3696
+ hour_start: hourStart,
3697
+ ...zeroTotals,
3698
+ }));
3699
+ if (provablyOrphaned) {
3700
+ orphansZeroed += 1;
3701
+ } else {
3702
+ stalePlacementsZeroed += 1;
3703
+ }
3656
3704
  }
3657
3705
 
3658
- await appendHermesReconciliationRows({
3659
- queuePath,
3660
- queueStatePath,
3661
- canonicalRows,
3662
- updatedAt,
3663
- });
3706
+ if (correctionRows.length > 0) {
3707
+ await appendHermesReconciliationRows({
3708
+ queuePath,
3709
+ queueStatePath,
3710
+ canonicalRows: correctionRows,
3711
+ updatedAt,
3712
+ });
3713
+ }
3664
3714
 
3665
3715
  const nextHermesState = hermesState && typeof hermesState === "object" ? hermesState : {};
3666
3716
  for (const { profileName, rows } of rowsBySource) {
@@ -3677,13 +3727,21 @@ async function reconcileHermesMixedModelUsage({ dbPaths, cursors, queuePath, que
3677
3727
  );
3678
3728
  }
3679
3729
  }
3730
+ for (const key of Object.keys(hourlyState.groupQueued || {})) {
3731
+ if (key.startsWith("hermes|")) delete hourlyState.groupQueued[key];
3732
+ }
3680
3733
  cursors.hourly = hourlyState;
3681
3734
  cursors.migrations ||= {};
3682
3735
  cursors.migrations[HERMES_MIXED_MODEL_RECONCILIATION_MIGRATION_KEY] = {
3683
3736
  status: "applied",
3684
3737
  appliedAt: updatedAt,
3685
- totalTokens: authoritativeTotals.total_tokens,
3686
- bucketsRebuilt: canonicalRows.length,
3738
+ totalTokens: Object.values(canonicalBuckets).reduce(
3739
+ (sum, bucket) => sum + Number(bucket.totals.total_tokens || 0), 0),
3740
+ bucketsRebuilt: Object.keys(canonicalBuckets).length,
3741
+ bucketsCorrected,
3742
+ bucketsSkipped,
3743
+ orphansZeroed,
3744
+ stalePlacementsZeroed,
3687
3745
  };
3688
3746
  return cursors.migrations[HERMES_MIXED_MODEL_RECONCILIATION_MIGRATION_KEY];
3689
3747
  }