@vibescore/tracker 0.1.1 → 0.1.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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/lib/rollout.js +268 -20
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibescore/tracker",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Codex CLI token usage tracker (macOS-first, notify-driven).",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -470,44 +470,207 @@ async function enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets })
470
470
  if (touchedGroups.size === 0) return 0;
471
471
 
472
472
  const groupQueued = hourlyState.groupQueued && typeof hourlyState.groupQueued === 'object' ? hourlyState.groupQueued : {};
473
+ let codexTouched = false;
473
474
  const legacyGroups = new Set();
474
475
  for (const groupKey of touchedGroups) {
475
476
  if (Object.prototype.hasOwnProperty.call(groupQueued, groupKey)) {
476
477
  legacyGroups.add(groupKey);
477
478
  }
479
+ if (!codexTouched && groupKey.startsWith(`${DEFAULT_SOURCE}${BUCKET_SEPARATOR}`)) {
480
+ codexTouched = true;
481
+ }
478
482
  }
479
483
 
480
- const toAppend = [];
481
- for (const bucketStart of touchedBuckets) {
482
- const parsed = parseBucketKey(bucketStart);
484
+ const groupedBuckets = new Map();
485
+ for (const [key, bucket] of Object.entries(hourlyState.buckets || {})) {
486
+ if (!bucket || !bucket.totals) continue;
487
+ const parsed = parseBucketKey(key);
483
488
  const hourStart = parsed.hourStart;
484
489
  if (!hourStart) continue;
485
490
  const groupKey = groupBucketKey(parsed.source, hourStart);
486
- if (legacyGroups.has(groupKey)) continue;
491
+ if (!touchedGroups.has(groupKey) || legacyGroups.has(groupKey)) continue;
492
+
493
+ const source = normalizeSourceInput(parsed.source) || DEFAULT_SOURCE;
494
+ const model = normalizeModelInput(parsed.model) || DEFAULT_MODEL;
495
+ let group = groupedBuckets.get(groupKey);
496
+ if (!group) {
497
+ group = { source, hourStart, buckets: new Map() };
498
+ groupedBuckets.set(groupKey, group);
499
+ }
487
500
 
488
- const normalizedKey = bucketKey(parsed.source, parsed.model, hourStart);
489
- const bucket = hourlyState.buckets ? hourlyState.buckets[normalizedKey] : null;
490
- if (!bucket || !bucket.totals) continue;
491
501
  if (bucket.queuedKey != null && typeof bucket.queuedKey !== 'string') {
492
502
  bucket.queuedKey = null;
493
503
  }
494
- const key = totalsKey(bucket.totals);
495
- if (bucket.queuedKey === key) continue;
496
- const source = normalizeSourceInput(parsed.source) || DEFAULT_SOURCE;
497
- const model = normalizeModelInput(parsed.model) || DEFAULT_MODEL;
504
+ group.buckets.set(model, bucket);
505
+ }
506
+
507
+ if (codexTouched) {
508
+ const recomputeGroups = new Set();
509
+ for (const [key, bucket] of Object.entries(hourlyState.buckets || {})) {
510
+ if (!bucket || !bucket.totals) continue;
511
+ const parsed = parseBucketKey(key);
512
+ const hourStart = parsed.hourStart;
513
+ if (!hourStart) continue;
514
+ const source = normalizeSourceInput(parsed.source) || DEFAULT_SOURCE;
515
+ if (source !== 'every-code') continue;
516
+ const groupKey = groupBucketKey(source, hourStart);
517
+ if (legacyGroups.has(groupKey) || groupedBuckets.has(groupKey)) continue;
518
+ const model = normalizeModelInput(parsed.model) || DEFAULT_MODEL;
519
+ if (model !== DEFAULT_MODEL) continue;
520
+ recomputeGroups.add(groupKey);
521
+ }
522
+
523
+ if (recomputeGroups.size > 0) {
524
+ for (const [key, bucket] of Object.entries(hourlyState.buckets || {})) {
525
+ if (!bucket || !bucket.totals) continue;
526
+ const parsed = parseBucketKey(key);
527
+ const hourStart = parsed.hourStart;
528
+ if (!hourStart) continue;
529
+ const source = normalizeSourceInput(parsed.source) || DEFAULT_SOURCE;
530
+ const groupKey = groupBucketKey(source, hourStart);
531
+ if (!recomputeGroups.has(groupKey)) continue;
532
+ let group = groupedBuckets.get(groupKey);
533
+ if (!group) {
534
+ group = { source, hourStart, buckets: new Map() };
535
+ groupedBuckets.set(groupKey, group);
536
+ }
537
+ if (bucket.queuedKey != null && typeof bucket.queuedKey !== 'string') {
538
+ bucket.queuedKey = null;
539
+ }
540
+ const model = normalizeModelInput(parsed.model) || DEFAULT_MODEL;
541
+ group.buckets.set(model, bucket);
542
+ }
543
+ }
544
+ }
545
+
546
+ const codexDominants = collectCodexDominantModels(hourlyState);
547
+
548
+ const toAppend = [];
549
+ for (const group of groupedBuckets.values()) {
550
+ const unknownBucket = group.buckets.get(DEFAULT_MODEL) || null;
551
+ const dominantModel = pickDominantModel(group.buckets);
552
+ let alignedModel = null;
553
+ if (unknownBucket?.alignedModel) {
554
+ const normalized = normalizeModelInput(unknownBucket.alignedModel);
555
+ alignedModel = normalized && normalized !== DEFAULT_MODEL ? normalized : null;
556
+ }
557
+ const zeroTotals = initTotals();
558
+ const zeroKey = totalsKey(zeroTotals);
559
+
560
+ if (dominantModel) {
561
+ if (alignedModel && !group.buckets.has(alignedModel)) {
562
+ toAppend.push(
563
+ JSON.stringify({
564
+ source: group.source,
565
+ model: alignedModel,
566
+ hour_start: group.hourStart,
567
+ input_tokens: zeroTotals.input_tokens,
568
+ cached_input_tokens: zeroTotals.cached_input_tokens,
569
+ output_tokens: zeroTotals.output_tokens,
570
+ reasoning_output_tokens: zeroTotals.reasoning_output_tokens,
571
+ total_tokens: zeroTotals.total_tokens
572
+ })
573
+ );
574
+ }
575
+ if (unknownBucket && !alignedModel && unknownBucket.queuedKey && unknownBucket.queuedKey !== zeroKey) {
576
+ if (unknownBucket.retractedUnknownKey !== zeroKey) {
577
+ toAppend.push(
578
+ JSON.stringify({
579
+ source: group.source,
580
+ model: DEFAULT_MODEL,
581
+ hour_start: group.hourStart,
582
+ input_tokens: zeroTotals.input_tokens,
583
+ cached_input_tokens: zeroTotals.cached_input_tokens,
584
+ output_tokens: zeroTotals.output_tokens,
585
+ reasoning_output_tokens: zeroTotals.reasoning_output_tokens,
586
+ total_tokens: zeroTotals.total_tokens
587
+ })
588
+ );
589
+ unknownBucket.retractedUnknownKey = zeroKey;
590
+ }
591
+ }
592
+ if (unknownBucket) unknownBucket.alignedModel = null;
593
+ for (const [model, bucket] of group.buckets.entries()) {
594
+ if (model === DEFAULT_MODEL) continue;
595
+ let totals = bucket.totals;
596
+ if (model === dominantModel && unknownBucket?.totals) {
597
+ totals = cloneTotals(bucket.totals);
598
+ addTotals(totals, unknownBucket.totals);
599
+ }
600
+ const key = totalsKey(totals);
601
+ if (bucket.queuedKey === key) continue;
602
+ toAppend.push(
603
+ JSON.stringify({
604
+ source: group.source,
605
+ model,
606
+ hour_start: group.hourStart,
607
+ input_tokens: totals.input_tokens,
608
+ cached_input_tokens: totals.cached_input_tokens,
609
+ output_tokens: totals.output_tokens,
610
+ reasoning_output_tokens: totals.reasoning_output_tokens,
611
+ total_tokens: totals.total_tokens
612
+ })
613
+ );
614
+ bucket.queuedKey = key;
615
+ }
616
+ continue;
617
+ }
618
+
619
+ if (!unknownBucket?.totals) continue;
620
+ let outputModel = DEFAULT_MODEL;
621
+ if (group.source === 'every-code') {
622
+ const aligned = findNearestCodexModel(group.hourStart, codexDominants);
623
+ if (aligned) outputModel = aligned;
624
+ }
625
+ const nextAligned = outputModel !== DEFAULT_MODEL ? outputModel : null;
626
+ if (alignedModel && alignedModel !== nextAligned) {
627
+ toAppend.push(
628
+ JSON.stringify({
629
+ source: group.source,
630
+ model: alignedModel,
631
+ hour_start: group.hourStart,
632
+ input_tokens: zeroTotals.input_tokens,
633
+ cached_input_tokens: zeroTotals.cached_input_tokens,
634
+ output_tokens: zeroTotals.output_tokens,
635
+ reasoning_output_tokens: zeroTotals.reasoning_output_tokens,
636
+ total_tokens: zeroTotals.total_tokens
637
+ })
638
+ );
639
+ }
640
+ if (!alignedModel && nextAligned && unknownBucket.queuedKey && unknownBucket.queuedKey !== zeroKey) {
641
+ if (unknownBucket.retractedUnknownKey !== zeroKey) {
642
+ toAppend.push(
643
+ JSON.stringify({
644
+ source: group.source,
645
+ model: DEFAULT_MODEL,
646
+ hour_start: group.hourStart,
647
+ input_tokens: zeroTotals.input_tokens,
648
+ cached_input_tokens: zeroTotals.cached_input_tokens,
649
+ output_tokens: zeroTotals.output_tokens,
650
+ reasoning_output_tokens: zeroTotals.reasoning_output_tokens,
651
+ total_tokens: zeroTotals.total_tokens
652
+ })
653
+ );
654
+ unknownBucket.retractedUnknownKey = zeroKey;
655
+ }
656
+ }
657
+ if (unknownBucket) unknownBucket.alignedModel = nextAligned;
658
+ const key = totalsKey(unknownBucket.totals);
659
+ const outputKey = outputModel === DEFAULT_MODEL ? key : `${key}|${outputModel}`;
660
+ if (unknownBucket.queuedKey === outputKey) continue;
498
661
  toAppend.push(
499
662
  JSON.stringify({
500
- source,
501
- model,
502
- hour_start: hourStart,
503
- input_tokens: bucket.totals.input_tokens,
504
- cached_input_tokens: bucket.totals.cached_input_tokens,
505
- output_tokens: bucket.totals.output_tokens,
506
- reasoning_output_tokens: bucket.totals.reasoning_output_tokens,
507
- total_tokens: bucket.totals.total_tokens
663
+ source: group.source,
664
+ model: outputModel,
665
+ hour_start: group.hourStart,
666
+ input_tokens: unknownBucket.totals.input_tokens,
667
+ cached_input_tokens: unknownBucket.totals.cached_input_tokens,
668
+ output_tokens: unknownBucket.totals.output_tokens,
669
+ reasoning_output_tokens: unknownBucket.totals.reasoning_output_tokens,
670
+ total_tokens: unknownBucket.totals.total_tokens
508
671
  })
509
672
  );
510
- bucket.queuedKey = key;
673
+ unknownBucket.queuedKey = outputKey;
511
674
  }
512
675
 
513
676
  if (legacyGroups.size > 0) {
@@ -564,6 +727,91 @@ async function enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets })
564
727
  return toAppend.length;
565
728
  }
566
729
 
730
+ function pickDominantModel(buckets) {
731
+ let dominantModel = null;
732
+ let dominantTotal = -1;
733
+ for (const [model, bucket] of buckets.entries()) {
734
+ if (model === DEFAULT_MODEL) continue;
735
+ const total = Number(bucket?.totals?.total_tokens || 0);
736
+ if (
737
+ dominantModel == null ||
738
+ total > dominantTotal ||
739
+ (total === dominantTotal && model < dominantModel)
740
+ ) {
741
+ dominantModel = model;
742
+ dominantTotal = total;
743
+ }
744
+ }
745
+ return dominantModel;
746
+ }
747
+
748
+ function cloneTotals(totals) {
749
+ const cloned = initTotals();
750
+ addTotals(cloned, totals || {});
751
+ return cloned;
752
+ }
753
+
754
+ function collectCodexDominantModels(hourlyState) {
755
+ const grouped = new Map();
756
+ for (const [key, bucket] of Object.entries(hourlyState.buckets || {})) {
757
+ if (!bucket || !bucket.totals) continue;
758
+ const parsed = parseBucketKey(key);
759
+ const hourStart = parsed.hourStart;
760
+ if (!hourStart) continue;
761
+ const source = normalizeSourceInput(parsed.source) || DEFAULT_SOURCE;
762
+ if (source !== DEFAULT_SOURCE) continue;
763
+ const model = normalizeModelInput(parsed.model) || DEFAULT_MODEL;
764
+ if (model === DEFAULT_MODEL) continue;
765
+
766
+ let models = grouped.get(hourStart);
767
+ if (!models) {
768
+ models = new Map();
769
+ grouped.set(hourStart, models);
770
+ }
771
+ const total = Number(bucket.totals.total_tokens || 0);
772
+ models.set(model, (models.get(model) || 0) + total);
773
+ }
774
+
775
+ const dominants = [];
776
+ for (const [hourStart, models] of grouped.entries()) {
777
+ let dominantModel = null;
778
+ let dominantTotal = -1;
779
+ for (const [model, total] of models.entries()) {
780
+ if (
781
+ dominantModel == null ||
782
+ total > dominantTotal ||
783
+ (total === dominantTotal && model < dominantModel)
784
+ ) {
785
+ dominantModel = model;
786
+ dominantTotal = total;
787
+ }
788
+ }
789
+ if (dominantModel) {
790
+ dominants.push({ hourStart, model: dominantModel });
791
+ }
792
+ }
793
+
794
+ return dominants;
795
+ }
796
+
797
+ function findNearestCodexModel(hourStart, dominants) {
798
+ if (!hourStart || !dominants || dominants.length === 0) return null;
799
+ const target = Date.parse(hourStart);
800
+ if (!Number.isFinite(target)) return null;
801
+
802
+ let best = null;
803
+ for (const entry of dominants) {
804
+ const candidate = Date.parse(entry.hourStart);
805
+ if (!Number.isFinite(candidate)) continue;
806
+ const diff = Math.abs(candidate - target);
807
+ if (!best || diff < best.diff || (diff === best.diff && candidate < best.time)) {
808
+ best = { diff, time: candidate, model: entry.model };
809
+ }
810
+ }
811
+
812
+ return best ? best.model : null;
813
+ }
814
+
567
815
  function normalizeHourlyState(raw) {
568
816
  const state = raw && typeof raw === 'object' ? raw : {};
569
817
  const version = Number(state.version || 1);