cctally 1.27.1 → 1.28.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.
- package/CHANGELOG.md +19 -0
- package/bin/_cctally_alerts.py +26 -1
- package/bin/_cctally_cache.py +278 -6
- package/bin/_cctally_config.py +153 -11
- package/bin/_cctally_core.py +230 -41
- package/bin/_cctally_dashboard.py +399 -37
- package/bin/_cctally_db.py +594 -163
- package/bin/_cctally_doctor.py +11 -0
- package/bin/_cctally_forecast.py +700 -57
- package/bin/_cctally_milestones.py +273 -28
- package/bin/_cctally_parser.py +44 -4
- package/bin/_cctally_record.py +328 -50
- package/bin/_cctally_weekrefs.py +30 -6
- package/bin/_lib_alert_axes.py +8 -1
- package/bin/_lib_alerts_payload.py +95 -3
- package/bin/_lib_budget.py +48 -0
- package/bin/_lib_conversation.py +162 -0
- package/bin/_lib_conversation_query.py +524 -0
- package/bin/_lib_doctor.py +60 -1
- package/bin/_lib_transcript_access.py +80 -0
- package/bin/cctally +27 -0
- package/dashboard/static/assets/{index-D34qf0LE.css → index-Bj5ckRUE.css} +1 -1
- package/dashboard/static/assets/index-Dw4G5FD9.js +18 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +4 -1
- package/dashboard/static/assets/index-C2F1_Mxt.js +0 -18
|
@@ -332,6 +332,7 @@ def insert_budget_milestone(
|
|
|
332
332
|
conn: sqlite3.Connection,
|
|
333
333
|
*,
|
|
334
334
|
week_start_at: str,
|
|
335
|
+
period: "str | None" = None,
|
|
335
336
|
threshold: int,
|
|
336
337
|
budget_usd: float,
|
|
337
338
|
spent_usd: float,
|
|
@@ -340,23 +341,77 @@ def insert_budget_milestone(
|
|
|
340
341
|
) -> int:
|
|
341
342
|
"""INSERT OR IGNORE a budget threshold crossing. Returns ``cur.rowcount``
|
|
342
343
|
(1 = genuinely new crossing, 0 = INSERT OR IGNORE no-op on a pre-existing
|
|
343
|
-
``(week_start_at, threshold)`` row).
|
|
344
|
+
``(week_start_at, period, threshold)`` row).
|
|
344
345
|
|
|
345
346
|
Mirrors :func:`insert_percent_milestone`'s rowcount contract so the
|
|
346
347
|
alert-fire predicate (`if inserted == 1`) is race-safe without a
|
|
347
|
-
follow-up SELECT. ``
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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.
|
|
352
358
|
"""
|
|
353
359
|
cur = conn.execute(
|
|
354
360
|
"INSERT OR IGNORE INTO budget_milestones "
|
|
355
|
-
"(week_start_at, threshold, budget_usd, spent_usd,
|
|
356
|
-
" crossed_at_utc) "
|
|
357
|
-
"VALUES (?, ?, ?, ?, ?, ?)",
|
|
361
|
+
"(week_start_at, period, threshold, budget_usd, spent_usd, "
|
|
362
|
+
" consumption_pct, crossed_at_utc) "
|
|
363
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
358
364
|
(
|
|
359
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
|
+
*,
|
|
382
|
+
period_start_at: str,
|
|
383
|
+
period: "str | None" = None,
|
|
384
|
+
threshold: int,
|
|
385
|
+
budget_usd: float,
|
|
386
|
+
spent_usd: float,
|
|
387
|
+
consumption_pct: float,
|
|
388
|
+
commit: bool = True,
|
|
389
|
+
) -> 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.
|
|
406
|
+
"""
|
|
407
|
+
cur = conn.execute(
|
|
408
|
+
"INSERT OR IGNORE INTO codex_budget_milestones "
|
|
409
|
+
"(period_start_at, period, threshold, budget_usd, spent_usd, "
|
|
410
|
+
" consumption_pct, crossed_at_utc) "
|
|
411
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
412
|
+
(
|
|
413
|
+
period_start_at,
|
|
414
|
+
period,
|
|
360
415
|
int(threshold),
|
|
361
416
|
float(budget_usd),
|
|
362
417
|
float(spent_usd),
|
|
@@ -419,6 +474,7 @@ def insert_projected_milestone(
|
|
|
419
474
|
conn: sqlite3.Connection,
|
|
420
475
|
*,
|
|
421
476
|
week_start_at: str,
|
|
477
|
+
period: "str | None" = None,
|
|
422
478
|
metric: str,
|
|
423
479
|
threshold: int,
|
|
424
480
|
projected_value: float,
|
|
@@ -427,23 +483,27 @@ def insert_projected_milestone(
|
|
|
427
483
|
) -> int:
|
|
428
484
|
"""INSERT OR IGNORE a projected-pace crossing. Returns ``cur.rowcount``
|
|
429
485
|
(1 = genuinely new crossing, 0 = INSERT OR IGNORE no-op on a pre-existing
|
|
430
|
-
``(week_start_at, metric, threshold)`` row).
|
|
486
|
+
``(week_start_at, period, metric, threshold)`` row).
|
|
431
487
|
|
|
432
488
|
Mirrors :func:`insert_budget_milestone`'s rowcount contract so the
|
|
433
489
|
alert-fire predicate (`if inserted == 1`) is race-safe without a follow-up
|
|
434
|
-
SELECT. ``
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
490
|
+
SELECT. ``period`` (#137) is the configured period at crossing — for the
|
|
491
|
+
``weekly_pct`` leg it is 'subscription-week', for ``budget_usd`` the Claude
|
|
492
|
+
configured period, for ``codex_budget_usd`` the Codex configured period;
|
|
493
|
+
NULL is the pre-011 unknown sentinel. ``alerted_at`` is left NULL — the
|
|
494
|
+
caller stamps it in the SAME transaction BEFORE dispatching (set-then-
|
|
495
|
+
dispatch invariant, CLAUDE.md Alerts gotcha). ``commit=False`` lets the
|
|
496
|
+
caller bundle the INSERT with the follow-up ``alerted_at`` UPDATE in one
|
|
497
|
+
transaction so a crash between them can't strand ``alerted_at`` NULL forever.
|
|
439
498
|
"""
|
|
440
499
|
cur = conn.execute(
|
|
441
500
|
"INSERT OR IGNORE INTO projected_milestones "
|
|
442
|
-
"(week_start_at, metric, threshold, projected_value,
|
|
443
|
-
" crossed_at_utc) "
|
|
444
|
-
"VALUES (?, ?, ?, ?, ?, ?)",
|
|
501
|
+
"(week_start_at, period, metric, threshold, projected_value, "
|
|
502
|
+
" denominator, crossed_at_utc) "
|
|
503
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
445
504
|
(
|
|
446
505
|
week_start_at,
|
|
506
|
+
period,
|
|
447
507
|
str(metric),
|
|
448
508
|
int(threshold),
|
|
449
509
|
float(projected_value),
|
|
@@ -460,42 +520,72 @@ def _projected_levels_already_latched(
|
|
|
460
520
|
conn: sqlite3.Connection,
|
|
461
521
|
*,
|
|
462
522
|
week_start_at: str,
|
|
523
|
+
period: "str | None" = None,
|
|
463
524
|
metric: str,
|
|
464
525
|
levels: "tuple[int, ...]",
|
|
465
526
|
) -> bool:
|
|
466
527
|
"""True iff EVERY level in ``levels`` already has a row for
|
|
467
|
-
``(week_start_at, metric)``.
|
|
528
|
+
``(week_start_at, period, metric)``.
|
|
468
529
|
|
|
469
530
|
Cheap indexed SELECT used as the pre-probe gate BEFORE any projection math
|
|
470
531
|
/ cost work ([Pre-probe before sync_cache]). Empty ``levels`` → True
|
|
471
532
|
(nothing owed). When False, at least one level is still un-recorded and the
|
|
472
533
|
caller must do the projection. Mirrors the per-week pre-probe SELECT in
|
|
473
534
|
:func:`maybe_record_budget_milestone`.
|
|
535
|
+
|
|
536
|
+
The ``period IS NULL`` arm (#137) means a pre-011 NULL-period row for the
|
|
537
|
+
current window counts as latched — so an upgrading user never re-fires a
|
|
538
|
+
spurious projected alert against a historical crossing.
|
|
474
539
|
"""
|
|
475
540
|
if not levels:
|
|
476
541
|
return True
|
|
477
542
|
rows = conn.execute(
|
|
478
543
|
"SELECT threshold FROM projected_milestones "
|
|
479
|
-
"WHERE week_start_at = ? AND
|
|
480
|
-
|
|
544
|
+
"WHERE week_start_at = ? AND (period = ? OR period IS NULL) "
|
|
545
|
+
" AND metric = ?",
|
|
546
|
+
(week_start_at, period, str(metric)),
|
|
481
547
|
).fetchall()
|
|
482
548
|
have = {int(r[0]) for r in rows}
|
|
483
549
|
return all(int(level) in have for level in levels)
|
|
484
550
|
|
|
485
551
|
|
|
486
|
-
def
|
|
552
|
+
def _resolve_claude_budget_window(conn, now_utc, *, period, config, tz):
|
|
553
|
+
"""Resolve the Claude budget's ``(start_utc, end_utc)`` window for the
|
|
554
|
+
configured ``period`` (calendar-period-codex-budgets generalization, spec
|
|
555
|
+
§6). Subscription-week → the existing ``_resolve_current_budget_window``
|
|
556
|
+
(snapshot-anchored; may return ``None`` when no usage snapshot has landed
|
|
557
|
+
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)."""
|
|
561
|
+
c = _cctally()
|
|
562
|
+
if period == "subscription-week":
|
|
563
|
+
return c._resolve_current_budget_window(conn, now_utc)
|
|
564
|
+
return c._resolve_calendar_window(period, now_utc, config, tz)
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def _reconcile_budget_milestones_on_set(
|
|
568
|
+
conn, *, target, thresholds, now_utc, period="subscription-week",
|
|
569
|
+
config=None, tz=None,
|
|
570
|
+
):
|
|
487
571
|
"""Forward-only-from-set reconcile (spec §5): on `budget set`, every
|
|
488
|
-
threshold ALREADY crossed for the current week is recorded with
|
|
572
|
+
threshold ALREADY crossed for the current week/period is recorded with
|
|
489
573
|
``alerted_at`` SET but WITHOUT dispatch — so setting a budget when you're
|
|
490
574
|
already at 95% does NOT instant-popup. Thresholds not yet crossed get NO
|
|
491
575
|
row, so they fire later via :func:`maybe_record_budget_milestone`.
|
|
492
576
|
|
|
493
577
|
A mid-week target change re-runs this; thresholds already alerted stay
|
|
494
|
-
deduped via UNIQUE(week_start_at, threshold) + the ``alerted_at IS
|
|
495
|
-
guard on the UPDATE (so an existing alerted row is never re-stamped).
|
|
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).
|
|
496
584
|
"""
|
|
497
585
|
c = _cctally()
|
|
498
|
-
window =
|
|
586
|
+
window = _resolve_claude_budget_window(
|
|
587
|
+
conn, now_utc, period=period, config=config, tz=tz
|
|
588
|
+
)
|
|
499
589
|
if window is None:
|
|
500
590
|
return
|
|
501
591
|
week_start_at, _week_end_at = window
|
|
@@ -509,20 +599,167 @@ def _reconcile_budget_milestones_on_set(conn, *, target, thresholds, now_utc):
|
|
|
509
599
|
insert_budget_milestone(
|
|
510
600
|
conn,
|
|
511
601
|
week_start_at=week_key,
|
|
602
|
+
period=period,
|
|
512
603
|
threshold=t,
|
|
513
604
|
budget_usd=target,
|
|
514
605
|
spent_usd=spent,
|
|
515
606
|
consumption_pct=consumption_pct,
|
|
516
607
|
commit=False,
|
|
517
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).
|
|
518
612
|
conn.execute(
|
|
519
613
|
"UPDATE budget_milestones SET alerted_at = ? "
|
|
520
|
-
"WHERE week_start_at = ? AND
|
|
521
|
-
|
|
614
|
+
"WHERE week_start_at = ? AND period = ? AND threshold = ? "
|
|
615
|
+
" AND alerted_at IS NULL",
|
|
616
|
+
(now_utc_iso(), week_key, period, t),
|
|
522
617
|
)
|
|
523
618
|
conn.commit()
|
|
524
619
|
|
|
525
620
|
|
|
621
|
+
def _resolve_codex_budget_period_window(period, now_utc, config, tz):
|
|
622
|
+
"""Resolve the Codex budget's ``(start_utc, end_utc)`` calendar window via
|
|
623
|
+
the forecast layer's ``_resolve_calendar_window`` (which routes to the pure
|
|
624
|
+
``calendar_month_window`` / ``calendar_week_window`` kernel functions). The
|
|
625
|
+
Codex axis NEVER touches ``weekly_usage_snapshots`` (no Anthropic week), so
|
|
626
|
+
the window comes purely from ``now`` + the configured calendar period.
|
|
627
|
+
``period`` is canonical (calendar-week / calendar-month — the validator
|
|
628
|
+
forbids subscription-week for Codex)."""
|
|
629
|
+
c = _cctally()
|
|
630
|
+
return c._resolve_calendar_window(period, now_utc, config, tz)
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
def _codex_budget_crossings(
|
|
634
|
+
conn, *, period_key, period=None, thresholds, target, spent, now_utc
|
|
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.
|
|
641
|
+
|
|
642
|
+
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
|
|
647
|
+
INSERT with its ``alerted_at`` UPDATE. Applies the +1e-9 float-floor snap
|
|
648
|
+
(CLAUDE.md gotcha). Returns ``[(threshold, crossed_at, spent, target,
|
|
649
|
+
consumption_pct), ...]`` for the rowcount==1 winners only.
|
|
650
|
+
|
|
651
|
+
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
|
+
instance OR an already-recorded threshold gets rowcount==0 and is skipped
|
|
654
|
+
([Dedup mustn't gate side effects])."""
|
|
655
|
+
consumption_pct = (spent / target * 100.0) if target > 0 else 0.0
|
|
656
|
+
fired: "list" = []
|
|
657
|
+
for t in sorted(thresholds):
|
|
658
|
+
if consumption_pct + 1e-9 >= t:
|
|
659
|
+
inserted = insert_codex_budget_milestone(
|
|
660
|
+
conn,
|
|
661
|
+
period_start_at=period_key,
|
|
662
|
+
period=period,
|
|
663
|
+
threshold=t,
|
|
664
|
+
budget_usd=target,
|
|
665
|
+
spent_usd=spent,
|
|
666
|
+
consumption_pct=consumption_pct,
|
|
667
|
+
commit=False,
|
|
668
|
+
)
|
|
669
|
+
if inserted == 1:
|
|
670
|
+
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.
|
|
674
|
+
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),
|
|
679
|
+
)
|
|
680
|
+
fired.append((t, crossed_at, spent, target, consumption_pct))
|
|
681
|
+
return fired
|
|
682
|
+
|
|
683
|
+
|
|
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
|
+
def _reconcile_codex_budget_on_config_write(validated_budget):
|
|
729
|
+
"""Forward-only reconcile shared by the Codex-budget config write paths
|
|
730
|
+
(`budget set --vendor codex`, `config set budget.codex`). Gated +
|
|
731
|
+
best-effort: a Codex budget with alerts off or no thresholds records
|
|
732
|
+
nothing; a stats.db failure never fails the write. Runs OUTSIDE any
|
|
733
|
+
config_writer_lock (open_db has its own locking). Mirrors
|
|
734
|
+
:func:`_reconcile_budget_on_config_write`."""
|
|
735
|
+
codex = (validated_budget or {}).get("codex")
|
|
736
|
+
if not codex:
|
|
737
|
+
return
|
|
738
|
+
thresholds = codex.get("alert_thresholds") or []
|
|
739
|
+
if not (codex.get("alerts_enabled") and codex.get("amount_usd") and thresholds):
|
|
740
|
+
return
|
|
741
|
+
c = _cctally()
|
|
742
|
+
try:
|
|
743
|
+
import argparse
|
|
744
|
+
config = c.load_config()
|
|
745
|
+
tz = c.resolve_display_tz(argparse.Namespace(tz=None), config)
|
|
746
|
+
conn = open_db()
|
|
747
|
+
try:
|
|
748
|
+
_reconcile_codex_budget_milestones_on_set(
|
|
749
|
+
conn,
|
|
750
|
+
target=codex["amount_usd"],
|
|
751
|
+
thresholds=thresholds,
|
|
752
|
+
now_utc=_command_as_of(),
|
|
753
|
+
period=codex["period"],
|
|
754
|
+
config=config,
|
|
755
|
+
tz=tz,
|
|
756
|
+
)
|
|
757
|
+
finally:
|
|
758
|
+
conn.close()
|
|
759
|
+
except Exception as exc: # best-effort; never fail the write
|
|
760
|
+
eprint(f"[codex-budget-milestone] reconcile on set failed: {exc}")
|
|
761
|
+
|
|
762
|
+
|
|
526
763
|
def _reconcile_budget_on_config_write(validated_budget):
|
|
527
764
|
"""Forward-only reconcile shared by all three budget-config write
|
|
528
765
|
paths (`budget set`, `config set budget.*`, dashboard POST
|
|
@@ -532,7 +769,12 @@ def _reconcile_budget_on_config_write(validated_budget):
|
|
|
532
769
|
thresholds = validated_budget.get("alert_thresholds") or []
|
|
533
770
|
if not (_budget_alerts_active(validated_budget) and thresholds):
|
|
534
771
|
return
|
|
772
|
+
c = _cctally()
|
|
773
|
+
period = validated_budget.get("period", "subscription-week")
|
|
535
774
|
try:
|
|
775
|
+
import argparse
|
|
776
|
+
config = c.load_config()
|
|
777
|
+
tz = c.resolve_display_tz(argparse.Namespace(tz=None), config)
|
|
536
778
|
conn = open_db()
|
|
537
779
|
try:
|
|
538
780
|
_reconcile_budget_milestones_on_set(
|
|
@@ -540,6 +782,9 @@ def _reconcile_budget_on_config_write(validated_budget):
|
|
|
540
782
|
target=validated_budget["weekly_usd"],
|
|
541
783
|
thresholds=thresholds,
|
|
542
784
|
now_utc=_command_as_of(),
|
|
785
|
+
period=period,
|
|
786
|
+
config=config,
|
|
787
|
+
tz=tz,
|
|
543
788
|
)
|
|
544
789
|
finally:
|
|
545
790
|
conn.close()
|
package/bin/_cctally_parser.py
CHANGED
|
@@ -1290,6 +1290,24 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1290
1290
|
"--project", nargs="?", const="__CWD__", default=None,
|
|
1291
1291
|
help="Set/unset a per-project budget for this git repo "
|
|
1292
1292
|
"(bare = current directory's git-root; or pass a path).")
|
|
1293
|
+
bg.add_argument(
|
|
1294
|
+
"--vendor", choices=["claude", "codex"], default="claude",
|
|
1295
|
+
help="Which vendor budget to set/unset (default claude). Codex "
|
|
1296
|
+
"budgets are calendar-period only.")
|
|
1297
|
+
bg.add_argument(
|
|
1298
|
+
"--period",
|
|
1299
|
+
# Accept both canonical and short spellings; the command handler
|
|
1300
|
+
# normalizes short->canonical and rejects `--vendor codex
|
|
1301
|
+
# --period subscription-week` (Codex has no Anthropic week). The choices
|
|
1302
|
+
# are single-sourced from `_BUDGET_PERIOD_CHOICES` (derived from the same
|
|
1303
|
+
# short→canonical map the normalizer uses), so they can't drift from the
|
|
1304
|
+
# handler (code-review #5).
|
|
1305
|
+
choices=c._BUDGET_PERIOD_CHOICES,
|
|
1306
|
+
default=None,
|
|
1307
|
+
help="Budget period: subscription-week (claude only) / calendar-week "
|
|
1308
|
+
"/ calendar-month. Default: preserve the stored period, else the "
|
|
1309
|
+
"per-vendor default (claude=subscription-week, codex="
|
|
1310
|
+
"calendar-month).")
|
|
1293
1311
|
bg.add_argument("--reveal-projects", action="store_true", dest="reveal_projects",
|
|
1294
1312
|
help="Show real project basenames in the per-project section "
|
|
1295
1313
|
"of --format output (default anonymizes to project-1/…).")
|
|
@@ -2192,16 +2210,21 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
2192
2210
|
cctally alerts test --axis five-hour --threshold 95
|
|
2193
2211
|
cctally alerts test --axis budget --threshold 100
|
|
2194
2212
|
cctally alerts test --axis project-budget --threshold 100
|
|
2213
|
+
cctally alerts test --axis codex-budget --threshold 100
|
|
2195
2214
|
cctally alerts test --axis projected --metric budget_usd
|
|
2215
|
+
cctally alerts test --axis projected --metric codex_budget_usd
|
|
2196
2216
|
"""),
|
|
2197
2217
|
)
|
|
2198
2218
|
p_alerts_test.add_argument(
|
|
2199
2219
|
"--axis",
|
|
2200
|
-
choices=[
|
|
2220
|
+
choices=[
|
|
2221
|
+
"weekly", "five-hour", "budget", "project-budget", "codex-budget",
|
|
2222
|
+
"projected",
|
|
2223
|
+
],
|
|
2201
2224
|
default="weekly",
|
|
2202
2225
|
help="Alert axis to simulate: weekly subscription window, 5h block, "
|
|
2203
|
-
"equiv-$ budget, per-project equiv-$ budget, or
|
|
2204
|
-
"(default: weekly).",
|
|
2226
|
+
"equiv-$ budget, per-project equiv-$ budget, Codex budget, or "
|
|
2227
|
+
"projected-pace (default: weekly).",
|
|
2205
2228
|
)
|
|
2206
2229
|
p_alerts_test.add_argument(
|
|
2207
2230
|
"--threshold",
|
|
@@ -2211,7 +2234,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
2211
2234
|
)
|
|
2212
2235
|
p_alerts_test.add_argument(
|
|
2213
2236
|
"--metric",
|
|
2214
|
-
choices=["weekly_pct", "budget_usd"],
|
|
2237
|
+
choices=["weekly_pct", "budget_usd", "codex_budget_usd"],
|
|
2215
2238
|
default="weekly_pct",
|
|
2216
2239
|
help="For --axis projected: which projected metric to preview "
|
|
2217
2240
|
"(default: weekly_pct).",
|
|
@@ -2334,6 +2357,23 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
2334
2357
|
)
|
|
2335
2358
|
db_unskip.set_defaults(func=c.cmd_db_unskip)
|
|
2336
2359
|
|
|
2360
|
+
db_recover = db_sub.add_parser(
|
|
2361
|
+
"recover",
|
|
2362
|
+
help="Revert a version-ahead DB to the known schema head (#145)",
|
|
2363
|
+
)
|
|
2364
|
+
db_recover.add_argument(
|
|
2365
|
+
"--db",
|
|
2366
|
+
required=True,
|
|
2367
|
+
choices=("cache", "stats"),
|
|
2368
|
+
help="Which DB to recover",
|
|
2369
|
+
)
|
|
2370
|
+
db_recover.add_argument(
|
|
2371
|
+
"--yes",
|
|
2372
|
+
action="store_true",
|
|
2373
|
+
help="Required for --db stats (non-re-derivable; may need a re-record)",
|
|
2374
|
+
)
|
|
2375
|
+
db_recover.set_defaults(func=c.cmd_db_recover)
|
|
2376
|
+
|
|
2337
2377
|
# ─── doctor (Diagnostics) ───────────────────────────────────────────
|
|
2338
2378
|
doctor_p = sub.add_parser(
|
|
2339
2379
|
"doctor",
|