cctally 1.28.0 → 1.29.0

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.
@@ -331,54 +331,7 @@ def insert_percent_milestone(
331
331
  def insert_budget_milestone(
332
332
  conn: sqlite3.Connection,
333
333
  *,
334
- week_start_at: str,
335
- period: "str | None" = None,
336
- threshold: int,
337
- budget_usd: float,
338
- spent_usd: float,
339
- consumption_pct: float,
340
- commit: bool = True,
341
- ) -> int:
342
- """INSERT OR IGNORE a budget threshold crossing. Returns ``cur.rowcount``
343
- (1 = genuinely new crossing, 0 = INSERT OR IGNORE no-op on a pre-existing
344
- ``(week_start_at, period, threshold)`` row).
345
-
346
- Mirrors :func:`insert_percent_milestone`'s rowcount contract so the
347
- alert-fire predicate (`if inserted == 1`) is race-safe without a
348
- follow-up SELECT. ``period`` (#137) is the configured period noun at
349
- crossing ('calendar-week'|'calendar-month'|'subscription-week'); it
350
- discriminates the UNIQUE key so calendar-week and calendar-month windows
351
- that share a start instant don't collide. A NULL ``period`` is the pre-011
352
- "unknown" sentinel (only seeded migration rows carry it). ``alerted_at`` is
353
- left NULL — the caller stamps it in the SAME transaction BEFORE dispatching
354
- (set-then-dispatch invariant, CLAUDE.md Alerts gotcha). ``commit=False``
355
- lets the caller bundle the INSERT with the follow-up ``alerted_at`` UPDATE
356
- in one transaction so a crash between them can't strand ``alerted_at`` NULL
357
- forever.
358
- """
359
- cur = conn.execute(
360
- "INSERT OR IGNORE INTO budget_milestones "
361
- "(week_start_at, period, threshold, budget_usd, spent_usd, "
362
- " consumption_pct, crossed_at_utc) "
363
- "VALUES (?, ?, ?, ?, ?, ?, ?)",
364
- (
365
- week_start_at,
366
- period,
367
- int(threshold),
368
- float(budget_usd),
369
- float(spent_usd),
370
- float(consumption_pct),
371
- now_utc_iso(),
372
- ),
373
- )
374
- if commit:
375
- conn.commit()
376
- return int(cur.rowcount)
377
-
378
-
379
- def insert_codex_budget_milestone(
380
- conn: sqlite3.Connection,
381
- *,
334
+ vendor: str,
382
335
  period_start_at: str,
383
336
  period: "str | None" = None,
384
337
  threshold: int,
@@ -387,29 +340,34 @@ def insert_codex_budget_milestone(
387
340
  consumption_pct: float,
388
341
  commit: bool = True,
389
342
  ) -> int:
390
- """INSERT OR IGNORE a Codex budget threshold crossing. Returns
391
- ``cur.rowcount`` (1 = genuinely new crossing, 0 = INSERT OR IGNORE no-op on a
392
- pre-existing ``(period_start_at, period, threshold)`` row).
393
-
394
- Mirrors :func:`insert_budget_milestone` byte-for-byte but keyed on
395
- ``period_start_at`` (the resolved CALENDAR-period window start instant) in
396
- place of ``week_start_at`` Codex has no Anthropic subscription week, so the
397
- budget runs over a calendar period (spec §6). ``period`` (#137) is the
398
- configured Codex period noun at crossing ('calendar-week'|'calendar-month');
399
- NULL is the pre-011 unknown sentinel. Same rowcount contract so the
400
- alert-fire predicate (`if inserted == 1`) is race-safe without a follow-up
401
- SELECT. ``alerted_at`` is left NULL — the caller stamps it in the SAME
402
- transaction BEFORE dispatching (set-then-dispatch invariant, CLAUDE.md
403
- Alerts gotcha). ``commit=False`` lets the caller bundle the INSERT with the
404
- follow-up ``alerted_at`` UPDATE in one transaction so a crash between them
405
- can't strand ``alerted_at`` NULL forever.
343
+ """INSERT OR IGNORE a budget threshold crossing into the unified vendor-tagged
344
+ table (#143). Returns ``cur.rowcount`` (1 = genuinely new crossing, 0 =
345
+ INSERT OR IGNORE no-op on a pre-existing ``(vendor, period_start_at, period,
346
+ threshold)`` row).
347
+
348
+ The merged ``budget_milestones`` table (migration 012) carries a ``vendor``
349
+ column (``'claude'``|``'codex'``) and the renamed ``period_start_at`` key
350
+ (the Claude subscription-week start OR the Codex calendar-period start
351
+ Codex has no Anthropic subscription week, spec §6). Mirrors
352
+ :func:`insert_percent_milestone`'s rowcount contract so the alert-fire
353
+ predicate (`if inserted == 1`) is race-safe without a follow-up SELECT.
354
+ ``period`` (#137) is the configured period noun at crossing
355
+ ('calendar-week'|'calendar-month'|'subscription-week'); it discriminates the
356
+ UNIQUE key so calendar-week and calendar-month windows that share a start
357
+ instant don't collide. A NULL ``period`` is the pre-011 "unknown" sentinel
358
+ (only seeded migration rows carry it). ``alerted_at`` is left NULL — the
359
+ caller stamps it in the SAME transaction BEFORE dispatching (set-then-dispatch
360
+ invariant, CLAUDE.md Alerts gotcha). ``commit=False`` lets the caller bundle
361
+ the INSERT with the follow-up ``alerted_at`` UPDATE in one transaction so a
362
+ crash between them can't strand ``alerted_at`` NULL forever.
406
363
  """
407
364
  cur = conn.execute(
408
- "INSERT OR IGNORE INTO codex_budget_milestones "
409
- "(period_start_at, period, threshold, budget_usd, spent_usd, "
365
+ "INSERT OR IGNORE INTO budget_milestones "
366
+ "(vendor, period_start_at, period, threshold, budget_usd, spent_usd, "
410
367
  " consumption_pct, crossed_at_utc) "
411
- "VALUES (?, ?, ?, ?, ?, ?, ?)",
368
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
412
369
  (
370
+ str(vendor),
413
371
  period_start_at,
414
372
  period,
415
373
  int(threshold),
@@ -555,50 +513,92 @@ def _resolve_claude_budget_window(conn, now_utc, *, period, config, tz):
555
513
  §6). Subscription-week → the existing ``_resolve_current_budget_window``
556
514
  (snapshot-anchored; may return ``None`` when no usage snapshot has landed
557
515
  yet). Calendar period → the pure ``_resolve_calendar_window`` (derived purely
558
- from ``now`` + the period; NEVER ``None``). The dedup key column stays
559
- ``week_start_at`` — for a calendar period it carries the resolved PERIOD-start
560
- instant (a back-compat misnomer)."""
516
+ from ``now`` + the period; NEVER ``None``). The dedup key column is now
517
+ ``period_start_at`` (#143) — it carries the resolved PERIOD-start instant
518
+ (subscription-week OR calendar period-start)."""
561
519
  c = _cctally()
562
520
  if period == "subscription-week":
563
521
  return c._resolve_current_budget_window(conn, now_utc)
564
522
  return c._resolve_calendar_window(period, now_utc, config, tz)
565
523
 
566
524
 
525
+ def _resolve_budget_window(conn, *, vendor, now_utc, period, config, tz):
526
+ """Resolve the budget period-start instant for ``vendor`` (#143). CHEAP — does
527
+ NO cost SUM, preserving the pre-probe-before-spend hot path (spec §4.2): the
528
+ firing paths resolve this cheap window, pre-probe which thresholds are already
529
+ latched, and skip the cost SUM entirely when nothing is pending.
530
+
531
+ Dispatches to the per-vendor window primitive:
532
+ * claude → :func:`_resolve_claude_budget_window` (snapshot-anchored for
533
+ subscription-week → may be ``None`` pre-snapshot; calendar period → the
534
+ pure calendar window, never ``None``).
535
+ * codex → :func:`_resolve_codex_budget_period_window` (pure calendar window;
536
+ never ``None``).
537
+
538
+ Returns the period-start ``datetime`` or ``None`` (claude subscription-week
539
+ pre-snapshot)."""
540
+ if vendor == "claude":
541
+ window = _resolve_claude_budget_window(
542
+ conn, now_utc, period=period, config=config, tz=tz
543
+ )
544
+ else:
545
+ window = _resolve_codex_budget_period_window(period, now_utc, config, tz)
546
+ if window is None:
547
+ return None
548
+ start_at, _end_at = window
549
+ return start_at
550
+
551
+
552
+ def _budget_spend_for_vendor(conn, *, vendor, start_at, now_utc) -> float:
553
+ """Spend over ``[start_at, now]`` for ``vendor`` (#143) — the COSTLY leg,
554
+ called only after the pre-probe finds pending thresholds (spec §4.2). claude
555
+ routes through the Claude cost SUM (``mode="auto"``); codex through the Codex
556
+ cost SUM."""
557
+ c = _cctally()
558
+ if vendor == "claude":
559
+ return c._sum_cost_for_range(start_at, now_utc, mode="auto")
560
+ return c._sum_codex_cost_for_range(start_at, now_utc)
561
+
562
+
567
563
  def _reconcile_budget_milestones_on_set(
568
- conn, *, target, thresholds, now_utc, period="subscription-week",
569
- config=None, tz=None,
564
+ conn, *, vendor, target, thresholds, now_utc, period, config=None, tz=None,
570
565
  ):
571
- """Forward-only-from-set reconcile (spec §5): on `budget set`, every
572
- threshold ALREADY crossed for the current week/period is recorded with
573
- ``alerted_at`` SET but WITHOUT dispatch — so setting a budget when you're
574
- already at 95% does NOT instant-popup. Thresholds not yet crossed get NO
575
- row, so they fire later via :func:`maybe_record_budget_milestone`.
576
-
577
- A mid-week target change re-runs this; thresholds already alerted stay
578
- deduped via UNIQUE(week_start_at, period, threshold) + the ``alerted_at IS
579
- NULL`` guard on the UPDATE (so an existing alerted row is never re-stamped).
580
-
581
- ``period`` defaults to subscription-week (byte-stable legacy behavior); a
582
- calendar period resolves the window from ``now`` + the period instead of the
583
- snapshot anchor (calendar-period-codex-budgets generalization, spec §6).
566
+ """Forward-only-from-set reconcile for the budget axis (both vendors, #143):
567
+ on `budget set`, every threshold ALREADY crossed for the current
568
+ window/period is recorded with ``alerted_at`` SET but WITHOUT dispatch — so
569
+ setting a budget when you're already at 95% does NOT instant-popup. Thresholds
570
+ not yet crossed get NO row, so they fire later via the firing path
571
+ (:func:`maybe_record_budget_milestone` / :func:`maybe_record_codex_budget_milestone`).
572
+
573
+ A mid-window target change re-runs this; thresholds already alerted stay
574
+ deduped via UNIQUE(vendor, period_start_at, period, threshold) + the
575
+ ``alerted_at IS NULL`` guard on the UPDATE (so an existing alerted row is never
576
+ re-stamped).
577
+
578
+ Cold path — no pre-probe; resolves the cheap window then computes spend right
579
+ after (spec §4.2 ordering). ``vendor`` selects the window + spend dispatcher;
580
+ claude subscription-week may resolve ``None`` pre-snapshot (early return).
581
+ Keeps its own stamp-no-dispatch tail (distinct from :func:`_budget_crossings`,
582
+ which dispatches) — that asymmetry is intrinsic, not duplication.
584
583
  """
585
- c = _cctally()
586
- window = _resolve_claude_budget_window(
587
- conn, now_utc, period=period, config=config, tz=tz
584
+ start_at = _resolve_budget_window(
585
+ conn, vendor=vendor, now_utc=now_utc, period=period, config=config, tz=tz
588
586
  )
589
- if window is None:
587
+ if start_at is None:
590
588
  return
591
- week_start_at, _week_end_at = window
592
- week_key = week_start_at.isoformat(timespec="seconds")
593
- spent = c._sum_cost_for_range(week_start_at, now_utc, mode="auto")
594
- # target > 0 guaranteed by the caller (_cmd_budget_set passes the validated
595
- # weekly_usd); the else is belt-and-suspenders.
589
+ period_key = start_at.isoformat(timespec="seconds")
590
+ spent = _budget_spend_for_vendor(
591
+ conn, vendor=vendor, start_at=start_at, now_utc=now_utc
592
+ )
593
+ # target > 0 guaranteed by the caller (validated weekly_usd / amount_usd);
594
+ # the else is belt-and-suspenders.
596
595
  consumption_pct = (spent / target * 100.0) if target > 0 else 0.0
597
596
  for t in sorted(thresholds):
598
597
  if consumption_pct + 1e-9 >= t:
599
598
  insert_budget_milestone(
600
599
  conn,
601
- week_start_at=week_key,
600
+ vendor=vendor,
601
+ period_start_at=period_key,
602
602
  period=period,
603
603
  threshold=t,
604
604
  budget_usd=target,
@@ -606,14 +606,14 @@ def _reconcile_budget_milestones_on_set(
606
606
  consumption_pct=consumption_pct,
607
607
  commit=False,
608
608
  )
609
- # alerted_at UPDATE keys on the CONCRETE period (not the wildcard):
610
- # only the row we just inserted under `period` is stamped, never a
611
- # pre-011 NULL-period sibling (#137).
609
+ # alerted_at UPDATE keys on the CONCRETE (vendor, period) (not the
610
+ # wildcard): only the row we just inserted is stamped, never a
611
+ # pre-011 NULL-period sibling (#137) or another vendor's row (#143).
612
612
  conn.execute(
613
613
  "UPDATE budget_milestones SET alerted_at = ? "
614
- "WHERE week_start_at = ? AND period = ? AND threshold = ? "
615
- " AND alerted_at IS NULL",
616
- (now_utc_iso(), week_key, period, t),
614
+ "WHERE vendor = ? AND period_start_at = ? AND period = ? "
615
+ " AND threshold = ? AND alerted_at IS NULL",
616
+ (now_utc_iso(), vendor, period_key, period, t),
617
617
  )
618
618
  conn.commit()
619
619
 
@@ -630,34 +630,36 @@ def _resolve_codex_budget_period_window(period, now_utc, config, tz):
630
630
  return c._resolve_calendar_window(period, now_utc, config, tz)
631
631
 
632
632
 
633
- def _codex_budget_crossings(
634
- conn, *, period_key, period=None, thresholds, target, spent, now_utc
633
+ def _budget_crossings(
634
+ conn, *, vendor, period_key, period=None, thresholds, target, spent, now_utc
635
635
  ):
636
- """Shared INSERT-and-arm core for the Codex budget axis: for every
637
- STILL-pending threshold that's been crossed at ``spent``, ``INSERT OR
638
- IGNORE`` a milestone (commit=False) and on the genuine-new-crossing winner
639
- (rowcount==1) — stamp ``alerted_at`` in the SAME transaction (set-then-
640
- dispatch), returning the list of crossings the caller must dispatch.
636
+ """Shared INSERT-and-arm core for the budget axis (both vendors, #143): for
637
+ every STILL-pending threshold that's been crossed at ``spent``, ``INSERT OR
638
+ IGNORE`` a milestone (commit=False) into the unified vendor-tagged table and —
639
+ on the genuine-new-crossing winner (rowcount==1) — stamp ``alerted_at`` in the
640
+ SAME transaction (set-then-dispatch), returning the list of crossings the
641
+ caller must dispatch.
641
642
 
642
643
  Pure of config/window resolution: both firing sites (record-usage +
643
- opportunistic ``cctally budget``) feed the already-resolved ``period_key`` /
644
- ``target`` / ``spent`` here, so the crossing arithmetic + the set-then-
645
- dispatch invariant live in ONE place (plan §3.6 "one shared helper"). Does
646
- NOT commit — the caller owns the single durable commit that bundles every
644
+ opportunistic ``cctally budget``), for either vendor, feed the
645
+ already-resolved ``vendor`` / ``period_key`` / ``target`` / ``spent`` here, so
646
+ the crossing arithmetic + the set-then-dispatch invariant live in ONE place.
647
+ Does NOT commit — the caller owns the single durable commit that bundles every
647
648
  INSERT with its ``alerted_at`` UPDATE. Applies the +1e-9 float-floor snap
648
649
  (CLAUDE.md gotcha). Returns ``[(threshold, crossed_at, spent, target,
649
650
  consumption_pct), ...]`` for the rowcount==1 winners only.
650
651
 
651
652
  Forward-only / fire-once is enforced by INSERT OR IGNORE's rowcount on the
652
- UNIQUE(period_start_at, period, threshold) key; a racing record-usage
653
+ UNIQUE(vendor, period_start_at, period, threshold) key; a racing record-usage
653
654
  instance OR an already-recorded threshold gets rowcount==0 and is skipped
654
655
  ([Dedup mustn't gate side effects])."""
655
656
  consumption_pct = (spent / target * 100.0) if target > 0 else 0.0
656
657
  fired: "list" = []
657
658
  for t in sorted(thresholds):
658
659
  if consumption_pct + 1e-9 >= t:
659
- inserted = insert_codex_budget_milestone(
660
+ inserted = insert_budget_milestone(
660
661
  conn,
662
+ vendor=vendor,
661
663
  period_start_at=period_key,
662
664
  period=period,
663
665
  threshold=t,
@@ -668,63 +670,19 @@ def _codex_budget_crossings(
668
670
  )
669
671
  if inserted == 1:
670
672
  crossed_at = now_utc_iso()
671
- # alerted_at UPDATE keys on the CONCRETE period (#137): only the
672
- # row just inserted under `period` is stamped, never a pre-011
673
- # NULL-period sibling.
673
+ # alerted_at UPDATE keys on the CONCRETE (vendor, period) (#137 /
674
+ # #143): only the row just inserted is stamped, never a pre-011
675
+ # NULL-period sibling or another vendor's row.
674
676
  conn.execute(
675
- "UPDATE codex_budget_milestones SET alerted_at = ? "
676
- "WHERE period_start_at = ? AND period = ? AND threshold = ? "
677
- " AND alerted_at IS NULL",
678
- (crossed_at, period_key, period, t),
677
+ "UPDATE budget_milestones SET alerted_at = ? "
678
+ "WHERE vendor = ? AND period_start_at = ? AND period = ? "
679
+ " AND threshold = ? AND alerted_at IS NULL",
680
+ (crossed_at, vendor, period_key, period, t),
679
681
  )
680
682
  fired.append((t, crossed_at, spent, target, consumption_pct))
681
683
  return fired
682
684
 
683
685
 
684
- def _reconcile_codex_budget_milestones_on_set(
685
- conn, *, target, thresholds, now_utc, period, config, tz
686
- ):
687
- """Forward-only-from-set reconcile for the Codex budget axis (spec §6),
688
- mirroring :func:`_reconcile_budget_milestones_on_set` but keyed on the
689
- resolved CALENDAR period window instead of the subscription week.
690
-
691
- On a Codex `budget set` (or `config set budget.codex`), every threshold
692
- ALREADY crossed for the current period is recorded with ``alerted_at`` SET
693
- but WITHOUT dispatch — so setting a Codex budget mid-month while already over
694
- does NOT instant-popup; a mid-period amount change never re-alerts an
695
- already-fired threshold (deduped via UNIQUE(period_start_at, period,
696
- threshold) + the ``alerted_at IS NULL`` UPDATE guard). Thresholds not yet
697
- crossed get NO row, so they fire later via
698
- :func:`maybe_record_codex_budget_milestone`."""
699
- c = _cctally()
700
- start_at, _end_at = _resolve_codex_budget_period_window(
701
- period, now_utc, config, tz
702
- )
703
- period_key = start_at.isoformat(timespec="seconds")
704
- spent = c._sum_codex_cost_for_range(start_at, now_utc)
705
- consumption_pct = (spent / target * 100.0) if target > 0 else 0.0
706
- for t in sorted(thresholds):
707
- if consumption_pct + 1e-9 >= t:
708
- insert_codex_budget_milestone(
709
- conn,
710
- period_start_at=period_key,
711
- period=period,
712
- threshold=t,
713
- budget_usd=target,
714
- spent_usd=spent,
715
- consumption_pct=consumption_pct,
716
- commit=False,
717
- )
718
- # alerted_at UPDATE keys on the CONCRETE period (#137).
719
- conn.execute(
720
- "UPDATE codex_budget_milestones SET alerted_at = ? "
721
- "WHERE period_start_at = ? AND period = ? AND threshold = ? "
722
- " AND alerted_at IS NULL",
723
- (now_utc_iso(), period_key, period, t),
724
- )
725
- conn.commit()
726
-
727
-
728
686
  def _reconcile_codex_budget_on_config_write(validated_budget):
729
687
  """Forward-only reconcile shared by the Codex-budget config write paths
730
688
  (`budget set --vendor codex`, `config set budget.codex`). Gated +
@@ -745,8 +703,9 @@ def _reconcile_codex_budget_on_config_write(validated_budget):
745
703
  tz = c.resolve_display_tz(argparse.Namespace(tz=None), config)
746
704
  conn = open_db()
747
705
  try:
748
- _reconcile_codex_budget_milestones_on_set(
706
+ _reconcile_budget_milestones_on_set(
749
707
  conn,
708
+ vendor="codex",
750
709
  target=codex["amount_usd"],
751
710
  thresholds=thresholds,
752
711
  now_utc=_command_as_of(),
@@ -779,6 +738,7 @@ def _reconcile_budget_on_config_write(validated_budget):
779
738
  try:
780
739
  _reconcile_budget_milestones_on_set(
781
740
  conn,
741
+ vendor="claude",
782
742
  target=validated_budget["weekly_usd"],
783
743
  thresholds=thresholds,
784
744
  now_utc=_command_as_of(),