cctally 1.79.1 → 1.80.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
- conn = open_db()
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
- cmd_sync_week(sync_ns)
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
- conn.commit()
789
- # Dispatch deferred to AFTER commit (matches 5h path). Per-payload
790
- # exception logged so a bad-payload alert can't suppress healthy ones.
791
- # Production caller ignores _dispatch_alert_notification's return
792
- # value (spec §6.4).
793
- for payload in pending_alerts:
794
- try:
795
- _dispatch_alert_notification(payload, mode="real")
796
- except Exception as dispatch_exc:
797
- eprint(f"[alerts] dispatch failed: {dispatch_exc}")
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
- conn.close()
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 = _command_as_of()
897
+ now_utc = _as_of_or_command(as_of)
831
898
  pending_alerts: list[dict[str, Any]] = []
832
- conn = open_db()
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.commit()
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
- conn.close()
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
- for payload in pending_alerts:
891
- try:
892
- _dispatch_alert_notification(payload, mode="real")
893
- except Exception as dispatch_exc:
894
- eprint(f"[budget-alerts:{vendor}] dispatch failed: {dispatch_exc}")
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(saved: dict[str, Any]) -> None:
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(saved: dict[str, Any]) -> None:
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 = _command_as_of()
1090
+ now_utc = _as_of_or_command(as_of)
997
1091
  pending_alerts: list[dict[str, Any]] = []
998
- conn = open_db()
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.commit()
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
- conn.close()
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
- for payload in pending_alerts:
1117
- try:
1118
- _dispatch_alert_notification(payload, mode="real")
1119
- except Exception as dispatch_exc:
1120
- eprint(f"[project-budget-alerts] dispatch failed: {dispatch_exc}")
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 = _command_as_of()
1469
+ now_utc = _as_of_or_command(as_of)
1357
1470
  pending: list[dict[str, Any]] = []
1358
- conn = open_db()
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.commit()
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
- conn.close()
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
- for p in fired:
1522
- try:
1523
- payload = _build_alert_payload_projected(
1524
- metric=p["metric"],
1525
- threshold=p["threshold"],
1526
- projected_value=p["projected_value"],
1527
- denominator=p["denominator"],
1528
- week_start_at=p["week_start_at"],
1529
- )
1530
- _dispatch_alert_notification(payload, mode="real")
1531
- except Exception as dispatch_exc:
1532
- eprint(f"[projected-alert] dispatch failed: {dispatch_exc}")
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(saved: dict[str, Any]) -> None:
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
- conn = open_db()
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
- conn.execute("BEGIN IMMEDIATE")
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
- conn.commit()
2342
+ if own_conn:
2343
+ conn.commit()
2160
2344
  except Exception:
2161
- conn.rollback()
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
- for payload in pending_alerts:
2174
- try:
2175
- _dispatch_alert_notification(
2176
- payload, mode="real", tz=display_tz_for_alerts
2177
- )
2178
- except Exception as dispatch_exc:
2179
- eprint(f"[alerts] dispatch failed: {dispatch_exc}")
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
- conn.close()
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
- conn.commit()
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
- conn.commit()
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
- """Insert the post-credit snapshot at plan.to_pct, tagged source='record-credit'."""
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
- "(captured_at_utc, week_start_date, week_end_date, week_start_at, "
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
- conn.commit()
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 `_force_clear_credit` or on
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
- conn.execute(
2420
- "INSERT OR IGNORE INTO weekly_credit_floors "
2421
- "(week_start_date, effective_at_utc, observed_pre_credit_pct, applied_at_utc) "
2422
- "VALUES (?, ?, ?, ?)",
2423
- (plan.week_start_date, effective_iso, pre_credit, now_utc_iso()),
2424
- )
2425
- conn.commit()
2426
-
2427
- # 4b. Force-write hwm-7d so the external statusline render reflects the
2428
- # post-credit value (the normal write-site monotonic guard would refuse to
2429
- # decrease the file).
2430
- try:
2431
- (_cctally_core.APP_DIR / "hwm-7d").write_text(
2432
- f"{plan.week_start_date} {plan.to_pct}\n"
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
- except OSError:
2435
- pass
3182
+ if commit:
3183
+ conn.commit()
2436
3184
 
2437
- # 4c. Stale-replay DELETE: drop pre-credit-valued replays that land at/after
2438
- # the floor (the gotcha_statusline_replay_race_after_credit defense; same
2439
- # 1.0pp band as the auto path). unixepoch() on both sides for offset safety.
2440
- try:
2441
- conn.execute(
2442
- "DELETE FROM weekly_usage_snapshots "
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
- (plan.week_start_date, effective_iso, pre_credit),
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
- conn.commit()
2449
- except sqlite3.DatabaseError as exc:
2450
- eprint(f"[record-credit] post-credit cleanup failed: {exc}")
2451
-
2452
- # 4d. INSERT the synthetic post-credit snapshot at plan.to_pct.
2453
- c._insert_credit_snapshot(conn, plan, five_hour=five_hour)
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 _force_clear_credit
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
- # Existing-floor handling remains exactly scoped to this week, but
2810
- # the weekly tombstone is already fail-closed immediately before
2811
- # either mutation kernel runs.
2812
- forced = False
2813
- if existing is not None and is_force:
2814
- _force_clear_credit(conn, plan.week_start_date)
2815
- forced = True
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
- _apply_credit(conn, plan, five_hour=five_hour)
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(args: argparse.Namespace) -> int:
2866
- """Record usage data from Claude Code status line rate_limits."""
2867
- c = _cctally()
2868
- config = load_config()
2869
- week_start_name = get_week_start_name(config, getattr(args, "week_start_name", None))
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
- # Derive week boundaries from resets_at (exact UTC epoch)
2943
- week_end_at_dt = dt.datetime.fromtimestamp(resets_at, tz=dt.timezone.utc)
2944
- week_start_at_dt = week_end_at_dt - dt.timedelta(days=7)
2945
- week_start_date = week_start_at_dt.date().isoformat()
2946
- week_end_date = week_end_at_dt.date().isoformat()
2947
- week_start_at = week_start_at_dt.isoformat(timespec="seconds")
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
- "capturedAt": now_utc_iso(),
3751
- "weeklyPercent": weekly_percent,
3752
- "weekStartDate": week_start_date,
3753
- "weekEndDate": week_end_date,
3754
- "weekStartAt": week_start_at,
3755
- "weekEndAt": week_end_at,
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
- payload["fiveHourPercent"] = five_hour_percent
3854
+ raw["five_hour_percent"] = five_hour_percent
3759
3855
  if five_hour_resets_at_str is not None:
3760
- payload["fiveHourResetsAt"] = five_hour_resets_at_str
3761
- if five_hour_window_key is not None:
3762
- payload["fiveHourWindowKey"] = five_hour_window_key
3763
-
3764
- saved = insert_usage_snapshot(payload, week_start_name)
3765
- try:
3766
- maybe_record_milestone(saved)
3767
- except Exception as exc:
3768
- eprint(f"[milestone] unexpected error: {exc}")
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
- budget_alerts = c.maybe_record_codex_budget_milestone(
4240
- {}, raise_errors=True,
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
- def insert_usage_snapshot(payload: dict[str, Any], week_start_name: str) -> dict[str, Any]:
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, captured_at_dt = _coerce_payload_captured_at(payload)
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
- conn = open_db()
4627
- try:
4628
- week_window = _derive_week_from_payload(payload, week_start_name)
4629
-
4630
- # Use the canonical boundary already established for this week_start_date.
4631
- # This prevents relative-reset drift from creating duplicate weeks.
4632
- date_str = week_window.week_start.isoformat()
4633
- canon_start, canon_end = _get_canonical_boundary_for_date(conn, date_str)
4634
- if canon_start and canon_end:
4635
- week_window = DerivedWeekWindow(
4636
- week_start=week_window.week_start,
4637
- week_end=week_window.week_end,
4638
- week_start_at=canon_start,
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
- week_start = week_window.week_start
4643
- week_end = week_window.week_end
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
- out = {
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()