cctally 1.79.2 → 1.80.1
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 +25 -0
- package/bin/_cctally_cache.py +148 -61
- package/bin/_cctally_config.py +23 -6
- package/bin/_cctally_core.py +342 -88
- package/bin/_cctally_dashboard.py +13 -7
- package/bin/_cctally_db.py +361 -36
- package/bin/_cctally_doctor.py +156 -0
- package/bin/_cctally_five_hour.py +19 -7
- package/bin/_cctally_forecast.py +43 -18
- package/bin/_cctally_journal.py +2148 -0
- package/bin/_cctally_milestones.py +116 -26
- package/bin/_cctally_parser.py +21 -3
- package/bin/_cctally_quota.py +195 -76
- package/bin/_cctally_record.py +1596 -1133
- package/bin/_cctally_statusline.py +13 -1
- package/bin/_cctally_store.py +508 -0
- package/bin/_cctally_sync_week.py +139 -2
- package/bin/_lib_doctor.py +209 -12
- package/bin/_lib_journal.py +188 -0
- package/bin/cctally +11 -1
- package/dashboard/static/assets/index-Cc2ykqF_.css +1 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +4 -1
- package/dashboard/static/assets/index-CrIlpAlQ.css +0 -1
- /package/dashboard/static/assets/{index-BDkr0bdD.js → index-B3y14l1o.js} +0 -0
package/bin/_cctally_record.py
CHANGED
|
@@ -199,6 +199,7 @@ from _cctally_core import (
|
|
|
199
199
|
_AlertsConfigError,
|
|
200
200
|
_BudgetConfigError,
|
|
201
201
|
_command_as_of,
|
|
202
|
+
_as_of_or_command,
|
|
202
203
|
)
|
|
203
204
|
from _lib_five_hour import _canonical_5h_window_key, five_hour_milestone_range
|
|
204
205
|
from _lib_pricing import _calculate_entry_cost
|
|
@@ -577,9 +578,37 @@ def _resolve_active_five_hour_reset_event_id(
|
|
|
577
578
|
|
|
578
579
|
def maybe_record_milestone(
|
|
579
580
|
saved: dict[str, Any],
|
|
581
|
+
*,
|
|
582
|
+
conn=None,
|
|
583
|
+
as_of: "str | None" = None,
|
|
584
|
+
alert_sink: "list | None" = None,
|
|
585
|
+
journal: "tuple | None" = None,
|
|
580
586
|
) -> None:
|
|
581
587
|
"""Check if a new integer percent threshold was crossed, and if so,
|
|
582
|
-
fetch cost and record the milestone. Errors are logged, not raised.
|
|
588
|
+
fetch cost and record the milestone. Errors are logged, not raised.
|
|
589
|
+
|
|
590
|
+
Transaction-neutral / capture-time-pure seam (DB journal redesign §5.2.3):
|
|
591
|
+
when ``conn`` is passed the crossing folds into the caller's transaction —
|
|
592
|
+
no internal ``open_db()``/``commit()``/``close()``, and alert dispatch is
|
|
593
|
+
left to the caller (the ingester's post-commit ALERT_DISPATCHER). ``as_of``
|
|
594
|
+
(ISO-Z) is stamped as the milestone ``captured_at_utc`` / ``alerted_at`` in
|
|
595
|
+
place of wall clock. Both defaults keep the legacy own-connection,
|
|
596
|
+
commit-and-dispatch behavior byte-identical.
|
|
597
|
+
|
|
598
|
+
``alert_sink`` (DB journal redesign Task 6): on the passed-conn path the new-
|
|
599
|
+
crossing alert payloads are APPENDED to this list instead of being dropped,
|
|
600
|
+
so the ingester dispatches them post-commit (spec §5.2 step 6). The
|
|
601
|
+
``alerted_at`` stamp already lands in-txn (before harvest), so it survives a
|
|
602
|
+
rebuild. ``None`` keeps today's compute-and-drop behavior on the passed-conn
|
|
603
|
+
path.
|
|
604
|
+
|
|
605
|
+
``journal`` (DB journal redesign Task 6, Design A): the ``(ctx, id_base)``
|
|
606
|
+
tuple threaded into the milestone's pre-record cost sync so the
|
|
607
|
+
``weekly_cost_snapshots`` row it inserts rides a Model-A
|
|
608
|
+
``weekly_cost_snapshot`` evt (``wcs:<id_base>:<week>``) instead of a bare
|
|
609
|
+
insert — the computed cost is journaled and replay reads it back verbatim
|
|
610
|
+
(Claude Code prunes the source JSONL). Only threaded on the passed-conn
|
|
611
|
+
(ingest) path; ``None`` / the legacy own-conn path keeps the bare insert."""
|
|
583
612
|
weekly_percent = saved.get("weeklyPercent")
|
|
584
613
|
if weekly_percent is None or weekly_percent < 1:
|
|
585
614
|
return
|
|
@@ -599,7 +628,9 @@ def maybe_record_milestone(
|
|
|
599
628
|
usage_snapshot_id = saved["id"]
|
|
600
629
|
five_hour_percent = saved.get("fiveHourPercent")
|
|
601
630
|
|
|
602
|
-
|
|
631
|
+
own_conn = conn is None
|
|
632
|
+
if own_conn:
|
|
633
|
+
conn = open_db()
|
|
603
634
|
try:
|
|
604
635
|
# Resolve the active segment for THIS captured moment. The segment
|
|
605
636
|
# is the latest week_reset_events row keyed on week_end_at whose
|
|
@@ -608,7 +639,7 @@ def maybe_record_milestone(
|
|
|
608
639
|
# +00:00 / Z offsets (see precedent at bin/cctally:_compute_block_totals
|
|
609
640
|
# cross-reset detection; also project gotcha
|
|
610
641
|
# ``unixepoch_for_cross_offset_compare``).
|
|
611
|
-
captured_at_iso = saved.get("capturedAt") or now_utc_iso()
|
|
642
|
+
captured_at_iso = saved.get("capturedAt") or as_of or now_utc_iso()
|
|
612
643
|
reset_event_id = 0
|
|
613
644
|
if week_end_at:
|
|
614
645
|
seg_row = conn.execute(
|
|
@@ -642,7 +673,18 @@ def maybe_record_milestone(
|
|
|
642
673
|
json=False,
|
|
643
674
|
quiet=True,
|
|
644
675
|
)
|
|
645
|
-
|
|
676
|
+
# On the passed-conn path thread the same connection + capture time
|
|
677
|
+
# so the cost snapshot folds into the caller's transaction; on the
|
|
678
|
+
# legacy path cmd_sync_week opens its own connection as today. On the
|
|
679
|
+
# ingest path also thread `journal=(ctx, id_base)` so the cost
|
|
680
|
+
# snapshot rides a Model-A `weekly_cost_snapshot` evt (Design A) —
|
|
681
|
+
# never on the legacy own-conn path.
|
|
682
|
+
cmd_sync_week(
|
|
683
|
+
sync_ns,
|
|
684
|
+
conn=(None if own_conn else conn),
|
|
685
|
+
as_of=as_of,
|
|
686
|
+
journal=(None if own_conn else journal),
|
|
687
|
+
)
|
|
646
688
|
except Exception as exc:
|
|
647
689
|
eprint(f"[milestone] cost sync failed, using latest available: {exc}")
|
|
648
690
|
|
|
@@ -730,6 +772,7 @@ def maybe_record_milestone(
|
|
|
730
772
|
five_hour_percent_at_crossing=five_hour_percent,
|
|
731
773
|
commit=False,
|
|
732
774
|
reset_event_id=reset_event_id,
|
|
775
|
+
as_of=as_of,
|
|
733
776
|
)
|
|
734
777
|
# ── Threshold-actions dispatch (set-then-dispatch, spec §3.2) ──
|
|
735
778
|
# Only the genuine-new-crossing winner (rowcount==1) reaches this
|
|
@@ -744,7 +787,7 @@ def maybe_record_milestone(
|
|
|
744
787
|
and alerts_cfg["enabled"]
|
|
745
788
|
and pct in alerts_cfg["weekly_thresholds"]
|
|
746
789
|
):
|
|
747
|
-
crossed_at = now_utc_iso()
|
|
790
|
+
crossed_at = as_of or now_utc_iso()
|
|
748
791
|
# set-then-dispatch: alerted_at lands on the row BEFORE
|
|
749
792
|
# the osascript Popen, so a dismissed-after-spawn
|
|
750
793
|
# notification still surfaces in the dashboard alerts
|
|
@@ -784,29 +827,53 @@ def maybe_record_milestone(
|
|
|
784
827
|
)
|
|
785
828
|
pending_alerts.append(payload)
|
|
786
829
|
# Single commit after the loop durably writes every milestone row
|
|
787
|
-
# AND its alerted_at marker together.
|
|
788
|
-
|
|
789
|
-
#
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
830
|
+
# AND its alerted_at marker together. On the passed-conn (ingest) path
|
|
831
|
+
# the caller owns the commit and the post-commit alert dispatch (the
|
|
832
|
+
# ingester's ALERT_DISPATCHER, spec §5.2.7), so both are skipped here.
|
|
833
|
+
if own_conn:
|
|
834
|
+
conn.commit()
|
|
835
|
+
# Dispatch deferred to AFTER commit (matches 5h path). Per-payload
|
|
836
|
+
# exception logged so a bad-payload alert can't suppress healthy
|
|
837
|
+
# ones. Production caller ignores _dispatch_alert_notification's
|
|
838
|
+
# return value (spec §6.4).
|
|
839
|
+
for payload in pending_alerts:
|
|
840
|
+
try:
|
|
841
|
+
_dispatch_alert_notification(payload, mode="real")
|
|
842
|
+
except Exception as dispatch_exc:
|
|
843
|
+
eprint(f"[alerts] dispatch failed: {dispatch_exc}")
|
|
844
|
+
elif alert_sink is not None:
|
|
845
|
+
# Passed-conn (ingest) path: hand the new-crossing payloads to the
|
|
846
|
+
# caller's sink so the ingester dispatches them post-commit
|
|
847
|
+
# (spec §5.2 step 6). alerted_at is already stamped in the caller's
|
|
848
|
+
# txn above, so a crash between commit and dispatch loses at most
|
|
849
|
+
# one notification — the set-then-dispatch trade.
|
|
850
|
+
alert_sink.extend(pending_alerts)
|
|
798
851
|
except Exception as exc:
|
|
852
|
+
# Exception discipline (6c-gate P1): on the passed-conn (ingest) path a
|
|
853
|
+
# chokepoint failure must ABORT the cycle — re-raise so the ingester
|
|
854
|
+
# rolls back and leaves the cursor unmoved (invariant ii). Legacy
|
|
855
|
+
# own-connection ticks keep the log-and-swallow.
|
|
856
|
+
if not own_conn:
|
|
857
|
+
raise
|
|
799
858
|
eprint(f"[milestone] error recording milestone: {exc}")
|
|
800
859
|
finally:
|
|
801
|
-
|
|
860
|
+
if own_conn:
|
|
861
|
+
conn.close()
|
|
802
862
|
|
|
803
863
|
|
|
804
864
|
def _record_budget_milestone_for_vendor(
|
|
805
865
|
*, vendor, target, thresholds, period, config, tz, build_payload,
|
|
806
|
-
raise_errors: bool = False,
|
|
866
|
+
raise_errors: bool = False, conn=None, as_of=None, alert_sink=None,
|
|
807
867
|
) -> int:
|
|
808
868
|
"""Shared budget-milestone firing core for both vendors (#143).
|
|
809
869
|
|
|
870
|
+
Transaction-neutral / capture-time-pure seam (DB journal redesign §5.2.3):
|
|
871
|
+
when ``conn`` is passed the crossings fold into the caller's transaction —
|
|
872
|
+
no internal ``open_db()``/``commit()``/``close()``, and alert dispatch is
|
|
873
|
+
left to the caller (the ingester's ALERT_DISPATCHER). ``as_of`` (ISO-Z) is
|
|
874
|
+
the capture-time reference for window/spend + the crossing timestamps. Both
|
|
875
|
+
defaults keep the legacy own-connection, commit-and-dispatch behavior.
|
|
876
|
+
|
|
810
877
|
Hot-path ordering is preserved verbatim (spec §4.2 / [Pre-probe before
|
|
811
878
|
sync_cache]): ``open_db`` → cheap ``_resolve_budget_window(vendor=…)`` →
|
|
812
879
|
unified pre-probe (which configured thresholds are STILL un-recorded for this
|
|
@@ -827,9 +894,11 @@ def _record_budget_milestone_for_vendor(
|
|
|
827
894
|
``threshold`` / ``crossed_at_utc`` / ``period_key`` / ``period`` /
|
|
828
895
|
``budget_usd`` / ``spent_usd`` / ``consumption_pct`` keyword args.
|
|
829
896
|
"""
|
|
830
|
-
now_utc =
|
|
897
|
+
now_utc = _as_of_or_command(as_of)
|
|
831
898
|
pending_alerts: list[dict[str, Any]] = []
|
|
832
|
-
|
|
899
|
+
own_conn = conn is None
|
|
900
|
+
if own_conn:
|
|
901
|
+
conn = open_db()
|
|
833
902
|
try:
|
|
834
903
|
start_at = _resolve_budget_window(
|
|
835
904
|
conn, vendor=vendor, now_utc=now_utc, period=period,
|
|
@@ -865,6 +934,7 @@ def _record_budget_milestone_for_vendor(
|
|
|
865
934
|
target=target,
|
|
866
935
|
spent=spent,
|
|
867
936
|
now_utc=now_utc,
|
|
937
|
+
as_of=as_of,
|
|
868
938
|
):
|
|
869
939
|
pending_alerts.append(build_payload(
|
|
870
940
|
threshold=t,
|
|
@@ -876,26 +946,44 @@ def _record_budget_milestone_for_vendor(
|
|
|
876
946
|
consumption_pct=pct,
|
|
877
947
|
))
|
|
878
948
|
# Single commit: every INSERT + its alerted_at marker durable together.
|
|
879
|
-
conn
|
|
949
|
+
# On the passed-conn (ingest) path the caller owns the commit + the
|
|
950
|
+
# post-commit dispatch (the ingester's ALERT_DISPATCHER).
|
|
951
|
+
if own_conn:
|
|
952
|
+
conn.commit()
|
|
880
953
|
except Exception as exc:
|
|
954
|
+
# Exception discipline (6c-gate P1): passed-conn (ingest) path re-raises
|
|
955
|
+
# so a failure ABORTS the cycle (invariant ii); this covers both budget
|
|
956
|
+
# axes (claude + codex share this core). Legacy own-connection swallows
|
|
957
|
+
# (or re-raises when a caller explicitly requested raise_errors).
|
|
958
|
+
if not own_conn:
|
|
959
|
+
raise
|
|
881
960
|
eprint(f"[budget-milestone:{vendor}] error recording budget milestone: {exc}")
|
|
882
961
|
if raise_errors:
|
|
883
962
|
raise
|
|
884
963
|
finally:
|
|
885
|
-
|
|
964
|
+
if own_conn:
|
|
965
|
+
conn.close()
|
|
886
966
|
|
|
887
967
|
# Dispatch AFTER commit; a dispatch failure NEVER rolls back the milestone
|
|
888
968
|
# (set-then-dispatch invariant — one queue attempt per crossing, deduped on
|
|
889
|
-
# the alerted_at column).
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
969
|
+
# the alerted_at column). Skipped on the passed-conn path (caller dispatches).
|
|
970
|
+
if own_conn:
|
|
971
|
+
for payload in pending_alerts:
|
|
972
|
+
try:
|
|
973
|
+
_dispatch_alert_notification(payload, mode="real")
|
|
974
|
+
except Exception as dispatch_exc:
|
|
975
|
+
eprint(f"[budget-alerts:{vendor}] dispatch failed: {dispatch_exc}")
|
|
976
|
+
elif alert_sink is not None:
|
|
977
|
+
# Passed-conn (ingest) path: hand the budget crossings to the caller's
|
|
978
|
+
# sink for the ingester's post-commit dispatch (spec §5.2 step 6).
|
|
979
|
+
alert_sink.extend(pending_alerts)
|
|
895
980
|
return len(pending_alerts)
|
|
896
981
|
|
|
897
982
|
|
|
898
|
-
def maybe_record_budget_milestone(
|
|
983
|
+
def maybe_record_budget_milestone(
|
|
984
|
+
saved: dict[str, Any], *, conn=None, as_of: "str | None" = None,
|
|
985
|
+
alert_sink: "list | None" = None,
|
|
986
|
+
) -> None:
|
|
899
987
|
"""Fire Claude equiv-$ budget alerts on ACTUAL-spend threshold crossings
|
|
900
988
|
(axis ``budget`` — called from ``cmd_record_usage`` alongside the weekly-% /
|
|
901
989
|
5h-% milestone helpers). Thin vendor adapter over
|
|
@@ -940,6 +1028,9 @@ def maybe_record_budget_milestone(saved: dict[str, Any]) -> None:
|
|
|
940
1028
|
period=period,
|
|
941
1029
|
config=config,
|
|
942
1030
|
tz=tz,
|
|
1031
|
+
conn=conn,
|
|
1032
|
+
as_of=as_of,
|
|
1033
|
+
alert_sink=alert_sink,
|
|
943
1034
|
# The Claude payload builder takes the legacy `week_start_at=` kwarg
|
|
944
1035
|
# (its value is the resolved period-start instant, == period_key), so
|
|
945
1036
|
# the at-fire dispatch id stays byte-stable `budget:<period_start_at>:<t>`.
|
|
@@ -955,7 +1046,10 @@ def maybe_record_budget_milestone(saved: dict[str, Any]) -> None:
|
|
|
955
1046
|
)
|
|
956
1047
|
|
|
957
1048
|
|
|
958
|
-
def maybe_record_project_budget_milestone(
|
|
1049
|
+
def maybe_record_project_budget_milestone(
|
|
1050
|
+
saved: dict[str, Any], *, conn=None, as_of: "str | None" = None,
|
|
1051
|
+
alert_sink: "list | None" = None,
|
|
1052
|
+
) -> None:
|
|
959
1053
|
"""Fire PER-PROJECT equiv-$ budget alerts on ACTUAL-spend threshold
|
|
960
1054
|
crossings (spec §6 — called from ``cmd_record_usage`` alongside the
|
|
961
1055
|
weekly-% / 5h-% / budget / projected milestone helpers). An independent
|
|
@@ -993,9 +1087,11 @@ def maybe_record_project_budget_milestone(saved: dict[str, Any]) -> None:
|
|
|
993
1087
|
if not thresholds:
|
|
994
1088
|
return
|
|
995
1089
|
|
|
996
|
-
now_utc =
|
|
1090
|
+
now_utc = _as_of_or_command(as_of)
|
|
997
1091
|
pending_alerts: list[dict[str, Any]] = []
|
|
998
|
-
|
|
1092
|
+
own_conn = conn is None
|
|
1093
|
+
if own_conn:
|
|
1094
|
+
conn = open_db()
|
|
999
1095
|
try:
|
|
1000
1096
|
window = _resolve_current_budget_window(conn, now_utc)
|
|
1001
1097
|
if window is None:
|
|
@@ -1067,12 +1163,13 @@ def maybe_record_project_budget_milestone(saved: dict[str, Any]) -> None:
|
|
|
1067
1163
|
spent_usd=spent,
|
|
1068
1164
|
consumption_pct=consumption_pct,
|
|
1069
1165
|
commit=False,
|
|
1166
|
+
as_of=as_of,
|
|
1070
1167
|
)
|
|
1071
1168
|
# Only the genuine-new-crossing winner (rowcount==1) dispatches; a
|
|
1072
1169
|
# racing record-usage instance OR an already-recorded pair gets
|
|
1073
1170
|
# rowcount==0 and skips.
|
|
1074
1171
|
if inserted == 1:
|
|
1075
|
-
crossed_at = now_utc_iso()
|
|
1172
|
+
crossed_at = as_of or now_utc_iso()
|
|
1076
1173
|
# set-then-dispatch: alerted_at lands on the row BEFORE the
|
|
1077
1174
|
# Popen, sharing this transaction with the INSERT (commit=False).
|
|
1078
1175
|
# `alerted_at IS NULL` is write-once defense-in-depth.
|
|
@@ -1101,27 +1198,39 @@ def maybe_record_project_budget_milestone(saved: dict[str, Any]) -> None:
|
|
|
1101
1198
|
consumption_pct=consumption_pct,
|
|
1102
1199
|
))
|
|
1103
1200
|
# Single commit: every INSERT + its alerted_at marker durable together.
|
|
1104
|
-
conn
|
|
1201
|
+
# On the passed-conn (ingest) path the caller owns commit + dispatch.
|
|
1202
|
+
if own_conn:
|
|
1203
|
+
conn.commit()
|
|
1105
1204
|
except Exception as exc:
|
|
1205
|
+
# Exception discipline (6c-gate P1): passed-conn (ingest) path re-raises
|
|
1206
|
+
# so a failure ABORTS the cycle (invariant ii); legacy own-connection
|
|
1207
|
+
# swallows so a standalone record-usage tick never regresses.
|
|
1208
|
+
if not own_conn:
|
|
1209
|
+
raise
|
|
1106
1210
|
eprint(
|
|
1107
1211
|
f"[project-budget-milestone] error recording project budget "
|
|
1108
1212
|
f"milestone: {exc}"
|
|
1109
1213
|
)
|
|
1110
1214
|
finally:
|
|
1111
|
-
|
|
1215
|
+
if own_conn:
|
|
1216
|
+
conn.close()
|
|
1112
1217
|
|
|
1113
1218
|
# Dispatch AFTER commit; a dispatch failure NEVER rolls back the milestone
|
|
1114
1219
|
# (set-then-dispatch invariant — one queue attempt per crossing, deduped on
|
|
1115
|
-
# the alerted_at column).
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1220
|
+
# the alerted_at column). Skipped on the passed-conn path (caller dispatches).
|
|
1221
|
+
if own_conn:
|
|
1222
|
+
for payload in pending_alerts:
|
|
1223
|
+
try:
|
|
1224
|
+
_dispatch_alert_notification(payload, mode="real")
|
|
1225
|
+
except Exception as dispatch_exc:
|
|
1226
|
+
eprint(f"[project-budget-alerts] dispatch failed: {dispatch_exc}")
|
|
1227
|
+
elif alert_sink is not None:
|
|
1228
|
+
alert_sink.extend(pending_alerts)
|
|
1121
1229
|
|
|
1122
1230
|
|
|
1123
1231
|
def maybe_record_codex_budget_milestone(
|
|
1124
|
-
saved: dict[str, Any], *, raise_errors: bool = False,
|
|
1232
|
+
saved: dict[str, Any], *, raise_errors: bool = False, conn=None, as_of=None,
|
|
1233
|
+
alert_sink: "list | None" = None,
|
|
1125
1234
|
) -> int:
|
|
1126
1235
|
"""Fire Codex budget alerts on ACTUAL-Codex-spend threshold crossings (axis
|
|
1127
1236
|
``codex_budget``, calendar-period-codex-budgets spec §6 — the gap the Codex
|
|
@@ -1186,6 +1295,9 @@ def maybe_record_codex_budget_milestone(
|
|
|
1186
1295
|
consumption_pct=kw["consumption_pct"],
|
|
1187
1296
|
),
|
|
1188
1297
|
raise_errors=raise_errors,
|
|
1298
|
+
conn=conn,
|
|
1299
|
+
as_of=as_of,
|
|
1300
|
+
alert_sink=alert_sink,
|
|
1189
1301
|
)
|
|
1190
1302
|
|
|
1191
1303
|
|
|
@@ -1243,7 +1355,8 @@ def _weekly_pct_week_avg_projection(conn, now_utc):
|
|
|
1243
1355
|
|
|
1244
1356
|
|
|
1245
1357
|
def maybe_record_projected_alert(
|
|
1246
|
-
saved: dict[str, Any], *, only_metrics=None
|
|
1358
|
+
saved: dict[str, Any], *, only_metrics=None, conn=None, as_of=None,
|
|
1359
|
+
alert_sink: "list | None" = None,
|
|
1247
1360
|
) -> None:
|
|
1248
1361
|
"""Projected-pace detect-and-arm (axis ``projected``, #121 / #135).
|
|
1249
1362
|
|
|
@@ -1353,9 +1466,11 @@ def maybe_record_projected_alert(
|
|
|
1353
1466
|
# forks / double-fires.
|
|
1354
1467
|
config_tz = resolve_display_tz(argparse.Namespace(tz=None), cfg)
|
|
1355
1468
|
|
|
1356
|
-
now_utc =
|
|
1469
|
+
now_utc = _as_of_or_command(as_of)
|
|
1357
1470
|
pending: list[dict[str, Any]] = []
|
|
1358
|
-
|
|
1471
|
+
own_conn = conn is None
|
|
1472
|
+
if own_conn:
|
|
1473
|
+
conn = open_db()
|
|
1359
1474
|
try:
|
|
1360
1475
|
# ── weekly_pct leg (snapshot-only, cheap) ───────────────────────────
|
|
1361
1476
|
if weekly_on:
|
|
@@ -1495,6 +1610,7 @@ def maybe_record_projected_alert(
|
|
|
1495
1610
|
projected_value=p["projected_value"],
|
|
1496
1611
|
denominator=p["denominator"],
|
|
1497
1612
|
commit=False,
|
|
1613
|
+
as_of=as_of,
|
|
1498
1614
|
)
|
|
1499
1615
|
# Only the genuine-new-crossing winner (rowcount==1) arms+dispatches;
|
|
1500
1616
|
# a racing record-usage instance gets rowcount==0 and skips. The
|
|
@@ -1504,32 +1620,57 @@ def maybe_record_projected_alert(
|
|
|
1504
1620
|
"UPDATE projected_milestones SET alerted_at = ? "
|
|
1505
1621
|
"WHERE week_start_at = ? AND period = ? AND metric = ? "
|
|
1506
1622
|
" AND threshold = ? AND alerted_at IS NULL",
|
|
1507
|
-
(now_utc_iso(), p["week_start_at"], p["period"],
|
|
1623
|
+
(as_of or now_utc_iso(), p["week_start_at"], p["period"],
|
|
1508
1624
|
p["metric"], p["threshold"]),
|
|
1509
1625
|
)
|
|
1510
1626
|
fired.append(p)
|
|
1511
1627
|
# Single commit: every INSERT + its alerted_at marker durable together.
|
|
1512
|
-
conn
|
|
1628
|
+
# On the passed-conn (ingest) path the caller owns commit + dispatch.
|
|
1629
|
+
if own_conn:
|
|
1630
|
+
conn.commit()
|
|
1513
1631
|
except Exception as exc:
|
|
1632
|
+
# Exception discipline (6c-gate P1): passed-conn (ingest) path re-raises
|
|
1633
|
+
# so a failure ABORTS the cycle (invariant ii); legacy own-connection
|
|
1634
|
+
# swallows so a standalone record-usage tick never regresses.
|
|
1635
|
+
if not own_conn:
|
|
1636
|
+
raise
|
|
1514
1637
|
eprint(f"[projected-alert] error recording projected milestone: {exc}")
|
|
1515
1638
|
fired = []
|
|
1516
1639
|
finally:
|
|
1517
|
-
|
|
1640
|
+
if own_conn:
|
|
1641
|
+
conn.close()
|
|
1518
1642
|
|
|
1519
1643
|
# Dispatch AFTER commit; a dispatch failure NEVER rolls back the milestone
|
|
1520
|
-
# (set-then-dispatch invariant).
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1644
|
+
# (set-then-dispatch invariant). Skipped on the passed-conn path (the
|
|
1645
|
+
# ingester's ALERT_DISPATCHER dispatches).
|
|
1646
|
+
if own_conn:
|
|
1647
|
+
for p in fired:
|
|
1648
|
+
try:
|
|
1649
|
+
payload = _build_alert_payload_projected(
|
|
1650
|
+
metric=p["metric"],
|
|
1651
|
+
threshold=p["threshold"],
|
|
1652
|
+
projected_value=p["projected_value"],
|
|
1653
|
+
denominator=p["denominator"],
|
|
1654
|
+
week_start_at=p["week_start_at"],
|
|
1655
|
+
)
|
|
1656
|
+
_dispatch_alert_notification(payload, mode="real")
|
|
1657
|
+
except Exception as dispatch_exc:
|
|
1658
|
+
eprint(f"[projected-alert] dispatch failed: {dispatch_exc}")
|
|
1659
|
+
elif alert_sink is not None:
|
|
1660
|
+
# Passed-conn (ingest) path: hand the projected crossings to the caller's
|
|
1661
|
+
# sink for the ingester's post-commit dispatch (spec §5.2 step 6). The
|
|
1662
|
+
# alerted_at stamp already landed in the caller's txn.
|
|
1663
|
+
for p in fired:
|
|
1664
|
+
try:
|
|
1665
|
+
alert_sink.append(_build_alert_payload_projected(
|
|
1666
|
+
metric=p["metric"],
|
|
1667
|
+
threshold=p["threshold"],
|
|
1668
|
+
projected_value=p["projected_value"],
|
|
1669
|
+
denominator=p["denominator"],
|
|
1670
|
+
week_start_at=p["week_start_at"],
|
|
1671
|
+
))
|
|
1672
|
+
except Exception as build_exc:
|
|
1673
|
+
eprint(f"[projected-alert] payload build failed: {build_exc}")
|
|
1533
1674
|
|
|
1534
1675
|
|
|
1535
1676
|
def _compute_block_totals(
|
|
@@ -1615,14 +1756,24 @@ def _compute_block_totals(
|
|
|
1615
1756
|
return totals
|
|
1616
1757
|
|
|
1617
1758
|
|
|
1618
|
-
def maybe_update_five_hour_block(
|
|
1759
|
+
def maybe_update_five_hour_block(
|
|
1760
|
+
saved: dict[str, Any], *, conn=None, as_of: "str | None" = None,
|
|
1761
|
+
alert_sink: "list | None" = None,
|
|
1762
|
+
) -> None:
|
|
1619
1763
|
"""Upsert the current 5h block in five_hour_blocks; close strictly
|
|
1620
1764
|
older open blocks; sweep naturally-expired blocks; flag blocks
|
|
1621
1765
|
spanning a recorded mid-week 7d-reset.
|
|
1622
1766
|
|
|
1623
1767
|
Errors are logged and swallowed — record-usage must not regress
|
|
1624
1768
|
because of this helper, same posture as maybe_record_milestone.
|
|
1625
|
-
|
|
1769
|
+
|
|
1770
|
+
Transaction-neutral / capture-time-pure seam (DB journal redesign §5.2.3):
|
|
1771
|
+
when ``conn`` is passed the block upsert + milestone fold + cross-flag sweep
|
|
1772
|
+
run on the caller's transaction — no internal ``open_db()``, no
|
|
1773
|
+
``BEGIN IMMEDIATE``/``commit()``/``close()``, and alert dispatch is left to
|
|
1774
|
+
the caller (the ingester's ALERT_DISPATCHER). ``as_of`` (ISO-Z) is stamped
|
|
1775
|
+
as ``last_updated_at_utc`` / ``alerted_at`` in place of wall clock. Both
|
|
1776
|
+
defaults keep the legacy own-connection, own-transaction behavior."""
|
|
1626
1777
|
five_hour_percent = saved.get("fiveHourPercent")
|
|
1627
1778
|
five_hour_resets_at = saved.get("fiveHourResetsAt")
|
|
1628
1779
|
five_hour_window_key = saved.get("fiveHourWindowKey")
|
|
@@ -1645,7 +1796,9 @@ def maybe_update_five_hour_block(saved: dict[str, Any]) -> None:
|
|
|
1645
1796
|
# five_hour_blocks is empty), so the cost is benign — but the count is
|
|
1646
1797
|
# surprising. If any future helper grows expensive open_db() side effects,
|
|
1647
1798
|
# consolidate by passing the connection through rather than reopening.
|
|
1648
|
-
|
|
1799
|
+
own_conn = conn is None
|
|
1800
|
+
if own_conn:
|
|
1801
|
+
conn = open_db()
|
|
1649
1802
|
try:
|
|
1650
1803
|
# Step 3 (per spec §3.2): read prior state including immutable
|
|
1651
1804
|
# fields we'll re-use. Re-deriving block_start_at from saved.
|
|
@@ -1661,6 +1814,17 @@ def maybe_update_five_hour_block(saved: dict[str, Any]) -> None:
|
|
|
1661
1814
|
(int(five_hour_window_key),),
|
|
1662
1815
|
).fetchone()
|
|
1663
1816
|
|
|
1817
|
+
# Whole-table emptiness BEFORE this tick's upsert — the exact trigger of
|
|
1818
|
+
# the legacy `_backfill_five_hour_blocks` (open_db()-gated on "blocks
|
|
1819
|
+
# empty + snapshots present"). It's dead in the DB journal redesign
|
|
1820
|
+
# (snapshot + block share one cycle txn), so the post-insert close below
|
|
1821
|
+
# replicates only its is_closed = (resets < now) effect for the FIRST
|
|
1822
|
+
# block. Gating on this (not just `resets < now`) keeps a strictly-newer
|
|
1823
|
+
# historical block open — the backfill never touched a non-first insert.
|
|
1824
|
+
blocks_were_empty = conn.execute(
|
|
1825
|
+
"SELECT COUNT(*) FROM five_hour_blocks"
|
|
1826
|
+
).fetchone()[0] == 0
|
|
1827
|
+
|
|
1664
1828
|
if prior is None:
|
|
1665
1829
|
# First observation of this window. Compute block_start_at
|
|
1666
1830
|
# from the canonical resets timestamp.
|
|
@@ -1716,14 +1880,17 @@ def maybe_update_five_hour_block(saved: dict[str, Any]) -> None:
|
|
|
1716
1880
|
# Steps 4-5 + 7: transaction wraps close-older + upsert so a
|
|
1717
1881
|
# mid-sequence failure doesn't leave the prior block closed
|
|
1718
1882
|
# without the current block opened/updated.
|
|
1719
|
-
now_iso = now_utc_iso()
|
|
1883
|
+
now_iso = as_of or now_utc_iso()
|
|
1720
1884
|
# BEGIN IMMEDIATE (not deferred): the first DML below is a write (the
|
|
1721
1885
|
# close-older UPDATE), so this transaction already takes the write lock
|
|
1722
1886
|
# up front today. Stating IMMEDIATE makes that the explicit contract —
|
|
1723
1887
|
# a future edit that slips a SELECT before the first write here cannot
|
|
1724
1888
|
# silently reintroduce a SQLITE_BUSY_SNAPSHOT crash (busy_timeout does
|
|
1725
|
-
# not absorb that). See cctally-dev#87.
|
|
1726
|
-
|
|
1889
|
+
# not absorb that). See cctally-dev#87. On the passed-conn (ingest) path
|
|
1890
|
+
# the caller already owns the transaction, so we do NOT open a nested
|
|
1891
|
+
# one — the DML runs directly in the caller's txn and the caller commits.
|
|
1892
|
+
if own_conn:
|
|
1893
|
+
conn.execute("BEGIN IMMEDIATE")
|
|
1727
1894
|
try:
|
|
1728
1895
|
# Step 5: close any STRICTLY OLDER open block. `<` not `!=`
|
|
1729
1896
|
# — record-usage runs in parallel via background hook-tick &
|
|
@@ -1832,6 +1999,22 @@ def maybe_update_five_hour_block(saved: dict[str, Any]) -> None:
|
|
|
1832
1999
|
).fetchone()
|
|
1833
2000
|
block_id = int(block_id_row["id"])
|
|
1834
2001
|
|
|
2002
|
+
# Close the just-upserted FIRST block if its window already expired at
|
|
2003
|
+
# capture time — replicating the (journal-model-dead)
|
|
2004
|
+
# `_backfill_five_hour_blocks` is_closed = (resets < now) effect. Gated
|
|
2005
|
+
# on `blocks_were_empty` (the backfill's own "first block" trigger) so
|
|
2006
|
+
# a strictly-newer historical block stays open (scenario D), and on
|
|
2007
|
+
# `is_closed = 0` so it is idempotent + a no-op for a legacy own-conn
|
|
2008
|
+
# caller whose backfill already closed the historical block. `<`
|
|
2009
|
+
# ISO-string-compares resets_at against now_iso, same as the sweep.
|
|
2010
|
+
if blocks_were_empty:
|
|
2011
|
+
conn.execute(
|
|
2012
|
+
"UPDATE five_hour_blocks SET is_closed = 1, last_updated_at_utc = ? "
|
|
2013
|
+
"WHERE five_hour_window_key = ? AND is_closed = 0 "
|
|
2014
|
+
" AND five_hour_resets_at < ?",
|
|
2015
|
+
(now_iso, int(five_hour_window_key), now_iso),
|
|
2016
|
+
)
|
|
2017
|
+
|
|
1835
2018
|
# ── Replace-all per-tick: per-(block, model) and per-(block, project_path)
|
|
1836
2019
|
# rollup-children. DELETE keyed on five_hour_window_key (NOT block_id) so
|
|
1837
2020
|
# orphan child rows from a prior parent rebuild are cleaned up automatically.
|
|
@@ -2036,7 +2219,7 @@ def maybe_update_five_hour_block(saved: dict[str, Any]) -> None:
|
|
|
2036
2219
|
and alerts_cfg["enabled"]
|
|
2037
2220
|
and pct in alerts_cfg["five_hour_thresholds"]
|
|
2038
2221
|
):
|
|
2039
|
-
crossed_at = now_utc_iso()
|
|
2222
|
+
crossed_at = as_of or now_utc_iso()
|
|
2040
2223
|
# Site D — alerted_at UPDATE scoped to the
|
|
2041
2224
|
# active segment, so the post-credit row
|
|
2042
2225
|
# gets stamped without overwriting an
|
|
@@ -2156,9 +2339,11 @@ def maybe_update_five_hour_block(saved: dict[str, Any]) -> None:
|
|
|
2156
2339
|
""",
|
|
2157
2340
|
)
|
|
2158
2341
|
|
|
2159
|
-
|
|
2342
|
+
if own_conn:
|
|
2343
|
+
conn.commit()
|
|
2160
2344
|
except Exception:
|
|
2161
|
-
|
|
2345
|
+
if own_conn:
|
|
2346
|
+
conn.rollback()
|
|
2162
2347
|
raise
|
|
2163
2348
|
|
|
2164
2349
|
# I1: dispatch deferred to AFTER the outer commit. The milestone
|
|
@@ -2170,17 +2355,35 @@ def maybe_update_five_hour_block(saved: dict[str, Any]) -> None:
|
|
|
2170
2355
|
# caller ignores _dispatch_alert_notification's return value
|
|
2171
2356
|
# (spec §6.4); a per-payload exception is logged and the loop
|
|
2172
2357
|
# continues so a bad-payload alert can't suppress healthy ones.
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2358
|
+
# On the passed-conn (ingest) path the caller owns the commit and the
|
|
2359
|
+
# post-commit alert dispatch (the ingester's ALERT_DISPATCHER), so both
|
|
2360
|
+
# are skipped here.
|
|
2361
|
+
if own_conn:
|
|
2362
|
+
for payload in pending_alerts:
|
|
2363
|
+
try:
|
|
2364
|
+
_dispatch_alert_notification(
|
|
2365
|
+
payload, mode="real", tz=display_tz_for_alerts
|
|
2366
|
+
)
|
|
2367
|
+
except Exception as dispatch_exc:
|
|
2368
|
+
eprint(f"[alerts] dispatch failed: {dispatch_exc}")
|
|
2369
|
+
elif alert_sink is not None:
|
|
2370
|
+
# Passed-conn (ingest) path: hand the 5h-milestone new-crossing
|
|
2371
|
+
# payloads to the caller's sink for post-commit dispatch (spec §5.2
|
|
2372
|
+
# step 6). alerted_at was stamped in the caller's txn above.
|
|
2373
|
+
alert_sink.extend(pending_alerts)
|
|
2180
2374
|
except Exception as exc:
|
|
2375
|
+
# Exception discipline (6b-gate P2): on the passed-conn (ingest) path a
|
|
2376
|
+
# chokepoint exception must ABORT the cycle — re-raise so the ingester
|
|
2377
|
+
# rolls back the txn, leaves the cursor unmoved, and makes no partial
|
|
2378
|
+
# commit (invariant ii). Only the legacy own-connection path keeps the
|
|
2379
|
+
# log-and-swallow so a standalone record-usage tick never regresses on a
|
|
2380
|
+
# 5h-block hiccup.
|
|
2381
|
+
if not own_conn:
|
|
2382
|
+
raise
|
|
2181
2383
|
eprint(f"[5h-block] error updating block: {exc}")
|
|
2182
2384
|
finally:
|
|
2183
|
-
|
|
2385
|
+
if own_conn:
|
|
2386
|
+
conn.close()
|
|
2184
2387
|
|
|
2185
2388
|
|
|
2186
2389
|
# ── Reset-to-zero debounce marker (issue #128) ─────────────────────────────
|
|
@@ -2254,11 +2457,29 @@ def _read_reset_zero_marker():
|
|
|
2254
2457
|
|
|
2255
2458
|
|
|
2256
2459
|
def _fire_in_place_credit(conn, week_start_date, cur_end_canon, weekly_percent,
|
|
2257
|
-
*, observed_pre_credit_pct, effective_dt
|
|
2460
|
+
*, observed_pre_credit_pct, effective_dt,
|
|
2461
|
+
as_of=None, commit=True, ctx=None):
|
|
2258
2462
|
"""Emit/refresh the in-place weekly-credit artifacts (issue #19 + #128).
|
|
2259
2463
|
Shared by the immediate >=25pp path and the debounced reset-to-zero
|
|
2260
2464
|
confirmation path.
|
|
2261
2465
|
|
|
2466
|
+
Transaction-neutral / capture-time-pure seam (DB journal redesign §5.2.3):
|
|
2467
|
+
``commit=False`` folds the event-row INSERT + stale-replica DELETE into the
|
|
2468
|
+
caller's transaction (the ingester's cycle) instead of committing inline;
|
|
2469
|
+
``as_of`` (ISO-Z) stamps ``week_reset_events.detected_at_utc`` in place of
|
|
2470
|
+
wall clock (the reset moment itself stays ``effective_dt``, a parameter).
|
|
2471
|
+
Both defaults keep the legacy inline-commit, wall-clock behavior.
|
|
2472
|
+
|
|
2473
|
+
Design B event+effects seam (§5.3): when an ``IngestContext`` ``ctx`` is
|
|
2474
|
+
passed AND this call is the genuine-new-reset winner (the ``already is None``
|
|
2475
|
+
pre-check AND the ``week_reset_events`` INSERT rowcount == 1), the doomed
|
|
2476
|
+
stale-replica snapshots' ``journal_id``s are captured (SAME predicate as the
|
|
2477
|
+
pivot-2 DELETE) into ``ctx.suppression_map`` keyed on the ``wr`` harvest
|
|
2478
|
+
natural key ``(old_week_end_at, new_week_end_at) = (effective_iso,
|
|
2479
|
+
cur_end_canon)`` BEFORE the DELETE runs, so ``_build_harvest_evt`` attaches
|
|
2480
|
+
the list to the ``wr`` evt and the destructive effect replays. ``ctx=None``
|
|
2481
|
+
(legacy) captures nothing.
|
|
2482
|
+
|
|
2262
2483
|
Side-effect ordering is load-bearing: the event-row INSERT is dedup-gated
|
|
2263
2484
|
on a pre-check, but the hwm force-write and stale-replica DELETE run
|
|
2264
2485
|
UNCONDITIONALLY — a prior run may have committed the event then died before
|
|
@@ -2281,15 +2502,33 @@ def _fire_in_place_credit(conn, week_start_date, cur_end_canon, weekly_percent,
|
|
|
2281
2502
|
# post_map fires on the credited week in _apply_reset_events_to_weekrefs
|
|
2282
2503
|
# (old==new collapses it to a zero-width window). observed_pre_credit_pct
|
|
2283
2504
|
# stamps the pre-credit baseline (issue #45).
|
|
2284
|
-
conn.execute(
|
|
2505
|
+
ins_wr = conn.execute(
|
|
2285
2506
|
"INSERT OR IGNORE INTO week_reset_events "
|
|
2286
2507
|
"(detected_at_utc, old_week_end_at, new_week_end_at, "
|
|
2287
2508
|
" effective_reset_at_utc, observed_pre_credit_pct) "
|
|
2288
2509
|
"VALUES (?, ?, ?, ?, ?)",
|
|
2289
|
-
(now_utc_iso(), effective_iso, cur_end_canon,
|
|
2510
|
+
(as_of or now_utc_iso(), effective_iso, cur_end_canon,
|
|
2290
2511
|
effective_iso, float(observed_pre_credit_pct)),
|
|
2291
2512
|
)
|
|
2292
|
-
|
|
2513
|
+
# Design B (§5.3 event+effects): on the ingest path, capture the doomed
|
|
2514
|
+
# stale-replica snapshots' journal_ids BEFORE the pivot-2 DELETE (SAME
|
|
2515
|
+
# predicate), keyed on the wr harvest natural key (old, new) =
|
|
2516
|
+
# (effective_iso, cur_end_canon). Gated on the genuine-new-reset winner
|
|
2517
|
+
# (rowcount == 1) so a crash-replayed reset never re-suppresses; ctx=None
|
|
2518
|
+
# (legacy) captures nothing.
|
|
2519
|
+
if ctx is not None and ins_wr.rowcount == 1:
|
|
2520
|
+
doomed = conn.execute(
|
|
2521
|
+
"SELECT journal_id FROM weekly_usage_snapshots "
|
|
2522
|
+
"WHERE week_start_date = ? "
|
|
2523
|
+
" AND unixepoch(captured_at_utc) >= unixepoch(?) "
|
|
2524
|
+
" AND ABS(weekly_percent - ?) < 1.0",
|
|
2525
|
+
(week_start_date, effective_iso, float(observed_pre_credit_pct)),
|
|
2526
|
+
).fetchall()
|
|
2527
|
+
ctx.suppression_map[(effective_iso, cur_end_canon)] = [
|
|
2528
|
+
r[0] for r in doomed
|
|
2529
|
+
]
|
|
2530
|
+
if commit:
|
|
2531
|
+
conn.commit()
|
|
2293
2532
|
# Unconditional pivot 1: force-write hwm-7d so the next status-line render
|
|
2294
2533
|
# reflects the post-credit value (the monotonic guard at the normal write
|
|
2295
2534
|
# site would refuse to decrease the file).
|
|
@@ -2312,11 +2551,454 @@ def _fire_in_place_credit(conn, week_start_date, cur_end_canon, weekly_percent,
|
|
|
2312
2551
|
" AND ABS(weekly_percent - ?) < 1.0",
|
|
2313
2552
|
(week_start_date, effective_iso, float(observed_pre_credit_pct)),
|
|
2314
2553
|
)
|
|
2315
|
-
|
|
2554
|
+
if commit:
|
|
2555
|
+
conn.commit()
|
|
2316
2556
|
except sqlite3.DatabaseError as exc:
|
|
2317
2557
|
eprint(f"[record-usage] post-credit cleanup failed: {exc}")
|
|
2318
2558
|
|
|
2319
2559
|
|
|
2560
|
+
def detect_reset_and_credit(conn, *, week_start_date, week_end_at,
|
|
2561
|
+
weekly_percent, five_hour_window_key,
|
|
2562
|
+
five_hour_percent, as_of=None, commit=True,
|
|
2563
|
+
ctx=None):
|
|
2564
|
+
"""Detect + record weekly and 5h reset/credit artifacts for one usage
|
|
2565
|
+
observation (extracted from ``cmd_record_usage``; DB journal redesign
|
|
2566
|
+
§5.2.3).
|
|
2567
|
+
|
|
2568
|
+
Runs the mid-week ``week_reset_events`` detection, the reset-to-zero
|
|
2569
|
+
debounce + >=25pp in-place-credit fire, and the parallel 5h in-place-
|
|
2570
|
+
credit detection, all on the caller's ``conn``.
|
|
2571
|
+
|
|
2572
|
+
Seam parameters keep legacy behavior at their defaults:
|
|
2573
|
+
|
|
2574
|
+
- ``as_of`` (ISO-8601 ``Z``/offset, or ``None``): the capture-time
|
|
2575
|
+
predicate clock. ``None`` -> ``_command_as_of()`` wall clock
|
|
2576
|
+
(honoring ``CCTALLY_AS_OF``), byte-identical to the pre-extraction
|
|
2577
|
+
inline code; the ingester injects the observation's ``at`` so
|
|
2578
|
+
detection is replay-deterministic. It also anchors the
|
|
2579
|
+
``detected_at_utc`` audit stamps (``as_of or now_utc_iso()``) and
|
|
2580
|
+
threads into ``_fire_in_place_credit``.
|
|
2581
|
+
- ``commit`` (default ``True``): the legacy own-transaction path
|
|
2582
|
+
commits once at the end; the ingest path passes ``commit=False`` so
|
|
2583
|
+
the cycle owns the single commit (invariant ii). ``commit=False``
|
|
2584
|
+
also flips the two outer handlers from log-and-swallow to re-raise,
|
|
2585
|
+
so a detection failure ABORTS the ingest cycle instead of silently
|
|
2586
|
+
dropping a reset.
|
|
2587
|
+
- ``ctx`` (default ``None``): the ingest cycle's ``IngestContext``.
|
|
2588
|
+
When present, the destructive 5h credit path (and, via the threaded
|
|
2589
|
+
``ctx``, ``_fire_in_place_credit``) captures its stale-replica
|
|
2590
|
+
DELETE's doomed ``journal_id``s into ``ctx.suppression_map`` BEFORE
|
|
2591
|
+
deleting (Design B event+effects, §5.3), keyed on the reset row's
|
|
2592
|
+
harvest natural key. Legacy (``ctx=None``) captures nothing.
|
|
2593
|
+
"""
|
|
2594
|
+
c = _cctally()
|
|
2595
|
+
now_utc = _as_of_or_command(as_of)
|
|
2596
|
+
# Mid-week reset detection. When `resets_at` advances before the
|
|
2597
|
+
# previously-declared reset actually fires (Anthropic-initiated
|
|
2598
|
+
# goodwill reset, or any API-side shift), record one week_reset_events
|
|
2599
|
+
# row so display + cost layers can treat the observed moment as the
|
|
2600
|
+
# old week's effective end AND the new week's effective start. The
|
|
2601
|
+
# monotonic check below stays keyed on week_start_date so it still
|
|
2602
|
+
# guards the new week against stale rate-limit data independently.
|
|
2603
|
+
# Both boundaries canonicalize to hour (same rule make_week_ref uses)
|
|
2604
|
+
# so minute/second-level Anthropic jitter doesn't masquerade as a
|
|
2605
|
+
# reset and the stored values match what WeekRef.week_end_at carries.
|
|
2606
|
+
# The 5h-block cross-flag is no longer threaded from here —
|
|
2607
|
+
# maybe_update_five_hour_block re-derives it every tick by JOINing
|
|
2608
|
+
# against week_reset_events (self-healing, see helper for rationale).
|
|
2609
|
+
try:
|
|
2610
|
+
cur_end_canon = _canonicalize_optional_iso(week_end_at, "record.cur")
|
|
2611
|
+
prior = conn.execute(
|
|
2612
|
+
"SELECT week_end_at, weekly_percent FROM weekly_usage_snapshots "
|
|
2613
|
+
"WHERE week_end_at IS NOT NULL "
|
|
2614
|
+
"ORDER BY captured_at_utc DESC, id DESC LIMIT 1"
|
|
2615
|
+
).fetchone()
|
|
2616
|
+
if prior and prior["week_end_at"] and cur_end_canon:
|
|
2617
|
+
prior_end_canon = _canonicalize_optional_iso(
|
|
2618
|
+
prior["week_end_at"], "record.prior"
|
|
2619
|
+
)
|
|
2620
|
+
prior_pct = prior["weekly_percent"]
|
|
2621
|
+
# `now_utc` is the capture-time predicate clock, bound once at the
|
|
2622
|
+
# top of the function from `_as_of_or_command(as_of)` — legacy
|
|
2623
|
+
# `as_of=None` resolves it to `_command_as_of()` (CCTALLY_AS_OF /
|
|
2624
|
+
# wall clock, byte-identical to the pre-extraction inline binding
|
|
2625
|
+
# that lived here); the ingester injects the observation's `at` so
|
|
2626
|
+
# mid-week-reset detection replays deterministically.
|
|
2627
|
+
if prior_end_canon and prior_end_canon != cur_end_canon:
|
|
2628
|
+
prior_end_dt = parse_iso_datetime(prior_end_canon, "prior.week_end_at")
|
|
2629
|
+
# Fire only when (a) prior window was still in the FUTURE
|
|
2630
|
+
# (Anthropic shifted the boundary before natural expiration),
|
|
2631
|
+
# AND (b) weekly_percent dropped by RESET_PCT_DROP_THRESHOLD
|
|
2632
|
+
# or more (filters out API flaps / transient boundary
|
|
2633
|
+
# jitter where usage stays roughly the same).
|
|
2634
|
+
if (
|
|
2635
|
+
prior_end_dt > now_utc
|
|
2636
|
+
and prior_pct is not None
|
|
2637
|
+
and c._is_reset_drop(prior_pct, weekly_percent)
|
|
2638
|
+
):
|
|
2639
|
+
# See _backfill_week_reset_events for why we floor
|
|
2640
|
+
# the reset moment to the hour (natural display
|
|
2641
|
+
# boundary, aligned with Anthropic's hour-only
|
|
2642
|
+
# resets_at values).
|
|
2643
|
+
effective_iso = _floor_to_hour(now_utc).isoformat(timespec="seconds")
|
|
2644
|
+
conn.execute(
|
|
2645
|
+
"INSERT OR IGNORE INTO week_reset_events "
|
|
2646
|
+
"(detected_at_utc, old_week_end_at, new_week_end_at, "
|
|
2647
|
+
" effective_reset_at_utc) VALUES (?, ?, ?, ?)",
|
|
2648
|
+
((as_of or now_utc_iso()), prior_end_canon, cur_end_canon,
|
|
2649
|
+
effective_iso),
|
|
2650
|
+
)
|
|
2651
|
+
# (inline commit removed — the function commits once at the
|
|
2652
|
+
# end on the legacy path; the ingest cycle owns the commit.)
|
|
2653
|
+
elif prior_end_canon and prior_end_canon == cur_end_canon:
|
|
2654
|
+
# In-place credit branch (v1.7.2) + reset-to-zero debounce
|
|
2655
|
+
# (issue #128). Same end_at across two captures. A >=25pp drop
|
|
2656
|
+
# is a goodwill credit and fires immediately; a reset-to-zero
|
|
2657
|
+
# (post <= floor, 3..25pp drop) is debounced against a
|
|
2658
|
+
# transient API zero — armed on the first ~0, confirmed only
|
|
2659
|
+
# if the next reading stays low (<= half the pre-zero
|
|
2660
|
+
# baseline), cleared on recovery toward baseline. The gate
|
|
2661
|
+
# drops the _is_reset_drop term so the recovery-clear path is
|
|
2662
|
+
# reachable. See the spec for the midpoint rationale.
|
|
2663
|
+
prior_end_dt = parse_iso_datetime(prior_end_canon, "prior.week_end_at")
|
|
2664
|
+
if prior_end_dt > now_utc and prior_pct is not None:
|
|
2665
|
+
# Read the pending reset-to-zero marker up front (pure
|
|
2666
|
+
# file read) and compute whether it is armed for THIS
|
|
2667
|
+
# window; the debounce CLASSIFIER (pure) decides the
|
|
2668
|
+
# action from those values + the c._RESET_* constants,
|
|
2669
|
+
# then the glue below executes the decided I/O. The 5
|
|
2670
|
+
# branch outcomes (fire-immediate / confirm / clear /
|
|
2671
|
+
# arm / none) map 1:1 to the pre-extraction structure.
|
|
2672
|
+
marker = _read_reset_zero_marker()
|
|
2673
|
+
armed = (
|
|
2674
|
+
marker is not None
|
|
2675
|
+
and marker[0] == week_start_date
|
|
2676
|
+
and marker[1] == cur_end_canon
|
|
2677
|
+
)
|
|
2678
|
+
decision = plan_weekly_credit_debounce(
|
|
2679
|
+
prior_pct, weekly_percent,
|
|
2680
|
+
drop_threshold=c._RESET_PCT_DROP_THRESHOLD,
|
|
2681
|
+
zero_floor_pct=c._RESET_ZERO_FLOOR_PCT,
|
|
2682
|
+
zero_min_drop_pct=c._RESET_ZERO_MIN_DROP_PCT,
|
|
2683
|
+
marker_armed=armed,
|
|
2684
|
+
marker_baseline=(marker[2] if armed else None),
|
|
2685
|
+
).action
|
|
2686
|
+
if decision == FIRE_IMMEDIATE:
|
|
2687
|
+
# >=25pp goodwill credit — fire immediately, never
|
|
2688
|
+
# debounced. Clear any pending arm (now moot).
|
|
2689
|
+
_clear_reset_zero_marker()
|
|
2690
|
+
_fire_in_place_credit(
|
|
2691
|
+
conn, week_start_date, cur_end_canon, weekly_percent,
|
|
2692
|
+
observed_pre_credit_pct=float(prior_pct),
|
|
2693
|
+
effective_dt=_floor_to_hour(now_utc),
|
|
2694
|
+
as_of=as_of, commit=commit, ctx=ctx,
|
|
2695
|
+
)
|
|
2696
|
+
elif decision == CONFIRM_RESET:
|
|
2697
|
+
# Second reading stayed low → confirm. Anchor the
|
|
2698
|
+
# reset at the FIRST-zero instant from the marker
|
|
2699
|
+
# (UTC-normalized like the backfill in-place path).
|
|
2700
|
+
first_zero_dt = parse_iso_datetime(
|
|
2701
|
+
marker[3], "reset_zero_marker.first_zero"
|
|
2702
|
+
).astimezone(dt.timezone.utc)
|
|
2703
|
+
_fire_in_place_credit(
|
|
2704
|
+
conn, week_start_date, cur_end_canon,
|
|
2705
|
+
weekly_percent,
|
|
2706
|
+
observed_pre_credit_pct=marker[2],
|
|
2707
|
+
effective_dt=_floor_to_hour(first_zero_dt),
|
|
2708
|
+
as_of=as_of, commit=commit, ctx=ctx,
|
|
2709
|
+
)
|
|
2710
|
+
# Clear ONLY after the fire completes (P2a): a
|
|
2711
|
+
# mid-fire crash leaves the marker armed so the next
|
|
2712
|
+
# zero re-confirms + re-runs the idempotent pivots.
|
|
2713
|
+
_clear_reset_zero_marker()
|
|
2714
|
+
elif decision == CLEAR_MARKER:
|
|
2715
|
+
# Recovered toward baseline → transient zero, not a
|
|
2716
|
+
# reset. Clear, do not fire.
|
|
2717
|
+
_clear_reset_zero_marker()
|
|
2718
|
+
elif decision == ARM_MARKER:
|
|
2719
|
+
# First ~0 → arm; do NOT fire. The write clamp
|
|
2720
|
+
# suppresses this 0 (no event row yet), so the prior
|
|
2721
|
+
# snapshot stays at the baseline and this shape
|
|
2722
|
+
# re-evaluates next tick. first_zero_iso is the
|
|
2723
|
+
# _command_as_of() value (now_utc), NOT wall-clock —
|
|
2724
|
+
# it becomes the effective anchor.
|
|
2725
|
+
_arm_reset_zero_marker(
|
|
2726
|
+
week_start_date, cur_end_canon,
|
|
2727
|
+
baseline_pct=float(prior_pct),
|
|
2728
|
+
first_zero_iso=now_utc.isoformat(timespec="seconds"),
|
|
2729
|
+
)
|
|
2730
|
+
# else NO_ACTION: not a reset shape and not armed →
|
|
2731
|
+
# nothing. A non-matching stale marker is inert
|
|
2732
|
+
# (ignored on key mismatch, overwritten by next arm).
|
|
2733
|
+
|
|
2734
|
+
# ── 5h in-place credit detection (parallel to weekly above) ──
|
|
2735
|
+
# Spec §2.2 of
|
|
2736
|
+
# docs/superpowers/specs/2026-05-16-5h-in-place-credit-detection.md.
|
|
2737
|
+
# Slot SECOND so the weekly branch retains control-flow
|
|
2738
|
+
# priority — both branches are independent (they touch
|
|
2739
|
+
# different tables) and the order has no behavioral
|
|
2740
|
+
# interaction. Same outer try/except wraps both so a
|
|
2741
|
+
# 5h-detection failure logs but does not regress the rest
|
|
2742
|
+
# of cmd_record_usage.
|
|
2743
|
+
#
|
|
2744
|
+
# Diverges from weekly in three places:
|
|
2745
|
+
# - Threshold: 5.0pp (constant on cctally module), not 25.0pp.
|
|
2746
|
+
# The 5h envelope is smaller so a 5pp move is
|
|
2747
|
+
# proportionally larger.
|
|
2748
|
+
# - Effective-iso floor: 10-min (matches
|
|
2749
|
+
# ``_canonical_5h_window_key``'s 600s floor), not hour.
|
|
2750
|
+
# Up to ~30 distinct slots per 5h block; same-slot
|
|
2751
|
+
# collisions absorbed by UNIQUE per spec §2.3.
|
|
2752
|
+
# - Pre-check: pair-checks the latest event's
|
|
2753
|
+
# ``(prior_percent, post_percent)`` against this tick's
|
|
2754
|
+
# ``(prior_5h_pct, five_hour_percent)``, not
|
|
2755
|
+
# ``new_week_end_at`` equality. A genuine replay matches
|
|
2756
|
+
# BOTH fields; a NEW credit-with-idle (prior_pct equals
|
|
2757
|
+
# the prior credit's post_pct because the user didn't
|
|
2758
|
+
# move between credits) matches only one field and
|
|
2759
|
+
# correctly proceeds to write a second event row.
|
|
2760
|
+
try:
|
|
2761
|
+
if (
|
|
2762
|
+
five_hour_window_key is not None
|
|
2763
|
+
and five_hour_percent is not None
|
|
2764
|
+
):
|
|
2765
|
+
prior_5h_row = conn.execute(
|
|
2766
|
+
"SELECT five_hour_window_key, five_hour_percent, "
|
|
2767
|
+
" five_hour_resets_at "
|
|
2768
|
+
" FROM weekly_usage_snapshots "
|
|
2769
|
+
" WHERE five_hour_window_key IS NOT NULL "
|
|
2770
|
+
" AND five_hour_percent IS NOT NULL "
|
|
2771
|
+
" ORDER BY captured_at_utc DESC, id DESC LIMIT 1"
|
|
2772
|
+
).fetchone()
|
|
2773
|
+
if (
|
|
2774
|
+
prior_5h_row is not None
|
|
2775
|
+
and int(prior_5h_row["five_hour_window_key"])
|
|
2776
|
+
== int(five_hour_window_key)
|
|
2777
|
+
and prior_5h_row["five_hour_resets_at"] is not None
|
|
2778
|
+
):
|
|
2779
|
+
prior_5h_pct = float(prior_5h_row["five_hour_percent"])
|
|
2780
|
+
prior_5h_resets_dt = parse_iso_datetime(
|
|
2781
|
+
prior_5h_row["five_hour_resets_at"],
|
|
2782
|
+
"prior.five_hour_resets_at",
|
|
2783
|
+
)
|
|
2784
|
+
# ``now_utc`` was bound earlier in this same
|
|
2785
|
+
# outer try block from
|
|
2786
|
+
# ``dt.datetime.now(dt.timezone.utc)``; reuse it
|
|
2787
|
+
# so both branches see the same instant.
|
|
2788
|
+
if plan_five_hour_credit(
|
|
2789
|
+
prior_5h_pct, float(five_hour_percent),
|
|
2790
|
+
drop_threshold=c._FIVE_HOUR_RESET_PCT_DROP_THRESHOLD,
|
|
2791
|
+
prior_resets_in_future=(prior_5h_resets_dt > now_utc),
|
|
2792
|
+
):
|
|
2793
|
+
# Pair-check dedup pre-check (spec §2.2;
|
|
2794
|
+
# refined by Codex r4 P1 finding). The
|
|
2795
|
+
# round-1 predicate compared only the
|
|
2796
|
+
# latest event's ``post_percent`` against
|
|
2797
|
+
# this tick's ``prior_5h_pct``; that
|
|
2798
|
+
# false-positived on a legitimate 2nd
|
|
2799
|
+
# credit where the user was idle between
|
|
2800
|
+
# credits (Credit 1 lands prior=20/post=5;
|
|
2801
|
+
# user does nothing; Credit 2 arrives with
|
|
2802
|
+
# CLI percent=0 so prior_5h_pct=5 reads
|
|
2803
|
+
# equal to stored post_percent=5 →
|
|
2804
|
+
# silently swallowed). Pair-checking
|
|
2805
|
+
# against BOTH fields disambiguates: a
|
|
2806
|
+
# genuine replay matches BOTH; a new
|
|
2807
|
+
# credit-with-idle matches at most ONE
|
|
2808
|
+
# (the prior side coincides but
|
|
2809
|
+
# post_percent differs).
|
|
2810
|
+
most_recent = conn.execute(
|
|
2811
|
+
"SELECT prior_percent, post_percent "
|
|
2812
|
+
" FROM five_hour_reset_events "
|
|
2813
|
+
" WHERE five_hour_window_key = ? "
|
|
2814
|
+
" ORDER BY id DESC LIMIT 1",
|
|
2815
|
+
(int(five_hour_window_key),),
|
|
2816
|
+
).fetchone()
|
|
2817
|
+
is_dup = (
|
|
2818
|
+
most_recent is not None
|
|
2819
|
+
and round(prior_5h_pct, 1)
|
|
2820
|
+
== round(float(most_recent["prior_percent"]), 1)
|
|
2821
|
+
and round(float(five_hour_percent), 1)
|
|
2822
|
+
== round(float(most_recent["post_percent"]), 1)
|
|
2823
|
+
)
|
|
2824
|
+
# 10-min floor (spec §2.3 — bounded
|
|
2825
|
+
# stacked-credit resolution; one event per
|
|
2826
|
+
# 10-min slot per block). Resolved BEFORE
|
|
2827
|
+
# the ``if not is_dup`` branch so it's in
|
|
2828
|
+
# scope for the pivots below (per memory
|
|
2829
|
+
# ``project_dedup_must_not_gate_side_effects.md``:
|
|
2830
|
+
# the recovery-tick path must still force
|
|
2831
|
+
# HWM + DELETE even when the INSERT is
|
|
2832
|
+
# absorbed by the pre-check or by
|
|
2833
|
+
# UNIQUE — see comment below for the
|
|
2834
|
+
# crash scenario). ``_floor_to_ten_minutes``
|
|
2835
|
+
# is a cctally module attribute; the
|
|
2836
|
+
# ``c.X`` accessor resolves at call time
|
|
2837
|
+
# so test ``monkeypatch.setitem(ns,
|
|
2838
|
+
# "_floor_to_ten_minutes", …)``
|
|
2839
|
+
# propagates.
|
|
2840
|
+
effective_dt = c._floor_to_ten_minutes(now_utc)
|
|
2841
|
+
effective_iso = effective_dt.isoformat(
|
|
2842
|
+
timespec="seconds"
|
|
2843
|
+
)
|
|
2844
|
+
if not is_dup:
|
|
2845
|
+
ins_fhc = conn.execute(
|
|
2846
|
+
"INSERT OR IGNORE INTO five_hour_reset_events "
|
|
2847
|
+
"(detected_at_utc, five_hour_window_key, "
|
|
2848
|
+
" prior_percent, post_percent, "
|
|
2849
|
+
" effective_reset_at_utc) "
|
|
2850
|
+
"VALUES (?, ?, ?, ?, ?)",
|
|
2851
|
+
(
|
|
2852
|
+
(as_of or now_utc_iso()),
|
|
2853
|
+
int(five_hour_window_key),
|
|
2854
|
+
prior_5h_pct,
|
|
2855
|
+
float(five_hour_percent),
|
|
2856
|
+
effective_iso,
|
|
2857
|
+
),
|
|
2858
|
+
)
|
|
2859
|
+
# Design B (§5.3 event+effects): on the ingest path,
|
|
2860
|
+
# capture the doomed stale-replica snapshots'
|
|
2861
|
+
# journal_ids BEFORE the unconditional DELETE below,
|
|
2862
|
+
# using the SAME predicate as that DELETE, and stash
|
|
2863
|
+
# them in ctx.suppression_map keyed on the fhc
|
|
2864
|
+
# harvest natural key (window_key, effective_iso).
|
|
2865
|
+
# _build_harvest_evt attaches them to the fhc evt so
|
|
2866
|
+
# the destructive effect replays deterministically.
|
|
2867
|
+
# Gated on the genuine-new-reset winner
|
|
2868
|
+
# (rowcount == 1) so a crash-replayed reset never
|
|
2869
|
+
# re-suppresses with a divergent list; ctx=None
|
|
2870
|
+
# (legacy) captures nothing.
|
|
2871
|
+
if ctx is not None and ins_fhc.rowcount == 1:
|
|
2872
|
+
doomed = conn.execute(
|
|
2873
|
+
"SELECT journal_id "
|
|
2874
|
+
"FROM weekly_usage_snapshots "
|
|
2875
|
+
" WHERE five_hour_window_key = ? "
|
|
2876
|
+
" AND unixepoch(captured_at_utc) "
|
|
2877
|
+
" >= unixepoch(?) "
|
|
2878
|
+
" AND ABS(five_hour_percent - ?) "
|
|
2879
|
+
" < 1.0",
|
|
2880
|
+
(
|
|
2881
|
+
int(five_hour_window_key),
|
|
2882
|
+
effective_iso,
|
|
2883
|
+
prior_5h_pct,
|
|
2884
|
+
),
|
|
2885
|
+
).fetchall()
|
|
2886
|
+
ctx.suppression_map[
|
|
2887
|
+
(int(five_hour_window_key), effective_iso)
|
|
2888
|
+
] = [r[0] for r in doomed]
|
|
2889
|
+
# (inline commit removed — one end-of-function commit
|
|
2890
|
+
# on legacy; the ingest cycle owns the commit.)
|
|
2891
|
+
# Pivots fire UNCONDITIONALLY whenever a
|
|
2892
|
+
# credit is detected — NOT gated on
|
|
2893
|
+
# ``not is_dup`` and NOT on
|
|
2894
|
+
# ``rowcount == 1``. Memory
|
|
2895
|
+
# ``project_dedup_must_not_gate_side_effects.md``:
|
|
2896
|
+
# "Skipping a no-op INSERT must NOT skip
|
|
2897
|
+
# milestones/rollups/alerts; prior run may
|
|
2898
|
+
# have died mid-flight." Crash scenario A:
|
|
2899
|
+
# tick N committed the event row, then died
|
|
2900
|
+
# before HWM + DELETE. Tick N+1's
|
|
2901
|
+
# INSERT OR IGNORE returns rowcount == 0
|
|
2902
|
+
# (UNIQUE absorbs) but the system is still
|
|
2903
|
+
# wedged on the pre-credit HWM + stale-
|
|
2904
|
+
# replica rows. Crash scenario B (the
|
|
2905
|
+
# Codex r4 finding): a recovery tick where
|
|
2906
|
+
# ``(prior, post)`` pair-matches the
|
|
2907
|
+
# already-stored event row also takes the
|
|
2908
|
+
# ``is_dup`` branch; without the hoist the
|
|
2909
|
+
# pivots would be skipped and the system
|
|
2910
|
+
# would stay wedged. The pivots are
|
|
2911
|
+
# individually idempotent (file overwrite
|
|
2912
|
+
# + DELETE on a stable predicate), so
|
|
2913
|
+
# re-running them on the recovery tick is
|
|
2914
|
+
# always safe. Mirrors the weekly hoist at
|
|
2915
|
+
# ``_cctally_record.py`` after the
|
|
2916
|
+
# ``if already is None`` block (grep
|
|
2917
|
+
# ``Force-write hwm-7d``).
|
|
2918
|
+
#
|
|
2919
|
+
# Force-write hwm-5h: bypasses the
|
|
2920
|
+
# monotonic guard at the normal hwm-5h
|
|
2921
|
+
# writer below. Lands AFTER
|
|
2922
|
+
# ``conn.commit()`` so a concurrent reader
|
|
2923
|
+
# doesn't see the new HWM before the
|
|
2924
|
+
# event row is durable. File format
|
|
2925
|
+
# matches the canonical writer:
|
|
2926
|
+
# ``<key> <percent>\n``.
|
|
2927
|
+
try:
|
|
2928
|
+
(_cctally_core.APP_DIR / "hwm-5h").write_text(
|
|
2929
|
+
f"{int(five_hour_window_key)} "
|
|
2930
|
+
f"{float(five_hour_percent)}\n"
|
|
2931
|
+
)
|
|
2932
|
+
except OSError:
|
|
2933
|
+
pass
|
|
2934
|
+
# Stale-replica DELETE (spec §4.3).
|
|
2935
|
+
# Defends against claude-statusline
|
|
2936
|
+
# replaying the pre-credit
|
|
2937
|
+
# ``--five-hour-percent`` value past the
|
|
2938
|
+
# credit moment from its own in-memory
|
|
2939
|
+
# HWM cache. 1.0pp tolerance band (issue
|
|
2940
|
+
# #48 — symmetric follow-up to weekly #45)
|
|
2941
|
+
# around the observed pre-credit baseline
|
|
2942
|
+
# absorbs any rounding drift between
|
|
2943
|
+
# cctally's OAuth read and statusline's
|
|
2944
|
+
# ``--five-hour-percent`` payload (today
|
|
2945
|
+
# they match byte-identically, but the
|
|
2946
|
+
# band future-proofs against Anthropic or
|
|
2947
|
+
# statusline changing 5h rounding). The
|
|
2948
|
+
# band stays well below the 5.0pp 5h
|
|
2949
|
+
# in-place credit detection threshold
|
|
2950
|
+
# (``_FIVE_HOUR_RESET_PCT_DROP_THRESHOLD``)
|
|
2951
|
+
# — 4pp safety margin — so legitimate
|
|
2952
|
+
# post-credit values are never caught.
|
|
2953
|
+
# ``unixepoch()`` on both sides for offset
|
|
2954
|
+
# robustness (Z vs +00:00). Bind is the
|
|
2955
|
+
# in-scope ``prior_5h_pct``, which equals
|
|
2956
|
+
# the just-stamped
|
|
2957
|
+
# ``five_hour_reset_events.prior_percent``
|
|
2958
|
+
# on the event row.
|
|
2959
|
+
try:
|
|
2960
|
+
conn.execute(
|
|
2961
|
+
"DELETE FROM weekly_usage_snapshots "
|
|
2962
|
+
" WHERE five_hour_window_key = ? "
|
|
2963
|
+
" AND unixepoch(captured_at_utc) "
|
|
2964
|
+
" >= unixepoch(?) "
|
|
2965
|
+
" AND ABS(five_hour_percent - ?) "
|
|
2966
|
+
" < 1.0",
|
|
2967
|
+
(
|
|
2968
|
+
int(five_hour_window_key),
|
|
2969
|
+
effective_iso,
|
|
2970
|
+
prior_5h_pct,
|
|
2971
|
+
),
|
|
2972
|
+
)
|
|
2973
|
+
# (inline commit removed — end-of-function commit on
|
|
2974
|
+
# legacy; the ingest cycle owns the commit.)
|
|
2975
|
+
except sqlite3.DatabaseError as exc:
|
|
2976
|
+
eprint(
|
|
2977
|
+
"[record-usage] 5h post-credit "
|
|
2978
|
+
f"cleanup failed: {exc}"
|
|
2979
|
+
)
|
|
2980
|
+
except (sqlite3.DatabaseError, ValueError, TypeError) as exc:
|
|
2981
|
+
# Exception discipline (6c-gate P1): on the ingest path
|
|
2982
|
+
# (commit=False, caller owns the txn) a 5h-detection failure must
|
|
2983
|
+
# ABORT the cycle — re-raise so the ingester rolls back and leaves
|
|
2984
|
+
# the cursor unmoved (invariant ii). Legacy (commit=True) keeps the
|
|
2985
|
+
# log-and-swallow so a standalone record-usage tick never regresses.
|
|
2986
|
+
if not commit:
|
|
2987
|
+
raise
|
|
2988
|
+
eprint(
|
|
2989
|
+
f"[record-usage] 5h in-place-credit detection "
|
|
2990
|
+
f"failed: {exc}"
|
|
2991
|
+
)
|
|
2992
|
+
except (sqlite3.DatabaseError, ValueError) as exc:
|
|
2993
|
+
# Same discipline as the inner 5h handler: propagate on the ingest path,
|
|
2994
|
+
# swallow-and-log on the legacy own-transaction path.
|
|
2995
|
+
if not commit:
|
|
2996
|
+
raise
|
|
2997
|
+
eprint(f"[record-usage] reset-event detection failed: {exc}")
|
|
2998
|
+
if commit:
|
|
2999
|
+
conn.commit()
|
|
3000
|
+
|
|
3001
|
+
|
|
2320
3002
|
def _resolve_reset_aware_hwm(conn, week_start_date, week_start_at, week_end_at):
|
|
2321
3003
|
"""The floored MAX(weekly_percent) the statusline _hwm_clamp computes:
|
|
2322
3004
|
MAX over snapshots captured at/after the latest in-week clamp floor. The
|
|
@@ -2341,8 +3023,24 @@ def _resolve_reset_aware_hwm(conn, week_start_date, week_start_at, week_end_at):
|
|
|
2341
3023
|
return None if not row or row[0] is None else float(row[0])
|
|
2342
3024
|
|
|
2343
3025
|
|
|
2344
|
-
def _insert_credit_snapshot(conn, plan, *, five_hour=(None, None, None)
|
|
2345
|
-
|
|
3026
|
+
def _insert_credit_snapshot(conn, plan, *, five_hour=(None, None, None),
|
|
3027
|
+
commit=True, journal=None):
|
|
3028
|
+
"""Insert the post-credit synthetic snapshot at plan.to_pct, tagged
|
|
3029
|
+
source='record-credit'. The SOLE synthetic-snapshot writer on BOTH paths
|
|
3030
|
+
(legacy inline + ingest), so the non-vacuity stub stays load-bearing.
|
|
3031
|
+
|
|
3032
|
+
``commit=False`` (DB journal redesign §5.2.3) folds the synthetic-snapshot
|
|
3033
|
+
INSERT into the caller's transaction instead of committing inline; the
|
|
3034
|
+
default keeps the legacy inline-commit behavior. Capture time comes from
|
|
3035
|
+
``plan.captured_iso`` (already the record's moment), so no ``as_of`` is
|
|
3036
|
+
needed here.
|
|
3037
|
+
|
|
3038
|
+
Design A (rev-3 emission rule): ``journal=(ctx, evt_id)`` routes the insert
|
|
3039
|
+
THROUGH ``emit_model_a`` as a ``snapshot_accept`` evt — the ONLY writer of
|
|
3040
|
+
``weekly_usage_snapshots`` on the ingest path — so the synthetic is journaled
|
|
3041
|
+
(``evt_id`` = ``sa:<id_base>:syn:0``) and replay reads it back verbatim.
|
|
3042
|
+
Default ``None`` is the legacy direct INSERT. Returns the target rowid on the
|
|
3043
|
+
journal path, or ``conn.total_changes`` on the direct path."""
|
|
2346
3044
|
fhp, fhr, fhk = five_hour
|
|
2347
3045
|
# Normalize effective to a +00:00 UTC spelling in the payload (matches the
|
|
2348
3046
|
# stored weekly_credit_floors.effective_at_utc on a non-UTC host).
|
|
@@ -2354,18 +3052,35 @@ def _insert_credit_snapshot(conn, plan, *, five_hour=(None, None, None)):
|
|
|
2354
3052
|
"effective": effective_utc},
|
|
2355
3053
|
separators=(",", ":"),
|
|
2356
3054
|
)
|
|
3055
|
+
columns = {
|
|
3056
|
+
"captured_at_utc": plan.captured_iso,
|
|
3057
|
+
"week_start_date": plan.week_start_date,
|
|
3058
|
+
"week_end_date": parse_iso_datetime(plan.week_end_at, "we").date().isoformat(),
|
|
3059
|
+
"week_start_at": plan.week_start_at,
|
|
3060
|
+
"week_end_at": plan.week_end_at,
|
|
3061
|
+
"weekly_percent": plan.to_pct,
|
|
3062
|
+
"page_url": None,
|
|
3063
|
+
"source": "record-credit",
|
|
3064
|
+
"payload_json": payload,
|
|
3065
|
+
"five_hour_percent": fhp,
|
|
3066
|
+
"five_hour_resets_at": fhr,
|
|
3067
|
+
"five_hour_window_key": fhk,
|
|
3068
|
+
}
|
|
3069
|
+
if journal is not None:
|
|
3070
|
+
ctx, evt_id = journal
|
|
3071
|
+
import _cctally_journal as _jr
|
|
3072
|
+
return _jr.emit_model_a(
|
|
3073
|
+
ctx, kind="snapshot_accept", evt_id=evt_id,
|
|
3074
|
+
table="weekly_usage_snapshots", columns=columns,
|
|
3075
|
+
at=plan.captured_iso)
|
|
3076
|
+
colnames = ", ".join(columns.keys())
|
|
3077
|
+
placeholders = ", ".join("?" for _ in columns)
|
|
2357
3078
|
conn.execute(
|
|
2358
|
-
"INSERT INTO weekly_usage_snapshots "
|
|
2359
|
-
|
|
2360
|
-
" week_end_at, weekly_percent, page_url, source, payload_json, "
|
|
2361
|
-
" five_hour_percent, five_hour_resets_at, five_hour_window_key) "
|
|
2362
|
-
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?)",
|
|
2363
|
-
(plan.captured_iso, plan.week_start_date,
|
|
2364
|
-
parse_iso_datetime(plan.week_end_at, "we").date().isoformat(),
|
|
2365
|
-
plan.week_start_at, plan.week_end_at, plan.to_pct, None,
|
|
2366
|
-
"record-credit", payload, fhp, fhr, fhk),
|
|
3079
|
+
f"INSERT INTO weekly_usage_snapshots ({colnames}) VALUES ({placeholders})",
|
|
3080
|
+
tuple(columns.values()),
|
|
2367
3081
|
)
|
|
2368
|
-
|
|
3082
|
+
if commit:
|
|
3083
|
+
conn.commit()
|
|
2369
3084
|
return conn.total_changes
|
|
2370
3085
|
|
|
2371
3086
|
|
|
@@ -2389,20 +3104,50 @@ def _resolve_prior_5h(conn, at_dt):
|
|
|
2389
3104
|
return (None, None, None)
|
|
2390
3105
|
|
|
2391
3106
|
|
|
2392
|
-
def _apply_credit(conn, plan, *, five_hour=(None, None, None)
|
|
3107
|
+
def _apply_credit(conn, plan, *, five_hour=(None, None, None), as_of=None,
|
|
3108
|
+
commit=True, ctx=None, id_base=None, forced=False):
|
|
2393
3109
|
"""Apply the M2 same-window partial-credit artifacts (record-credit, #209,
|
|
2394
3110
|
spec §4). Unlike the >=25pp auto-credit path (`_fire_in_place_credit`), this
|
|
2395
3111
|
writes NO `week_reset_events` row — the window-resolution code never sees a
|
|
2396
3112
|
credit, so the week is NOT re-anchored. It only lowers the clamp floor.
|
|
2397
3113
|
|
|
3114
|
+
Transaction-neutral / capture-time-pure seam (DB journal redesign §5.2.3):
|
|
3115
|
+
``commit=False`` folds the floor INSERT + stale-replay DELETE + synthetic
|
|
3116
|
+
snapshot into the caller's transaction (the ingester's cycle) instead of
|
|
3117
|
+
committing inline; ``as_of`` (ISO-Z) stamps ``weekly_credit_floors``'
|
|
3118
|
+
``applied_at_utc`` in place of wall clock (the reset moment stays
|
|
3119
|
+
``plan.effective_iso``). Both defaults keep the legacy behavior.
|
|
3120
|
+
|
|
3121
|
+
Design B/event+effects seam (§5.3): the same-window sub-25pp credit writes
|
|
3122
|
+
NO reset row, so on the ingest path (``ctx`` passed, ``id_base`` = the
|
|
3123
|
+
triggering ``record-credit`` op's journal id) its DESTRUCTIVE effects ride a
|
|
3124
|
+
``weekly_credit_effects`` Model-A evt (``wce:<id_base>``) carrying the
|
|
3125
|
+
stale-replica suppression list (the doomed snapshots' ``journal_id``s,
|
|
3126
|
+
captured with the SAME predicate as the DELETE) + the forced ``hwm_floor``;
|
|
3127
|
+
the synthetic post-credit snapshot rides its own ``snapshot_accept`` evt
|
|
3128
|
+
(``sa:<id_base>:syn:0``) because ``weekly_usage_snapshots`` is written ONLY
|
|
3129
|
+
via ``snapshot_accept`` now (rev-3 emission rule). ``emit_model_a`` applies
|
|
3130
|
+
each evt through the same fold replay uses, so the inline hwm/DELETE/synthetic
|
|
3131
|
+
are SKIPPED on the ingest path (the evt appliers do them). ``ctx=None``
|
|
3132
|
+
(legacy) keeps the inline path verbatim.
|
|
3133
|
+
|
|
3134
|
+
``forced`` (ingest path only) journals the ``--force`` re-record's destructive
|
|
3135
|
+
clear that legacy ``_force_clear_credit`` did inline: the ``wce`` evt's
|
|
3136
|
+
``suppression`` list widens to also delete this week's OLD command-owned
|
|
3137
|
+
synthetic snapshots and a ``floor_suppression`` list deletes its OLD
|
|
3138
|
+
``weekly_credit_floors`` rows (both by logical ``journal_id``), so a
|
|
3139
|
+
``--force`` re-record replays deterministically. The NEW floor (op fold,
|
|
3140
|
+
``journal_id = id_base``) and NEW synthetic (``sa:<id_base>:syn:0``) are
|
|
3141
|
+
excluded from both lists, so the clear is order-independent and idempotent.
|
|
3142
|
+
|
|
2398
3143
|
Side-effect ordering mirrors `_fire_in_place_credit`'s discipline: the
|
|
2399
3144
|
INSERT OR IGNORE of the floor row is dedup-gated by UNIQUE(week_start_date,
|
|
2400
3145
|
effective_at_utc), but the hwm force-write, stale-replay DELETE, and
|
|
2401
3146
|
synthetic-snapshot INSERT run UNCONDITIONALLY so a rerun finishes a crash-
|
|
2402
3147
|
half-applied credit (memory: project_dedup_must_not_gate_side_effects). All
|
|
2403
3148
|
are individually idempotent (file overwrite; DELETE on a stable predicate;
|
|
2404
|
-
the synthetic snapshot is re-INSERTed only after
|
|
2405
|
-
the completion path where none exists yet).
|
|
3149
|
+
the synthetic snapshot is re-INSERTed only after a ``--force`` re-record's
|
|
3150
|
+
destructive clear or on the completion path where none exists yet).
|
|
2406
3151
|
|
|
2407
3152
|
`plan.effective_iso` is `floor_to_hour(at)`. parse_iso_datetime returns a
|
|
2408
3153
|
host-local-offset aware datetime; convert to UTC so `effective_at_utc`
|
|
@@ -2416,60 +3161,159 @@ def _apply_credit(conn, plan, *, five_hour=(None, None, None)):
|
|
|
2416
3161
|
pre_credit = float(plan.from_pct)
|
|
2417
3162
|
|
|
2418
3163
|
# 4a. INSERT the credit floor (no week_reset_events row — the whole point).
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
#
|
|
2428
|
-
#
|
|
2429
|
-
#
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
3164
|
+
# LEGACY-ONLY (Option (i), 6e): on the ingest path (`ctx` passed) the floor
|
|
3165
|
+
# row is written by the built-in op fold `_apply_op_weekly_credit_floor`,
|
|
3166
|
+
# which runs FIRST per record from the record-credit `op` line and stamps
|
|
3167
|
+
# `journal_id = record["id"]` — so the floor row is journal-identified and
|
|
3168
|
+
# rebuild-replayable, and the op fold is the SOLE `weekly_credit_floors`
|
|
3169
|
+
# writer on the ingest path. `_apply_credit`'s own INSERT here would race the
|
|
3170
|
+
# op fold for the UNIQUE(week_start_date, effective_at_utc) slot and leave a
|
|
3171
|
+
# NULL-`journal_id` row on the losing INSERT; gating it on `ctx is None`
|
|
3172
|
+
# makes the "op fold owns the floor" invariant explicit and keeps the floor's
|
|
3173
|
+
# journal_id unambiguous. On the legacy path (`ctx=None`) the INSERT is
|
|
3174
|
+
# idempotent via that same UNIQUE.
|
|
3175
|
+
if ctx is None:
|
|
3176
|
+
conn.execute(
|
|
3177
|
+
"INSERT OR IGNORE INTO weekly_credit_floors "
|
|
3178
|
+
"(week_start_date, effective_at_utc, observed_pre_credit_pct, applied_at_utc) "
|
|
3179
|
+
"VALUES (?, ?, ?, ?)",
|
|
3180
|
+
(plan.week_start_date, effective_iso, pre_credit, as_of or now_utc_iso()),
|
|
2433
3181
|
)
|
|
2434
|
-
|
|
2435
|
-
|
|
3182
|
+
if commit:
|
|
3183
|
+
conn.commit()
|
|
2436
3184
|
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
3185
|
+
if ctx is None:
|
|
3186
|
+
# ── Legacy inline path (verbatim) ──────────────────────────────────
|
|
3187
|
+
# 4b. Force-write hwm-7d so the external statusline render reflects the
|
|
3188
|
+
# post-credit value (the normal write-site monotonic guard would refuse
|
|
3189
|
+
# to decrease the file).
|
|
3190
|
+
try:
|
|
3191
|
+
(_cctally_core.APP_DIR / "hwm-7d").write_text(
|
|
3192
|
+
f"{plan.week_start_date} {plan.to_pct}\n"
|
|
3193
|
+
)
|
|
3194
|
+
except OSError:
|
|
3195
|
+
pass
|
|
3196
|
+
|
|
3197
|
+
# 4c. Stale-replay DELETE: drop pre-credit-valued replays that land
|
|
3198
|
+
# at/after the floor (the gotcha_statusline_replay_race_after_credit
|
|
3199
|
+
# defense; same 1.0pp band as the auto path). unixepoch() on both sides
|
|
3200
|
+
# for offset safety.
|
|
3201
|
+
try:
|
|
3202
|
+
conn.execute(
|
|
3203
|
+
"DELETE FROM weekly_usage_snapshots "
|
|
3204
|
+
"WHERE week_start_date = ? "
|
|
3205
|
+
" AND unixepoch(captured_at_utc) >= unixepoch(?) "
|
|
3206
|
+
" AND ABS(weekly_percent - ?) < 1.0",
|
|
3207
|
+
(plan.week_start_date, effective_iso, pre_credit),
|
|
3208
|
+
)
|
|
3209
|
+
if commit:
|
|
3210
|
+
conn.commit()
|
|
3211
|
+
except sqlite3.DatabaseError as exc:
|
|
3212
|
+
eprint(f"[record-credit] post-credit cleanup failed: {exc}")
|
|
3213
|
+
|
|
3214
|
+
# 4d. INSERT the synthetic post-credit snapshot at plan.to_pct.
|
|
3215
|
+
c._insert_credit_snapshot(conn, plan, five_hour=five_hour, commit=commit)
|
|
3216
|
+
else:
|
|
3217
|
+
# ── Ingest path (Design B, §5.3 event+effects) ─────────────────────
|
|
3218
|
+
# Journal the destructive effects + synthetic instead of running them
|
|
3219
|
+
# inline. Capture the doomed stale-replica journal_ids BEFORE the effect
|
|
3220
|
+
# applies its DELETE (SAME predicate as the legacy 4c DELETE).
|
|
3221
|
+
import _cctally_journal as _jr
|
|
3222
|
+
# Exclude THIS op's OWN synthetic ids (`sa:<id_base>:syn:%`) from the
|
|
3223
|
+
# doomed stale-replica capture, by deterministic id — NOT merely by
|
|
3224
|
+
# emission timing (6g P2 / Task 7 Item 0, the sibling of the `old_syn`
|
|
3225
|
+
# forced-path fix below). A sub-1.0pp credit (legal) puts the synthetic
|
|
3226
|
+
# (at `plan.to_pct`) INSIDE this band (`ABS(weekly_percent - from_pct) <
|
|
3227
|
+
# 1.0`); under crash-replay (evts fsync'd, COMMIT lost) the next cycle
|
|
3228
|
+
# replays `sa:<id_base>:syn:0` at fold order 10 BEFORE step 4b re-runs
|
|
3229
|
+
# `_apply_credit`, so a timing-only capture would re-fold the just-replayed
|
|
3230
|
+
# NEW synthetic into a second `wce` whose suppression deletes the very row
|
|
3231
|
+
# it must preserve when a rebuild folds all snapshot_accept before all wce.
|
|
3232
|
+
# The prefix exclusion makes `supp` a PURE FUNCTION of the op — identical
|
|
3233
|
+
# whether or not the new synthetic has been replayed — and applies on the
|
|
3234
|
+
# NON-force path too (`supp = doomed` here, before `if forced:`).
|
|
3235
|
+
# (`id_base` is a content digest `o:<hex>`, no LIKE metacharacters.)
|
|
3236
|
+
doomed = conn.execute(
|
|
3237
|
+
"SELECT journal_id FROM weekly_usage_snapshots "
|
|
2443
3238
|
"WHERE week_start_date = ? "
|
|
2444
3239
|
" AND unixepoch(captured_at_utc) >= unixepoch(?) "
|
|
2445
|
-
" AND ABS(weekly_percent - ?) < 1.0"
|
|
2446
|
-
|
|
3240
|
+
" AND ABS(weekly_percent - ?) < 1.0 "
|
|
3241
|
+
" AND journal_id IS NOT NULL "
|
|
3242
|
+
" AND journal_id NOT LIKE 'sa:' || ? || ':syn:%'",
|
|
3243
|
+
(plan.week_start_date, effective_iso, pre_credit, id_base),
|
|
3244
|
+
).fetchall()
|
|
3245
|
+
supp = [r[0] for r in doomed]
|
|
3246
|
+
floor_supp: list = []
|
|
3247
|
+
# `--force` re-record: journal the destructive clear that legacy
|
|
3248
|
+
# `_force_clear_credit` did inline (delete this week's command-owned
|
|
3249
|
+
# synthetic snapshots + its credit-floor rows). Ride the SAME wce
|
|
3250
|
+
# vehicle by widening its suppression lists (spec §5.3 event+effects) —
|
|
3251
|
+
# captured BEFORE the new synthetic is emitted, and EXCLUDING the new
|
|
3252
|
+
# op's own floor (journal_id = id_base; the op fold owns it), so replay
|
|
3253
|
+
# is order-independent and idempotent. On the non-force path both extra
|
|
3254
|
+
# lists are empty and the wce is exactly the same shape as before.
|
|
3255
|
+
if forced:
|
|
3256
|
+
# Exclude THIS op's own synthetic ids (`sa:<id_base>:syn:%`) from the
|
|
3257
|
+
# old-synthetic capture, by deterministic id — NOT merely by emission
|
|
3258
|
+
# timing (6f review P1). Under crash-replay (crash between evt fsync
|
|
3259
|
+
# and COMMIT) the next cycle replays `sa:<id_base>:syn:0` at fold
|
|
3260
|
+
# order 10 BEFORE step 4b re-runs `_apply_credit(forced=True)`; a
|
|
3261
|
+
# timing-only exclusion would then re-capture the just-replayed NEW
|
|
3262
|
+
# synthetic into a second `wce` whose suppression deletes the very row
|
|
3263
|
+
# it must preserve. Excluding the whole `sa:<id_base>:syn:%` prefix
|
|
3264
|
+
# makes the `wce` suppression a PURE FUNCTION of the op — identical
|
|
3265
|
+
# whether or not the new synthetic has been replayed — mirroring the
|
|
3266
|
+
# floor list's `journal_id != id_base` filter below. (`id_base` is a
|
|
3267
|
+
# content digest `o:<hex>`, no LIKE metacharacters.)
|
|
3268
|
+
old_syn = conn.execute(
|
|
3269
|
+
"SELECT journal_id FROM weekly_usage_snapshots "
|
|
3270
|
+
"WHERE week_start_date = ? AND source = 'record-credit' "
|
|
3271
|
+
" AND journal_id IS NOT NULL "
|
|
3272
|
+
" AND journal_id NOT LIKE 'sa:' || ? || ':syn:%'",
|
|
3273
|
+
(plan.week_start_date, id_base),
|
|
3274
|
+
).fetchall()
|
|
3275
|
+
supp.extend(r[0] for r in old_syn if r[0] not in supp)
|
|
3276
|
+
old_floors = conn.execute(
|
|
3277
|
+
"SELECT journal_id FROM weekly_credit_floors "
|
|
3278
|
+
"WHERE week_start_date = ? AND journal_id IS NOT NULL "
|
|
3279
|
+
" AND journal_id != ?",
|
|
3280
|
+
(plan.week_start_date, id_base),
|
|
3281
|
+
).fetchall()
|
|
3282
|
+
floor_supp = [r[0] for r in old_floors]
|
|
3283
|
+
# wce evt (effects-only, table=None): snapshot suppression list + floor
|
|
3284
|
+
# suppression list (--force clear) + forced hwm floor. emit_model_a
|
|
3285
|
+
# appends+fsyncs the line then applies it via `_apply_weekly_credit_effects`
|
|
3286
|
+
# (DELETE by journal_id from both tables + hwm-7d write).
|
|
3287
|
+
_jr.emit_model_a(
|
|
3288
|
+
ctx,
|
|
3289
|
+
kind="weekly_credit_effects",
|
|
3290
|
+
evt_id=f"wce:{id_base}",
|
|
3291
|
+
table=None,
|
|
3292
|
+
columns={
|
|
3293
|
+
"suppression": supp,
|
|
3294
|
+
"suppression_table": "weekly_usage_snapshots",
|
|
3295
|
+
"floor_suppression": floor_supp,
|
|
3296
|
+
"hwm_floor": {
|
|
3297
|
+
"week_start_date": plan.week_start_date,
|
|
3298
|
+
"weekly_percent": plan.to_pct,
|
|
3299
|
+
},
|
|
3300
|
+
},
|
|
3301
|
+
at=(as_of or plan.captured_iso),
|
|
2447
3302
|
)
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
3303
|
+
# Synthetic post-credit snapshot as a snapshot_accept evt (the only
|
|
3304
|
+
# writer of weekly_usage_snapshots on the ingest path). Route it through
|
|
3305
|
+
# `_insert_credit_snapshot(journal=...)` — the SOLE synthetic writer on
|
|
3306
|
+
# both paths — so the non-vacuity stub of `_insert_credit_snapshot` stays
|
|
3307
|
+
# load-bearing (stubbing it disables the synthetic on the ingest path too).
|
|
3308
|
+
c._insert_credit_snapshot(
|
|
3309
|
+
conn, plan, five_hour=five_hour, commit=False,
|
|
3310
|
+
journal=(ctx, f"sa:{id_base}:syn:0"))
|
|
2454
3311
|
|
|
2455
3312
|
# 4e. Clear a stale same-week reset-zero marker so the next record-usage
|
|
2456
3313
|
# tick can't confirm a phantom reset-to-zero off it.
|
|
2457
3314
|
_clear_reset_zero_marker()
|
|
2458
3315
|
|
|
2459
3316
|
|
|
2460
|
-
def _force_clear_credit(conn, week_start_date):
|
|
2461
|
-
"""--force scope (M2, spec §4): delete ONLY this week's command-owned
|
|
2462
|
-
synthetic snapshots (`source='record-credit'`) + its `weekly_credit_floors`
|
|
2463
|
-
row(s). Never touches real status-line snapshots, and never
|
|
2464
|
-
`week_reset_events` / `percent_milestones` — a partial credit writes
|
|
2465
|
-
neither (no event row -> no segmentation; no milestones)."""
|
|
2466
|
-
conn.execute("DELETE FROM weekly_usage_snapshots "
|
|
2467
|
-
" WHERE week_start_date=? AND source='record-credit'", (week_start_date,))
|
|
2468
|
-
conn.execute("DELETE FROM weekly_credit_floors "
|
|
2469
|
-
" WHERE week_start_date=?", (week_start_date,))
|
|
2470
|
-
conn.commit()
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
3317
|
def _count_stale_replays(conn, plan):
|
|
2474
3318
|
"""Count the pre-credit replay rows the _apply_credit stale-replay DELETE
|
|
2475
3319
|
(step 4c, inline) will touch (captured at/after the credit moment within a
|
|
@@ -2648,8 +3492,8 @@ def cmd_record_credit(args) -> int:
|
|
|
2648
3492
|
# apply-time completion/refuse/force branch (M2 keys on
|
|
2649
3493
|
# weekly_credit_floors, NOT week_reset_events — a partial credit never
|
|
2650
3494
|
# writes a reset-event row). Latest floor wins (a --force re-apply at a
|
|
2651
|
-
# new effective leaves the old row only until
|
|
2652
|
-
# deletes it; pick the newest defensively).
|
|
3495
|
+
# new effective leaves the old row only until the ingest-path wce evt's
|
|
3496
|
+
# floor_suppression deletes it; pick the newest defensively).
|
|
2653
3497
|
existing = conn.execute(
|
|
2654
3498
|
"SELECT id, effective_at_utc, observed_pre_credit_pct "
|
|
2655
3499
|
"FROM weekly_credit_floors WHERE week_start_date=? "
|
|
@@ -2806,15 +3650,51 @@ def cmd_record_credit(args) -> int:
|
|
|
2806
3650
|
eprint(f"record-credit: could not prepare authoritative state: {exc}")
|
|
2807
3651
|
return 3
|
|
2808
3652
|
|
|
2809
|
-
#
|
|
2810
|
-
# the
|
|
2811
|
-
#
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
3653
|
+
# 6f writer reroute (Appendix A): the credit no longer writes stats.db
|
|
3654
|
+
# directly. Build the `record-credit` `op` line (floor columns + the
|
|
3655
|
+
# full 9-field plan + the command-time prior-5h read + the `forced`
|
|
3656
|
+
# flag) and run an AUTHORITATIVE ingest so the command observes its
|
|
3657
|
+
# own write synchronously. The op fold writes the `weekly_credit_floors`
|
|
3658
|
+
# row (Option (i), journal_id = the op line id); `_pipeline_record_credit`
|
|
3659
|
+
# -> `_apply_credit(ctx=..., forced=...)` journals the `weekly_credit_effects`
|
|
3660
|
+
# evt (stale-replica suppression + the `--force` clear of OLD synthetics
|
|
3661
|
+
# + OLD floors) and the synthetic `snapshot_accept` evt. Preview/refusal
|
|
3662
|
+
# paths above appended NOTHING; the weekly tombstone is already
|
|
3663
|
+
# fail-closed immediately before this single append+ingest.
|
|
3664
|
+
forced = bool(existing is not None and is_force)
|
|
3665
|
+
# `five_hour` is the COMMAND-time prior-5h read (before the credit),
|
|
3666
|
+
# carried in the op so the ingest hook derives the synthetic against
|
|
3667
|
+
# the same 5h state the command saw.
|
|
2816
3668
|
five_hour = _resolve_prior_5h(conn, at_dt)
|
|
2817
|
-
|
|
3669
|
+
capture_iso = now_utc_iso(now)
|
|
3670
|
+
effective_utc = parse_iso_datetime(
|
|
3671
|
+
plan.effective_iso, "op.effective"
|
|
3672
|
+
).astimezone(dt.timezone.utc).isoformat(timespec="seconds")
|
|
3673
|
+
import _cctally_journal as _jr
|
|
3674
|
+
import _lib_journal as _lj
|
|
3675
|
+
op = _lj.make_op(
|
|
3676
|
+
at=capture_iso,
|
|
3677
|
+
src="record-credit",
|
|
3678
|
+
payload={
|
|
3679
|
+
"kind": "weekly_credit_floor",
|
|
3680
|
+
"week_start_date": plan.week_start_date,
|
|
3681
|
+
"effective_at_utc": effective_utc,
|
|
3682
|
+
"observed_pre_credit_pct": float(plan.from_pct),
|
|
3683
|
+
"applied_at_utc": capture_iso,
|
|
3684
|
+
"plan": dict(vars(plan)),
|
|
3685
|
+
"five_hour": list(five_hour),
|
|
3686
|
+
"forced": forced,
|
|
3687
|
+
},
|
|
3688
|
+
)
|
|
3689
|
+
# Close the command's read connection BEFORE the ingester (the sole
|
|
3690
|
+
# stats.db writer) opens its own — the reroute keeps a single live
|
|
3691
|
+
# stats.db writer. The authoritative cycle runs inline through this
|
|
3692
|
+
# appended op and commits; the stable-projection read below then
|
|
3693
|
+
# observes it.
|
|
3694
|
+
conn.close()
|
|
3695
|
+
conn = None
|
|
3696
|
+
_jr.append_record(op)
|
|
3697
|
+
_jr.run_stats_ingest(mode="authoritative")
|
|
2818
3698
|
|
|
2819
3699
|
# A successful credit is authoritative only after the post-credit
|
|
2820
3700
|
# DB state has a stable projection and all selected artifacts are
|
|
@@ -2862,11 +3742,26 @@ def cmd_record_credit(args) -> int:
|
|
|
2862
3742
|
conn.close()
|
|
2863
3743
|
|
|
2864
3744
|
|
|
2865
|
-
def cmd_record_usage(
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
3745
|
+
def cmd_record_usage(
|
|
3746
|
+
args: argparse.Namespace, *,
|
|
3747
|
+
ingest_mode: str = "authoritative",
|
|
3748
|
+
writer: str = "record-usage",
|
|
3749
|
+
) -> int:
|
|
3750
|
+
"""Record usage from the Claude Code status line rate_limits — DB journal
|
|
3751
|
+
redesign reroute (Appendix A).
|
|
3752
|
+
|
|
3753
|
+
Validates the ingress (weekly plausibility -> exit 2; out-of-band 5h dropped
|
|
3754
|
+
non-fatally), builds the RAW obs line (raw capture only, NO derived week
|
|
3755
|
+
columns), appends it to the journal, and runs the single-flight ingest cycle
|
|
3756
|
+
that derives + journals every stats fact via ``_pipeline_claude_usage``
|
|
3757
|
+
(snapshot_accept / milestones / 5h block / reset-credit / cost snapshot /
|
|
3758
|
+
dollar axes). stats.db is written ONLY through ``run_stats_ingest`` now.
|
|
3759
|
+
|
|
3760
|
+
``ingest_mode`` -- "authoritative" (default: CLI + statusline publication)
|
|
3761
|
+
observes its own write synchronously; "opportunistic" (hook-tick OAuth /
|
|
3762
|
+
dedup ticks) skips a busy ingest lock and lets the winner consume the line.
|
|
3763
|
+
``writer`` is the obs line ``src``. Returns 0 on a recorded/deduped tick, 2
|
|
3764
|
+
on an implausible weekly resets_at."""
|
|
2870
3765
|
|
|
2871
3766
|
# ULP-noise sanitization is applied at the cmd_record_usage ingress
|
|
2872
3767
|
# boundary so every downstream consumer (HWM files, DB rows,
|
|
@@ -2939,925 +3834,34 @@ def cmd_record_usage(args: argparse.Namespace) -> int:
|
|
|
2939
3834
|
# See _canonical_5h_window_key docstring (spec invariant #3:
|
|
2940
3835
|
# boundary-straddling jitter must collapse to the first-seen key).
|
|
2941
3836
|
|
|
2942
|
-
#
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
week_end_at = week_end_at_dt.isoformat(timespec="seconds")
|
|
2949
|
-
|
|
2950
|
-
# Deduplication: skip if nothing changed since last snapshot
|
|
2951
|
-
should_insert = True
|
|
2952
|
-
conn = open_db()
|
|
2953
|
-
try:
|
|
2954
|
-
# Resolve the canonical 5h window key. Pass the most-recent stored
|
|
2955
|
-
# sample as the prior anchor so seconds-level jitter that straddles
|
|
2956
|
-
# a 600-second floor-bucket boundary (e.g. resets_at=1746014999 vs.
|
|
2957
|
-
# 1746015000) collapses to the first-seen key instead of forking
|
|
2958
|
-
# a new one. Without this, both the DB clamp below and the hwm-5h
|
|
2959
|
-
# file write further down would treat the same physical window as
|
|
2960
|
-
# distinct, regressing the monotonic 5h percent (spec invariant #3).
|
|
2961
|
-
if five_hour_resets_at_epoch is not None:
|
|
2962
|
-
prior_5h_epoch: int | None = None
|
|
2963
|
-
prior_5h_key: int | None = None
|
|
2964
|
-
# Tier 1: blocks-table lookup (steady state). Find the closest
|
|
2965
|
-
# canonical block whose five_hour_resets_at is within ±1800s of
|
|
2966
|
-
# the new resets_at. The blocks table has one canonical row per
|
|
2967
|
-
# physical window after the merge_5h_block_duplicates_v1
|
|
2968
|
-
# migration, so this is more reliable than scanning
|
|
2969
|
-
# weekly_usage_snapshots for an "anchor" row — snapshots can be
|
|
2970
|
-
# noisy when the status line returns out-of-order rate-limit
|
|
2971
|
-
# data from older windows (the F4 incident: snap N+1 carrying a
|
|
2972
|
-
# window-A boundary-jitter resets_at, but snap N reported an
|
|
2973
|
-
# OLDER window B; pre-fix the prior-anchor lookup picked B as
|
|
2974
|
-
# the anchor and the |epoch-prior| > 600 check then forked a
|
|
2975
|
-
# new key for what was actually still window A). 1800s is wide
|
|
2976
|
-
# enough to absorb known jitter, narrow enough that consecutive
|
|
2977
|
-
# 5h blocks (>4h apart in resets_at) cannot collide.
|
|
2978
|
-
try:
|
|
2979
|
-
prior_block_row = conn.execute(
|
|
2980
|
-
"""
|
|
2981
|
-
SELECT five_hour_window_key, five_hour_resets_at
|
|
2982
|
-
FROM five_hour_blocks
|
|
2983
|
-
WHERE abs(? - CAST(strftime('%s', five_hour_resets_at) AS INTEGER)) <= ?
|
|
2984
|
-
ORDER BY abs(? - CAST(strftime('%s', five_hour_resets_at) AS INTEGER)) ASC
|
|
2985
|
-
LIMIT 1
|
|
2986
|
-
""",
|
|
2987
|
-
(
|
|
2988
|
-
five_hour_resets_at_epoch,
|
|
2989
|
-
c._FIVE_HOUR_JITTER_FLOOR_SECONDS * 3,
|
|
2990
|
-
five_hour_resets_at_epoch,
|
|
2991
|
-
),
|
|
2992
|
-
).fetchone()
|
|
2993
|
-
if prior_block_row is not None:
|
|
2994
|
-
prior_iso = prior_block_row["five_hour_resets_at"]
|
|
2995
|
-
prior_5h_epoch = int(parse_iso_datetime(
|
|
2996
|
-
prior_iso, "prior 5h block anchor"
|
|
2997
|
-
).timestamp())
|
|
2998
|
-
prior_5h_key = int(prior_block_row["five_hour_window_key"])
|
|
2999
|
-
except (sqlite3.DatabaseError, ValueError, TypeError) as exc:
|
|
3000
|
-
eprint(f"[record-usage] prior 5h block-anchor lookup failed: {exc}")
|
|
3001
|
-
|
|
3002
|
-
# Tier 2: snapshot lookup (legacy fallback). Only run when Tier
|
|
3003
|
-
# 1 missed (no canonical block row exists yet — the brand-new-
|
|
3004
|
-
# window case before any record-usage tick has materialized a
|
|
3005
|
-
# five_hour_blocks row). Tier 1's empty-result guard is the
|
|
3006
|
-
# `prior_block_row is not None` test above; replicating it
|
|
3007
|
-
# here keeps Tier 2 strictly secondary.
|
|
3008
|
-
if prior_5h_key is None:
|
|
3009
|
-
try:
|
|
3010
|
-
prior_5h_row = conn.execute(
|
|
3011
|
-
"SELECT five_hour_resets_at, five_hour_window_key "
|
|
3012
|
-
"FROM weekly_usage_snapshots "
|
|
3013
|
-
"WHERE five_hour_resets_at IS NOT NULL "
|
|
3014
|
-
" AND five_hour_window_key IS NOT NULL "
|
|
3015
|
-
"ORDER BY captured_at_utc DESC, id DESC LIMIT 1"
|
|
3016
|
-
).fetchone()
|
|
3017
|
-
if prior_5h_row is not None:
|
|
3018
|
-
prior_iso = prior_5h_row["five_hour_resets_at"]
|
|
3019
|
-
prior_5h_epoch = int(parse_iso_datetime(
|
|
3020
|
-
prior_iso, "prior 5h anchor"
|
|
3021
|
-
).timestamp())
|
|
3022
|
-
prior_5h_key = int(prior_5h_row["five_hour_window_key"])
|
|
3023
|
-
except (sqlite3.DatabaseError, ValueError, TypeError) as exc:
|
|
3024
|
-
eprint(f"[record-usage] prior 5h anchor lookup failed: {exc}")
|
|
3025
|
-
|
|
3026
|
-
# Tier 3 is implicit: with no anchor, _canonical_5h_window_key
|
|
3027
|
-
# falls back to the pure 600-second floor.
|
|
3028
|
-
five_hour_window_key = _canonical_5h_window_key(
|
|
3029
|
-
five_hour_resets_at_epoch,
|
|
3030
|
-
prior_epoch=prior_5h_epoch,
|
|
3031
|
-
prior_key=prior_5h_key,
|
|
3032
|
-
)
|
|
3033
|
-
|
|
3034
|
-
# Mid-week reset detection. When `resets_at` advances before the
|
|
3035
|
-
# previously-declared reset actually fires (Anthropic-initiated
|
|
3036
|
-
# goodwill reset, or any API-side shift), record one week_reset_events
|
|
3037
|
-
# row so display + cost layers can treat the observed moment as the
|
|
3038
|
-
# old week's effective end AND the new week's effective start. The
|
|
3039
|
-
# monotonic check below stays keyed on week_start_date so it still
|
|
3040
|
-
# guards the new week against stale rate-limit data independently.
|
|
3041
|
-
# Both boundaries canonicalize to hour (same rule make_week_ref uses)
|
|
3042
|
-
# so minute/second-level Anthropic jitter doesn't masquerade as a
|
|
3043
|
-
# reset and the stored values match what WeekRef.week_end_at carries.
|
|
3044
|
-
# The 5h-block cross-flag is no longer threaded from here —
|
|
3045
|
-
# maybe_update_five_hour_block re-derives it every tick by JOINing
|
|
3046
|
-
# against week_reset_events (self-healing, see helper for rationale).
|
|
3047
|
-
try:
|
|
3048
|
-
cur_end_canon = _canonicalize_optional_iso(week_end_at, "record.cur")
|
|
3049
|
-
prior = conn.execute(
|
|
3050
|
-
"SELECT week_end_at, weekly_percent FROM weekly_usage_snapshots "
|
|
3051
|
-
"WHERE week_end_at IS NOT NULL "
|
|
3052
|
-
"ORDER BY captured_at_utc DESC, id DESC LIMIT 1"
|
|
3053
|
-
).fetchone()
|
|
3054
|
-
if prior and prior["week_end_at"] and cur_end_canon:
|
|
3055
|
-
prior_end_canon = _canonicalize_optional_iso(
|
|
3056
|
-
prior["week_end_at"], "record.prior"
|
|
3057
|
-
)
|
|
3058
|
-
prior_pct = prior["weekly_percent"]
|
|
3059
|
-
# Use _command_as_of() so CCTALLY_AS_OF pins the predicate
|
|
3060
|
-
# for tests (no behavior change in production — falls back
|
|
3061
|
-
# to wall-clock when the env hook is unset). This makes
|
|
3062
|
-
# mid-week-reset detection deterministic against fixtures
|
|
3063
|
-
# whose `prior_end` is a fixed historical instant.
|
|
3064
|
-
now_utc = _command_as_of()
|
|
3065
|
-
if prior_end_canon and prior_end_canon != cur_end_canon:
|
|
3066
|
-
prior_end_dt = parse_iso_datetime(prior_end_canon, "prior.week_end_at")
|
|
3067
|
-
# Fire only when (a) prior window was still in the FUTURE
|
|
3068
|
-
# (Anthropic shifted the boundary before natural expiration),
|
|
3069
|
-
# AND (b) weekly_percent dropped by RESET_PCT_DROP_THRESHOLD
|
|
3070
|
-
# or more (filters out API flaps / transient boundary
|
|
3071
|
-
# jitter where usage stays roughly the same).
|
|
3072
|
-
if (
|
|
3073
|
-
prior_end_dt > now_utc
|
|
3074
|
-
and prior_pct is not None
|
|
3075
|
-
and c._is_reset_drop(prior_pct, weekly_percent)
|
|
3076
|
-
):
|
|
3077
|
-
# See _backfill_week_reset_events for why we floor
|
|
3078
|
-
# the reset moment to the hour (natural display
|
|
3079
|
-
# boundary, aligned with Anthropic's hour-only
|
|
3080
|
-
# resets_at values).
|
|
3081
|
-
effective_iso = _floor_to_hour(now_utc).isoformat(timespec="seconds")
|
|
3082
|
-
conn.execute(
|
|
3083
|
-
"INSERT OR IGNORE INTO week_reset_events "
|
|
3084
|
-
"(detected_at_utc, old_week_end_at, new_week_end_at, "
|
|
3085
|
-
" effective_reset_at_utc) VALUES (?, ?, ?, ?)",
|
|
3086
|
-
(now_utc_iso(), prior_end_canon, cur_end_canon,
|
|
3087
|
-
effective_iso),
|
|
3088
|
-
)
|
|
3089
|
-
conn.commit()
|
|
3090
|
-
elif prior_end_canon and prior_end_canon == cur_end_canon:
|
|
3091
|
-
# In-place credit branch (v1.7.2) + reset-to-zero debounce
|
|
3092
|
-
# (issue #128). Same end_at across two captures. A >=25pp drop
|
|
3093
|
-
# is a goodwill credit and fires immediately; a reset-to-zero
|
|
3094
|
-
# (post <= floor, 3..25pp drop) is debounced against a
|
|
3095
|
-
# transient API zero — armed on the first ~0, confirmed only
|
|
3096
|
-
# if the next reading stays low (<= half the pre-zero
|
|
3097
|
-
# baseline), cleared on recovery toward baseline. The gate
|
|
3098
|
-
# drops the _is_reset_drop term so the recovery-clear path is
|
|
3099
|
-
# reachable. See the spec for the midpoint rationale.
|
|
3100
|
-
prior_end_dt = parse_iso_datetime(prior_end_canon, "prior.week_end_at")
|
|
3101
|
-
if prior_end_dt > now_utc and prior_pct is not None:
|
|
3102
|
-
# Read the pending reset-to-zero marker up front (pure
|
|
3103
|
-
# file read) and compute whether it is armed for THIS
|
|
3104
|
-
# window; the debounce CLASSIFIER (pure) decides the
|
|
3105
|
-
# action from those values + the c._RESET_* constants,
|
|
3106
|
-
# then the glue below executes the decided I/O. The 5
|
|
3107
|
-
# branch outcomes (fire-immediate / confirm / clear /
|
|
3108
|
-
# arm / none) map 1:1 to the pre-extraction structure.
|
|
3109
|
-
marker = _read_reset_zero_marker()
|
|
3110
|
-
armed = (
|
|
3111
|
-
marker is not None
|
|
3112
|
-
and marker[0] == week_start_date
|
|
3113
|
-
and marker[1] == cur_end_canon
|
|
3114
|
-
)
|
|
3115
|
-
decision = plan_weekly_credit_debounce(
|
|
3116
|
-
prior_pct, weekly_percent,
|
|
3117
|
-
drop_threshold=c._RESET_PCT_DROP_THRESHOLD,
|
|
3118
|
-
zero_floor_pct=c._RESET_ZERO_FLOOR_PCT,
|
|
3119
|
-
zero_min_drop_pct=c._RESET_ZERO_MIN_DROP_PCT,
|
|
3120
|
-
marker_armed=armed,
|
|
3121
|
-
marker_baseline=(marker[2] if armed else None),
|
|
3122
|
-
).action
|
|
3123
|
-
if decision == FIRE_IMMEDIATE:
|
|
3124
|
-
# >=25pp goodwill credit — fire immediately, never
|
|
3125
|
-
# debounced. Clear any pending arm (now moot).
|
|
3126
|
-
_clear_reset_zero_marker()
|
|
3127
|
-
_fire_in_place_credit(
|
|
3128
|
-
conn, week_start_date, cur_end_canon, weekly_percent,
|
|
3129
|
-
observed_pre_credit_pct=float(prior_pct),
|
|
3130
|
-
effective_dt=_floor_to_hour(now_utc),
|
|
3131
|
-
)
|
|
3132
|
-
elif decision == CONFIRM_RESET:
|
|
3133
|
-
# Second reading stayed low → confirm. Anchor the
|
|
3134
|
-
# reset at the FIRST-zero instant from the marker
|
|
3135
|
-
# (UTC-normalized like the backfill in-place path).
|
|
3136
|
-
first_zero_dt = parse_iso_datetime(
|
|
3137
|
-
marker[3], "reset_zero_marker.first_zero"
|
|
3138
|
-
).astimezone(dt.timezone.utc)
|
|
3139
|
-
_fire_in_place_credit(
|
|
3140
|
-
conn, week_start_date, cur_end_canon,
|
|
3141
|
-
weekly_percent,
|
|
3142
|
-
observed_pre_credit_pct=marker[2],
|
|
3143
|
-
effective_dt=_floor_to_hour(first_zero_dt),
|
|
3144
|
-
)
|
|
3145
|
-
# Clear ONLY after the fire completes (P2a): a
|
|
3146
|
-
# mid-fire crash leaves the marker armed so the next
|
|
3147
|
-
# zero re-confirms + re-runs the idempotent pivots.
|
|
3148
|
-
_clear_reset_zero_marker()
|
|
3149
|
-
elif decision == CLEAR_MARKER:
|
|
3150
|
-
# Recovered toward baseline → transient zero, not a
|
|
3151
|
-
# reset. Clear, do not fire.
|
|
3152
|
-
_clear_reset_zero_marker()
|
|
3153
|
-
elif decision == ARM_MARKER:
|
|
3154
|
-
# First ~0 → arm; do NOT fire. The write clamp
|
|
3155
|
-
# suppresses this 0 (no event row yet), so the prior
|
|
3156
|
-
# snapshot stays at the baseline and this shape
|
|
3157
|
-
# re-evaluates next tick. first_zero_iso is the
|
|
3158
|
-
# _command_as_of() value (now_utc), NOT wall-clock —
|
|
3159
|
-
# it becomes the effective anchor.
|
|
3160
|
-
_arm_reset_zero_marker(
|
|
3161
|
-
week_start_date, cur_end_canon,
|
|
3162
|
-
baseline_pct=float(prior_pct),
|
|
3163
|
-
first_zero_iso=now_utc.isoformat(timespec="seconds"),
|
|
3164
|
-
)
|
|
3165
|
-
# else NO_ACTION: not a reset shape and not armed →
|
|
3166
|
-
# nothing. A non-matching stale marker is inert
|
|
3167
|
-
# (ignored on key mismatch, overwritten by next arm).
|
|
3168
|
-
|
|
3169
|
-
# ── 5h in-place credit detection (parallel to weekly above) ──
|
|
3170
|
-
# Spec §2.2 of
|
|
3171
|
-
# docs/superpowers/specs/2026-05-16-5h-in-place-credit-detection.md.
|
|
3172
|
-
# Slot SECOND so the weekly branch retains control-flow
|
|
3173
|
-
# priority — both branches are independent (they touch
|
|
3174
|
-
# different tables) and the order has no behavioral
|
|
3175
|
-
# interaction. Same outer try/except wraps both so a
|
|
3176
|
-
# 5h-detection failure logs but does not regress the rest
|
|
3177
|
-
# of cmd_record_usage.
|
|
3178
|
-
#
|
|
3179
|
-
# Diverges from weekly in three places:
|
|
3180
|
-
# - Threshold: 5.0pp (constant on cctally module), not 25.0pp.
|
|
3181
|
-
# The 5h envelope is smaller so a 5pp move is
|
|
3182
|
-
# proportionally larger.
|
|
3183
|
-
# - Effective-iso floor: 10-min (matches
|
|
3184
|
-
# ``_canonical_5h_window_key``'s 600s floor), not hour.
|
|
3185
|
-
# Up to ~30 distinct slots per 5h block; same-slot
|
|
3186
|
-
# collisions absorbed by UNIQUE per spec §2.3.
|
|
3187
|
-
# - Pre-check: pair-checks the latest event's
|
|
3188
|
-
# ``(prior_percent, post_percent)`` against this tick's
|
|
3189
|
-
# ``(prior_5h_pct, five_hour_percent)``, not
|
|
3190
|
-
# ``new_week_end_at`` equality. A genuine replay matches
|
|
3191
|
-
# BOTH fields; a NEW credit-with-idle (prior_pct equals
|
|
3192
|
-
# the prior credit's post_pct because the user didn't
|
|
3193
|
-
# move between credits) matches only one field and
|
|
3194
|
-
# correctly proceeds to write a second event row.
|
|
3195
|
-
try:
|
|
3196
|
-
if (
|
|
3197
|
-
five_hour_window_key is not None
|
|
3198
|
-
and five_hour_percent is not None
|
|
3199
|
-
):
|
|
3200
|
-
prior_5h_row = conn.execute(
|
|
3201
|
-
"SELECT five_hour_window_key, five_hour_percent, "
|
|
3202
|
-
" five_hour_resets_at "
|
|
3203
|
-
" FROM weekly_usage_snapshots "
|
|
3204
|
-
" WHERE five_hour_window_key IS NOT NULL "
|
|
3205
|
-
" AND five_hour_percent IS NOT NULL "
|
|
3206
|
-
" ORDER BY captured_at_utc DESC, id DESC LIMIT 1"
|
|
3207
|
-
).fetchone()
|
|
3208
|
-
if (
|
|
3209
|
-
prior_5h_row is not None
|
|
3210
|
-
and int(prior_5h_row["five_hour_window_key"])
|
|
3211
|
-
== int(five_hour_window_key)
|
|
3212
|
-
and prior_5h_row["five_hour_resets_at"] is not None
|
|
3213
|
-
):
|
|
3214
|
-
prior_5h_pct = float(prior_5h_row["five_hour_percent"])
|
|
3215
|
-
prior_5h_resets_dt = parse_iso_datetime(
|
|
3216
|
-
prior_5h_row["five_hour_resets_at"],
|
|
3217
|
-
"prior.five_hour_resets_at",
|
|
3218
|
-
)
|
|
3219
|
-
# ``now_utc`` was bound earlier in this same
|
|
3220
|
-
# outer try block from
|
|
3221
|
-
# ``dt.datetime.now(dt.timezone.utc)``; reuse it
|
|
3222
|
-
# so both branches see the same instant.
|
|
3223
|
-
if plan_five_hour_credit(
|
|
3224
|
-
prior_5h_pct, float(five_hour_percent),
|
|
3225
|
-
drop_threshold=c._FIVE_HOUR_RESET_PCT_DROP_THRESHOLD,
|
|
3226
|
-
prior_resets_in_future=(prior_5h_resets_dt > now_utc),
|
|
3227
|
-
):
|
|
3228
|
-
# Pair-check dedup pre-check (spec §2.2;
|
|
3229
|
-
# refined by Codex r4 P1 finding). The
|
|
3230
|
-
# round-1 predicate compared only the
|
|
3231
|
-
# latest event's ``post_percent`` against
|
|
3232
|
-
# this tick's ``prior_5h_pct``; that
|
|
3233
|
-
# false-positived on a legitimate 2nd
|
|
3234
|
-
# credit where the user was idle between
|
|
3235
|
-
# credits (Credit 1 lands prior=20/post=5;
|
|
3236
|
-
# user does nothing; Credit 2 arrives with
|
|
3237
|
-
# CLI percent=0 so prior_5h_pct=5 reads
|
|
3238
|
-
# equal to stored post_percent=5 →
|
|
3239
|
-
# silently swallowed). Pair-checking
|
|
3240
|
-
# against BOTH fields disambiguates: a
|
|
3241
|
-
# genuine replay matches BOTH; a new
|
|
3242
|
-
# credit-with-idle matches at most ONE
|
|
3243
|
-
# (the prior side coincides but
|
|
3244
|
-
# post_percent differs).
|
|
3245
|
-
most_recent = conn.execute(
|
|
3246
|
-
"SELECT prior_percent, post_percent "
|
|
3247
|
-
" FROM five_hour_reset_events "
|
|
3248
|
-
" WHERE five_hour_window_key = ? "
|
|
3249
|
-
" ORDER BY id DESC LIMIT 1",
|
|
3250
|
-
(int(five_hour_window_key),),
|
|
3251
|
-
).fetchone()
|
|
3252
|
-
is_dup = (
|
|
3253
|
-
most_recent is not None
|
|
3254
|
-
and round(prior_5h_pct, 1)
|
|
3255
|
-
== round(float(most_recent["prior_percent"]), 1)
|
|
3256
|
-
and round(float(five_hour_percent), 1)
|
|
3257
|
-
== round(float(most_recent["post_percent"]), 1)
|
|
3258
|
-
)
|
|
3259
|
-
# 10-min floor (spec §2.3 — bounded
|
|
3260
|
-
# stacked-credit resolution; one event per
|
|
3261
|
-
# 10-min slot per block). Resolved BEFORE
|
|
3262
|
-
# the ``if not is_dup`` branch so it's in
|
|
3263
|
-
# scope for the pivots below (per memory
|
|
3264
|
-
# ``project_dedup_must_not_gate_side_effects.md``:
|
|
3265
|
-
# the recovery-tick path must still force
|
|
3266
|
-
# HWM + DELETE even when the INSERT is
|
|
3267
|
-
# absorbed by the pre-check or by
|
|
3268
|
-
# UNIQUE — see comment below for the
|
|
3269
|
-
# crash scenario). ``_floor_to_ten_minutes``
|
|
3270
|
-
# is a cctally module attribute; the
|
|
3271
|
-
# ``c.X`` accessor resolves at call time
|
|
3272
|
-
# so test ``monkeypatch.setitem(ns,
|
|
3273
|
-
# "_floor_to_ten_minutes", …)``
|
|
3274
|
-
# propagates.
|
|
3275
|
-
effective_dt = c._floor_to_ten_minutes(now_utc)
|
|
3276
|
-
effective_iso = effective_dt.isoformat(
|
|
3277
|
-
timespec="seconds"
|
|
3278
|
-
)
|
|
3279
|
-
if not is_dup:
|
|
3280
|
-
conn.execute(
|
|
3281
|
-
"INSERT OR IGNORE INTO five_hour_reset_events "
|
|
3282
|
-
"(detected_at_utc, five_hour_window_key, "
|
|
3283
|
-
" prior_percent, post_percent, "
|
|
3284
|
-
" effective_reset_at_utc) "
|
|
3285
|
-
"VALUES (?, ?, ?, ?, ?)",
|
|
3286
|
-
(
|
|
3287
|
-
now_utc_iso(),
|
|
3288
|
-
int(five_hour_window_key),
|
|
3289
|
-
prior_5h_pct,
|
|
3290
|
-
float(five_hour_percent),
|
|
3291
|
-
effective_iso,
|
|
3292
|
-
),
|
|
3293
|
-
)
|
|
3294
|
-
conn.commit()
|
|
3295
|
-
# Pivots fire UNCONDITIONALLY whenever a
|
|
3296
|
-
# credit is detected — NOT gated on
|
|
3297
|
-
# ``not is_dup`` and NOT on
|
|
3298
|
-
# ``rowcount == 1``. Memory
|
|
3299
|
-
# ``project_dedup_must_not_gate_side_effects.md``:
|
|
3300
|
-
# "Skipping a no-op INSERT must NOT skip
|
|
3301
|
-
# milestones/rollups/alerts; prior run may
|
|
3302
|
-
# have died mid-flight." Crash scenario A:
|
|
3303
|
-
# tick N committed the event row, then died
|
|
3304
|
-
# before HWM + DELETE. Tick N+1's
|
|
3305
|
-
# INSERT OR IGNORE returns rowcount == 0
|
|
3306
|
-
# (UNIQUE absorbs) but the system is still
|
|
3307
|
-
# wedged on the pre-credit HWM + stale-
|
|
3308
|
-
# replica rows. Crash scenario B (the
|
|
3309
|
-
# Codex r4 finding): a recovery tick where
|
|
3310
|
-
# ``(prior, post)`` pair-matches the
|
|
3311
|
-
# already-stored event row also takes the
|
|
3312
|
-
# ``is_dup`` branch; without the hoist the
|
|
3313
|
-
# pivots would be skipped and the system
|
|
3314
|
-
# would stay wedged. The pivots are
|
|
3315
|
-
# individually idempotent (file overwrite
|
|
3316
|
-
# + DELETE on a stable predicate), so
|
|
3317
|
-
# re-running them on the recovery tick is
|
|
3318
|
-
# always safe. Mirrors the weekly hoist at
|
|
3319
|
-
# ``_cctally_record.py`` after the
|
|
3320
|
-
# ``if already is None`` block (grep
|
|
3321
|
-
# ``Force-write hwm-7d``).
|
|
3322
|
-
#
|
|
3323
|
-
# Force-write hwm-5h: bypasses the
|
|
3324
|
-
# monotonic guard at the normal hwm-5h
|
|
3325
|
-
# writer below. Lands AFTER
|
|
3326
|
-
# ``conn.commit()`` so a concurrent reader
|
|
3327
|
-
# doesn't see the new HWM before the
|
|
3328
|
-
# event row is durable. File format
|
|
3329
|
-
# matches the canonical writer:
|
|
3330
|
-
# ``<key> <percent>\n``.
|
|
3331
|
-
try:
|
|
3332
|
-
(_cctally_core.APP_DIR / "hwm-5h").write_text(
|
|
3333
|
-
f"{int(five_hour_window_key)} "
|
|
3334
|
-
f"{float(five_hour_percent)}\n"
|
|
3335
|
-
)
|
|
3336
|
-
except OSError:
|
|
3337
|
-
pass
|
|
3338
|
-
# Stale-replica DELETE (spec §4.3).
|
|
3339
|
-
# Defends against claude-statusline
|
|
3340
|
-
# replaying the pre-credit
|
|
3341
|
-
# ``--five-hour-percent`` value past the
|
|
3342
|
-
# credit moment from its own in-memory
|
|
3343
|
-
# HWM cache. 1.0pp tolerance band (issue
|
|
3344
|
-
# #48 — symmetric follow-up to weekly #45)
|
|
3345
|
-
# around the observed pre-credit baseline
|
|
3346
|
-
# absorbs any rounding drift between
|
|
3347
|
-
# cctally's OAuth read and statusline's
|
|
3348
|
-
# ``--five-hour-percent`` payload (today
|
|
3349
|
-
# they match byte-identically, but the
|
|
3350
|
-
# band future-proofs against Anthropic or
|
|
3351
|
-
# statusline changing 5h rounding). The
|
|
3352
|
-
# band stays well below the 5.0pp 5h
|
|
3353
|
-
# in-place credit detection threshold
|
|
3354
|
-
# (``_FIVE_HOUR_RESET_PCT_DROP_THRESHOLD``)
|
|
3355
|
-
# — 4pp safety margin — so legitimate
|
|
3356
|
-
# post-credit values are never caught.
|
|
3357
|
-
# ``unixepoch()`` on both sides for offset
|
|
3358
|
-
# robustness (Z vs +00:00). Bind is the
|
|
3359
|
-
# in-scope ``prior_5h_pct``, which equals
|
|
3360
|
-
# the just-stamped
|
|
3361
|
-
# ``five_hour_reset_events.prior_percent``
|
|
3362
|
-
# on the event row.
|
|
3363
|
-
try:
|
|
3364
|
-
conn.execute(
|
|
3365
|
-
"DELETE FROM weekly_usage_snapshots "
|
|
3366
|
-
" WHERE five_hour_window_key = ? "
|
|
3367
|
-
" AND unixepoch(captured_at_utc) "
|
|
3368
|
-
" >= unixepoch(?) "
|
|
3369
|
-
" AND ABS(five_hour_percent - ?) "
|
|
3370
|
-
" < 1.0",
|
|
3371
|
-
(
|
|
3372
|
-
int(five_hour_window_key),
|
|
3373
|
-
effective_iso,
|
|
3374
|
-
prior_5h_pct,
|
|
3375
|
-
),
|
|
3376
|
-
)
|
|
3377
|
-
conn.commit()
|
|
3378
|
-
except sqlite3.DatabaseError as exc:
|
|
3379
|
-
eprint(
|
|
3380
|
-
"[record-usage] 5h post-credit "
|
|
3381
|
-
f"cleanup failed: {exc}"
|
|
3382
|
-
)
|
|
3383
|
-
except (sqlite3.DatabaseError, ValueError, TypeError) as exc:
|
|
3384
|
-
eprint(
|
|
3385
|
-
f"[record-usage] 5h in-place-credit detection "
|
|
3386
|
-
f"failed: {exc}"
|
|
3387
|
-
)
|
|
3388
|
-
except (sqlite3.DatabaseError, ValueError) as exc:
|
|
3389
|
-
eprint(f"[record-usage] reset-event detection failed: {exc}")
|
|
3390
|
-
|
|
3391
|
-
# 7-day usage is monotonically non-decreasing within a billing week
|
|
3392
|
-
# — UNTIL an in-place weekly credit lowers it. There are TWO credit
|
|
3393
|
-
# shapes and BOTH must floor this clamp, or a real post-credit tick
|
|
3394
|
-
# is suppressed and never stored:
|
|
3395
|
-
# (a) an Anthropic mid-week reset / >=25pp auto-credit writes a
|
|
3396
|
-
# `week_reset_events` row (it also re-anchors the window); and
|
|
3397
|
-
# (b) a manual `record-credit` partial credit writes a
|
|
3398
|
-
# `weekly_credit_floors` row WITHOUT re-anchoring the week
|
|
3399
|
-
# (record-credit M2, #209).
|
|
3400
|
-
# `_reset_aware_floor` returns the LATEST in-week effective across
|
|
3401
|
-
# both legs. The MAX query then filters to samples captured at-or-
|
|
3402
|
-
# after that floor, so a fresh post-credit OAuth value (e.g. 37%
|
|
3403
|
-
# after a 46->31 credit) lands instead of being held back by stale
|
|
3404
|
-
# pre-credit history (46%). Without the credit-floor leg, the 37%
|
|
3405
|
-
# tick is `round(37,1) < round(46,1)` -> should_insert=False ->
|
|
3406
|
-
# never stored, cascading to every latest-snapshot surface
|
|
3407
|
-
# (the M2 linchpin; spec §4a, test S13).
|
|
3408
|
-
# When neither leg has a row, the floor is None -> '1970-...' epoch-
|
|
3409
|
-
# zero default -> the filter is a no-op and legacy clamp behavior is
|
|
3410
|
-
# preserved byte-identically.
|
|
3411
|
-
# NB: comparison wrapped with ``unixepoch()`` on BOTH sides.
|
|
3412
|
-
# ``captured_at_utc`` is stored with `Z` suffix, but the floor may
|
|
3413
|
-
# carry a non-UTC / +00:00 offset spelling. Lex string compare on
|
|
3414
|
-
# mixed offsets silently mis-orders moments for non-UTC hosts
|
|
3415
|
-
# (CLAUDE.md gotcha: 5h-block cross-reset flag — "all comparisons go
|
|
3416
|
-
# through unixepoch(), NOT lex BETWEEN/`<`/`>`"). Same rule here, and
|
|
3417
|
-
# inside `_reset_aware_floor`'s own ORDER BY.
|
|
3418
|
-
clamp_floor_iso = _reset_aware_floor(
|
|
3419
|
-
conn, week_start_date, week_start_at, week_end_at,
|
|
3420
|
-
) or "1970-01-01T00:00:00Z"
|
|
3421
|
-
max_row = conn.execute(
|
|
3422
|
-
"""
|
|
3423
|
-
SELECT MAX(weekly_percent) AS v
|
|
3424
|
-
FROM weekly_usage_snapshots
|
|
3425
|
-
WHERE week_start_date = ?
|
|
3426
|
-
AND unixepoch(captured_at_utc) >= unixepoch(?)
|
|
3427
|
-
""",
|
|
3428
|
-
(week_start_date, clamp_floor_iso),
|
|
3429
|
-
).fetchone()
|
|
3430
|
-
if hwm_clamp_applies(weekly_percent, max_row["v"] if max_row else None):
|
|
3431
|
-
should_insert = False
|
|
3432
|
-
else:
|
|
3433
|
-
# 5-hour usage is monotonically non-decreasing within a window
|
|
3434
|
-
# — UNTIL an in-place 5h credit fires. When a
|
|
3435
|
-
# ``five_hour_reset_events`` row exists for THIS
|
|
3436
|
-
# ``five_hour_window_key``, the MAX query filters to samples
|
|
3437
|
-
# captured at-or-after the event's ``effective_reset_at_utc``
|
|
3438
|
-
# so a fresh post-credit OAuth value (e.g. 4%) lands instead
|
|
3439
|
-
# of being re-clamped to the pre-credit max (e.g. 28%). When
|
|
3440
|
-
# no event row exists, ``COALESCE`` defaults to epoch-zero so
|
|
3441
|
-
# the filter is a no-op and legacy clamp behavior is preserved
|
|
3442
|
-
# byte-identically.
|
|
3443
|
-
#
|
|
3444
|
-
# ``unixepoch()`` on BOTH sides for offset robustness — stored
|
|
3445
|
-
# ``captured_at_utc`` carries ``Z`` while
|
|
3446
|
-
# ``effective_reset_at_utc`` carries ``+00:00``. Lex compare
|
|
3447
|
-
# would silently mis-order moments for non-UTC hosts (same
|
|
3448
|
-
# gotcha as the weekly clamp / 5h-block cross-reset flag).
|
|
3449
|
-
#
|
|
3450
|
-
# Joining on ``five_hour_window_key`` (canonical 10-min-floored
|
|
3451
|
-
# epoch) absorbs Anthropic's seconds-level jitter on
|
|
3452
|
-
# ``resets_at``; an ISO-string equality at this site silently
|
|
3453
|
-
# skipped the clamp every time a jittered fetch landed in
|
|
3454
|
-
# the same physical 5h window (spec Bug B).
|
|
3455
|
-
#
|
|
3456
|
-
# Spec §4.1 of
|
|
3457
|
-
# docs/superpowers/specs/2026-05-16-5h-in-place-credit-detection.md.
|
|
3458
|
-
if five_hour_percent is not None and five_hour_window_key is not None:
|
|
3459
|
-
max_5h_row = conn.execute(
|
|
3460
|
-
"""
|
|
3461
|
-
SELECT MAX(five_hour_percent) AS v
|
|
3462
|
-
FROM weekly_usage_snapshots
|
|
3463
|
-
WHERE five_hour_window_key = ?
|
|
3464
|
-
AND unixepoch(captured_at_utc) >= unixepoch(COALESCE(
|
|
3465
|
-
(SELECT effective_reset_at_utc
|
|
3466
|
-
FROM five_hour_reset_events
|
|
3467
|
-
WHERE five_hour_window_key = ?
|
|
3468
|
-
ORDER BY id DESC
|
|
3469
|
-
LIMIT 1),
|
|
3470
|
-
'1970-01-01T00:00:00Z'
|
|
3471
|
-
))
|
|
3472
|
-
""",
|
|
3473
|
-
(int(five_hour_window_key), int(five_hour_window_key)),
|
|
3474
|
-
).fetchone()
|
|
3475
|
-
if hwm_clamp_applies(
|
|
3476
|
-
five_hour_percent, max_5h_row["v"] if max_5h_row else None
|
|
3477
|
-
):
|
|
3478
|
-
five_hour_percent = float(max_5h_row["v"])
|
|
3479
|
-
|
|
3480
|
-
# Dedup vs last snapshot: if BOTH weekly_percent and
|
|
3481
|
-
# five_hour_percent are unchanged from the most recent row in
|
|
3482
|
-
# this week, swallow the insert. Tests of the 5h clamp must
|
|
3483
|
-
# vary --percent (or --five-hour-percent) between calls, or
|
|
3484
|
-
# the second call is dropped here before the clamp even runs
|
|
3485
|
-
# — see bin/cctally-5h-canonical-test scenario B.
|
|
3486
|
-
last = conn.execute(
|
|
3487
|
-
"""
|
|
3488
|
-
SELECT weekly_percent, five_hour_percent
|
|
3489
|
-
FROM weekly_usage_snapshots
|
|
3490
|
-
WHERE week_start_date = ?
|
|
3491
|
-
ORDER BY captured_at_utc DESC, id DESC
|
|
3492
|
-
LIMIT 1
|
|
3493
|
-
""",
|
|
3494
|
-
(week_start_date,),
|
|
3495
|
-
).fetchone()
|
|
3496
|
-
if last is not None:
|
|
3497
|
-
if float(last["weekly_percent"]) == weekly_percent:
|
|
3498
|
-
last_5h = last["five_hour_percent"]
|
|
3499
|
-
if five_hour_percent is None or (
|
|
3500
|
-
last_5h is not None and float(last_5h) == five_hour_percent
|
|
3501
|
-
):
|
|
3502
|
-
should_insert = False
|
|
3503
|
-
|
|
3504
|
-
# No backfill of 5h data on existing milestones — we don't have
|
|
3505
|
-
# authentic crossing-time values for them. New milestones created
|
|
3506
|
-
# by the status line path will have 5h data set at creation time
|
|
3507
|
-
# via maybe_record_milestone().
|
|
3508
|
-
finally:
|
|
3509
|
-
conn.close()
|
|
3510
|
-
|
|
3511
|
-
if not should_insert:
|
|
3512
|
-
# Self-heal: a prior record-usage invocation may have inserted
|
|
3513
|
-
# the snapshot but been killed (CC self-update, machine sleep,
|
|
3514
|
-
# OOM) before maybe_record_milestone / maybe_update_five_hour_block
|
|
3515
|
-
# could run. Pre-probe both surfaces with cheap indexed SELECTs
|
|
3516
|
-
# and only invoke the helpers when a row is actually missing or
|
|
3517
|
-
# stale. Steady-state cost: 1-3 SELECTs (latest snapshot always;
|
|
3518
|
-
# +max_milestone if floor>=1; +block last_observed if window_key
|
|
3519
|
-
# is set); ZERO JSONL re-ingest on healthy ticks. The helpers themselves are idempotent under
|
|
3520
|
-
# concurrent record-usage instances (INSERT OR IGNORE for
|
|
3521
|
-
# percent_milestones; SQLite write-lock serialization for the
|
|
3522
|
-
# 5h upsert). Without the pre-probe, every dedup tick would
|
|
3523
|
-
# trigger sync_cache + a window walk + replace-all rollups via
|
|
3524
|
-
# maybe_update_five_hour_block's unconditional _compute_block_totals
|
|
3525
|
-
# call. Regression: bin/cctally-record-usage-selfheal-test.
|
|
3526
|
-
try:
|
|
3527
|
-
heal_conn = open_db()
|
|
3528
|
-
try:
|
|
3529
|
-
latest_row = heal_conn.execute(
|
|
3530
|
-
"SELECT * FROM weekly_usage_snapshots "
|
|
3531
|
-
"WHERE week_start_date = ? "
|
|
3532
|
-
"ORDER BY captured_at_utc DESC, id DESC LIMIT 1",
|
|
3533
|
-
(week_start_date,),
|
|
3534
|
-
).fetchone()
|
|
3535
|
-
if latest_row is None:
|
|
3536
|
-
return 0
|
|
3537
|
-
latest_saved = _saved_dict_from_usage_row(latest_row)
|
|
3538
|
-
|
|
3539
|
-
# Probe 1: do we owe a percent milestone? Snap up before
|
|
3540
|
-
# floor (status-line API returns 0.N*100 which can fall
|
|
3541
|
-
# one ULP short of N — same convention as
|
|
3542
|
-
# maybe_record_milestone).
|
|
3543
|
-
latest_floor = math.floor(
|
|
3544
|
-
float(latest_row["weekly_percent"]) + 1e-9
|
|
3545
|
-
)
|
|
3546
|
-
need_milestone_heal = False
|
|
3547
|
-
if latest_floor >= 1:
|
|
3548
|
-
# v1.7.2: scope the heal probe to the ACTIVE segment.
|
|
3549
|
-
# Without this, a credited week's MAX over the whole
|
|
3550
|
-
# ledger would still read the pre-credit ceiling
|
|
3551
|
-
# (e.g. 67%) and silently suppress the post-credit
|
|
3552
|
-
# ledger's heal even though it has zero rows.
|
|
3553
|
-
captured_at_for_probe = latest_row["captured_at_utc"]
|
|
3554
|
-
week_end_at_for_probe = latest_row["week_end_at"]
|
|
3555
|
-
heal_segment = 0
|
|
3556
|
-
if week_end_at_for_probe and captured_at_for_probe:
|
|
3557
|
-
seg = heal_conn.execute(
|
|
3558
|
-
"SELECT id FROM week_reset_events "
|
|
3559
|
-
"WHERE new_week_end_at = ? "
|
|
3560
|
-
" AND unixepoch(effective_reset_at_utc) <= unixepoch(?) "
|
|
3561
|
-
"ORDER BY id DESC LIMIT 1",
|
|
3562
|
-
(week_end_at_for_probe, captured_at_for_probe),
|
|
3563
|
-
).fetchone()
|
|
3564
|
-
if seg is not None:
|
|
3565
|
-
heal_segment = int(seg["id"])
|
|
3566
|
-
max_existing = heal_conn.execute(
|
|
3567
|
-
"SELECT MAX(percent_threshold) AS m "
|
|
3568
|
-
"FROM percent_milestones "
|
|
3569
|
-
"WHERE week_start_date = ? AND reset_event_id = ?",
|
|
3570
|
-
(week_start_date, heal_segment),
|
|
3571
|
-
).fetchone()
|
|
3572
|
-
existing_m = (
|
|
3573
|
-
int(max_existing["m"])
|
|
3574
|
-
if max_existing and max_existing["m"] is not None
|
|
3575
|
-
else None
|
|
3576
|
-
)
|
|
3577
|
-
if milestone_coverage_owes(existing_m, latest_floor):
|
|
3578
|
-
need_milestone_heal = True
|
|
3579
|
-
|
|
3580
|
-
# Probe 2: do we owe a 5h-block update? Either no row
|
|
3581
|
-
# for this canonical window, or the existing row's
|
|
3582
|
-
# last_observed_at_utc is stale relative to the latest
|
|
3583
|
-
# snapshot's captured_at_utc (the kill landed between
|
|
3584
|
-
# insert_usage_snapshot and maybe_update_five_hour_block).
|
|
3585
|
-
#
|
|
3586
|
-
# Round-3: ALSO scope the milestone-coverage half of
|
|
3587
|
-
# this probe by ACTIVE 5h SEGMENT. Without this, a
|
|
3588
|
-
# credited block's MAX over the whole milestone ledger
|
|
3589
|
-
# would still read the pre-credit ceiling (e.g. 28%) and
|
|
3590
|
-
# silently suppress the post-credit ledger's heal even
|
|
3591
|
-
# though it has zero rows. Mirrors weekly Probe 1's
|
|
3592
|
-
# segment-aware fix above. Uses
|
|
3593
|
-
# ``_resolve_active_five_hour_reset_event_id`` to find
|
|
3594
|
-
# the active segment for the latest snapshot's window.
|
|
3595
|
-
need_5h_heal = False
|
|
3596
|
-
incoming_block_saved: dict[str, Any] | None = None
|
|
3597
|
-
window_key = latest_row["five_hour_window_key"]
|
|
3598
|
-
if window_key is not None:
|
|
3599
|
-
block_row = heal_conn.execute(
|
|
3600
|
-
"SELECT last_observed_at_utc "
|
|
3601
|
-
"FROM five_hour_blocks "
|
|
3602
|
-
"WHERE five_hour_window_key = ?",
|
|
3603
|
-
(int(window_key),),
|
|
3604
|
-
).fetchone()
|
|
3605
|
-
if block_row is None:
|
|
3606
|
-
need_5h_heal = True
|
|
3607
|
-
elif (
|
|
3608
|
-
block_row["last_observed_at_utc"]
|
|
3609
|
-
< latest_row["captured_at_utc"]
|
|
3610
|
-
):
|
|
3611
|
-
need_5h_heal = True
|
|
3612
|
-
else:
|
|
3613
|
-
# Block row exists AND last_observed is fresh
|
|
3614
|
-
# — but the post-credit milestone segment may
|
|
3615
|
-
# still owe rows. Scope MAX(percent_threshold)
|
|
3616
|
-
# by the active reset_event_id segment so
|
|
3617
|
-
# post-credit climbs from threshold 1 trigger
|
|
3618
|
-
# heal even when the pre-credit segment already
|
|
3619
|
-
# crossed higher thresholds. Probe shape mirrors
|
|
3620
|
-
# weekly Probe 1 (lines 1922-1956).
|
|
3621
|
-
five_hour_percent_for_probe = latest_row[
|
|
3622
|
-
"five_hour_percent"
|
|
3623
|
-
]
|
|
3624
|
-
if five_hour_percent_for_probe is not None:
|
|
3625
|
-
latest_5h_floor = math.floor(
|
|
3626
|
-
float(five_hour_percent_for_probe) + 1e-9
|
|
3627
|
-
)
|
|
3628
|
-
if latest_5h_floor >= 1:
|
|
3629
|
-
heal_5h_segment = (
|
|
3630
|
-
_resolve_active_five_hour_reset_event_id(
|
|
3631
|
-
heal_conn, int(window_key)
|
|
3632
|
-
)
|
|
3633
|
-
)
|
|
3634
|
-
max_5h_existing = heal_conn.execute(
|
|
3635
|
-
"SELECT MAX(percent_threshold) AS m "
|
|
3636
|
-
"FROM five_hour_milestones "
|
|
3637
|
-
"WHERE five_hour_window_key = ? "
|
|
3638
|
-
" AND reset_event_id = ?",
|
|
3639
|
-
(int(window_key), heal_5h_segment),
|
|
3640
|
-
).fetchone()
|
|
3641
|
-
existing_5h_m = (
|
|
3642
|
-
int(max_5h_existing["m"])
|
|
3643
|
-
if max_5h_existing
|
|
3644
|
-
and max_5h_existing["m"] is not None
|
|
3645
|
-
else None
|
|
3646
|
-
)
|
|
3647
|
-
if milestone_coverage_owes(
|
|
3648
|
-
existing_5h_m, latest_5h_floor
|
|
3649
|
-
):
|
|
3650
|
-
need_5h_heal = True
|
|
3651
|
-
# Window-rollover heal: this dedup tick observed a NEW 5h
|
|
3652
|
-
# window (its canonical ``five_hour_window_key`` differs
|
|
3653
|
-
# from the latest STORED snapshot's) whose
|
|
3654
|
-
# ``five_hour_blocks`` anchor does not exist yet. The dedup
|
|
3655
|
-
# above swallowed the snapshot insert because the weekly/5h
|
|
3656
|
-
# percents were flat, so ``latest_row`` still points at the
|
|
3657
|
-
# PREVIOUS window and the ``need_5h_heal`` probe only ever
|
|
3658
|
-
# checked that old (still-fresh) window — leaving the
|
|
3659
|
-
# current window unanchored. Materialize the missing block
|
|
3660
|
-
# now so ``blocks`` and the dashboard anchor the ACTIVE
|
|
3661
|
-
# block to its API-derived window instead of falling back to
|
|
3662
|
-
# the heuristic "~" until the percent next moves (the
|
|
3663
|
-
# statusline is unaffected — it renders the live
|
|
3664
|
-
# rate_limits, not the DB). Block-only: NO snapshot is
|
|
3665
|
-
# inserted (the tick stays deduped); the "latest snapshot"
|
|
3666
|
-
# weekly/5h surfaces and monotonicity clamps are untouched.
|
|
3667
|
-
# ``maybe_update_five_hour_block`` is an upsert keyed on
|
|
3668
|
-
# ``five_hour_window_key``, so later flat ticks in the same
|
|
3669
|
-
# window re-run it as an idempotent no-op.
|
|
3670
|
-
if (
|
|
3671
|
-
five_hour_window_key is not None
|
|
3672
|
-
and five_hour_percent is not None
|
|
3673
|
-
and five_hour_resets_at_str is not None
|
|
3674
|
-
and (
|
|
3675
|
-
window_key is None
|
|
3676
|
-
or int(window_key) != int(five_hour_window_key)
|
|
3677
|
-
)
|
|
3678
|
-
):
|
|
3679
|
-
incoming_block_row = heal_conn.execute(
|
|
3680
|
-
"SELECT 1 FROM five_hour_blocks "
|
|
3681
|
-
"WHERE five_hour_window_key = ? LIMIT 1",
|
|
3682
|
-
(int(five_hour_window_key),),
|
|
3683
|
-
).fetchone()
|
|
3684
|
-
if incoming_block_row is None:
|
|
3685
|
-
incoming_block_saved = {
|
|
3686
|
-
# ``id`` is extracted-but-unused by
|
|
3687
|
-
# maybe_update_five_hour_block; reuse latest_row's
|
|
3688
|
-
# for output-dict shape parity with a real insert.
|
|
3689
|
-
"id": int(latest_row["id"]),
|
|
3690
|
-
"capturedAt": now_utc_iso(),
|
|
3691
|
-
"weeklyPercent": weekly_percent,
|
|
3692
|
-
"fiveHourPercent": five_hour_percent,
|
|
3693
|
-
"fiveHourResetsAt": five_hour_resets_at_str,
|
|
3694
|
-
"fiveHourWindowKey": int(five_hour_window_key),
|
|
3695
|
-
}
|
|
3696
|
-
finally:
|
|
3697
|
-
heal_conn.close()
|
|
3698
|
-
|
|
3699
|
-
if need_milestone_heal or need_5h_heal or incoming_block_saved:
|
|
3700
|
-
if need_milestone_heal:
|
|
3701
|
-
try:
|
|
3702
|
-
maybe_record_milestone(latest_saved)
|
|
3703
|
-
except Exception as exc:
|
|
3704
|
-
eprint(f"[milestone] self-heal error: {exc}")
|
|
3705
|
-
if need_5h_heal:
|
|
3706
|
-
try:
|
|
3707
|
-
maybe_update_five_hour_block(latest_saved)
|
|
3708
|
-
except Exception as exc:
|
|
3709
|
-
eprint(f"[5h-block] self-heal error: {exc}")
|
|
3710
|
-
if incoming_block_saved is not None:
|
|
3711
|
-
try:
|
|
3712
|
-
maybe_update_five_hour_block(incoming_block_saved)
|
|
3713
|
-
except Exception as exc:
|
|
3714
|
-
eprint(f"[5h-block] window-rollover heal error: {exc}")
|
|
3715
|
-
|
|
3716
|
-
# Dollar-decoupled axes (budget / project-budget / projected) heal on
|
|
3717
|
-
# EVERY dedup tick — USD spend can cross a $ threshold while the
|
|
3718
|
-
# weekly/5h percent is flat (so should_insert is False), and a prior
|
|
3719
|
-
# run may have died after insert_usage_snapshot but before the
|
|
3720
|
-
# post-insert axis block. Each helper gates first on config (a cheap
|
|
3721
|
-
# read for non-users) and pre-probes its recorded set before any cost
|
|
3722
|
-
# scan, so non-budget users pay ~nothing here. Order matches the
|
|
3723
|
-
# post-insert block so the projected budget_usd leg's skip_sync=True
|
|
3724
|
-
# cache-warming dependency on the actual-budget axis still holds.
|
|
3725
|
-
# [Dedup mustn't gate side effects]
|
|
3726
|
-
for _heal_fn, _heal_tag in (
|
|
3727
|
-
(maybe_record_budget_milestone, "budget-milestone"),
|
|
3728
|
-
(maybe_record_project_budget_milestone,
|
|
3729
|
-
"project-budget-milestone"),
|
|
3730
|
-
(maybe_record_codex_budget_milestone,
|
|
3731
|
-
"codex-budget-milestone"),
|
|
3732
|
-
(maybe_record_projected_alert, "projected-alert"),
|
|
3733
|
-
):
|
|
3734
|
-
try:
|
|
3735
|
-
_heal_fn(latest_saved)
|
|
3736
|
-
except Exception as exc:
|
|
3737
|
-
eprint(f"[{_heal_tag}] self-heal error: {exc}")
|
|
3738
|
-
except Exception as exc:
|
|
3739
|
-
eprint(f"[record-usage] self-heal lookup failed: {exc}")
|
|
3740
|
-
return 0
|
|
3741
|
-
|
|
3742
|
-
payload = {
|
|
3743
|
-
# Record the true feeder. Defaults to "statusline" for the public
|
|
3744
|
-
# `record-usage` CLI (preserves prior behavior); the OAuth callers
|
|
3745
|
-
# (_hook_tick_oauth_refresh / _refresh_usage_inproc) pass "api", and
|
|
3746
|
-
# the statusline persist feeder passes "statusline". Previously this
|
|
3747
|
-
# was hard-coded "statusline" for EVERY caller, mislabeling OAuth
|
|
3748
|
-
# rows (spec §5). No migration — the column already exists.
|
|
3837
|
+
# Build the RAW observation (spec 4.2 / 5.3 -- raw capture only, NO derived
|
|
3838
|
+
# week columns; `_pipeline_claude_usage` canonicalizes the week boundaries +
|
|
3839
|
+
# the 5h window key at ingest). Append it, then run the single-flight cycle.
|
|
3840
|
+
raw: dict[str, Any] = {
|
|
3841
|
+
"weekly_percent": weekly_percent,
|
|
3842
|
+
"resets_at": resets_at,
|
|
3749
3843
|
"source": getattr(args, "source", "statusline"),
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3844
|
+
# The CAPTURE wall clock (now_utc_iso(), NOT _command_as_of()) — the
|
|
3845
|
+
# snapshot `captured_at_utc` + the milestone/5h-block/dollar-axis stamps
|
|
3846
|
+
# + the block cost-sum range end. Legacy used wall clock for these while
|
|
3847
|
+
# DETECTION (reset/credit) used `_command_as_of()`; the obs line `at`
|
|
3848
|
+
# carries `_command_as_of()` (below) so the hook preserves that split.
|
|
3849
|
+
# Captured ONCE here (append time) and journaled, so a delayed ingest is
|
|
3850
|
+
# deterministic (never the ingest wall clock).
|
|
3851
|
+
"captured_at": now_utc_iso(),
|
|
3756
3852
|
}
|
|
3757
3853
|
if five_hour_percent is not None:
|
|
3758
|
-
|
|
3854
|
+
raw["five_hour_percent"] = five_hour_percent
|
|
3759
3855
|
if five_hour_resets_at_str is not None:
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
# NEW: 5h-block rollup (paired with maybe_record_milestone for 7d).
|
|
3771
|
-
# The helper performs an opportunistic JOIN against week_reset_events
|
|
3772
|
-
# every tick to flag any open block whose interval contains a recorded
|
|
3773
|
-
# reset; no per-call plumbing needed (self-healing).
|
|
3774
|
-
try:
|
|
3775
|
-
maybe_update_five_hour_block(saved)
|
|
3776
|
-
except Exception as exc:
|
|
3777
|
-
eprint(f"[5h-block] unexpected error: {exc}")
|
|
3778
|
-
|
|
3779
|
-
# NEW: equiv-$ budget alert firing (Approach A, issue #19). Gated on a
|
|
3780
|
-
# set budget + alerts_enabled FIRST — non-budget users pay zero overhead.
|
|
3781
|
-
try:
|
|
3782
|
-
maybe_record_budget_milestone(saved)
|
|
3783
|
-
except Exception as exc:
|
|
3784
|
-
eprint(f"[budget-milestone] unexpected error: {exc}")
|
|
3785
|
-
|
|
3786
|
-
# NEW: per-project equiv-$ budget alert firing (axis `project_budget`,
|
|
3787
|
-
# #19/#121). Runs AFTER the global budget axis, but the per-project scan is
|
|
3788
|
-
# self-sufficient — it passes skip_sync=False so a project-only user (no
|
|
3789
|
-
# global budget warming the cache) still resolves live spend on this tick.
|
|
3790
|
-
# Gated FIRST on a non-empty budget.projects + project_alerts_enabled — non-
|
|
3791
|
-
# users pay only one config read.
|
|
3792
|
-
try:
|
|
3793
|
-
maybe_record_project_budget_milestone(saved)
|
|
3794
|
-
except Exception as exc:
|
|
3795
|
-
eprint(f"[project-budget-milestone] unexpected error: {exc}")
|
|
3796
|
-
|
|
3797
|
-
# NEW: Codex budget alert firing (axis `codex_budget`, calendar-period-codex-
|
|
3798
|
-
# budgets). Gated FIRST on a configured budget.codex + alerts_enabled — non-
|
|
3799
|
-
# Codex-budget users pay only one config read. Codex usage never flows through
|
|
3800
|
-
# record-usage, so this is one of the two firing triggers (the other is the
|
|
3801
|
-
# opportunistic fire on `cctally budget`); forward-only/fire-once means the
|
|
3802
|
-
# double-trigger never double-fires.
|
|
3803
|
-
try:
|
|
3804
|
-
maybe_record_codex_budget_milestone(saved)
|
|
3805
|
-
except Exception as exc:
|
|
3806
|
-
eprint(f"[codex-budget-milestone] unexpected error: {exc}")
|
|
3807
|
-
|
|
3808
|
-
# NEW: projected-pace alert firing (axis `projected`, #121/#135). Runs in
|
|
3809
|
-
# its OWN detect-and-arm AFTER the weekly/5h/budget/codex blocks; gated up
|
|
3810
|
-
# front on (alerts.enabled && alerts.projected_enabled) ||
|
|
3811
|
-
# (_budget_alerts_active && budget.projected_enabled — ANY Claude period,
|
|
3812
|
-
# #135) || (codex.alerts_enabled && codex.projected_enabled, #135) — all
|
|
3813
|
-
# toggles default OFF, so non-projected users pay only a cheap config read.
|
|
3814
|
-
# No only_metrics on the record path: every enabled leg runs. The Codex leg
|
|
3815
|
-
# relies on the codex-budget block above (:3129) having warmed the cache,
|
|
3816
|
-
# but is robust even if it short-circuited (skip_sync=False self-syncs, R5).
|
|
3817
|
-
try:
|
|
3818
|
-
maybe_record_projected_alert(saved)
|
|
3819
|
-
except Exception as exc:
|
|
3820
|
-
eprint(f"[projected-alert] unexpected error: {exc}")
|
|
3821
|
-
|
|
3822
|
-
# Write high-water mark so the status line never displays a regression.
|
|
3823
|
-
# The file contains "week_start_date weekly_percent" on one line.
|
|
3824
|
-
try:
|
|
3825
|
-
hwm_path = _cctally_core.APP_DIR / "hwm-7d"
|
|
3826
|
-
existing_hwm = 0.0
|
|
3827
|
-
try:
|
|
3828
|
-
parts = hwm_path.read_text().strip().split()
|
|
3829
|
-
if len(parts) == 2 and parts[0] == week_start_date:
|
|
3830
|
-
existing_hwm = float(parts[1])
|
|
3831
|
-
except (FileNotFoundError, ValueError, OSError):
|
|
3832
|
-
pass
|
|
3833
|
-
if hwm_file_next(existing_hwm, weekly_percent) is not None:
|
|
3834
|
-
hwm_path.write_text(f"{week_start_date} {weekly_percent}\n")
|
|
3835
|
-
except OSError:
|
|
3836
|
-
pass
|
|
3837
|
-
|
|
3838
|
-
# Symmetric 5h HWM. Keyed by the canonical five_hour_window_key derived
|
|
3839
|
-
# under the prior-anchor logic above (NOT a fresh pure-floor recompute) so
|
|
3840
|
-
# boundary-straddling jitter writes to the same file key as the matching
|
|
3841
|
-
# DB row. File format: "<canonical_5h_window_key> <percent>".
|
|
3842
|
-
if (
|
|
3843
|
-
five_hour_percent is not None
|
|
3844
|
-
and five_hour_window_key is not None
|
|
3845
|
-
):
|
|
3846
|
-
try:
|
|
3847
|
-
five_resets_key = five_hour_window_key
|
|
3848
|
-
hwm5_path = _cctally_core.APP_DIR / "hwm-5h"
|
|
3849
|
-
existing_hwm5 = 0.0
|
|
3850
|
-
try:
|
|
3851
|
-
parts5 = hwm5_path.read_text().strip().split()
|
|
3852
|
-
if len(parts5) == 2 and parts5[0] == str(five_resets_key):
|
|
3853
|
-
existing_hwm5 = float(parts5[1])
|
|
3854
|
-
except (FileNotFoundError, ValueError, OSError):
|
|
3855
|
-
pass
|
|
3856
|
-
if hwm_file_next(existing_hwm5, five_hour_percent) is not None:
|
|
3857
|
-
hwm5_path.write_text(f"{five_resets_key} {five_hour_percent}\n")
|
|
3858
|
-
except OSError:
|
|
3859
|
-
pass
|
|
3860
|
-
|
|
3856
|
+
raw["five_hour_resets_at"] = five_hour_resets_at_str
|
|
3857
|
+
|
|
3858
|
+
import _cctally_journal as _jr
|
|
3859
|
+
import _lib_journal as _lj
|
|
3860
|
+
_jr.append_record(_lj.make_obs(
|
|
3861
|
+
at=now_utc_iso(now_dt), src=writer, provider="claude", payload=raw))
|
|
3862
|
+
# authoritative observes its own write synchronously; opportunistic skips a
|
|
3863
|
+
# busy ingest lock and lets the current holder consume the appended line.
|
|
3864
|
+
_jr.run_stats_ingest(mode=ingest_mode)
|
|
3861
3865
|
return 0
|
|
3862
3866
|
|
|
3863
3867
|
|
|
@@ -4235,10 +4239,24 @@ def _cmd_hook_tick_codex(args: argparse.Namespace, *, event: str = "unknown") ->
|
|
|
4235
4239
|
now=dt.datetime.now(dt.timezone.utc),
|
|
4236
4240
|
)
|
|
4237
4241
|
# Vendor-scoped spend is intentionally evaluated once per
|
|
4238
|
-
# successful due-set tick, not once per root.
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
)
|
|
4242
|
+
# successful due-set tick, not once per root. Task 7 Item 4: the
|
|
4243
|
+
# on-demand Codex budget firing routes through the single-flight
|
|
4244
|
+
# ingest cycle (the `codex_apply` seam) instead of opening its own
|
|
4245
|
+
# stats connection — so its `budget_milestones` (vendor=codex)
|
|
4246
|
+
# crossings are journaled by the budget harvest and its alerts
|
|
4247
|
+
# dispatch post-commit (set-then-dispatch). AUTHORITATIVE so a failure
|
|
4248
|
+
# propagates to this leg's try/except (raise_errors=True preserved).
|
|
4249
|
+
import _cctally_journal as _jr_codex
|
|
4250
|
+
_budget_holder = {"n": 0}
|
|
4251
|
+
|
|
4252
|
+
def _codex_budget_leg(ictx):
|
|
4253
|
+
_budget_holder["n"] = int(c.maybe_record_codex_budget_milestone(
|
|
4254
|
+
{}, conn=ictx.conn, alert_sink=ictx.pending_alerts,
|
|
4255
|
+
raise_errors=True) or 0)
|
|
4256
|
+
|
|
4257
|
+
_jr_codex.run_stats_ingest(
|
|
4258
|
+
mode="authoritative", codex_apply=_codex_budget_leg)
|
|
4259
|
+
budget_alerts = _budget_holder["n"]
|
|
4242
4260
|
mark_lifecycle_success(locks)
|
|
4243
4261
|
log_outcome(
|
|
4244
4262
|
sync="ok", result="success", projection=projection,
|
|
@@ -4588,9 +4606,27 @@ def _derive_week_from_payload(payload: dict[str, Any], week_start_name: str) ->
|
|
|
4588
4606
|
return DerivedWeekWindow(week_start=start, week_end=end)
|
|
4589
4607
|
|
|
4590
4608
|
|
|
4591
|
-
|
|
4609
|
+
_USAGE_SNAPSHOT_COLUMNS = (
|
|
4610
|
+
"captured_at_utc", "week_start_date", "week_end_date", "week_start_at",
|
|
4611
|
+
"week_end_at", "weekly_percent", "page_url", "source", "payload_json",
|
|
4612
|
+
"five_hour_percent", "five_hour_resets_at", "five_hour_window_key",
|
|
4613
|
+
)
|
|
4614
|
+
|
|
4615
|
+
|
|
4616
|
+
def _usage_snapshot_columns(conn, payload, week_start_name):
|
|
4617
|
+
"""Compute the ``weekly_usage_snapshots`` column map + output ``saved`` dict
|
|
4618
|
+
for a payload — the exact canonicalization ``insert_usage_snapshot`` does —
|
|
4619
|
+
WITHOUT inserting (DB journal redesign §5.3).
|
|
4620
|
+
|
|
4621
|
+
Returns ``(cols, saved)`` where ``cols`` is the ordered column→value map
|
|
4622
|
+
(no ``id`` / ``journal_id``, key order == ``_USAGE_SNAPSHOT_COLUMNS``) and
|
|
4623
|
+
``saved`` is the output dict minus ``id``. Shared by
|
|
4624
|
+
``insert_usage_snapshot`` (bare INSERT, legacy) and the ingest obs pipeline
|
|
4625
|
+
hook (``snapshot_accept`` Model-A emit) so the two write paths never drift.
|
|
4626
|
+
``conn`` is used only for the ``_get_canonical_boundary_for_date`` override.
|
|
4627
|
+
"""
|
|
4592
4628
|
weekly_percent = _safe_float(payload.get("weeklyPercent"))
|
|
4593
|
-
captured_at,
|
|
4629
|
+
captured_at, _captured_at_dt = _coerce_payload_captured_at(payload)
|
|
4594
4630
|
|
|
4595
4631
|
page_url = payload.get("pageUrl") if isinstance(payload.get("pageUrl"), str) else None
|
|
4596
4632
|
source = payload.get("source") if isinstance(payload.get("source"), str) else "userscript"
|
|
@@ -4610,10 +4646,6 @@ def insert_usage_snapshot(payload: dict[str, Any], week_start_name: str) -> dict
|
|
|
4610
4646
|
# misbehaving caller doesn't spam the log on every insert.
|
|
4611
4647
|
global _logged_window_key_coerce_failure
|
|
4612
4648
|
if not _logged_window_key_coerce_failure:
|
|
4613
|
-
# Use the local (already extracted from payload at line
|
|
4614
|
-
# ~13858) instead of re-reading; payload is mutable and
|
|
4615
|
-
# could in principle change between extraction and the
|
|
4616
|
-
# except branch.
|
|
4617
4649
|
eprint(
|
|
4618
4650
|
f"[record-usage] fiveHourWindowKey coerce failed "
|
|
4619
4651
|
f"(got {type(five_hour_window_key).__name__}: "
|
|
@@ -4623,25 +4655,63 @@ def insert_usage_snapshot(payload: dict[str, Any], week_start_name: str) -> dict
|
|
|
4623
4655
|
_logged_window_key_coerce_failure = True
|
|
4624
4656
|
five_hour_window_key = None
|
|
4625
4657
|
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
week_window
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
week_end_at=canon_end,
|
|
4640
|
-
)
|
|
4658
|
+
week_window = _derive_week_from_payload(payload, week_start_name)
|
|
4659
|
+
|
|
4660
|
+
# Use the canonical boundary already established for this week_start_date.
|
|
4661
|
+
# This prevents relative-reset drift from creating duplicate weeks.
|
|
4662
|
+
date_str = week_window.week_start.isoformat()
|
|
4663
|
+
canon_start, canon_end = _get_canonical_boundary_for_date(conn, date_str)
|
|
4664
|
+
if canon_start and canon_end:
|
|
4665
|
+
week_window = DerivedWeekWindow(
|
|
4666
|
+
week_start=week_window.week_start,
|
|
4667
|
+
week_end=week_window.week_end,
|
|
4668
|
+
week_start_at=canon_start,
|
|
4669
|
+
week_end_at=canon_end,
|
|
4670
|
+
)
|
|
4641
4671
|
|
|
4642
|
-
|
|
4643
|
-
|
|
4672
|
+
week_start = week_window.week_start
|
|
4673
|
+
week_end = week_window.week_end
|
|
4674
|
+
|
|
4675
|
+
cols = {
|
|
4676
|
+
"captured_at_utc": captured_at,
|
|
4677
|
+
"week_start_date": week_start.isoformat(),
|
|
4678
|
+
"week_end_date": week_end.isoformat(),
|
|
4679
|
+
"week_start_at": week_window.week_start_at,
|
|
4680
|
+
"week_end_at": week_window.week_end_at,
|
|
4681
|
+
"weekly_percent": weekly_percent,
|
|
4682
|
+
"page_url": page_url,
|
|
4683
|
+
"source": source,
|
|
4684
|
+
"payload_json": json.dumps(payload, separators=(",", ":")),
|
|
4685
|
+
"five_hour_percent": five_hour_percent,
|
|
4686
|
+
"five_hour_resets_at": five_hour_resets_at,
|
|
4687
|
+
"five_hour_window_key": five_hour_window_key,
|
|
4688
|
+
}
|
|
4644
4689
|
|
|
4690
|
+
saved = {
|
|
4691
|
+
"capturedAt": captured_at,
|
|
4692
|
+
"weekStartDate": week_start.isoformat(),
|
|
4693
|
+
"weekEndDate": week_end.isoformat(),
|
|
4694
|
+
"weeklyPercent": weekly_percent,
|
|
4695
|
+
}
|
|
4696
|
+
if week_window.week_start_at:
|
|
4697
|
+
saved["weekStartAt"] = week_window.week_start_at
|
|
4698
|
+
if week_window.week_end_at:
|
|
4699
|
+
saved["weekEndAt"] = week_window.week_end_at
|
|
4700
|
+
if isinstance(payload.get("resetText"), str):
|
|
4701
|
+
saved["resetText"] = payload["resetText"]
|
|
4702
|
+
if five_hour_percent is not None:
|
|
4703
|
+
saved["fiveHourPercent"] = five_hour_percent
|
|
4704
|
+
if five_hour_resets_at is not None:
|
|
4705
|
+
saved["fiveHourResetsAt"] = five_hour_resets_at
|
|
4706
|
+
if five_hour_window_key is not None:
|
|
4707
|
+
saved["fiveHourWindowKey"] = five_hour_window_key
|
|
4708
|
+
return cols, saved
|
|
4709
|
+
|
|
4710
|
+
|
|
4711
|
+
def insert_usage_snapshot(payload: dict[str, Any], week_start_name: str) -> dict[str, Any]:
|
|
4712
|
+
conn = open_db()
|
|
4713
|
+
try:
|
|
4714
|
+
cols, saved = _usage_snapshot_columns(conn, payload, week_start_name)
|
|
4645
4715
|
cur = conn.execute(
|
|
4646
4716
|
"""
|
|
4647
4717
|
INSERT INTO weekly_usage_snapshots
|
|
@@ -4661,46 +4731,25 @@ def insert_usage_snapshot(payload: dict[str, Any], week_start_name: str) -> dict
|
|
|
4661
4731
|
)
|
|
4662
4732
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
4663
4733
|
""",
|
|
4664
|
-
(
|
|
4665
|
-
captured_at,
|
|
4666
|
-
week_start.isoformat(),
|
|
4667
|
-
week_end.isoformat(),
|
|
4668
|
-
week_window.week_start_at,
|
|
4669
|
-
week_window.week_end_at,
|
|
4670
|
-
weekly_percent,
|
|
4671
|
-
page_url,
|
|
4672
|
-
source,
|
|
4673
|
-
json.dumps(payload, separators=(",", ":")),
|
|
4674
|
-
five_hour_percent,
|
|
4675
|
-
five_hour_resets_at,
|
|
4676
|
-
five_hour_window_key,
|
|
4677
|
-
),
|
|
4734
|
+
tuple(cols[k] for k in _USAGE_SNAPSHOT_COLUMNS),
|
|
4678
4735
|
)
|
|
4679
|
-
conn.commit()
|
|
4680
4736
|
snapshot_id = int(cur.lastrowid)
|
|
4737
|
+
# DB journal redesign: every weekly_usage_snapshots row carries a
|
|
4738
|
+
# journal_id now — production writes go through the ``snapshot_accept``
|
|
4739
|
+
# evt (grep-proof: no production caller of this bare-insert helper
|
|
4740
|
+
# remains; it is test/fixture-only). Stamp a deterministic synthetic id
|
|
4741
|
+
# so a row inserted here is still reverse-referenceable by a milestone's
|
|
4742
|
+
# ``usage_snapshot_id`` at ingest harvest (a NULL journal_id would be a
|
|
4743
|
+
# harvest-order violation — the harvest cannot build a logical FK to it).
|
|
4744
|
+
conn.execute(
|
|
4745
|
+
"UPDATE weekly_usage_snapshots SET journal_id = ? WHERE id = ?",
|
|
4746
|
+
(f"sa:direct:{snapshot_id}", snapshot_id),
|
|
4747
|
+
)
|
|
4748
|
+
conn.commit()
|
|
4681
4749
|
finally:
|
|
4682
4750
|
conn.close()
|
|
4683
4751
|
|
|
4684
|
-
|
|
4685
|
-
"id": snapshot_id,
|
|
4686
|
-
"capturedAt": captured_at,
|
|
4687
|
-
"weekStartDate": week_start.isoformat(),
|
|
4688
|
-
"weekEndDate": week_end.isoformat(),
|
|
4689
|
-
"weeklyPercent": weekly_percent,
|
|
4690
|
-
}
|
|
4691
|
-
if week_window.week_start_at:
|
|
4692
|
-
out["weekStartAt"] = week_window.week_start_at
|
|
4693
|
-
if week_window.week_end_at:
|
|
4694
|
-
out["weekEndAt"] = week_window.week_end_at
|
|
4695
|
-
if isinstance(payload.get("resetText"), str):
|
|
4696
|
-
out["resetText"] = payload["resetText"]
|
|
4697
|
-
if five_hour_percent is not None:
|
|
4698
|
-
out["fiveHourPercent"] = five_hour_percent
|
|
4699
|
-
if five_hour_resets_at is not None:
|
|
4700
|
-
out["fiveHourResetsAt"] = five_hour_resets_at
|
|
4701
|
-
if five_hour_window_key is not None:
|
|
4702
|
-
out["fiveHourWindowKey"] = five_hour_window_key
|
|
4703
|
-
return out
|
|
4752
|
+
return {"id": snapshot_id, **saved}
|
|
4704
4753
|
|
|
4705
4754
|
|
|
4706
4755
|
def _saved_dict_from_usage_row(row: sqlite3.Row) -> dict[str, Any]:
|
|
@@ -4740,3 +4789,417 @@ def _saved_dict_from_usage_row(row: sqlite3.Row) -> dict[str, Any]:
|
|
|
4740
4789
|
if row["five_hour_window_key"] is not None:
|
|
4741
4790
|
out["fiveHourWindowKey"] = int(row["five_hour_window_key"])
|
|
4742
4791
|
return out
|
|
4792
|
+
|
|
4793
|
+
|
|
4794
|
+
# ==========================================================================
|
|
4795
|
+
# DB journal redesign — ingest pipeline hooks (spec §5.2 step 4b / §5.3)
|
|
4796
|
+
#
|
|
4797
|
+
# These are the Task-6 (6e) COMPOSITION of the reviewed machinery: the obs
|
|
4798
|
+
# derivation transplant (`_pipeline_claude_usage`), the record-credit op
|
|
4799
|
+
# (`_pipeline_record_credit`), and the sync-week op (`_pipeline_sync_week`).
|
|
4800
|
+
# Registered ONCE into `_cctally_journal.PIPELINE` at module wiring time (below).
|
|
4801
|
+
# The CLI/hook call sites append `make_obs`/`make_op` lines and invoke
|
|
4802
|
+
# `run_stats_ingest`; the cycle drives these hooks per record on ctx.conn with
|
|
4803
|
+
# `as_of = record["at"]` and the alert sink `ctx.pending_alerts`.
|
|
4804
|
+
# ==========================================================================
|
|
4805
|
+
|
|
4806
|
+
# Claude rate-limit obs writer identities (spec §4.2 `src`). The obs hook fires
|
|
4807
|
+
# only for these + provider==claude + a payload that carries the raw capture
|
|
4808
|
+
# (`resets_at` + `weekly_percent`); anything else (a codex quota obs, a minimal
|
|
4809
|
+
# test obs) is skipped so a foreign line never mis-drives the Claude derivation.
|
|
4810
|
+
_CLAUDE_OBS_SRCS = frozenset(
|
|
4811
|
+
{"statusline", "hook-tick", "refresh-usage", "record-usage"}
|
|
4812
|
+
)
|
|
4813
|
+
|
|
4814
|
+
|
|
4815
|
+
def _derive_5h_window_key(conn, five_hour_resets_at_epoch):
|
|
4816
|
+
"""Resolve the canonical 5h window key for a raw `five_hour_resets_at` epoch,
|
|
4817
|
+
on `conn` (extracted from cmd_record_usage's Tier 1/2/3 prior-anchor logic).
|
|
4818
|
+
|
|
4819
|
+
Tier 1: nearest canonical `five_hour_blocks` row within ±3×jitter-floor;
|
|
4820
|
+
Tier 2: latest snapshot anchor (legacy fallback); Tier 3 (implicit): the pure
|
|
4821
|
+
600s floor. Passing the prior anchor collapses boundary-straddling
|
|
4822
|
+
seconds-jitter to the first-seen key (spec 5h invariant #3). Runs at ingest
|
|
4823
|
+
(not capture) now, so the key is derived against the same DB state the fold
|
|
4824
|
+
decision + clamp read."""
|
|
4825
|
+
c = _cctally()
|
|
4826
|
+
prior_5h_epoch = None
|
|
4827
|
+
prior_5h_key = None
|
|
4828
|
+
try:
|
|
4829
|
+
prior_block_row = conn.execute(
|
|
4830
|
+
"""
|
|
4831
|
+
SELECT five_hour_window_key, five_hour_resets_at
|
|
4832
|
+
FROM five_hour_blocks
|
|
4833
|
+
WHERE abs(? - CAST(strftime('%s', five_hour_resets_at) AS INTEGER)) <= ?
|
|
4834
|
+
ORDER BY abs(? - CAST(strftime('%s', five_hour_resets_at) AS INTEGER)) ASC
|
|
4835
|
+
LIMIT 1
|
|
4836
|
+
""",
|
|
4837
|
+
(
|
|
4838
|
+
five_hour_resets_at_epoch,
|
|
4839
|
+
c._FIVE_HOUR_JITTER_FLOOR_SECONDS * 3,
|
|
4840
|
+
five_hour_resets_at_epoch,
|
|
4841
|
+
),
|
|
4842
|
+
).fetchone()
|
|
4843
|
+
if prior_block_row is not None:
|
|
4844
|
+
prior_5h_epoch = int(parse_iso_datetime(
|
|
4845
|
+
prior_block_row["five_hour_resets_at"], "prior 5h block anchor"
|
|
4846
|
+
).timestamp())
|
|
4847
|
+
prior_5h_key = int(prior_block_row["five_hour_window_key"])
|
|
4848
|
+
except (sqlite3.DatabaseError, ValueError, TypeError) as exc:
|
|
4849
|
+
eprint(f"[ingest] prior 5h block-anchor lookup failed: {exc}")
|
|
4850
|
+
|
|
4851
|
+
if prior_5h_key is None:
|
|
4852
|
+
try:
|
|
4853
|
+
prior_5h_row = conn.execute(
|
|
4854
|
+
"SELECT five_hour_resets_at, five_hour_window_key "
|
|
4855
|
+
"FROM weekly_usage_snapshots "
|
|
4856
|
+
"WHERE five_hour_resets_at IS NOT NULL "
|
|
4857
|
+
" AND five_hour_window_key IS NOT NULL "
|
|
4858
|
+
"ORDER BY captured_at_utc DESC, id DESC LIMIT 1"
|
|
4859
|
+
).fetchone()
|
|
4860
|
+
if prior_5h_row is not None:
|
|
4861
|
+
prior_5h_epoch = int(parse_iso_datetime(
|
|
4862
|
+
prior_5h_row["five_hour_resets_at"], "prior 5h anchor"
|
|
4863
|
+
).timestamp())
|
|
4864
|
+
prior_5h_key = int(prior_5h_row["five_hour_window_key"])
|
|
4865
|
+
except (sqlite3.DatabaseError, ValueError, TypeError) as exc:
|
|
4866
|
+
eprint(f"[ingest] prior 5h anchor lookup failed: {exc}")
|
|
4867
|
+
|
|
4868
|
+
return _canonical_5h_window_key(
|
|
4869
|
+
five_hour_resets_at_epoch,
|
|
4870
|
+
prior_epoch=prior_5h_epoch,
|
|
4871
|
+
prior_key=prior_5h_key,
|
|
4872
|
+
)
|
|
4873
|
+
|
|
4874
|
+
|
|
4875
|
+
def _run_dollar_axes(saved, *, conn, as_of, alert_sink):
|
|
4876
|
+
"""The four dollar-decoupled alert axes in cmd_record_usage's legacy order
|
|
4877
|
+
(budget → project-budget → codex-budget → projected). Runs on BOTH the accept
|
|
4878
|
+
path AND every dedup-skip tick (spec §4.5: USD spend can cross a $ threshold
|
|
4879
|
+
while the weekly/5h percent is flat). Passed-conn → the chokepoints fold into
|
|
4880
|
+
the cycle txn and re-raise on failure (invariant ii); their crossings' alert
|
|
4881
|
+
payloads land in `alert_sink` for post-commit dispatch."""
|
|
4882
|
+
c = _cctally()
|
|
4883
|
+
c.maybe_record_budget_milestone(
|
|
4884
|
+
saved, conn=conn, as_of=as_of, alert_sink=alert_sink)
|
|
4885
|
+
c.maybe_record_project_budget_milestone(
|
|
4886
|
+
saved, conn=conn, as_of=as_of, alert_sink=alert_sink)
|
|
4887
|
+
c.maybe_record_codex_budget_milestone(
|
|
4888
|
+
saved, conn=conn, as_of=as_of, alert_sink=alert_sink)
|
|
4889
|
+
c.maybe_record_projected_alert(
|
|
4890
|
+
saved, conn=conn, as_of=as_of, alert_sink=alert_sink)
|
|
4891
|
+
|
|
4892
|
+
|
|
4893
|
+
def _write_hwm_files(week_start_date, weekly_percent,
|
|
4894
|
+
five_hour_window_key, five_hour_percent):
|
|
4895
|
+
"""Write the hwm-7d / hwm-5h projection files (statusline no-regression), the
|
|
4896
|
+
monotonic-guarded tail of cmd_record_usage's accept path. Projection files
|
|
4897
|
+
(never journaled); re-materialized on rebuild. Best-effort (OSError-swallow),
|
|
4898
|
+
matching the legacy write sites."""
|
|
4899
|
+
try:
|
|
4900
|
+
hwm_path = _cctally_core.APP_DIR / "hwm-7d"
|
|
4901
|
+
existing_hwm = 0.0
|
|
4902
|
+
try:
|
|
4903
|
+
parts = hwm_path.read_text().strip().split()
|
|
4904
|
+
if len(parts) == 2 and parts[0] == week_start_date:
|
|
4905
|
+
existing_hwm = float(parts[1])
|
|
4906
|
+
except (FileNotFoundError, ValueError, OSError):
|
|
4907
|
+
pass
|
|
4908
|
+
if hwm_file_next(existing_hwm, weekly_percent) is not None:
|
|
4909
|
+
hwm_path.write_text(f"{week_start_date} {weekly_percent}\n")
|
|
4910
|
+
except OSError:
|
|
4911
|
+
pass
|
|
4912
|
+
|
|
4913
|
+
if five_hour_percent is not None and five_hour_window_key is not None:
|
|
4914
|
+
try:
|
|
4915
|
+
hwm5_path = _cctally_core.APP_DIR / "hwm-5h"
|
|
4916
|
+
existing_hwm5 = 0.0
|
|
4917
|
+
try:
|
|
4918
|
+
parts5 = hwm5_path.read_text().strip().split()
|
|
4919
|
+
if len(parts5) == 2 and parts5[0] == str(five_hour_window_key):
|
|
4920
|
+
existing_hwm5 = float(parts5[1])
|
|
4921
|
+
except (FileNotFoundError, ValueError, OSError):
|
|
4922
|
+
pass
|
|
4923
|
+
if hwm_file_next(existing_hwm5, five_hour_percent) is not None:
|
|
4924
|
+
hwm5_path.write_text(
|
|
4925
|
+
f"{five_hour_window_key} {five_hour_percent}\n")
|
|
4926
|
+
except OSError:
|
|
4927
|
+
pass
|
|
4928
|
+
|
|
4929
|
+
|
|
4930
|
+
def _pipeline_claude_usage(ctx, rec):
|
|
4931
|
+
"""Obs-derivation pipeline hook (spec §5.2 step 4b) — the ingest-cycle
|
|
4932
|
+
transplant of ``cmd_record_usage``'s derivation body.
|
|
4933
|
+
|
|
4934
|
+
Fires per Claude rate-limit obs (src in the writer set, provider claude, a
|
|
4935
|
+
raw-capture payload). Canonicalizes the RAW capture on ``ctx.conn`` (week
|
|
4936
|
+
boundaries from ``resets_at`` + the Tier 1/2/3 5h window key), runs
|
|
4937
|
+
``detect_reset_and_credit`` (transaction-neutral, capture-time, Design B
|
|
4938
|
+
``ctx``), then makes the snapshot accept/skip decision ONCE via
|
|
4939
|
+
``_usage_snapshot_fold_decision``. On ACCEPT it journals the row through a
|
|
4940
|
+
``snapshot_accept`` Model-A evt (the sole ``weekly_usage_snapshots`` writer).
|
|
4941
|
+
The fold decision gates ONLY the snapshot insert — the derivation chokepoints
|
|
4942
|
+
run for EVERY line (spec §4.5, "dedup must not gate side effects"): milestone
|
|
4943
|
+
(with the cost-sync ``journal`` threaded), 5h block, then the four dollar
|
|
4944
|
+
axes, in legacy order. On ACCEPT they derive against the fresh row + the hwm
|
|
4945
|
+
projection files are written; on a dedup SKIP they re-run (idempotently)
|
|
4946
|
+
against the latest snapshot, which is what subsumes today's kill-window
|
|
4947
|
+
self-heal probes (a flat tick that owes a milestone or crosses a $ threshold
|
|
4948
|
+
still derives it)."""
|
|
4949
|
+
if rec.get("t") != "obs" or rec.get("provider") != "claude":
|
|
4950
|
+
return
|
|
4951
|
+
if rec.get("src") not in _CLAUDE_OBS_SRCS:
|
|
4952
|
+
return
|
|
4953
|
+
payload = rec.get("payload") or {}
|
|
4954
|
+
if "resets_at" not in payload or "weekly_percent" not in payload:
|
|
4955
|
+
return
|
|
4956
|
+
|
|
4957
|
+
c = _cctally()
|
|
4958
|
+
conn = ctx.conn
|
|
4959
|
+
# `as_of` is the record's `at` (= `_command_as_of()` at capture, CCTALLY_AS_OF
|
|
4960
|
+
# in tests) — DETECTION timing (reset/credit) AND the dollar-axis budget/period
|
|
4961
|
+
# WINDOW clock (6g FIX 2b moved the four dollar axes off `capture_at` onto
|
|
4962
|
+
# `as_of`; see `_run_dollar_axes` below), matching legacy. `capture_at` is the
|
|
4963
|
+
# CAPTURE wall clock — the snapshot `captured_at_utc` + the milestone/5h-block
|
|
4964
|
+
# stamps + the block cost-sum range end, also matching legacy (which used
|
|
4965
|
+
# `now_utc_iso()` for those). In production the
|
|
4966
|
+
# two are the same instant; the split only shows under the CCTALLY_AS_OF test
|
|
4967
|
+
# hook. Falls back to `as_of` for a payload without `captured_at` (robustness).
|
|
4968
|
+
as_of = ctx.as_of_for(rec)
|
|
4969
|
+
capture_at = payload.get("captured_at") or as_of
|
|
4970
|
+
weekly_percent = float(payload["weekly_percent"])
|
|
4971
|
+
resets_at = int(payload["resets_at"])
|
|
4972
|
+
source = payload.get("source", "statusline")
|
|
4973
|
+
|
|
4974
|
+
# Week boundaries from resets_at (cmd_record_usage canonicalization).
|
|
4975
|
+
week_end_at_dt = dt.datetime.fromtimestamp(resets_at, tz=dt.timezone.utc)
|
|
4976
|
+
week_start_at_dt = week_end_at_dt - dt.timedelta(days=7)
|
|
4977
|
+
week_start_date = week_start_at_dt.date().isoformat()
|
|
4978
|
+
week_end_date = week_end_at_dt.date().isoformat()
|
|
4979
|
+
week_start_at = week_start_at_dt.isoformat(timespec="seconds")
|
|
4980
|
+
week_end_at = week_end_at_dt.isoformat(timespec="seconds")
|
|
4981
|
+
|
|
4982
|
+
# 5h fields (raw, post-ingress-drop) + canonical window key on ctx.conn.
|
|
4983
|
+
five_hour_percent = payload.get("five_hour_percent")
|
|
4984
|
+
if five_hour_percent is not None:
|
|
4985
|
+
five_hour_percent = float(five_hour_percent)
|
|
4986
|
+
five_hour_resets_at_str = payload.get("five_hour_resets_at")
|
|
4987
|
+
five_hour_window_key = None
|
|
4988
|
+
if five_hour_resets_at_str is not None:
|
|
4989
|
+
try:
|
|
4990
|
+
fh_epoch = int(parse_iso_datetime(
|
|
4991
|
+
five_hour_resets_at_str, "obs.five_hour_resets_at").timestamp())
|
|
4992
|
+
five_hour_window_key = _derive_5h_window_key(conn, fh_epoch)
|
|
4993
|
+
except (ValueError, TypeError) as exc:
|
|
4994
|
+
eprint(f"[ingest] 5h window-key derivation failed: {exc}")
|
|
4995
|
+
|
|
4996
|
+
# 1. Reset/credit detection (fold into the cycle txn; Design B suppression).
|
|
4997
|
+
c.detect_reset_and_credit(
|
|
4998
|
+
conn,
|
|
4999
|
+
week_start_date=week_start_date,
|
|
5000
|
+
week_end_at=week_end_at,
|
|
5001
|
+
weekly_percent=weekly_percent,
|
|
5002
|
+
five_hour_window_key=five_hour_window_key,
|
|
5003
|
+
five_hour_percent=five_hour_percent,
|
|
5004
|
+
as_of=as_of,
|
|
5005
|
+
commit=False,
|
|
5006
|
+
ctx=ctx,
|
|
5007
|
+
)
|
|
5008
|
+
|
|
5009
|
+
# 2. Accept/skip DECISION (clamp + dedup), made ONCE and journaled via the
|
|
5010
|
+
# snapshot_accept evt (so replay never re-derives it — spec §5.3).
|
|
5011
|
+
import _cctally_journal as jr
|
|
5012
|
+
skip, adjusted_5h = jr._usage_snapshot_fold_decision(conn, {
|
|
5013
|
+
"week_start_date": week_start_date,
|
|
5014
|
+
"week_start_at": week_start_at,
|
|
5015
|
+
"week_end_at": week_end_at,
|
|
5016
|
+
"weekly_percent": weekly_percent,
|
|
5017
|
+
"five_hour_percent": five_hour_percent,
|
|
5018
|
+
"five_hour_window_key": five_hour_window_key,
|
|
5019
|
+
})
|
|
5020
|
+
five_hour_percent = adjusted_5h
|
|
5021
|
+
|
|
5022
|
+
# 3. Resolve the derivation target `saved`. The fold DECISION gates ONLY the
|
|
5023
|
+
# snapshot INSERT (snapshot_accept); the derivations below run for EVERY
|
|
5024
|
+
# line (spec §4.5: "the ingester's snapshot-insert dedup is the same rule
|
|
5025
|
+
# as today's ... while derivations run for every line — preserving the
|
|
5026
|
+
# 'dedup must not gate side effects' invariant structurally"). A dedup-skip
|
|
5027
|
+
# tick re-runs the (idempotent) chokepoints against the latest snapshot —
|
|
5028
|
+
# which is exactly what subsumes today's kill-window self-heal probes.
|
|
5029
|
+
# On ACCEPT `saved` is the freshly-journaled row; on SKIP it is the latest.
|
|
5030
|
+
if not skip:
|
|
5031
|
+
out_payload = {
|
|
5032
|
+
"source": source,
|
|
5033
|
+
"capturedAt": capture_at,
|
|
5034
|
+
"weeklyPercent": weekly_percent,
|
|
5035
|
+
"weekStartDate": week_start_date,
|
|
5036
|
+
"weekEndDate": week_end_date,
|
|
5037
|
+
"weekStartAt": week_start_at,
|
|
5038
|
+
"weekEndAt": week_end_at,
|
|
5039
|
+
}
|
|
5040
|
+
if five_hour_percent is not None:
|
|
5041
|
+
out_payload["fiveHourPercent"] = five_hour_percent
|
|
5042
|
+
if five_hour_resets_at_str is not None:
|
|
5043
|
+
out_payload["fiveHourResetsAt"] = five_hour_resets_at_str
|
|
5044
|
+
if five_hour_window_key is not None:
|
|
5045
|
+
out_payload["fiveHourWindowKey"] = five_hour_window_key
|
|
5046
|
+
|
|
5047
|
+
week_start_name = get_week_start_name(ctx.config or {}, None)
|
|
5048
|
+
cols, saved = _usage_snapshot_columns(conn, out_payload, week_start_name)
|
|
5049
|
+
rowid = jr.emit_model_a(
|
|
5050
|
+
ctx,
|
|
5051
|
+
kind="snapshot_accept",
|
|
5052
|
+
evt_id=f"sa:{rec['id']}",
|
|
5053
|
+
table="weekly_usage_snapshots",
|
|
5054
|
+
columns=cols,
|
|
5055
|
+
at=capture_at,
|
|
5056
|
+
)
|
|
5057
|
+
saved["id"] = rowid
|
|
5058
|
+
else:
|
|
5059
|
+
latest = conn.execute(
|
|
5060
|
+
"SELECT * FROM weekly_usage_snapshots WHERE week_start_date = ? "
|
|
5061
|
+
"ORDER BY captured_at_utc DESC, id DESC LIMIT 1",
|
|
5062
|
+
(week_start_date,),
|
|
5063
|
+
).fetchone()
|
|
5064
|
+
if latest is None:
|
|
5065
|
+
return # nothing recorded yet -> nothing to derive against
|
|
5066
|
+
saved = _saved_dict_from_usage_row(latest)
|
|
5067
|
+
|
|
5068
|
+
# 4. Derivations run for EVERY line, legacy order (milestone → 5h block → $
|
|
5069
|
+
# axes). maybe_record_milestone threads journal=(ctx, rec id) for its cost
|
|
5070
|
+
# sync; maybe_update_five_hour_block writes the block before its
|
|
5071
|
+
# 5h-milestone block_id read (P2-8). Idempotent under re-run on a dedup
|
|
5072
|
+
# tick (INSERT OR IGNORE / upsert), so a flat tick that owes a milestone or
|
|
5073
|
+
# crosses a $ threshold still derives it.
|
|
5074
|
+
c.maybe_record_milestone(
|
|
5075
|
+
saved, conn=conn, as_of=capture_at, alert_sink=ctx.pending_alerts,
|
|
5076
|
+
journal=(ctx, rec["id"]))
|
|
5077
|
+
c.maybe_update_five_hour_block(
|
|
5078
|
+
saved, conn=conn, as_of=capture_at, alert_sink=ctx.pending_alerts)
|
|
5079
|
+
# The dollar-decoupled axes resolve a CURRENT budget/period WINDOW from
|
|
5080
|
+
# "now" and must see the record's DETECTION clock (`as_of` = rec["at"] =
|
|
5081
|
+
# `_command_as_of()`), NOT the wall-clock capture stamp: legacy
|
|
5082
|
+
# cmd_record_usage's dedup self-heal called these with as_of=None ->
|
|
5083
|
+
# `_command_as_of()`, so a CCTALLY_AS_OF-pinned tick resolved the pinned
|
|
5084
|
+
# week's window (capture_at is the raw wall clock, which lands outside a
|
|
5085
|
+
# pinned fixture window and silently drops the crossing). Both collapse to
|
|
5086
|
+
# real-now in production; the split only matters under a pinned as_of.
|
|
5087
|
+
_run_dollar_axes(saved, conn=conn, as_of=as_of,
|
|
5088
|
+
alert_sink=ctx.pending_alerts)
|
|
5089
|
+
|
|
5090
|
+
# 4'. Window-rollover 5h-block heal (SKIP path only). A dedup skip swallows
|
|
5091
|
+
# the snapshot insert, so `saved` (the dedup target — the LATEST stored
|
|
5092
|
+
# row) still carries the PREVIOUS 5h window; the derivations above only
|
|
5093
|
+
# ever touched that old (still-fresh) window. When the INCOMING record
|
|
5094
|
+
# observed a NEW canonical `five_hour_window_key` whose `five_hour_blocks`
|
|
5095
|
+
# anchor doesn't exist yet, materialize it BLOCK-ONLY — no snapshot
|
|
5096
|
+
# insert, the tick stays deduped — against a saved dict carrying the
|
|
5097
|
+
# INCOMING record's 5h identity, NOT `saved`'s. Without this the active
|
|
5098
|
+
# window is left unanchored (blocks/dashboard fall back to the heuristic
|
|
5099
|
+
# "~") until the percent next moves. Ported from the legacy
|
|
5100
|
+
# cmd_record_usage dedup self-heal (spec §4.5 "dedup must not gate side
|
|
5101
|
+
# effects"; regression: bin/cctally-record-usage-selfheal-test
|
|
5102
|
+
# window-rollover scenario).
|
|
5103
|
+
if (skip and five_hour_window_key is not None
|
|
5104
|
+
and five_hour_percent is not None
|
|
5105
|
+
and five_hour_resets_at_str is not None):
|
|
5106
|
+
latest_wk = saved.get("fiveHourWindowKey")
|
|
5107
|
+
if latest_wk is None or int(latest_wk) != int(five_hour_window_key):
|
|
5108
|
+
if conn.execute(
|
|
5109
|
+
"SELECT 1 FROM five_hour_blocks WHERE five_hour_window_key = ? "
|
|
5110
|
+
"LIMIT 1", (int(five_hour_window_key),),
|
|
5111
|
+
).fetchone() is None:
|
|
5112
|
+
c.maybe_update_five_hour_block(
|
|
5113
|
+
{
|
|
5114
|
+
"id": saved.get("id"),
|
|
5115
|
+
"capturedAt": capture_at,
|
|
5116
|
+
"weeklyPercent": weekly_percent,
|
|
5117
|
+
"fiveHourPercent": five_hour_percent,
|
|
5118
|
+
"fiveHourResetsAt": five_hour_resets_at_str,
|
|
5119
|
+
"fiveHourWindowKey": int(five_hour_window_key),
|
|
5120
|
+
},
|
|
5121
|
+
conn=conn, as_of=capture_at, alert_sink=ctx.pending_alerts)
|
|
5122
|
+
|
|
5123
|
+
# 5. hwm-7d / hwm-5h projection files — ACCEPT path only (the monotonic
|
|
5124
|
+
# writer; a dedup tick's percent is already <= the stored HWM). In-place
|
|
5125
|
+
# credit force-writes live in detect_reset_and_credit.
|
|
5126
|
+
if not skip:
|
|
5127
|
+
_write_hwm_files(week_start_date, weekly_percent,
|
|
5128
|
+
five_hour_window_key, five_hour_percent)
|
|
5129
|
+
|
|
5130
|
+
|
|
5131
|
+
def _pipeline_record_credit(ctx, rec):
|
|
5132
|
+
"""Op-derivation hook for a ``record-credit`` op line (spec §5.3 event+effects).
|
|
5133
|
+
|
|
5134
|
+
The built-in ``_pipeline_op_fold`` runs FIRST and writes the
|
|
5135
|
+
``weekly_credit_floors`` row from this op's floor columns (Option (i): the
|
|
5136
|
+
op fold is the SOLE floor writer on the ingest path, stamping
|
|
5137
|
+
``journal_id = rec['id']``). This hook then reconstructs the ``CreditPlan``
|
|
5138
|
+
from the op payload and applies the same-window credit's DESTRUCTIVE effects
|
|
5139
|
+
via ``_apply_credit(ctx=..., id_base=rec['id'])`` — which emits the
|
|
5140
|
+
``weekly_credit_effects`` evt (suppression list + forced hwm floor) and the
|
|
5141
|
+
synthetic post-credit ``snapshot_accept`` evt, and (Option (i)) SKIPS its own
|
|
5142
|
+
floor INSERT.
|
|
5143
|
+
|
|
5144
|
+
``payload['forced']`` (a ``--force`` re-record) threads into ``_apply_credit``
|
|
5145
|
+
so the wce evt ALSO journals the destructive clear of this week's OLD
|
|
5146
|
+
synthetic snapshots + OLD credit-floor rows — the ingest-path replacement for
|
|
5147
|
+
legacy ``_force_clear_credit``'s inline DELETEs (spec §5.3)."""
|
|
5148
|
+
if rec.get("t") != "op":
|
|
5149
|
+
return
|
|
5150
|
+
payload = rec.get("payload") or {}
|
|
5151
|
+
if payload.get("kind") != "weekly_credit_floor":
|
|
5152
|
+
return
|
|
5153
|
+
plan_data = payload.get("plan")
|
|
5154
|
+
if not plan_data:
|
|
5155
|
+
return
|
|
5156
|
+
c = _cctally()
|
|
5157
|
+
plan = argparse.Namespace(**plan_data)
|
|
5158
|
+
five_hour = tuple(payload.get("five_hour") or (None, None, None))
|
|
5159
|
+
c._apply_credit(
|
|
5160
|
+
ctx.conn, plan, five_hour=five_hour, as_of=rec["at"],
|
|
5161
|
+
commit=False, ctx=ctx, id_base=rec["id"],
|
|
5162
|
+
forced=bool(payload.get("forced")))
|
|
5163
|
+
|
|
5164
|
+
|
|
5165
|
+
def _pipeline_sync_week(ctx, rec):
|
|
5166
|
+
"""Op-derivation hook for a ``sync_week`` op line (spec §5.3 / Appendix A):
|
|
5167
|
+
compute the week cost on ``ctx.conn`` and journal the ``weekly_cost_snapshots``
|
|
5168
|
+
row via a Model-A ``weekly_cost_snapshot`` evt (``journal=(ctx, rec['id'])``),
|
|
5169
|
+
so the authoritative CLI caller reads the row back for output and replay reads
|
|
5170
|
+
the cost verbatim (never recomputing from pruned provider JSONL)."""
|
|
5171
|
+
if rec.get("t") != "op":
|
|
5172
|
+
return
|
|
5173
|
+
payload = rec.get("payload") or {}
|
|
5174
|
+
if payload.get("kind") != "sync_week":
|
|
5175
|
+
return
|
|
5176
|
+
c = _cctally()
|
|
5177
|
+
args = argparse.Namespace(
|
|
5178
|
+
week_start=payload.get("week_start"),
|
|
5179
|
+
week_end=payload.get("week_end"),
|
|
5180
|
+
week_start_name=payload.get("week_start_name"),
|
|
5181
|
+
mode=payload.get("mode", "auto"),
|
|
5182
|
+
offline=payload.get("offline", False),
|
|
5183
|
+
project=payload.get("project"),
|
|
5184
|
+
json=False,
|
|
5185
|
+
quiet=True,
|
|
5186
|
+
)
|
|
5187
|
+
c.cmd_sync_week(args, conn=ctx.conn, as_of=rec["at"],
|
|
5188
|
+
journal=(ctx, rec["id"]))
|
|
5189
|
+
|
|
5190
|
+
|
|
5191
|
+
def _register_ingest_pipeline_hooks():
|
|
5192
|
+
"""Register the 6e obs/op derivation hooks into ``_cctally_journal.PIPELINE``
|
|
5193
|
+
exactly once (spec §5.2 — ops fold first via the built-in
|
|
5194
|
+
``_pipeline_op_fold``, then these). Idempotent: ``load_script()`` drops +
|
|
5195
|
+
reloads both siblings together (fresh ``PIPELINE`` == ``[_pipeline_op_fold]``),
|
|
5196
|
+
and the ``not in`` guard makes a bare re-import of this module without a
|
|
5197
|
+
journal reset a no-op rather than a double-registration."""
|
|
5198
|
+
import _cctally_journal as jr
|
|
5199
|
+
for hook in (_pipeline_claude_usage, _pipeline_record_credit,
|
|
5200
|
+
_pipeline_sync_week):
|
|
5201
|
+
if hook not in jr.PIPELINE:
|
|
5202
|
+
jr.PIPELINE.append(hook)
|
|
5203
|
+
|
|
5204
|
+
|
|
5205
|
+
_register_ingest_pipeline_hooks()
|