cctally 1.22.4 → 1.24.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 +20 -0
- package/bin/_cctally_alerts.py +133 -24
- package/bin/_cctally_config.py +195 -14
- package/bin/_cctally_core.py +102 -2
- package/bin/_cctally_dashboard.py +277 -62
- package/bin/_cctally_forecast.py +25 -3
- package/bin/_cctally_milestones.py +68 -0
- package/bin/_cctally_parser.py +10 -2
- package/bin/_cctally_record.py +470 -137
- package/bin/_cctally_tui.py +1 -0
- package/bin/_lib_alert_axes.py +53 -0
- package/bin/_lib_alert_dispatch.py +141 -0
- package/bin/_lib_alerts_payload.py +67 -0
- package/bin/_lib_budget.py +8 -0
- package/bin/cctally +17 -0
- package/dashboard/static/assets/{index-BxmaYT1y.css → index-CsqqtRBB.css} +1 -1
- package/dashboard/static/assets/index-DwuW39Tv.js +18 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +3 -1
- package/dashboard/static/assets/index-CLcd-Tnm.js +0 -18
package/bin/_cctally_record.py
CHANGED
|
@@ -285,6 +285,38 @@ def insert_budget_milestone(*args, **kwargs):
|
|
|
285
285
|
return sys.modules["cctally"].insert_budget_milestone(*args, **kwargs)
|
|
286
286
|
|
|
287
287
|
|
|
288
|
+
def insert_projected_milestone(*args, **kwargs):
|
|
289
|
+
return sys.modules["cctally"].insert_projected_milestone(*args, **kwargs)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _projected_levels_already_latched(*args, **kwargs):
|
|
293
|
+
return sys.modules["cctally"]._projected_levels_already_latched(*args, **kwargs)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _build_alert_payload_projected(*args, **kwargs):
|
|
297
|
+
return sys.modules["cctally"]._build_alert_payload_projected(*args, **kwargs)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _fetch_current_week_snapshots(*args, **kwargs):
|
|
301
|
+
return sys.modules["cctally"]._fetch_current_week_snapshots(*args, **kwargs)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _apply_midweek_reset_override(*args, **kwargs):
|
|
305
|
+
return sys.modules["cctally"]._apply_midweek_reset_override(*args, **kwargs)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _assess_forecast_confidence(*args, **kwargs):
|
|
309
|
+
return sys.modules["cctally"]._assess_forecast_confidence(*args, **kwargs)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _build_budget_status_inputs(*args, **kwargs):
|
|
313
|
+
return sys.modules["cctally"]._build_budget_status_inputs(*args, **kwargs)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def compute_budget_status(*args, **kwargs):
|
|
317
|
+
return sys.modules["cctally"].compute_budget_status(*args, **kwargs)
|
|
318
|
+
|
|
319
|
+
|
|
288
320
|
def _dispatch_alert_notification(*args, **kwargs):
|
|
289
321
|
return sys.modules["cctally"]._dispatch_alert_notification(*args, **kwargs)
|
|
290
322
|
|
|
@@ -788,6 +820,235 @@ def maybe_record_budget_milestone(saved: dict[str, Any]) -> None:
|
|
|
788
820
|
eprint(f"[budget-alerts] dispatch failed: {dispatch_exc}")
|
|
789
821
|
|
|
790
822
|
|
|
823
|
+
def _weekly_pct_week_avg_projection(conn, now_utc):
|
|
824
|
+
"""Compute the week-AVERAGE weekly-% projection for the current
|
|
825
|
+
subscription week, snapshot-only (CHEAP — no cost SUM, no ``sync_cache``).
|
|
826
|
+
|
|
827
|
+
Returns ``(projected_pct, low_conf)`` or ``None`` when no current-week
|
|
828
|
+
snapshot resolves. The value is computed by the IDENTICAL formula +
|
|
829
|
+
IDENTICAL inputs that produce ``ForecastOutput.week_avg_projection_pct``
|
|
830
|
+
(``_load_forecast_inputs`` → ``_compute_forecast``): ``p_now`` / elapsed /
|
|
831
|
+
remaining come from ``_fetch_current_week_snapshots`` +
|
|
832
|
+
``_apply_midweek_reset_override`` (the same reset-aware window resolution
|
|
833
|
+
forecast uses), ``r_avg = p_now / elapsed_hours`` (``p_week_start`` treated
|
|
834
|
+
as 0, matching the forecast kernel), and
|
|
835
|
+
``projected = p_now + r_avg * remaining_hours``. The reconcile invariant
|
|
836
|
+
binds the fired value to forecast's ``week_avg_projection_pct`` within
|
|
837
|
+
1e-9, so the two MUST share the formula by construction.
|
|
838
|
+
|
|
839
|
+
LOW CONF mirrors the displayed forecast confidence: the binary
|
|
840
|
+
``_assess_forecast_confidence(elapsed_hours, p_now, len(samples))`` plus the
|
|
841
|
+
``no_sample_ge_24h`` clause ``_load_forecast_inputs`` appends — so a thin
|
|
842
|
+
early-week window that forecast renders ``LOW CONF`` never fires a
|
|
843
|
+
projected alert.
|
|
844
|
+
|
|
845
|
+
Deliberately does NOT call ``_sum_cost_for_range`` (the weekly-% projection
|
|
846
|
+
needs no spend; the forecast kernel's ``week_avg_projection_pct`` is also
|
|
847
|
+
spend-free).
|
|
848
|
+
"""
|
|
849
|
+
fetched = _fetch_current_week_snapshots(conn, now_utc)
|
|
850
|
+
if fetched is None:
|
|
851
|
+
return None
|
|
852
|
+
week_start_at, week_end_at, samples = fetched
|
|
853
|
+
week_start_at, samples = _apply_midweek_reset_override(
|
|
854
|
+
conn, week_start_at, week_end_at, samples
|
|
855
|
+
)
|
|
856
|
+
if not samples:
|
|
857
|
+
return None
|
|
858
|
+
p_now = samples[-1][1]
|
|
859
|
+
elapsed_hours = (now_utc - week_start_at).total_seconds() / 3600.0
|
|
860
|
+
remaining_hours = max(0.0, (week_end_at - now_utc).total_seconds() / 3600.0)
|
|
861
|
+
r_avg = p_now / elapsed_hours if elapsed_hours > 0 else 0.0
|
|
862
|
+
projected_pct = p_now + r_avg * remaining_hours
|
|
863
|
+
|
|
864
|
+
# Confidence: same predicate + the same no_sample_ge_24h augmentation that
|
|
865
|
+
# _load_forecast_inputs applies, so this LOW CONF gate == forecast's.
|
|
866
|
+
confidence, _reasons = _assess_forecast_confidence(
|
|
867
|
+
elapsed_hours, p_now, len(samples)
|
|
868
|
+
)
|
|
869
|
+
target_24h = now_utc - dt.timedelta(hours=24)
|
|
870
|
+
has_sample_ge_24h = any(s[0] <= target_24h for s in samples)
|
|
871
|
+
if not has_sample_ge_24h:
|
|
872
|
+
confidence = "low"
|
|
873
|
+
return (projected_pct, confidence == "low")
|
|
874
|
+
|
|
875
|
+
|
|
876
|
+
def maybe_record_projected_alert(saved: dict[str, Any]) -> None:
|
|
877
|
+
"""Projected-pace detect-and-arm (axis ``projected``, #121).
|
|
878
|
+
|
|
879
|
+
Fires on the WEEK-AVERAGE projection (never the displayed high-end verdict
|
|
880
|
+
band) for ``weekly_pct`` and/or ``budget_usd``. Its OWN detect-and-arm —
|
|
881
|
+
NOT folded into ``maybe_record_milestone`` (Section 1 / Codex P0-3) — called
|
|
882
|
+
from ``cmd_record_usage`` in its own ``try`` after the weekly/5h/budget
|
|
883
|
+
blocks.
|
|
884
|
+
|
|
885
|
+
Master gates (Codex P1-2): ``weekly_pct`` fires only under
|
|
886
|
+
``alerts.enabled && alerts.projected_enabled``; ``budget_usd`` only under
|
|
887
|
+
``_budget_alerts_active(budget_cfg) && budget.projected_enabled``. Both
|
|
888
|
+
toggles default OFF (no surprise notifications on upgrade). When NEITHER is
|
|
889
|
+
on, returns after only a cheap config read — no projection math, no cost
|
|
890
|
+
work.
|
|
891
|
+
|
|
892
|
+
Pre-probe (Codex P1-1): a metric whose levels are ALL already latched is
|
|
893
|
+
skipped BEFORE any projection / cost work.
|
|
894
|
+
|
|
895
|
+
Snap-up (Codex P2-1): a level fires when ``projected + 1e-9 >= threshold``.
|
|
896
|
+
Latch / fire-once: ``UNIQUE(week_start_at, metric, threshold)`` + the
|
|
897
|
+
rowcount==1 predicate — a later recovery neither un-fires nor re-fires.
|
|
898
|
+
Mid-week reset re-anchors ``week_start_at`` (budget pattern; no
|
|
899
|
+
``reset_event_id``).
|
|
900
|
+
|
|
901
|
+
Set-then-dispatch: INSERT ``commit=False``, stamp ``alerted_at`` in the same
|
|
902
|
+
txn, commit, THEN best-effort dispatch. A dispatch failure never rolls back
|
|
903
|
+
the milestone.
|
|
904
|
+
|
|
905
|
+
The ``budget_usd`` leg reuses the SAME ``_build_budget_status_inputs`` +
|
|
906
|
+
``compute_budget_status`` path that produces ``budget --json``'s
|
|
907
|
+
``week_avg_projection_usd`` (the reconcile-bound field) — value-exact by
|
|
908
|
+
construction. The cache is already warm from the actual-budget axis's spend
|
|
909
|
+
SUM this same tick, so this is not a second aggregation pass; the pre-probe
|
|
910
|
+
additionally skips it entirely when all budget levels are latched.
|
|
911
|
+
"""
|
|
912
|
+
# The `projected_enabled` toggles are validated keys on the alerts/budget
|
|
913
|
+
# blocks (bool-validated; default OFF), so read them straight off the
|
|
914
|
+
# validated getter dicts — no raw-block fallback (which would re-emit the
|
|
915
|
+
# "unknown alerts config key" warning every tick and bypass bool
|
|
916
|
+
# validation). Master gates still compose with the parent-axis predicates.
|
|
917
|
+
cfg = load_config()
|
|
918
|
+
try:
|
|
919
|
+
alerts_cfg = _get_alerts_config(cfg)
|
|
920
|
+
except _AlertsConfigError as exc:
|
|
921
|
+
_warn_alerts_bad_config_once(exc)
|
|
922
|
+
alerts_cfg = {"enabled": False, "projected_enabled": False}
|
|
923
|
+
try:
|
|
924
|
+
budget_cfg = _get_budget_config(cfg)
|
|
925
|
+
except _BudgetConfigError as exc:
|
|
926
|
+
_warn_budget_bad_config_once(exc)
|
|
927
|
+
budget_cfg = {}
|
|
928
|
+
|
|
929
|
+
weekly_on = bool(alerts_cfg.get("enabled")) and bool(
|
|
930
|
+
alerts_cfg.get("projected_enabled")
|
|
931
|
+
)
|
|
932
|
+
budget_on = _budget_alerts_active(budget_cfg) and bool(
|
|
933
|
+
budget_cfg.get("projected_enabled")
|
|
934
|
+
)
|
|
935
|
+
if not (weekly_on or budget_on):
|
|
936
|
+
return # cheap config-only path — non-projected users pay nothing
|
|
937
|
+
|
|
938
|
+
now_utc = _command_as_of()
|
|
939
|
+
pending: list[dict[str, Any]] = []
|
|
940
|
+
conn = open_db()
|
|
941
|
+
try:
|
|
942
|
+
# ── weekly_pct leg (snapshot-only, cheap) ───────────────────────────
|
|
943
|
+
if weekly_on:
|
|
944
|
+
w_window = _fetch_current_week_snapshots(conn, now_utc)
|
|
945
|
+
if w_window is not None:
|
|
946
|
+
ws_at, we_at, samples = w_window
|
|
947
|
+
ws_at, _ = _apply_midweek_reset_override(
|
|
948
|
+
conn, ws_at, we_at, samples
|
|
949
|
+
)
|
|
950
|
+
week_key = ws_at.isoformat(timespec="seconds")
|
|
951
|
+
levels = (90, 100)
|
|
952
|
+
if not _projected_levels_already_latched(
|
|
953
|
+
conn, week_start_at=week_key, metric="weekly_pct",
|
|
954
|
+
levels=levels,
|
|
955
|
+
):
|
|
956
|
+
proj = _weekly_pct_week_avg_projection(conn, now_utc)
|
|
957
|
+
if proj is not None and not proj[1]:
|
|
958
|
+
value = proj[0]
|
|
959
|
+
for t in levels:
|
|
960
|
+
if value + 1e-9 >= t:
|
|
961
|
+
pending.append(dict(
|
|
962
|
+
week_start_at=week_key,
|
|
963
|
+
metric="weekly_pct",
|
|
964
|
+
threshold=t,
|
|
965
|
+
projected_value=value,
|
|
966
|
+
denominator=100.0,
|
|
967
|
+
))
|
|
968
|
+
|
|
969
|
+
# ── budget_usd leg (reuses the tick's spend via the shared path) ─────
|
|
970
|
+
if budget_on:
|
|
971
|
+
target = budget_cfg["weekly_usd"]
|
|
972
|
+
thresholds = tuple(
|
|
973
|
+
sorted(set(int(t) for t in budget_cfg["alert_thresholds"]))
|
|
974
|
+
)
|
|
975
|
+
window = _resolve_current_budget_window(conn, now_utc)
|
|
976
|
+
if window is not None and thresholds:
|
|
977
|
+
b_ws_at, _b_we_at = window
|
|
978
|
+
b_week_key = b_ws_at.isoformat(timespec="seconds")
|
|
979
|
+
if not _projected_levels_already_latched(
|
|
980
|
+
conn, week_start_at=b_week_key, metric="budget_usd",
|
|
981
|
+
levels=thresholds,
|
|
982
|
+
):
|
|
983
|
+
# skip_sync=True: the actual-budget axis
|
|
984
|
+
# (maybe_record_budget_milestone) already ran a
|
|
985
|
+
# _sum_cost_for_range this same tick, warming the cache.
|
|
986
|
+
# Avoids a redundant JSONL ingest pass here.
|
|
987
|
+
inputs = _build_budget_status_inputs(
|
|
988
|
+
conn, target_usd=target, now_utc=now_utc,
|
|
989
|
+
alert_thresholds=thresholds, skip_sync=True,
|
|
990
|
+
)
|
|
991
|
+
if inputs is not None:
|
|
992
|
+
status = compute_budget_status(inputs)
|
|
993
|
+
if not status.low_confidence:
|
|
994
|
+
value = status.week_avg_projection_usd
|
|
995
|
+
for t in thresholds:
|
|
996
|
+
if value + 1e-9 >= (t / 100.0) * float(target):
|
|
997
|
+
pending.append(dict(
|
|
998
|
+
week_start_at=b_week_key,
|
|
999
|
+
metric="budget_usd",
|
|
1000
|
+
threshold=t,
|
|
1001
|
+
projected_value=value,
|
|
1002
|
+
denominator=float(target),
|
|
1003
|
+
))
|
|
1004
|
+
|
|
1005
|
+
# ── arm (set-then-dispatch): INSERT + stamp alerted_at in one txn ────
|
|
1006
|
+
fired: list[dict[str, Any]] = []
|
|
1007
|
+
for p in pending:
|
|
1008
|
+
inserted = insert_projected_milestone(
|
|
1009
|
+
conn,
|
|
1010
|
+
week_start_at=p["week_start_at"],
|
|
1011
|
+
metric=p["metric"],
|
|
1012
|
+
threshold=p["threshold"],
|
|
1013
|
+
projected_value=p["projected_value"],
|
|
1014
|
+
denominator=p["denominator"],
|
|
1015
|
+
commit=False,
|
|
1016
|
+
)
|
|
1017
|
+
# Only the genuine-new-crossing winner (rowcount==1) arms+dispatches;
|
|
1018
|
+
# a racing record-usage instance gets rowcount==0 and skips.
|
|
1019
|
+
if inserted == 1:
|
|
1020
|
+
conn.execute(
|
|
1021
|
+
"UPDATE projected_milestones SET alerted_at = ? "
|
|
1022
|
+
"WHERE week_start_at = ? AND metric = ? AND threshold = ? "
|
|
1023
|
+
" AND alerted_at IS NULL",
|
|
1024
|
+
(now_utc_iso(), p["week_start_at"], p["metric"],
|
|
1025
|
+
p["threshold"]),
|
|
1026
|
+
)
|
|
1027
|
+
fired.append(p)
|
|
1028
|
+
# Single commit: every INSERT + its alerted_at marker durable together.
|
|
1029
|
+
conn.commit()
|
|
1030
|
+
except Exception as exc:
|
|
1031
|
+
eprint(f"[projected-alert] error recording projected milestone: {exc}")
|
|
1032
|
+
fired = []
|
|
1033
|
+
finally:
|
|
1034
|
+
conn.close()
|
|
1035
|
+
|
|
1036
|
+
# Dispatch AFTER commit; a dispatch failure NEVER rolls back the milestone
|
|
1037
|
+
# (set-then-dispatch invariant).
|
|
1038
|
+
for p in fired:
|
|
1039
|
+
try:
|
|
1040
|
+
payload = _build_alert_payload_projected(
|
|
1041
|
+
metric=p["metric"],
|
|
1042
|
+
threshold=p["threshold"],
|
|
1043
|
+
projected_value=p["projected_value"],
|
|
1044
|
+
denominator=p["denominator"],
|
|
1045
|
+
week_start_at=p["week_start_at"],
|
|
1046
|
+
)
|
|
1047
|
+
_dispatch_alert_notification(payload, mode="real")
|
|
1048
|
+
except Exception as dispatch_exc:
|
|
1049
|
+
eprint(f"[projected-alert] dispatch failed: {dispatch_exc}")
|
|
1050
|
+
|
|
1051
|
+
|
|
791
1052
|
def _compute_block_totals(
|
|
792
1053
|
block_start_at: dt.datetime,
|
|
793
1054
|
range_end: dt.datetime,
|
|
@@ -1434,6 +1695,134 @@ def maybe_update_five_hour_block(saved: dict[str, Any]) -> None:
|
|
|
1434
1695
|
conn.close()
|
|
1435
1696
|
|
|
1436
1697
|
|
|
1698
|
+
# ── Reset-to-zero debounce marker (issue #128) ─────────────────────────────
|
|
1699
|
+
# A transient Anthropic OAuth zero (cold replica / outage) against non-trivial
|
|
1700
|
+
# usage would otherwise mis-fire the live in-place reset-to-zero detector. We
|
|
1701
|
+
# debounce: the first ~0 ARMS this marker (it does not fire); the next reading
|
|
1702
|
+
# CONFIRMS (fires) only if usage stayed low, or CLEARS on recovery toward the
|
|
1703
|
+
# baseline. The marker is needed because the write-site clamp suppresses the
|
|
1704
|
+
# deferred first zero, so it leaves no DB trace. Losing the marker is always
|
|
1705
|
+
# safe (a real reset re-arms and fires one tick later). Best-effort file I/O —
|
|
1706
|
+
# the detector must never crash on a marker hiccup. See
|
|
1707
|
+
# docs/superpowers/specs/2026-06-02-reset-zero-debounce-design.md.
|
|
1708
|
+
_RESET_ZERO_MARKER_NAME = "pending-reset-zero-7d"
|
|
1709
|
+
|
|
1710
|
+
|
|
1711
|
+
def _reset_zero_marker_path():
|
|
1712
|
+
return _cctally_core.APP_DIR / _RESET_ZERO_MARKER_NAME
|
|
1713
|
+
|
|
1714
|
+
|
|
1715
|
+
def _arm_reset_zero_marker(week_start_date, cur_end_canon, *,
|
|
1716
|
+
baseline_pct, first_zero_iso):
|
|
1717
|
+
"""Persist the pending reset-to-zero candidate. ``first_zero_iso`` MUST be
|
|
1718
|
+
the ``_command_as_of()`` clock value (it becomes the effective anchor on
|
|
1719
|
+
confirm), NOT wall-clock."""
|
|
1720
|
+
try:
|
|
1721
|
+
_reset_zero_marker_path().write_text(
|
|
1722
|
+
f"{week_start_date} {cur_end_canon} "
|
|
1723
|
+
f"{float(baseline_pct)} {first_zero_iso}\n"
|
|
1724
|
+
)
|
|
1725
|
+
except OSError:
|
|
1726
|
+
pass
|
|
1727
|
+
|
|
1728
|
+
|
|
1729
|
+
def _clear_reset_zero_marker():
|
|
1730
|
+
try:
|
|
1731
|
+
_reset_zero_marker_path().unlink(missing_ok=True)
|
|
1732
|
+
except OSError:
|
|
1733
|
+
pass
|
|
1734
|
+
|
|
1735
|
+
|
|
1736
|
+
def _read_reset_zero_marker():
|
|
1737
|
+
"""Return ``(week_start_date, cur_end_canon, baseline_pct, first_zero_iso)``
|
|
1738
|
+
or ``None`` when missing / empty / garbled. Validates ALL fields (arity,
|
|
1739
|
+
float baseline, parseable timestamp) so a malformed marker re-arms cleanly
|
|
1740
|
+
rather than wedging the confirm path."""
|
|
1741
|
+
try:
|
|
1742
|
+
raw = _reset_zero_marker_path().read_text().strip()
|
|
1743
|
+
except OSError:
|
|
1744
|
+
return None
|
|
1745
|
+
if not raw:
|
|
1746
|
+
return None
|
|
1747
|
+
parts = raw.split()
|
|
1748
|
+
if len(parts) != 4:
|
|
1749
|
+
return None
|
|
1750
|
+
week_start_date, cur_end_canon, baseline_raw, first_zero_iso = parts
|
|
1751
|
+
try:
|
|
1752
|
+
baseline_pct = float(baseline_raw)
|
|
1753
|
+
except ValueError:
|
|
1754
|
+
return None
|
|
1755
|
+
try:
|
|
1756
|
+
parse_iso_datetime(first_zero_iso, "reset_zero_marker.first_zero")
|
|
1757
|
+
except ValueError:
|
|
1758
|
+
return None
|
|
1759
|
+
return (week_start_date, cur_end_canon, baseline_pct, first_zero_iso)
|
|
1760
|
+
|
|
1761
|
+
|
|
1762
|
+
def _fire_in_place_credit(conn, week_start_date, cur_end_canon, weekly_percent,
|
|
1763
|
+
*, observed_pre_credit_pct, effective_dt):
|
|
1764
|
+
"""Emit/refresh the in-place weekly-credit artifacts (issue #19 + #128).
|
|
1765
|
+
Shared by the immediate >=25pp path and the debounced reset-to-zero
|
|
1766
|
+
confirmation path.
|
|
1767
|
+
|
|
1768
|
+
Side-effect ordering is load-bearing: the event-row INSERT is dedup-gated
|
|
1769
|
+
on a pre-check, but the hwm force-write and stale-replica DELETE run
|
|
1770
|
+
UNCONDITIONALLY — a prior run may have committed the event then died before
|
|
1771
|
+
the pivots (memory: project_dedup_must_not_gate_side_effects). The pivots
|
|
1772
|
+
are individually idempotent (file overwrite + DELETE on a stable predicate).
|
|
1773
|
+
|
|
1774
|
+
``effective_dt`` is the (already-resolved) reset moment; the immediate path
|
|
1775
|
+
passes ``_floor_to_hour(now_utc)``, the debounced path passes the floored
|
|
1776
|
+
first-zero instant from the marker."""
|
|
1777
|
+
effective_iso = effective_dt.isoformat(timespec="seconds")
|
|
1778
|
+
# Pre-check keyed on new_week_end_at: suppress a duplicate event row across
|
|
1779
|
+
# ticks. UNIQUE(old, new) also dedups, but the pre-check avoids a useless
|
|
1780
|
+
# write attempt and keeps logs clean.
|
|
1781
|
+
already = conn.execute(
|
|
1782
|
+
"SELECT 1 FROM week_reset_events WHERE new_week_end_at = ? LIMIT 1",
|
|
1783
|
+
(cur_end_canon,),
|
|
1784
|
+
).fetchone()
|
|
1785
|
+
if already is None:
|
|
1786
|
+
# Row shape: old=effective_iso, new=cur_end_canon (DISTINCT) so only
|
|
1787
|
+
# post_map fires on the credited week in _apply_reset_events_to_weekrefs
|
|
1788
|
+
# (old==new collapses it to a zero-width window). observed_pre_credit_pct
|
|
1789
|
+
# stamps the pre-credit baseline (issue #45).
|
|
1790
|
+
conn.execute(
|
|
1791
|
+
"INSERT OR IGNORE INTO week_reset_events "
|
|
1792
|
+
"(detected_at_utc, old_week_end_at, new_week_end_at, "
|
|
1793
|
+
" effective_reset_at_utc, observed_pre_credit_pct) "
|
|
1794
|
+
"VALUES (?, ?, ?, ?, ?)",
|
|
1795
|
+
(now_utc_iso(), effective_iso, cur_end_canon,
|
|
1796
|
+
effective_iso, float(observed_pre_credit_pct)),
|
|
1797
|
+
)
|
|
1798
|
+
conn.commit()
|
|
1799
|
+
# Unconditional pivot 1: force-write hwm-7d so the next status-line render
|
|
1800
|
+
# reflects the post-credit value (the monotonic guard at the normal write
|
|
1801
|
+
# site would refuse to decrease the file).
|
|
1802
|
+
try:
|
|
1803
|
+
(_cctally_core.APP_DIR / "hwm-7d").write_text(
|
|
1804
|
+
f"{week_start_date} {weekly_percent}\n"
|
|
1805
|
+
)
|
|
1806
|
+
except OSError:
|
|
1807
|
+
pass
|
|
1808
|
+
# Unconditional pivot 2: race-defensive cleanup of stale pre-credit replays
|
|
1809
|
+
# (external claude-statusline can replay pre-credit --percent values that
|
|
1810
|
+
# land captured_at >= effective with pct ~= baseline and dominate the
|
|
1811
|
+
# reset-aware clamp). 1.0pp tolerance band absorbs rounding drift; both
|
|
1812
|
+
# sides wrapped in unixepoch() for offset robustness.
|
|
1813
|
+
try:
|
|
1814
|
+
conn.execute(
|
|
1815
|
+
"DELETE FROM weekly_usage_snapshots "
|
|
1816
|
+
"WHERE week_start_date = ? "
|
|
1817
|
+
" AND unixepoch(captured_at_utc) >= unixepoch(?) "
|
|
1818
|
+
" AND ABS(weekly_percent - ?) < 1.0",
|
|
1819
|
+
(week_start_date, effective_iso, float(observed_pre_credit_pct)),
|
|
1820
|
+
)
|
|
1821
|
+
conn.commit()
|
|
1822
|
+
except sqlite3.DatabaseError as exc:
|
|
1823
|
+
eprint(f"[record-usage] post-credit cleanup failed: {exc}")
|
|
1824
|
+
|
|
1825
|
+
|
|
1437
1826
|
def cmd_record_usage(args: argparse.Namespace) -> int:
|
|
1438
1827
|
"""Record usage data from Claude Code status line rate_limits."""
|
|
1439
1828
|
c = _cctally()
|
|
@@ -1656,146 +2045,80 @@ def cmd_record_usage(args: argparse.Namespace) -> int:
|
|
|
1656
2045
|
)
|
|
1657
2046
|
conn.commit()
|
|
1658
2047
|
elif prior_end_canon and prior_end_canon == cur_end_canon:
|
|
1659
|
-
# In-place credit branch (v1.7.2)
|
|
1660
|
-
#
|
|
1661
|
-
#
|
|
1662
|
-
#
|
|
1663
|
-
#
|
|
1664
|
-
#
|
|
1665
|
-
#
|
|
1666
|
-
# the
|
|
2048
|
+
# In-place credit branch (v1.7.2) + reset-to-zero debounce
|
|
2049
|
+
# (issue #128). Same end_at across two captures. A >=25pp drop
|
|
2050
|
+
# is a goodwill credit and fires immediately; a reset-to-zero
|
|
2051
|
+
# (post <= floor, 3..25pp drop) is debounced against a
|
|
2052
|
+
# transient API zero — armed on the first ~0, confirmed only
|
|
2053
|
+
# if the next reading stays low (<= half the pre-zero
|
|
2054
|
+
# baseline), cleared on recovery toward baseline. The gate
|
|
2055
|
+
# drops the _is_reset_drop term so the recovery-clear path is
|
|
2056
|
+
# reachable. See the spec for the midpoint rationale.
|
|
1667
2057
|
prior_end_dt = parse_iso_datetime(prior_end_canon, "prior.week_end_at")
|
|
1668
|
-
if
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
"WHERE new_week_end_at = ? LIMIT 1",
|
|
1685
|
-
(cur_end_canon,),
|
|
1686
|
-
).fetchone()
|
|
1687
|
-
effective_dt = _floor_to_hour(now_utc)
|
|
1688
|
-
effective_iso = effective_dt.isoformat(timespec="seconds")
|
|
1689
|
-
if already is None:
|
|
1690
|
-
# Row shape: old=effective_iso, new=cur_end_canon
|
|
1691
|
-
# (distinct values). The previous shape stored
|
|
1692
|
-
# old==new==cur_end_canon, which let BOTH
|
|
1693
|
-
# _apply_reset_events_to_weekrefs maps
|
|
1694
|
-
# (pre_map[old] and post_map[new]) fire on the
|
|
1695
|
-
# SAME WeekRef — pre_map rewrote week_end_at to
|
|
1696
|
-
# effective, post_map rewrote week_start_at to
|
|
1697
|
-
# effective, collapsing the credited week to a
|
|
1698
|
-
# zero-width window in downstream renders. With
|
|
1699
|
-
# old==effective and new==cur_end_canon, only
|
|
1700
|
-
# post_map fires on the credited week (setting
|
|
1701
|
-
# week_start_at = effective, the intended
|
|
1702
|
-
# behavior); pre_map keys on effective_iso and
|
|
1703
|
-
# finds no matching WeekRef in practice. The
|
|
1704
|
-
# UNIQUE(old, new) constraint permits this
|
|
1705
|
-
# row, and the pre-check above keys on
|
|
1706
|
-
# new_week_end_at so dedup still works.
|
|
1707
|
-
# Stamp ``observed_pre_credit_pct = prior_pct``
|
|
1708
|
-
# (issue #45): durable record of the pre-credit
|
|
1709
|
-
# baseline we observed at write time. Decouples
|
|
1710
|
-
# any future cleanup tooling from re-deriving
|
|
1711
|
-
# prior_pct via SELECT. Existing rows from
|
|
1712
|
-
# migration 007 carry NULL.
|
|
1713
|
-
conn.execute(
|
|
1714
|
-
"INSERT OR IGNORE INTO week_reset_events "
|
|
1715
|
-
"(detected_at_utc, old_week_end_at, new_week_end_at, "
|
|
1716
|
-
" effective_reset_at_utc, observed_pre_credit_pct) "
|
|
1717
|
-
"VALUES (?, ?, ?, ?, ?)",
|
|
1718
|
-
(now_utc_iso(), effective_iso, cur_end_canon,
|
|
1719
|
-
effective_iso, float(prior_pct)),
|
|
1720
|
-
)
|
|
1721
|
-
conn.commit()
|
|
1722
|
-
# Pivots fire UNCONDITIONALLY whenever a credit
|
|
1723
|
-
# is detected — they're NOT gated on
|
|
1724
|
-
# ``already is None``. Memory
|
|
1725
|
-
# ``project_dedup_must_not_gate_side_effects.md``:
|
|
1726
|
-
# "Skipping a no-op INSERT must NOT skip
|
|
1727
|
-
# milestones/rollups/alerts; prior run may have
|
|
1728
|
-
# died mid-flight." Crash scenario: tick N
|
|
1729
|
-
# committed the event row, then died before
|
|
1730
|
-
# HWM + DELETE. Tick N+1's pre-check sees
|
|
1731
|
-
# ``already`` non-None (the row IS in the
|
|
1732
|
-
# table) and would skip the pivots, leaving
|
|
1733
|
-
# the system wedged on pre-credit HWM + stale-
|
|
1734
|
-
# replica rows. Pivots are individually
|
|
1735
|
-
# idempotent (file overwrite + DELETE on stable
|
|
1736
|
-
# predicate), so re-running them is safe.
|
|
1737
|
-
# ``effective_iso`` is resolved above; on a
|
|
1738
|
-
# recovery tick it lands on the SAME 10-min
|
|
1739
|
-
# slot as the original (now_utc has drifted
|
|
1740
|
-
# only seconds), so the DELETE predicate's
|
|
1741
|
-
# ``unixepoch(captured_at_utc) >= unixepoch(?)``
|
|
1742
|
-
# still matches every stale-replica row.
|
|
1743
|
-
#
|
|
1744
|
-
# Force-write hwm-7d so the next status-line
|
|
1745
|
-
# render reflects the post-credit value. The
|
|
1746
|
-
# monotonic guard at the normal write site
|
|
1747
|
-
# (below) would refuse to decrease the file;
|
|
1748
|
-
# this write is the credit-only escape hatch.
|
|
1749
|
-
# Lands AFTER the conn.commit() so a concurrent
|
|
1750
|
-
# record-usage reader doesn't see the new HWM
|
|
1751
|
-
# before the event row is durable.
|
|
1752
|
-
try:
|
|
1753
|
-
(_cctally_core.APP_DIR / "hwm-7d").write_text(
|
|
1754
|
-
f"{week_start_date} {weekly_percent}\n"
|
|
1755
|
-
)
|
|
1756
|
-
except OSError:
|
|
1757
|
-
pass
|
|
1758
|
-
|
|
1759
|
-
# Race-defensive cleanup. Between the moment
|
|
1760
|
-
# Anthropic credited the user (effective_iso)
|
|
1761
|
-
# and this code firing, the EXTERNAL
|
|
1762
|
-
# claude-statusline tool can replay stale
|
|
1763
|
-
# pre-credit `--percent` values (it has its
|
|
1764
|
-
# own in-memory HWM cache and re-runs us once
|
|
1765
|
-
# per status-line tick). Those replays land
|
|
1766
|
-
# captured_at_utc >= effective_iso with
|
|
1767
|
-
# weekly_percent near prior_pct (the pre-credit
|
|
1768
|
-
# value), and they dominate the reset-aware
|
|
1769
|
-
# clamp's MAX over the post-credit segment so
|
|
1770
|
-
# legitimate fresh OAuth values are rejected.
|
|
1771
|
-
# 1.0pp tolerance band (issue #45) around the
|
|
1772
|
-
# observed pre-credit baseline absorbs any
|
|
1773
|
-
# rounding drift between cctally's OAuth read
|
|
1774
|
-
# and statusline's --percent payload (today
|
|
1775
|
-
# they match byte-identically, but the band
|
|
1776
|
-
# future-proofs against Anthropic or statusline
|
|
1777
|
-
# changing rounding). The band stays well below
|
|
1778
|
-
# the 25pp in-place credit detection threshold,
|
|
1779
|
-
# so legitimate post-credit values are never
|
|
1780
|
-
# caught. Bind is the in-scope ``prior_pct``,
|
|
1781
|
-
# which equals the just-stamped
|
|
1782
|
-
# ``observed_pre_credit_pct`` on the event row.
|
|
1783
|
-
try:
|
|
1784
|
-
conn.execute(
|
|
1785
|
-
"DELETE FROM weekly_usage_snapshots "
|
|
1786
|
-
"WHERE week_start_date = ? "
|
|
1787
|
-
" AND unixepoch(captured_at_utc) >= "
|
|
1788
|
-
" unixepoch(?) "
|
|
1789
|
-
" AND ABS(weekly_percent - ?) < 1.0",
|
|
1790
|
-
(week_start_date, effective_iso,
|
|
1791
|
-
float(prior_pct)),
|
|
2058
|
+
if prior_end_dt > now_utc and prior_pct is not None:
|
|
2059
|
+
drop = float(prior_pct) - float(weekly_percent)
|
|
2060
|
+
big_drop = drop >= c._RESET_PCT_DROP_THRESHOLD
|
|
2061
|
+
zero_only = (
|
|
2062
|
+
(not big_drop)
|
|
2063
|
+
and float(weekly_percent) <= c._RESET_ZERO_FLOOR_PCT
|
|
2064
|
+
and drop >= c._RESET_ZERO_MIN_DROP_PCT
|
|
2065
|
+
)
|
|
2066
|
+
if big_drop:
|
|
2067
|
+
# >=25pp goodwill credit — fire immediately, never
|
|
2068
|
+
# debounced. Clear any pending arm (now moot).
|
|
2069
|
+
_clear_reset_zero_marker()
|
|
2070
|
+
_fire_in_place_credit(
|
|
2071
|
+
conn, week_start_date, cur_end_canon, weekly_percent,
|
|
2072
|
+
observed_pre_credit_pct=float(prior_pct),
|
|
2073
|
+
effective_dt=_floor_to_hour(now_utc),
|
|
1792
2074
|
)
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
2075
|
+
else:
|
|
2076
|
+
marker = _read_reset_zero_marker()
|
|
2077
|
+
armed = (
|
|
2078
|
+
marker is not None
|
|
2079
|
+
and marker[0] == week_start_date
|
|
2080
|
+
and marker[1] == cur_end_canon
|
|
1798
2081
|
)
|
|
2082
|
+
if armed:
|
|
2083
|
+
baseline_pct = marker[2]
|
|
2084
|
+
if float(weekly_percent) <= baseline_pct / 2.0:
|
|
2085
|
+
# Second reading stayed low → confirm. Anchor
|
|
2086
|
+
# the reset at the FIRST-zero instant from the
|
|
2087
|
+
# marker (UTC-normalized like the backfill
|
|
2088
|
+
# in-place path).
|
|
2089
|
+
first_zero_dt = parse_iso_datetime(
|
|
2090
|
+
marker[3], "reset_zero_marker.first_zero"
|
|
2091
|
+
).astimezone(dt.timezone.utc)
|
|
2092
|
+
_fire_in_place_credit(
|
|
2093
|
+
conn, week_start_date, cur_end_canon,
|
|
2094
|
+
weekly_percent,
|
|
2095
|
+
observed_pre_credit_pct=baseline_pct,
|
|
2096
|
+
effective_dt=_floor_to_hour(first_zero_dt),
|
|
2097
|
+
)
|
|
2098
|
+
# Clear ONLY after the fire completes (P2a):
|
|
2099
|
+
# a mid-fire crash leaves the marker armed so
|
|
2100
|
+
# the next zero re-confirms + re-runs the
|
|
2101
|
+
# idempotent pivots.
|
|
2102
|
+
_clear_reset_zero_marker()
|
|
2103
|
+
else:
|
|
2104
|
+
# Recovered toward baseline → transient zero,
|
|
2105
|
+
# not a reset. Clear, do not fire.
|
|
2106
|
+
_clear_reset_zero_marker()
|
|
2107
|
+
elif zero_only:
|
|
2108
|
+
# First ~0 → arm; do NOT fire. The write clamp
|
|
2109
|
+
# suppresses this 0 (no event row yet), so the
|
|
2110
|
+
# prior snapshot stays at the baseline and this
|
|
2111
|
+
# shape re-evaluates next tick. first_zero_iso is
|
|
2112
|
+
# the _command_as_of() value (now_utc), NOT
|
|
2113
|
+
# wall-clock — it becomes the effective anchor.
|
|
2114
|
+
_arm_reset_zero_marker(
|
|
2115
|
+
week_start_date, cur_end_canon,
|
|
2116
|
+
baseline_pct=float(prior_pct),
|
|
2117
|
+
first_zero_iso=now_utc.isoformat(timespec="seconds"),
|
|
2118
|
+
)
|
|
2119
|
+
# else: not a reset shape and not armed → nothing.
|
|
2120
|
+
# A non-matching stale marker is inert (ignored
|
|
2121
|
+
# on key mismatch, overwritten by the next arm).
|
|
1799
2122
|
|
|
1800
2123
|
# ── 5h in-place credit detection (parallel to weekly above) ──
|
|
1801
2124
|
# Spec §2.2 of
|
|
@@ -2329,6 +2652,16 @@ def cmd_record_usage(args: argparse.Namespace) -> int:
|
|
|
2329
2652
|
except Exception as exc:
|
|
2330
2653
|
eprint(f"[budget-milestone] unexpected error: {exc}")
|
|
2331
2654
|
|
|
2655
|
+
# NEW: projected-pace alert firing (axis `projected`, #121). Runs in its
|
|
2656
|
+
# OWN detect-and-arm AFTER the weekly/5h/budget blocks; gated up front on
|
|
2657
|
+
# (alerts.enabled && alerts.projected_enabled) ||
|
|
2658
|
+
# (_budget_alerts_active && budget.projected_enabled) — both toggles
|
|
2659
|
+
# default OFF, so non-projected users pay only a cheap config read.
|
|
2660
|
+
try:
|
|
2661
|
+
maybe_record_projected_alert(saved)
|
|
2662
|
+
except Exception as exc:
|
|
2663
|
+
eprint(f"[projected-alert] unexpected error: {exc}")
|
|
2664
|
+
|
|
2332
2665
|
# Write high-water mark so the status line never displays a regression.
|
|
2333
2666
|
# The file contains "week_start_date weekly_percent" on one line.
|
|
2334
2667
|
try:
|
package/bin/_cctally_tui.py
CHANGED
|
@@ -1193,6 +1193,7 @@ class DataSnapshot:
|
|
|
1193
1193
|
r_recent=r_recent,
|
|
1194
1194
|
final_percent_low=final_low,
|
|
1195
1195
|
final_percent_high=final_high,
|
|
1196
|
+
week_avg_projection_pct=used_pct + r_avg * remaining_hours,
|
|
1196
1197
|
projected_cap=final_high >= 100.0,
|
|
1197
1198
|
already_capped=False,
|
|
1198
1199
|
cap_at=None,
|