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
package/bin/_cctally_record.py
CHANGED
|
@@ -269,6 +269,30 @@ def _build_alert_payload_project_budget(*args, **kwargs):
|
|
|
269
269
|
return sys.modules["cctally"]._build_alert_payload_project_budget(*args, **kwargs)
|
|
270
270
|
|
|
271
271
|
|
|
272
|
+
def _build_alert_payload_codex_budget(*args, **kwargs):
|
|
273
|
+
return sys.modules["cctally"]._build_alert_payload_codex_budget(*args, **kwargs)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def insert_codex_budget_milestone(*args, **kwargs):
|
|
277
|
+
return sys.modules["cctally"].insert_codex_budget_milestone(*args, **kwargs)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _codex_budget_crossings(*args, **kwargs):
|
|
281
|
+
return sys.modules["cctally"]._codex_budget_crossings(*args, **kwargs)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _resolve_codex_budget_period_window(*args, **kwargs):
|
|
285
|
+
return sys.modules["cctally"]._resolve_codex_budget_period_window(*args, **kwargs)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _sum_codex_cost_for_range(*args, **kwargs):
|
|
289
|
+
return sys.modules["cctally"]._sum_codex_cost_for_range(*args, **kwargs)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def resolve_display_tz(*args, **kwargs):
|
|
293
|
+
return sys.modules["cctally"].resolve_display_tz(*args, **kwargs)
|
|
294
|
+
|
|
295
|
+
|
|
272
296
|
def _sum_cost_by_project(*args, **kwargs):
|
|
273
297
|
return sys.modules["cctally"]._sum_cost_by_project(*args, **kwargs)
|
|
274
298
|
|
|
@@ -297,6 +321,10 @@ def _resolve_current_budget_window(*args, **kwargs):
|
|
|
297
321
|
return sys.modules["cctally"]._resolve_current_budget_window(*args, **kwargs)
|
|
298
322
|
|
|
299
323
|
|
|
324
|
+
def _resolve_claude_budget_window(*args, **kwargs):
|
|
325
|
+
return sys.modules["cctally"]._resolve_claude_budget_window(*args, **kwargs)
|
|
326
|
+
|
|
327
|
+
|
|
300
328
|
def _sum_cost_for_range(*args, **kwargs):
|
|
301
329
|
return sys.modules["cctally"]._sum_cost_for_range(*args, **kwargs)
|
|
302
330
|
|
|
@@ -329,8 +357,8 @@ def _assess_forecast_confidence(*args, **kwargs):
|
|
|
329
357
|
return sys.modules["cctally"]._assess_forecast_confidence(*args, **kwargs)
|
|
330
358
|
|
|
331
359
|
|
|
332
|
-
def
|
|
333
|
-
return sys.modules["cctally"].
|
|
360
|
+
def _build_vendor_budget_inputs(*args, **kwargs):
|
|
361
|
+
return sys.modules["cctally"]._build_vendor_budget_inputs(*args, **kwargs)
|
|
334
362
|
|
|
335
363
|
|
|
336
364
|
def compute_budget_status(*args, **kwargs):
|
|
@@ -744,9 +772,11 @@ def maybe_record_budget_milestone(saved: dict[str, Any]) -> None:
|
|
|
744
772
|
# overhead for non-budget users. `load_config()` is safe outside any
|
|
745
773
|
# writer lock — atomic-rename guarantees whole-byte reads. A malformed
|
|
746
774
|
# budget block is a quiet warn-once no-op (mirrors weekly/5h), NOT an
|
|
747
|
-
# unthrottled per-tick stderr via the caller's wrapper.
|
|
775
|
+
# unthrottled per-tick stderr via the caller's wrapper. One config read
|
|
776
|
+
# services both the gate and the calendar-window tz resolution.
|
|
777
|
+
config = load_config()
|
|
748
778
|
try:
|
|
749
|
-
budget_cfg = _get_budget_config(
|
|
779
|
+
budget_cfg = _get_budget_config(config)
|
|
750
780
|
except _BudgetConfigError as exc:
|
|
751
781
|
_warn_budget_bad_config_once(exc)
|
|
752
782
|
return
|
|
@@ -756,12 +786,21 @@ def maybe_record_budget_milestone(saved: dict[str, Any]) -> None:
|
|
|
756
786
|
thresholds = budget_cfg["alert_thresholds"]
|
|
757
787
|
if not thresholds:
|
|
758
788
|
return
|
|
759
|
-
|
|
789
|
+
# Period generalization (spec §6): subscription-week resolves the snapshot-
|
|
790
|
+
# anchored window; a calendar period (calendar-week / calendar-month)
|
|
791
|
+
# resolves the window purely from `now` + the period and stores the
|
|
792
|
+
# period-start instant in the SAME `week_start_at` key column (back-compat
|
|
793
|
+
# misnomer). config/tz are resolved once for the calendar branch.
|
|
794
|
+
period = budget_cfg.get("period", "subscription-week")
|
|
795
|
+
|
|
796
|
+
tz = resolve_display_tz(argparse.Namespace(tz=None), config)
|
|
760
797
|
now_utc = _command_as_of()
|
|
761
798
|
pending_alerts: list[dict[str, Any]] = []
|
|
762
799
|
conn = open_db()
|
|
763
800
|
try:
|
|
764
|
-
window =
|
|
801
|
+
window = _resolve_claude_budget_window(
|
|
802
|
+
conn, now_utc, period=period, config=config, tz=tz
|
|
803
|
+
)
|
|
765
804
|
if window is None:
|
|
766
805
|
return # no resolvable week window yet (spec §6 worst case)
|
|
767
806
|
week_start_at, _week_end_at = window
|
|
@@ -773,10 +812,15 @@ def maybe_record_budget_milestone(saved: dict[str, Any]) -> None:
|
|
|
773
812
|
# — so a partial prior run that recorded some-but-not-all thresholds
|
|
774
813
|
# still gets the remaining ones a SUM + crossing-check. The skip never
|
|
775
814
|
# owes a crossing: an un-recorded threshold always forces the SUM.
|
|
815
|
+
# The `period IS NULL` arm (#137) makes a pre-011 NULL-period row for
|
|
816
|
+
# this window count as already-recorded, so an upgrading user never
|
|
817
|
+
# re-fires a spurious alert against a historical crossing; a row stored
|
|
818
|
+
# under the SAME concrete `period` also counts (fire-once).
|
|
776
819
|
present = {
|
|
777
820
|
int(r[0]) for r in conn.execute(
|
|
778
|
-
"SELECT threshold FROM budget_milestones
|
|
779
|
-
(
|
|
821
|
+
"SELECT threshold FROM budget_milestones "
|
|
822
|
+
"WHERE week_start_at = ? AND (period = ? OR period IS NULL)",
|
|
823
|
+
(week_key, period),
|
|
780
824
|
)
|
|
781
825
|
}
|
|
782
826
|
pending = [t for t in sorted(thresholds) if t not in present]
|
|
@@ -794,6 +838,7 @@ def maybe_record_budget_milestone(saved: dict[str, Any]) -> None:
|
|
|
794
838
|
inserted = insert_budget_milestone(
|
|
795
839
|
conn,
|
|
796
840
|
week_start_at=week_key,
|
|
841
|
+
period=period,
|
|
797
842
|
threshold=t,
|
|
798
843
|
budget_usd=target,
|
|
799
844
|
spent_usd=spent,
|
|
@@ -807,13 +852,14 @@ def maybe_record_budget_milestone(saved: dict[str, Any]) -> None:
|
|
|
807
852
|
# set-then-dispatch: alerted_at lands on the row BEFORE
|
|
808
853
|
# the osascript Popen, sharing this transaction with the
|
|
809
854
|
# INSERT (commit=False) so a crash between them is
|
|
810
|
-
# impossible.
|
|
811
|
-
#
|
|
855
|
+
# impossible. The UPDATE keys on the CONCRETE `period` (#137)
|
|
856
|
+
# — never a NULL-period sibling. `alerted_at IS NULL` guard
|
|
857
|
+
# is write-once defense-in-depth.
|
|
812
858
|
conn.execute(
|
|
813
859
|
"UPDATE budget_milestones SET alerted_at = ? "
|
|
814
|
-
"WHERE week_start_at = ? AND threshold = ? "
|
|
860
|
+
"WHERE week_start_at = ? AND period = ? AND threshold = ? "
|
|
815
861
|
" AND alerted_at IS NULL",
|
|
816
|
-
(crossed_at, week_key, t),
|
|
862
|
+
(crossed_at, week_key, period, t),
|
|
817
863
|
)
|
|
818
864
|
pending_alerts.append(_build_alert_payload_budget(
|
|
819
865
|
threshold=t,
|
|
@@ -822,6 +868,7 @@ def maybe_record_budget_milestone(saved: dict[str, Any]) -> None:
|
|
|
822
868
|
budget_usd=target,
|
|
823
869
|
spent_usd=spent,
|
|
824
870
|
consumption_pct=consumption_pct,
|
|
871
|
+
period=period,
|
|
825
872
|
))
|
|
826
873
|
# Single commit: every INSERT + its alerted_at marker durable together.
|
|
827
874
|
conn.commit()
|
|
@@ -1005,6 +1052,114 @@ def maybe_record_project_budget_milestone(saved: dict[str, Any]) -> None:
|
|
|
1005
1052
|
eprint(f"[project-budget-alerts] dispatch failed: {dispatch_exc}")
|
|
1006
1053
|
|
|
1007
1054
|
|
|
1055
|
+
def maybe_record_codex_budget_milestone(saved: dict[str, Any]) -> None:
|
|
1056
|
+
"""Fire Codex budget alerts on ACTUAL-Codex-spend threshold crossings (axis
|
|
1057
|
+
``codex_budget``, calendar-period-codex-budgets spec §6 — the gap the Codex
|
|
1058
|
+
spec review flagged: Codex usage never flows through ``record-usage``, so the
|
|
1059
|
+
Claude budget axes can't catch it). Called from ``cmd_record_usage``
|
|
1060
|
+
alongside the weekly-% / 5h-% / budget / project-budget milestone helpers AND
|
|
1061
|
+
opportunistically from ``cmd_budget`` (the Codex section already resolves
|
|
1062
|
+
spend + crossings). Forward-only / fire-once, so the double-trigger never
|
|
1063
|
+
double-fires. Gated, hot-path-cheap, set-then-dispatch. Errors are logged,
|
|
1064
|
+
not raised (the caller also wraps).
|
|
1065
|
+
|
|
1066
|
+
Unlike the Claude budget axis, Codex has NO subscription week: the period
|
|
1067
|
+
window is resolved purely from ``now`` + the configured calendar period
|
|
1068
|
+
(calendar-week / calendar-month) via the pure ``calendar_*_window``
|
|
1069
|
+
functions — it NEVER touches ``weekly_usage_snapshots``. The crossing +
|
|
1070
|
+
set-then-dispatch arithmetic is factored into the shared
|
|
1071
|
+
``_codex_budget_crossings`` helper so this firing path and the
|
|
1072
|
+
``cctally budget`` opportunistic path stay byte-identical (plan §3.6).
|
|
1073
|
+
|
|
1074
|
+
``saved`` is accepted for call-site symmetry with the sibling helpers but is
|
|
1075
|
+
unused: Codex spend is resolved from the cache DB (``_sum_codex_cost_for_range``)
|
|
1076
|
+
independent of the just-recorded 7d-% snapshot.
|
|
1077
|
+
"""
|
|
1078
|
+
# Gate FIRST (hot-path discipline): no Codex budget OR alerts off → zero
|
|
1079
|
+
# overhead for non-Codex-budget users. `load_config()` is safe outside any
|
|
1080
|
+
# writer lock (atomic-rename). A malformed budget block is a quiet warn-once
|
|
1081
|
+
# no-op (mirrors maybe_record_budget_milestone). One config read services
|
|
1082
|
+
# both the gate and the calendar-window tz resolution.
|
|
1083
|
+
config = load_config()
|
|
1084
|
+
try:
|
|
1085
|
+
budget_cfg = _get_budget_config(config)
|
|
1086
|
+
except _BudgetConfigError as exc:
|
|
1087
|
+
_warn_budget_bad_config_once(exc)
|
|
1088
|
+
return
|
|
1089
|
+
codex_cfg = budget_cfg.get("codex")
|
|
1090
|
+
if not codex_cfg or not codex_cfg.get("alerts_enabled"):
|
|
1091
|
+
return
|
|
1092
|
+
target = codex_cfg.get("amount_usd")
|
|
1093
|
+
thresholds = codex_cfg.get("alert_thresholds") or []
|
|
1094
|
+
if target is None or not thresholds:
|
|
1095
|
+
return
|
|
1096
|
+
period = codex_cfg["period"]
|
|
1097
|
+
|
|
1098
|
+
tz = resolve_display_tz(argparse.Namespace(tz=None), config)
|
|
1099
|
+
now_utc = _command_as_of()
|
|
1100
|
+
pending_alerts: list[dict[str, Any]] = []
|
|
1101
|
+
conn = open_db()
|
|
1102
|
+
try:
|
|
1103
|
+
start_at, _end_at = _resolve_codex_budget_period_window(
|
|
1104
|
+
period, now_utc, config, tz
|
|
1105
|
+
)
|
|
1106
|
+
period_key = start_at.isoformat(timespec="seconds")
|
|
1107
|
+
|
|
1108
|
+
# Pre-probe (hot-path discipline + [Dedup mustn't gate side effects]):
|
|
1109
|
+
# which configured thresholds are STILL un-recorded for this period? The
|
|
1110
|
+
# Codex cost SUM is skipped ONLY when every threshold already has a row.
|
|
1111
|
+
# The `period IS NULL` arm (#137) makes a pre-011 NULL-period row for
|
|
1112
|
+
# this period count as already-recorded (no spurious upgrade re-fire); a
|
|
1113
|
+
# row under the SAME concrete `period` also counts (fire-once).
|
|
1114
|
+
present = {
|
|
1115
|
+
int(r[0]) for r in conn.execute(
|
|
1116
|
+
"SELECT threshold FROM codex_budget_milestones "
|
|
1117
|
+
"WHERE period_start_at = ? AND (period = ? OR period IS NULL)",
|
|
1118
|
+
(period_key, period),
|
|
1119
|
+
)
|
|
1120
|
+
}
|
|
1121
|
+
pending = [t for t in sorted(thresholds) if t not in present]
|
|
1122
|
+
if not pending:
|
|
1123
|
+
return # nothing left to cross this period → skip the cost SUM
|
|
1124
|
+
|
|
1125
|
+
spent = _sum_codex_cost_for_range(start_at, now_utc)
|
|
1126
|
+
# Shared INSERT-and-arm core (set-then-dispatch, fire-once via rowcount);
|
|
1127
|
+
# commit=False inside, so this conn owns the single durable commit below.
|
|
1128
|
+
for t, crossed_at, sp, tg, pct in _codex_budget_crossings(
|
|
1129
|
+
conn,
|
|
1130
|
+
period_key=period_key,
|
|
1131
|
+
period=period,
|
|
1132
|
+
thresholds=pending,
|
|
1133
|
+
target=target,
|
|
1134
|
+
spent=spent,
|
|
1135
|
+
now_utc=now_utc,
|
|
1136
|
+
):
|
|
1137
|
+
pending_alerts.append(_build_alert_payload_codex_budget(
|
|
1138
|
+
threshold=t,
|
|
1139
|
+
crossed_at_utc=crossed_at,
|
|
1140
|
+
period_start_at=period_key,
|
|
1141
|
+
period=period,
|
|
1142
|
+
budget_usd=tg,
|
|
1143
|
+
spent_usd=sp,
|
|
1144
|
+
consumption_pct=pct,
|
|
1145
|
+
))
|
|
1146
|
+
# Single commit: every INSERT + its alerted_at marker durable together.
|
|
1147
|
+
conn.commit()
|
|
1148
|
+
except Exception as exc:
|
|
1149
|
+
eprint(f"[codex-budget-milestone] error recording codex budget milestone: {exc}")
|
|
1150
|
+
finally:
|
|
1151
|
+
conn.close()
|
|
1152
|
+
|
|
1153
|
+
# Dispatch AFTER commit; a dispatch failure NEVER rolls back the milestone
|
|
1154
|
+
# (set-then-dispatch invariant — one queue attempt per crossing, deduped on
|
|
1155
|
+
# the alerted_at column).
|
|
1156
|
+
for payload in pending_alerts:
|
|
1157
|
+
try:
|
|
1158
|
+
_dispatch_alert_notification(payload, mode="real")
|
|
1159
|
+
except Exception as dispatch_exc:
|
|
1160
|
+
eprint(f"[codex-budget-alerts] dispatch failed: {dispatch_exc}")
|
|
1161
|
+
|
|
1162
|
+
|
|
1008
1163
|
def _weekly_pct_week_avg_projection(conn, now_utc):
|
|
1009
1164
|
"""Compute the week-AVERAGE weekly-% projection for the current
|
|
1010
1165
|
subscription week, snapshot-only (CHEAP — no cost SUM, no ``sync_cache``).
|
|
@@ -1058,27 +1213,39 @@ def _weekly_pct_week_avg_projection(conn, now_utc):
|
|
|
1058
1213
|
return (projected_pct, confidence == "low")
|
|
1059
1214
|
|
|
1060
1215
|
|
|
1061
|
-
def maybe_record_projected_alert(
|
|
1062
|
-
|
|
1216
|
+
def maybe_record_projected_alert(
|
|
1217
|
+
saved: dict[str, Any], *, only_metrics=None
|
|
1218
|
+
) -> None:
|
|
1219
|
+
"""Projected-pace detect-and-arm (axis ``projected``, #121 / #135).
|
|
1063
1220
|
|
|
1064
1221
|
Fires on the WEEK-AVERAGE projection (never the displayed high-end verdict
|
|
1065
|
-
band) for ``weekly_pct
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
blocks.
|
|
1222
|
+
band) for ``weekly_pct``, ``budget_usd`` (any Claude period — #135) and/or
|
|
1223
|
+
``codex_budget_usd`` (#135). Its OWN detect-and-arm — NOT folded into
|
|
1224
|
+
``maybe_record_milestone`` (Section 1 / Codex P0-3) — called from
|
|
1225
|
+
``cmd_record_usage`` in its own ``try`` after the weekly/5h/budget blocks.
|
|
1069
1226
|
|
|
1070
1227
|
Master gates (Codex P1-2): ``weekly_pct`` fires only under
|
|
1071
1228
|
``alerts.enabled && alerts.projected_enabled``; ``budget_usd`` only under
|
|
1072
|
-
``_budget_alerts_active(budget_cfg) && budget.projected_enabled
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1229
|
+
``_budget_alerts_active(budget_cfg) && budget.projected_enabled`` (#135:
|
|
1230
|
+
ALL Claude periods, not just subscription-week); ``codex_budget_usd`` only
|
|
1231
|
+
under ``codex.alerts_enabled && codex.projected_enabled`` with a set
|
|
1232
|
+
``amount_usd`` + ``alert_thresholds`` (mirrors
|
|
1233
|
+
``maybe_record_codex_budget_milestone``'s gate — there is no
|
|
1234
|
+
``_codex_budget_alerts_active`` helper). All toggles default OFF (no
|
|
1235
|
+
surprise notifications on upgrade). When NONE is on, returns after only a
|
|
1236
|
+
cheap config read — no projection math, no cost work.
|
|
1237
|
+
|
|
1238
|
+
``only_metrics`` (#135): when a set of metric names is passed (the
|
|
1239
|
+
opportunistic ``cctally budget`` fire passes ``{"codex_budget_usd"}``), only
|
|
1240
|
+
those legs run — so that interactive fire never pops a ``weekly_pct`` /
|
|
1241
|
+
Claude-``budget_usd`` notification. ``None`` (the record path) = every
|
|
1242
|
+
enabled leg.
|
|
1076
1243
|
|
|
1077
1244
|
Pre-probe (Codex P1-1): a metric whose levels are ALL already latched is
|
|
1078
1245
|
skipped BEFORE any projection / cost work.
|
|
1079
1246
|
|
|
1080
1247
|
Snap-up (Codex P2-1): a level fires when ``projected + 1e-9 >= threshold``.
|
|
1081
|
-
Latch / fire-once: ``UNIQUE(week_start_at, metric, threshold)`` + the
|
|
1248
|
+
Latch / fire-once: ``UNIQUE(week_start_at, period, metric, threshold)`` + the
|
|
1082
1249
|
rowcount==1 predicate — a later recovery neither un-fires nor re-fires.
|
|
1083
1250
|
Mid-week reset re-anchors ``week_start_at`` (budget pattern; no
|
|
1084
1251
|
``reset_event_id``).
|
|
@@ -1087,12 +1254,17 @@ def maybe_record_projected_alert(saved: dict[str, Any]) -> None:
|
|
|
1087
1254
|
txn, commit, THEN best-effort dispatch. A dispatch failure never rolls back
|
|
1088
1255
|
the milestone.
|
|
1089
1256
|
|
|
1090
|
-
|
|
1257
|
+
Both budget legs reuse the SAME ``_build_vendor_budget_inputs`` +
|
|
1091
1258
|
``compute_budget_status`` path that produces ``budget --json``'s
|
|
1092
1259
|
``week_avg_projection_usd`` (the reconcile-bound field) — value-exact by
|
|
1093
|
-
construction
|
|
1094
|
-
|
|
1095
|
-
|
|
1260
|
+
construction, keyed on the calendar/subscription period-start instant in the
|
|
1261
|
+
back-compat ``week_start_at`` column. The Claude leg passes ``skip_sync=True``
|
|
1262
|
+
(the cache is warmed by the actual-budget axis's spend SUM this same tick);
|
|
1263
|
+
the Codex leg passes ``skip_sync=False`` (R5: Codex has no other record-path
|
|
1264
|
+
warmer — ``maybe_record_codex_budget_milestone`` short-circuits before its SUM
|
|
1265
|
+
when all actual levels are latched, so a ``skip_sync=True`` Codex leg could
|
|
1266
|
+
read a cold cache and under-count; the delta-sync is a near-no-op when warm).
|
|
1267
|
+
The pre-probe skips each leg entirely when all its levels are already latched.
|
|
1096
1268
|
"""
|
|
1097
1269
|
# The `projected_enabled` toggles are validated keys on the alerts/budget
|
|
1098
1270
|
# blocks (bool-validated; default OFF), so read them straight off the
|
|
@@ -1114,12 +1286,44 @@ def maybe_record_projected_alert(saved: dict[str, Any]) -> None:
|
|
|
1114
1286
|
weekly_on = bool(alerts_cfg.get("enabled")) and bool(
|
|
1115
1287
|
alerts_cfg.get("projected_enabled")
|
|
1116
1288
|
)
|
|
1289
|
+
# #135: the Claude `budget_usd` leg now fires for ANY period (calendar-week /
|
|
1290
|
+
# calendar-month / subscription-week). `_build_vendor_budget_inputs` resolves
|
|
1291
|
+
# the correct window per period, and the milestone keys on that period-start
|
|
1292
|
+
# instant (in the back-compat `week_start_at` column) — the same key the
|
|
1293
|
+
# actual-budget axis uses — so there is no window/key mismatch any more.
|
|
1117
1294
|
budget_on = _budget_alerts_active(budget_cfg) and bool(
|
|
1118
1295
|
budget_cfg.get("projected_enabled")
|
|
1119
1296
|
)
|
|
1120
|
-
|
|
1297
|
+
# #135: the Codex `codex_budget_usd` leg. No `_codex_budget_alerts_active`
|
|
1298
|
+
# helper exists, so inline the gate mirroring
|
|
1299
|
+
# `maybe_record_codex_budget_milestone`: a Codex budget block with alerts +
|
|
1300
|
+
# projected on and a set amount/thresholds. (Projected requires
|
|
1301
|
+
# `alerts_enabled` too — same as the Claude leg, where `_budget_alerts_active`
|
|
1302
|
+
# requires it — documented in budget.md, not UI-enforced.)
|
|
1303
|
+
codex_cfg = budget_cfg.get("codex") or {}
|
|
1304
|
+
codex_on = (
|
|
1305
|
+
bool(codex_cfg)
|
|
1306
|
+
and bool(codex_cfg.get("alerts_enabled"))
|
|
1307
|
+
and bool(codex_cfg.get("projected_enabled"))
|
|
1308
|
+
and codex_cfg.get("amount_usd") is not None
|
|
1309
|
+
and bool(codex_cfg.get("alert_thresholds"))
|
|
1310
|
+
)
|
|
1311
|
+
# only_metrics scopes the opportunistic `cctally budget` fire to the Codex
|
|
1312
|
+
# leg so it never pops a weekly_pct / Claude budget_usd notification.
|
|
1313
|
+
if only_metrics is not None:
|
|
1314
|
+
weekly_on = weekly_on and "weekly_pct" in only_metrics
|
|
1315
|
+
budget_on = budget_on and "budget_usd" in only_metrics
|
|
1316
|
+
codex_on = codex_on and "codex_budget_usd" in only_metrics
|
|
1317
|
+
if not (weekly_on or budget_on or codex_on):
|
|
1121
1318
|
return # cheap config-only path — non-projected users pay nothing
|
|
1122
1319
|
|
|
1320
|
+
# Both budget legs resolve their window via _build_vendor_budget_inputs in
|
|
1321
|
+
# CONFIG tz (Namespace(tz=None)) — like maybe_record_codex_budget_milestone
|
|
1322
|
+
# — so a `cctally budget --tz X` opportunistic fire near a period boundary
|
|
1323
|
+
# resolves the SAME period_start_at dedup key as the record path and never
|
|
1324
|
+
# forks / double-fires.
|
|
1325
|
+
config_tz = resolve_display_tz(argparse.Namespace(tz=None), cfg)
|
|
1326
|
+
|
|
1123
1327
|
now_utc = _command_as_of()
|
|
1124
1328
|
pending: list[dict[str, Any]] = []
|
|
1125
1329
|
conn = open_db()
|
|
@@ -1134,9 +1338,10 @@ def maybe_record_projected_alert(saved: dict[str, Any]) -> None:
|
|
|
1134
1338
|
)
|
|
1135
1339
|
week_key = ws_at.isoformat(timespec="seconds")
|
|
1136
1340
|
levels = (90, 100)
|
|
1341
|
+
# weekly_pct is the Anthropic subscription week (#137).
|
|
1137
1342
|
if not _projected_levels_already_latched(
|
|
1138
|
-
conn, week_start_at=week_key,
|
|
1139
|
-
levels=levels,
|
|
1343
|
+
conn, week_start_at=week_key, period="subscription-week",
|
|
1344
|
+
metric="weekly_pct", levels=levels,
|
|
1140
1345
|
):
|
|
1141
1346
|
proj = _weekly_pct_week_avg_projection(conn, now_utc)
|
|
1142
1347
|
if proj is not None and not proj[1]:
|
|
@@ -1145,33 +1350,40 @@ def maybe_record_projected_alert(saved: dict[str, Any]) -> None:
|
|
|
1145
1350
|
if value + 1e-9 >= t:
|
|
1146
1351
|
pending.append(dict(
|
|
1147
1352
|
week_start_at=week_key,
|
|
1353
|
+
period="subscription-week",
|
|
1148
1354
|
metric="weekly_pct",
|
|
1149
1355
|
threshold=t,
|
|
1150
1356
|
projected_value=value,
|
|
1151
1357
|
denominator=100.0,
|
|
1152
1358
|
))
|
|
1153
1359
|
|
|
1154
|
-
# ── budget_usd leg (
|
|
1360
|
+
# ── budget_usd leg (any Claude period — #135; shared factory) ────────
|
|
1155
1361
|
if budget_on:
|
|
1156
1362
|
target = budget_cfg["weekly_usd"]
|
|
1157
1363
|
thresholds = tuple(
|
|
1158
1364
|
sorted(set(int(t) for t in budget_cfg["alert_thresholds"]))
|
|
1159
1365
|
)
|
|
1160
|
-
|
|
1366
|
+
claude_period = budget_cfg.get("period", "subscription-week")
|
|
1367
|
+
# Resolve the window key CHEAPLY first (SUM-free, same resolver the
|
|
1368
|
+
# actual-budget axis uses) so the pre-probe can short-circuit BEFORE
|
|
1369
|
+
# _build_vendor_budget_inputs runs any cost SUM / cache sync — the
|
|
1370
|
+
# pre-probe-runs-first contract (spec §3.4; mirrors the actual axis).
|
|
1371
|
+
window = _resolve_claude_budget_window(
|
|
1372
|
+
conn, now_utc, period=claude_period, config=cfg, tz=config_tz
|
|
1373
|
+
)
|
|
1161
1374
|
if window is not None and thresholds:
|
|
1162
1375
|
b_ws_at, _b_we_at = window
|
|
1163
1376
|
b_week_key = b_ws_at.isoformat(timespec="seconds")
|
|
1164
1377
|
if not _projected_levels_already_latched(
|
|
1165
|
-
conn, week_start_at=b_week_key,
|
|
1166
|
-
levels=thresholds,
|
|
1378
|
+
conn, week_start_at=b_week_key, period=claude_period,
|
|
1379
|
+
metric="budget_usd", levels=thresholds,
|
|
1167
1380
|
):
|
|
1168
|
-
# skip_sync=True: the actual-budget axis
|
|
1169
|
-
# (maybe_record_budget_milestone) already ran a
|
|
1381
|
+
# skip_sync=True: the actual-budget axis already ran a
|
|
1170
1382
|
# _sum_cost_for_range this same tick, warming the cache.
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1383
|
+
inputs = _build_vendor_budget_inputs(
|
|
1384
|
+
vendor="claude", period=claude_period, target_usd=target,
|
|
1385
|
+
alert_thresholds=thresholds, now_utc=now_utc, config=cfg,
|
|
1386
|
+
tz=config_tz, skip_sync=True,
|
|
1175
1387
|
)
|
|
1176
1388
|
if inputs is not None:
|
|
1177
1389
|
status = compute_budget_status(inputs)
|
|
@@ -1181,18 +1393,66 @@ def maybe_record_projected_alert(saved: dict[str, Any]) -> None:
|
|
|
1181
1393
|
if value + 1e-9 >= (t / 100.0) * float(target):
|
|
1182
1394
|
pending.append(dict(
|
|
1183
1395
|
week_start_at=b_week_key,
|
|
1396
|
+
period=claude_period,
|
|
1184
1397
|
metric="budget_usd",
|
|
1185
1398
|
threshold=t,
|
|
1186
1399
|
projected_value=value,
|
|
1187
1400
|
denominator=float(target),
|
|
1188
1401
|
))
|
|
1189
1402
|
|
|
1403
|
+
# ── codex_budget_usd leg (#135; skip_sync=False — R5) ────────────────
|
|
1404
|
+
if codex_on:
|
|
1405
|
+
c_target = codex_cfg["amount_usd"]
|
|
1406
|
+
c_thresholds = tuple(
|
|
1407
|
+
sorted(set(int(t) for t in codex_cfg["alert_thresholds"]))
|
|
1408
|
+
)
|
|
1409
|
+
c_period = codex_cfg["period"]
|
|
1410
|
+
# Cheap, SUM-free window key first (pure calendar resolution), so the
|
|
1411
|
+
# pre-probe short-circuits BEFORE any Codex cache sync / cost SUM —
|
|
1412
|
+
# spec §3.4 (pre-probe runs FIRST).
|
|
1413
|
+
c_window = _resolve_codex_budget_period_window(
|
|
1414
|
+
c_period, now_utc, cfg, config_tz
|
|
1415
|
+
)
|
|
1416
|
+
if c_window is not None and c_thresholds:
|
|
1417
|
+
c_ws_at, _c_we_at = c_window
|
|
1418
|
+
c_week_key = c_ws_at.isoformat(timespec="seconds")
|
|
1419
|
+
if not _projected_levels_already_latched(
|
|
1420
|
+
conn, week_start_at=c_week_key, period=c_period,
|
|
1421
|
+
metric="codex_budget_usd", levels=c_thresholds,
|
|
1422
|
+
):
|
|
1423
|
+
# skip_sync=False (R5): Codex has no other record-path cache
|
|
1424
|
+
# warmer (maybe_record_codex_budget_milestone short-circuits
|
|
1425
|
+
# before its SUM when all actual levels are latched), so a
|
|
1426
|
+
# skip_sync=True leg could read a cold cache and under-count.
|
|
1427
|
+
# The pre-probe above already gated this, so a sync only runs
|
|
1428
|
+
# when a cross is genuinely owed; it's a near-no-op when warm.
|
|
1429
|
+
c_inputs = _build_vendor_budget_inputs(
|
|
1430
|
+
vendor="codex", period=c_period, target_usd=c_target,
|
|
1431
|
+
alert_thresholds=c_thresholds, now_utc=now_utc,
|
|
1432
|
+
config=cfg, tz=config_tz, skip_sync=False,
|
|
1433
|
+
)
|
|
1434
|
+
if c_inputs is not None:
|
|
1435
|
+
c_status = compute_budget_status(c_inputs)
|
|
1436
|
+
if not c_status.low_confidence:
|
|
1437
|
+
value = c_status.week_avg_projection_usd
|
|
1438
|
+
for t in c_thresholds:
|
|
1439
|
+
if value + 1e-9 >= (t / 100.0) * float(c_target):
|
|
1440
|
+
pending.append(dict(
|
|
1441
|
+
week_start_at=c_week_key,
|
|
1442
|
+
period=c_period,
|
|
1443
|
+
metric="codex_budget_usd",
|
|
1444
|
+
threshold=t,
|
|
1445
|
+
projected_value=value,
|
|
1446
|
+
denominator=float(c_target),
|
|
1447
|
+
))
|
|
1448
|
+
|
|
1190
1449
|
# ── arm (set-then-dispatch): INSERT + stamp alerted_at in one txn ────
|
|
1191
1450
|
fired: list[dict[str, Any]] = []
|
|
1192
1451
|
for p in pending:
|
|
1193
1452
|
inserted = insert_projected_milestone(
|
|
1194
1453
|
conn,
|
|
1195
1454
|
week_start_at=p["week_start_at"],
|
|
1455
|
+
period=p["period"],
|
|
1196
1456
|
metric=p["metric"],
|
|
1197
1457
|
threshold=p["threshold"],
|
|
1198
1458
|
projected_value=p["projected_value"],
|
|
@@ -1200,14 +1460,15 @@ def maybe_record_projected_alert(saved: dict[str, Any]) -> None:
|
|
|
1200
1460
|
commit=False,
|
|
1201
1461
|
)
|
|
1202
1462
|
# Only the genuine-new-crossing winner (rowcount==1) arms+dispatches;
|
|
1203
|
-
# a racing record-usage instance gets rowcount==0 and skips.
|
|
1463
|
+
# a racing record-usage instance gets rowcount==0 and skips. The
|
|
1464
|
+
# alerted_at UPDATE keys on the CONCRETE `period` (#137).
|
|
1204
1465
|
if inserted == 1:
|
|
1205
1466
|
conn.execute(
|
|
1206
1467
|
"UPDATE projected_milestones SET alerted_at = ? "
|
|
1207
|
-
"WHERE week_start_at = ? AND
|
|
1208
|
-
" AND alerted_at IS NULL",
|
|
1209
|
-
(now_utc_iso(), p["week_start_at"], p["
|
|
1210
|
-
p["threshold"]),
|
|
1468
|
+
"WHERE week_start_at = ? AND period = ? AND metric = ? "
|
|
1469
|
+
" AND threshold = ? AND alerted_at IS NULL",
|
|
1470
|
+
(now_utc_iso(), p["week_start_at"], p["period"],
|
|
1471
|
+
p["metric"], p["threshold"]),
|
|
1211
1472
|
)
|
|
1212
1473
|
fired.append(p)
|
|
1213
1474
|
# Single commit: every INSERT + its alerted_at marker durable together.
|
|
@@ -2810,6 +3071,8 @@ def cmd_record_usage(args: argparse.Namespace) -> int:
|
|
|
2810
3071
|
(maybe_record_budget_milestone, "budget-milestone"),
|
|
2811
3072
|
(maybe_record_project_budget_milestone,
|
|
2812
3073
|
"project-budget-milestone"),
|
|
3074
|
+
(maybe_record_codex_budget_milestone,
|
|
3075
|
+
"codex-budget-milestone"),
|
|
2813
3076
|
(maybe_record_projected_alert, "projected-alert"),
|
|
2814
3077
|
):
|
|
2815
3078
|
try:
|
|
@@ -2869,11 +3132,26 @@ def cmd_record_usage(args: argparse.Namespace) -> int:
|
|
|
2869
3132
|
except Exception as exc:
|
|
2870
3133
|
eprint(f"[project-budget-milestone] unexpected error: {exc}")
|
|
2871
3134
|
|
|
2872
|
-
# NEW:
|
|
2873
|
-
#
|
|
2874
|
-
#
|
|
2875
|
-
#
|
|
2876
|
-
#
|
|
3135
|
+
# NEW: Codex budget alert firing (axis `codex_budget`, calendar-period-codex-
|
|
3136
|
+
# budgets). Gated FIRST on a configured budget.codex + alerts_enabled — non-
|
|
3137
|
+
# Codex-budget users pay only one config read. Codex usage never flows through
|
|
3138
|
+
# record-usage, so this is one of the two firing triggers (the other is the
|
|
3139
|
+
# opportunistic fire on `cctally budget`); forward-only/fire-once means the
|
|
3140
|
+
# double-trigger never double-fires.
|
|
3141
|
+
try:
|
|
3142
|
+
maybe_record_codex_budget_milestone(saved)
|
|
3143
|
+
except Exception as exc:
|
|
3144
|
+
eprint(f"[codex-budget-milestone] unexpected error: {exc}")
|
|
3145
|
+
|
|
3146
|
+
# NEW: projected-pace alert firing (axis `projected`, #121/#135). Runs in
|
|
3147
|
+
# its OWN detect-and-arm AFTER the weekly/5h/budget/codex blocks; gated up
|
|
3148
|
+
# front on (alerts.enabled && alerts.projected_enabled) ||
|
|
3149
|
+
# (_budget_alerts_active && budget.projected_enabled — ANY Claude period,
|
|
3150
|
+
# #135) || (codex.alerts_enabled && codex.projected_enabled, #135) — all
|
|
3151
|
+
# toggles default OFF, so non-projected users pay only a cheap config read.
|
|
3152
|
+
# No only_metrics on the record path: every enabled leg runs. The Codex leg
|
|
3153
|
+
# relies on the codex-budget block above (:3129) having warmed the cache,
|
|
3154
|
+
# but is robust even if it short-circuited (skip_sync=False self-syncs, R5).
|
|
2877
3155
|
try:
|
|
2878
3156
|
maybe_record_projected_alert(saved)
|
|
2879
3157
|
except Exception as exc:
|
package/bin/_cctally_weekrefs.py
CHANGED
|
@@ -233,6 +233,15 @@ def _backfill_week_reset_events(conn: sqlite3.Connection) -> None:
|
|
|
233
233
|
effective reset moment is floored to the hour via `_floor_to_hour`
|
|
234
234
|
so minute/second-level Anthropic jitter ("in X hr Y min" relative-text
|
|
235
235
|
drift) doesn't masquerade as a reset.
|
|
236
|
+
|
|
237
|
+
ONE deliberate divergence from the live rule: backfill passes
|
|
238
|
+
``allow_reset_to_zero=False`` to ``_is_reset_drop``, so it fires only on
|
|
239
|
+
the unambiguous ``>=25pp`` drop. The lenient reset-to-zero signal is
|
|
240
|
+
live-only — the live path debounces a transient API zero (issue #128),
|
|
241
|
+
but this one-shot historical scan has no debounce and would otherwise
|
|
242
|
+
mis-read a stale-replica 0% blip (``6% → 0% → 1%`` on a still-future
|
|
243
|
+
week_end) as a credit, segmenting the week into a degenerate zero-width
|
|
244
|
+
window. See ``_is_reset_drop`` for the full rationale.
|
|
236
245
|
"""
|
|
237
246
|
c = _cctally()
|
|
238
247
|
try:
|
|
@@ -278,7 +287,7 @@ def _backfill_week_reset_events(conn: sqlite3.Connection) -> None:
|
|
|
278
287
|
if (
|
|
279
288
|
captured_dt < prior_end_dt
|
|
280
289
|
and prior_pct is not None and cur_pct is not None
|
|
281
|
-
and _is_reset_drop(prior_pct, cur_pct)
|
|
290
|
+
and _is_reset_drop(prior_pct, cur_pct, allow_reset_to_zero=False)
|
|
282
291
|
):
|
|
283
292
|
# Floor to the hour so the display boundary lands on the
|
|
284
293
|
# natural hour mark (Anthropic's reset times are always
|
|
@@ -309,7 +318,7 @@ def _backfill_week_reset_events(conn: sqlite3.Connection) -> None:
|
|
|
309
318
|
if (
|
|
310
319
|
captured_dt < prior_end_dt
|
|
311
320
|
and prior_pct is not None and cur_pct is not None
|
|
312
|
-
and _is_reset_drop(prior_pct, cur_pct)
|
|
321
|
+
and _is_reset_drop(prior_pct, cur_pct, allow_reset_to_zero=False)
|
|
313
322
|
):
|
|
314
323
|
# Pre-check on ``new_week_end_at`` (mirrors the live
|
|
315
324
|
# detection path's pre-check). Necessary because the
|
|
@@ -392,7 +401,9 @@ _RESET_ZERO_FLOOR_PCT = 1.0
|
|
|
392
401
|
_RESET_ZERO_MIN_DROP_PCT = 3.0
|
|
393
402
|
|
|
394
403
|
|
|
395
|
-
def _is_reset_drop(
|
|
404
|
+
def _is_reset_drop(
|
|
405
|
+
prior_pct: float, cur_pct: float, *, allow_reset_to_zero: bool = True
|
|
406
|
+
) -> bool:
|
|
396
407
|
"""True when ``prior_pct → cur_pct`` is a genuine weekly reset/credit.
|
|
397
408
|
|
|
398
409
|
Two independent percent-shape signals (OR):
|
|
@@ -400,17 +411,30 @@ def _is_reset_drop(prior_pct: float, cur_pct: float) -> bool:
|
|
|
400
411
|
* **Partial credit** — drop ``>= _RESET_PCT_DROP_THRESHOLD`` (25pp).
|
|
401
412
|
* **Reset-to-zero** — ``cur_pct`` collapses to ~0
|
|
402
413
|
(``<= _RESET_ZERO_FLOOR_PCT``) with a drop clearing
|
|
403
|
-
``_RESET_ZERO_MIN_DROP_PCT``.
|
|
414
|
+
``_RESET_ZERO_MIN_DROP_PCT``. Gated on ``allow_reset_to_zero``.
|
|
415
|
+
|
|
416
|
+
``allow_reset_to_zero`` scopes the lenient reset-to-zero signal to the
|
|
417
|
+
sites that can afford it. **Live** current-week detection passes the
|
|
418
|
+
default ``True``: the live in-place path debounces a transient API zero
|
|
419
|
+
(issue #128 — arm on the first ~0, confirm only if it stays low, clear
|
|
420
|
+
on recovery). The **historical backfill**
|
|
421
|
+
(``_backfill_week_reset_events``) passes ``False`` — it is a one-shot
|
|
422
|
+
scan with NO debounce, so a single stale-replica 0% reading on a
|
|
423
|
+
still-future ``week_end`` (e.g. a ``6% → 0% → 1%`` blip) would otherwise
|
|
424
|
+
be mis-read as a goodwill credit and segment the week into a degenerate
|
|
425
|
+
zero-width window. Backfill therefore fires only on the unambiguous
|
|
426
|
+
``>=25pp`` drop and defers sub-25pp reset-to-zero to the live path.
|
|
404
427
|
|
|
405
428
|
Callers retain the boundary predicates (same/advanced ``week_end_at``
|
|
406
429
|
AND ``prior_end_dt > now``); this helper owns ONLY the percent-shape
|
|
407
|
-
discrimination
|
|
408
|
-
in-place, backfill advance, backfill in-place) stay byte-identical.
|
|
430
|
+
discrimination.
|
|
409
431
|
"""
|
|
410
432
|
cur = float(cur_pct)
|
|
411
433
|
drop = float(prior_pct) - cur
|
|
412
434
|
if drop >= _RESET_PCT_DROP_THRESHOLD:
|
|
413
435
|
return True
|
|
436
|
+
if not allow_reset_to_zero:
|
|
437
|
+
return False
|
|
414
438
|
return cur <= _RESET_ZERO_FLOOR_PCT and drop >= _RESET_ZERO_MIN_DROP_PCT
|
|
415
439
|
|
|
416
440
|
|