cctally 1.7.2 → 1.7.4

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.
@@ -156,25 +156,40 @@ def _cctally():
156
156
  return sys.modules["cctally"]
157
157
 
158
158
 
159
+ # === Honest imports from extracted homes ===================================
160
+ # Spec 2026-05-17-cctally-core-kernel-extraction.md §3.3.
161
+ from _cctally_core import (
162
+ eprint,
163
+ now_utc_iso,
164
+ parse_iso_datetime,
165
+ open_db,
166
+ get_week_start_name,
167
+ compute_week_bounds,
168
+ parse_date_str,
169
+ _canonicalize_optional_iso,
170
+ make_week_ref,
171
+ _get_alerts_config,
172
+ _AlertsConfigError,
173
+ )
174
+ from _lib_five_hour import _canonical_5h_window_key
175
+ from _lib_pricing import _calculate_entry_cost
176
+
177
+
159
178
  # Module-level back-ref shims. Each shim resolves
160
179
  # ``sys.modules['cctally'].X`` at CALL TIME (not bind time), so
161
180
  # monkeypatches on cctally's namespace propagate into the moved code
162
- # unchanged. Mirrors the precedent established in
163
- # ``bin/_cctally_cache.py`` and ``bin/_cctally_db.py``.
164
- def eprint(*args, **kwargs):
165
- return sys.modules["cctally"].eprint(*args, **kwargs)
166
-
167
-
168
- def now_utc_iso(*args, **kwargs):
169
- return sys.modules["cctally"].now_utc_iso(*args, **kwargs)
170
-
171
-
172
- def parse_iso_datetime(*args, **kwargs):
173
- return sys.modules["cctally"].parse_iso_datetime(*args, **kwargs)
181
+ # unchanged. `load_config` and `get_claude_session_entries` STAY as
182
+ # shims even though their natural homes are decentralized
183
+ # (_cctally_config / _cctally_cache) — tests monkeypatch them via
184
+ # `ns["X"]` (21 sites total, audited 2026-05-17); direct imports would
185
+ # silently bypass the patches.
186
+ # See spec §3.5 (carve-out) and §3.7 (stays-on-shim allowlist).
187
+ def load_config(*args, **kwargs):
188
+ return sys.modules["cctally"].load_config(*args, **kwargs)
174
189
 
175
190
 
176
- def open_db(*args, **kwargs):
177
- return sys.modules["cctally"].open_db(*args, **kwargs)
191
+ def get_claude_session_entries(*args, **kwargs):
192
+ return sys.modules["cctally"].get_claude_session_entries(*args, **kwargs)
178
193
 
179
194
 
180
195
  def open_cache_db(*args, **kwargs):
@@ -185,30 +200,6 @@ def sync_cache(*args, **kwargs):
185
200
  return sys.modules["cctally"].sync_cache(*args, **kwargs)
186
201
 
187
202
 
188
- def load_config(*args, **kwargs):
189
- return sys.modules["cctally"].load_config(*args, **kwargs)
190
-
191
-
192
- def get_week_start_name(*args, **kwargs):
193
- return sys.modules["cctally"].get_week_start_name(*args, **kwargs)
194
-
195
-
196
- def compute_week_bounds(*args, **kwargs):
197
- return sys.modules["cctally"].compute_week_bounds(*args, **kwargs)
198
-
199
-
200
- def parse_date_str(*args, **kwargs):
201
- return sys.modules["cctally"].parse_date_str(*args, **kwargs)
202
-
203
-
204
- def _canonicalize_optional_iso(*args, **kwargs):
205
- return sys.modules["cctally"]._canonicalize_optional_iso(*args, **kwargs)
206
-
207
-
208
- def _canonical_5h_window_key(*args, **kwargs):
209
- return sys.modules["cctally"]._canonical_5h_window_key(*args, **kwargs)
210
-
211
-
212
203
  def _floor_to_hour(*args, **kwargs):
213
204
  return sys.modules["cctally"]._floor_to_hour(*args, **kwargs)
214
205
 
@@ -245,22 +236,10 @@ def insert_percent_milestone(*args, **kwargs):
245
236
  return sys.modules["cctally"].insert_percent_milestone(*args, **kwargs)
246
237
 
247
238
 
248
- def make_week_ref(*args, **kwargs):
249
- return sys.modules["cctally"].make_week_ref(*args, **kwargs)
250
-
251
-
252
239
  def cmd_sync_week(*args, **kwargs):
253
240
  return sys.modules["cctally"].cmd_sync_week(*args, **kwargs)
254
241
 
255
242
 
256
- def _calculate_entry_cost(*args, **kwargs):
257
- return sys.modules["cctally"]._calculate_entry_cost(*args, **kwargs)
258
-
259
-
260
- def get_claude_session_entries(*args, **kwargs):
261
- return sys.modules["cctally"].get_claude_session_entries(*args, **kwargs)
262
-
263
-
264
243
  def _resolve_primary_model_for_block(*args, **kwargs):
265
244
  return sys.modules["cctally"]._resolve_primary_model_for_block(*args, **kwargs)
266
245
 
@@ -281,10 +260,6 @@ def _dispatch_alert_notification(*args, **kwargs):
281
260
  return sys.modules["cctally"]._dispatch_alert_notification(*args, **kwargs)
282
261
 
283
262
 
284
- def _get_alerts_config(*args, **kwargs):
285
- return sys.modules["cctally"]._get_alerts_config(*args, **kwargs)
286
-
287
-
288
263
  def _warn_alerts_bad_config_once(*args, **kwargs):
289
264
  return sys.modules["cctally"]._warn_alerts_bad_config_once(*args, **kwargs)
290
265
 
@@ -372,6 +347,47 @@ def _normalize_percent(value: "float | int | None") -> "float | None":
372
347
  return round(float(value), _PERCENT_NORMALIZE_DECIMALS)
373
348
 
374
349
 
350
+ def _resolve_active_five_hour_reset_event_id(
351
+ conn: "sqlite3.Connection",
352
+ five_hour_window_key: int,
353
+ ) -> int:
354
+ """Return ``id`` of the most-recent ``five_hour_reset_events`` row for
355
+ ``five_hour_window_key``, else 0 (pre-credit / no-event sentinel).
356
+
357
+ Mirrors the weekly active-segment resolution pattern used by
358
+ ``maybe_record_milestone`` for ``percent_milestones.reset_event_id``.
359
+ Called once per ``maybe_update_five_hour_block`` invocation and the
360
+ return value is threaded through every read/write site that keys on
361
+ ``(five_hour_window_key, percent_threshold)`` so post-credit threshold
362
+ crossings land as a distinct row from any pre-credit one at the same
363
+ threshold. See spec
364
+ docs/superpowers/specs/2026-05-16-5h-in-place-credit-detection.md §3.3.
365
+
366
+ Returns ``0`` when:
367
+ - The window has no ``five_hour_reset_events`` row (most blocks).
368
+ - The table doesn't exist yet (DB predates this feature).
369
+
370
+ Returns the largest ``id`` matching the window otherwise; the
371
+ ``ORDER BY id DESC LIMIT 1`` clause is what *defines* "active" in
372
+ the stacked-credit case (spec §2.3 — multiple events across distinct
373
+ 10-min slots): pre-credit milestones key on ``seg=0``, milestones
374
+ between credit 1 and credit 2 key on event-1's id, and milestones
375
+ after credit 2 key on event-2's id.
376
+ """
377
+ try:
378
+ row = conn.execute(
379
+ "SELECT id FROM five_hour_reset_events "
380
+ "WHERE five_hour_window_key = ? "
381
+ "ORDER BY id DESC LIMIT 1",
382
+ (int(five_hour_window_key),),
383
+ ).fetchone()
384
+ except sqlite3.DatabaseError:
385
+ return 0
386
+ if row is None:
387
+ return 0
388
+ return int(row["id"])
389
+
390
+
375
391
  def maybe_record_milestone(
376
392
  saved: dict[str, Any],
377
393
  ) -> None:
@@ -491,7 +507,7 @@ def maybe_record_milestone(
491
507
  # the underlying problem is config-wide, not axis-specific.
492
508
  try:
493
509
  alerts_cfg: "dict | None" = _get_alerts_config(load_config())
494
- except sys.modules["cctally"]._AlertsConfigError as exc:
510
+ except _AlertsConfigError as exc:
495
511
  _warn_alerts_bad_config_once(exc)
496
512
  alerts_cfg = None
497
513
 
@@ -762,7 +778,7 @@ def maybe_update_five_hour_block(saved: dict[str, Any]) -> None:
762
778
  cfg_for_alerts = load_config()
763
779
  try:
764
780
  alerts_cfg: "dict | None" = _get_alerts_config(cfg_for_alerts)
765
- except sys.modules["cctally"]._AlertsConfigError as exc:
781
+ except _AlertsConfigError as exc:
766
782
  _warn_alerts_bad_config_once(exc)
767
783
  alerts_cfg = None
768
784
  # Resolve display.tz once (shares the cfg load above). Threaded
@@ -963,7 +979,23 @@ def maybe_update_five_hour_block(saved: dict[str, Any]) -> None:
963
979
  # Snap-up-by-1e-9 per the gotcha: 0.50 * 100 == 49.99...9 in
964
980
  # IEEE-754, so bare math.floor would miss the 50 threshold.
965
981
  current_floor = math.floor(float(five_hour_percent) + 1e-9)
982
+
983
+ # Resolve active segment ONCE so every per-site read + write
984
+ # below sees the same value within this transaction. Spec
985
+ # §3.3 & §3.4 of
986
+ # docs/superpowers/specs/2026-05-16-5h-in-place-credit-detection.md:
987
+ # the active segment is the latest five_hour_reset_events row
988
+ # for this window_key, else sentinel 0 (pre-credit).
989
+ active_reset_event_id = _resolve_active_five_hour_reset_event_id(
990
+ conn, int(five_hour_window_key)
991
+ )
992
+
966
993
  if current_floor >= 1:
994
+ # Site A — MAX(percent_threshold) scoped to active segment.
995
+ # Without the reset_event_id filter, MAX returns the
996
+ # pre-credit max and post-credit milestones from 1..max
997
+ # are silently never emitted.
998
+ #
967
999
  # Use max(percent_threshold) directly (not prior block's
968
1000
  # final_pct) so first-observation already-mid-stream doesn't
969
1001
  # synthesize crossings 1..(current_floor - 1) we never had
@@ -971,8 +1003,8 @@ def maybe_update_five_hour_block(saved: dict[str, Any]) -> None:
971
1003
  # maybe_record_milestone's max_existing path.
972
1004
  row = conn.execute(
973
1005
  "SELECT MAX(percent_threshold) AS m FROM five_hour_milestones "
974
- "WHERE five_hour_window_key = ?",
975
- (int(five_hour_window_key),),
1006
+ "WHERE five_hour_window_key = ? AND reset_event_id = ?",
1007
+ (int(five_hour_window_key), active_reset_event_id),
976
1008
  ).fetchone()
977
1009
  max_existing = row["m"] if row and row["m"] is not None else None
978
1010
 
@@ -985,14 +1017,21 @@ def maybe_update_five_hour_block(saved: dict[str, Any]) -> None:
985
1017
  # block_id was resolved above (before the children writes) and
986
1018
  # is still in scope here.
987
1019
 
1020
+ # Site B — prior-cost lookup scoped to active segment.
988
1021
  # Marginal-cost lookup for the start_threshold milestone
989
1022
  # (only when there's a prior milestone in this block).
1023
+ # Without the reset_event_id filter, marginal could be
1024
+ # computed against a pre-credit row whose block_cost is
1025
+ # unrelated to the post-credit segment's totals.
990
1026
  prior_cost: float | None = None
991
1027
  if max_existing is not None:
992
1028
  prev_row = conn.execute(
993
1029
  "SELECT block_cost_usd FROM five_hour_milestones "
994
- "WHERE five_hour_window_key = ? AND percent_threshold = ?",
995
- (int(five_hour_window_key), int(max_existing)),
1030
+ "WHERE five_hour_window_key = ? "
1031
+ " AND percent_threshold = ? "
1032
+ " AND reset_event_id = ?",
1033
+ (int(five_hour_window_key), int(max_existing),
1034
+ active_reset_event_id),
996
1035
  ).fetchone()
997
1036
  if prev_row is not None:
998
1037
  prior_cost = float(prev_row["block_cost_usd"])
@@ -1002,6 +1041,12 @@ def maybe_update_five_hour_block(saved: dict[str, Any]) -> None:
1002
1041
  marginal: float | None = totals["cost_usd"] - prior_cost
1003
1042
  else:
1004
1043
  marginal = None
1044
+ # Site C — INSERT stamps the resolved
1045
+ # ``active_reset_event_id`` (0 = pre-credit, else
1046
+ # the latest five_hour_reset_events.id). UNIQUE
1047
+ # is now (window_key, threshold, reset_event_id)
1048
+ # so post-credit threshold crossings re-fire
1049
+ # fresh — not absorbed into the pre-credit row.
1005
1050
  cur = conn.execute(
1006
1051
  """
1007
1052
  INSERT OR IGNORE INTO five_hour_milestones (
@@ -1016,9 +1061,10 @@ def maybe_update_five_hour_block(saved: dict[str, Any]) -> None:
1016
1061
  block_cache_read_tokens,
1017
1062
  block_cost_usd,
1018
1063
  marginal_cost_usd,
1019
- seven_day_pct_at_crossing
1064
+ seven_day_pct_at_crossing,
1065
+ reset_event_id
1020
1066
  )
1021
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1067
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1022
1068
  """,
1023
1069
  (
1024
1070
  block_id,
@@ -1033,6 +1079,7 @@ def maybe_update_five_hour_block(saved: dict[str, Any]) -> None:
1033
1079
  totals["cost_usd"],
1034
1080
  marginal,
1035
1081
  weekly_percent,
1082
+ active_reset_event_id,
1036
1083
  ),
1037
1084
  )
1038
1085
  # ── Threshold-actions dispatch (set-then-dispatch, spec §3.2) ──
@@ -1061,11 +1108,19 @@ def maybe_update_five_hour_block(saved: dict[str, Any]) -> None:
1061
1108
  and pct in alerts_cfg["five_hour_thresholds"]
1062
1109
  ):
1063
1110
  crossed_at = now_utc_iso()
1111
+ # Site D — alerted_at UPDATE scoped to the
1112
+ # active segment, so the post-credit row
1113
+ # gets stamped without overwriting an
1114
+ # already-alerted pre-credit row at the
1115
+ # same threshold.
1064
1116
  conn.execute(
1065
1117
  "UPDATE five_hour_milestones SET alerted_at = ? "
1066
- "WHERE five_hour_window_key = ? AND percent_threshold = ? "
1067
- "AND alerted_at IS NULL",
1068
- (crossed_at, int(five_hour_window_key), int(pct)),
1118
+ "WHERE five_hour_window_key = ? "
1119
+ " AND percent_threshold = ? "
1120
+ " AND reset_event_id = ? "
1121
+ " AND alerted_at IS NULL",
1122
+ (crossed_at, int(five_hour_window_key),
1123
+ int(pct), active_reset_event_id),
1069
1124
  )
1070
1125
  # Cheap re-reads inside BEGIN are SELECT-only and
1071
1126
  # safe; values reflect post-INSERT state. We
@@ -1073,10 +1128,17 @@ def maybe_update_five_hour_block(saved: dict[str, Any]) -> None:
1073
1128
  # are in scope) and defer ONLY the Popen-side
1074
1129
  # _dispatch_alert_notification to after the outer
1075
1130
  # commit.
1131
+ # Site E — alert-payload reread scoped to
1132
+ # the active segment so the dispatch shows
1133
+ # post-credit cost, not the pre-credit
1134
+ # row's stale value at the same threshold.
1076
1135
  cost_row = conn.execute(
1077
1136
  "SELECT block_cost_usd FROM five_hour_milestones "
1078
- "WHERE five_hour_window_key = ? AND percent_threshold = ?",
1079
- (int(five_hour_window_key), int(pct)),
1137
+ "WHERE five_hour_window_key = ? "
1138
+ " AND percent_threshold = ? "
1139
+ " AND reset_event_id = ?",
1140
+ (int(five_hour_window_key), int(pct),
1141
+ active_reset_event_id),
1080
1142
  ).fetchone()
1081
1143
  block_row = conn.execute(
1082
1144
  "SELECT block_start_at FROM five_hour_blocks "
@@ -1393,9 +1455,9 @@ def cmd_record_usage(args: argparse.Namespace) -> int:
1393
1455
  "WHERE new_week_end_at = ? LIMIT 1",
1394
1456
  (cur_end_canon,),
1395
1457
  ).fetchone()
1458
+ effective_dt = _floor_to_hour(now_utc)
1459
+ effective_iso = effective_dt.isoformat(timespec="seconds")
1396
1460
  if already is None:
1397
- effective_dt = _floor_to_hour(now_utc)
1398
- effective_iso = effective_dt.isoformat(timespec="seconds")
1399
1461
  # Row shape: old=effective_iso, new=cur_end_canon
1400
1462
  # (distinct values). The previous shape stored
1401
1463
  # old==new==cur_end_canon, which let BOTH
@@ -1413,64 +1475,318 @@ def cmd_record_usage(args: argparse.Namespace) -> int:
1413
1475
  # UNIQUE(old, new) constraint permits this
1414
1476
  # row, and the pre-check above keys on
1415
1477
  # new_week_end_at so dedup still works.
1478
+ # Stamp ``observed_pre_credit_pct = prior_pct``
1479
+ # (issue #45): durable record of the pre-credit
1480
+ # baseline we observed at write time. Decouples
1481
+ # any future cleanup tooling from re-deriving
1482
+ # prior_pct via SELECT. Existing rows from
1483
+ # migration 007 carry NULL.
1416
1484
  conn.execute(
1417
1485
  "INSERT OR IGNORE INTO week_reset_events "
1418
1486
  "(detected_at_utc, old_week_end_at, new_week_end_at, "
1419
- " effective_reset_at_utc) VALUES (?, ?, ?, ?)",
1487
+ " effective_reset_at_utc, observed_pre_credit_pct) "
1488
+ "VALUES (?, ?, ?, ?, ?)",
1420
1489
  (now_utc_iso(), effective_iso, cur_end_canon,
1421
- effective_iso),
1490
+ effective_iso, float(prior_pct)),
1422
1491
  )
1423
1492
  conn.commit()
1424
- # Force-write hwm-7d so the next status-line
1425
- # render reflects the post-credit value. The
1426
- # monotonic guard at the normal write site (below)
1427
- # would refuse to decrease the file; this write
1428
- # is the credit-only escape hatch. Lands AFTER
1429
- # the conn.commit() so a concurrent record-usage
1430
- # reader doesn't see the new HWM before the
1431
- # event row is durable.
1493
+ # Pivots fire UNCONDITIONALLY whenever a credit
1494
+ # is detected they're NOT gated on
1495
+ # ``already is None``. Memory
1496
+ # ``project_dedup_must_not_gate_side_effects.md``:
1497
+ # "Skipping a no-op INSERT must NOT skip
1498
+ # milestones/rollups/alerts; prior run may have
1499
+ # died mid-flight." Crash scenario: tick N
1500
+ # committed the event row, then died before
1501
+ # HWM + DELETE. Tick N+1's pre-check sees
1502
+ # ``already`` non-None (the row IS in the
1503
+ # table) and would skip the pivots, leaving
1504
+ # the system wedged on pre-credit HWM + stale-
1505
+ # replica rows. Pivots are individually
1506
+ # idempotent (file overwrite + DELETE on stable
1507
+ # predicate), so re-running them is safe.
1508
+ # ``effective_iso`` is resolved above; on a
1509
+ # recovery tick it lands on the SAME 10-min
1510
+ # slot as the original (now_utc has drifted
1511
+ # only seconds), so the DELETE predicate's
1512
+ # ``unixepoch(captured_at_utc) >= unixepoch(?)``
1513
+ # still matches every stale-replica row.
1514
+ #
1515
+ # Force-write hwm-7d so the next status-line
1516
+ # render reflects the post-credit value. The
1517
+ # monotonic guard at the normal write site
1518
+ # (below) would refuse to decrease the file;
1519
+ # this write is the credit-only escape hatch.
1520
+ # Lands AFTER the conn.commit() so a concurrent
1521
+ # record-usage reader doesn't see the new HWM
1522
+ # before the event row is durable.
1523
+ try:
1524
+ (c.APP_DIR / "hwm-7d").write_text(
1525
+ f"{week_start_date} {weekly_percent}\n"
1526
+ )
1527
+ except OSError:
1528
+ pass
1529
+
1530
+ # Race-defensive cleanup. Between the moment
1531
+ # Anthropic credited the user (effective_iso)
1532
+ # and this code firing, the EXTERNAL
1533
+ # claude-statusline tool can replay stale
1534
+ # pre-credit `--percent` values (it has its
1535
+ # own in-memory HWM cache and re-runs us once
1536
+ # per status-line tick). Those replays land
1537
+ # captured_at_utc >= effective_iso with
1538
+ # weekly_percent near prior_pct (the pre-credit
1539
+ # value), and they dominate the reset-aware
1540
+ # clamp's MAX over the post-credit segment so
1541
+ # legitimate fresh OAuth values are rejected.
1542
+ # 1.0pp tolerance band (issue #45) around the
1543
+ # observed pre-credit baseline absorbs any
1544
+ # rounding drift between cctally's OAuth read
1545
+ # and statusline's --percent payload (today
1546
+ # they match byte-identically, but the band
1547
+ # future-proofs against Anthropic or statusline
1548
+ # changing rounding). The band stays well below
1549
+ # the 25pp in-place credit detection threshold,
1550
+ # so legitimate post-credit values are never
1551
+ # caught. Bind is the in-scope ``prior_pct``,
1552
+ # which equals the just-stamped
1553
+ # ``observed_pre_credit_pct`` on the event row.
1554
+ try:
1555
+ conn.execute(
1556
+ "DELETE FROM weekly_usage_snapshots "
1557
+ "WHERE week_start_date = ? "
1558
+ " AND unixepoch(captured_at_utc) >= "
1559
+ " unixepoch(?) "
1560
+ " AND ABS(weekly_percent - ?) < 1.0",
1561
+ (week_start_date, effective_iso,
1562
+ float(prior_pct)),
1563
+ )
1564
+ conn.commit()
1565
+ except sqlite3.DatabaseError as exc:
1566
+ eprint(
1567
+ "[record-usage] post-credit cleanup "
1568
+ f"failed: {exc}"
1569
+ )
1570
+
1571
+ # ── 5h in-place credit detection (parallel to weekly above) ──
1572
+ # Spec §2.2 of
1573
+ # docs/superpowers/specs/2026-05-16-5h-in-place-credit-detection.md.
1574
+ # Slot SECOND so the weekly branch retains control-flow
1575
+ # priority — both branches are independent (they touch
1576
+ # different tables) and the order has no behavioral
1577
+ # interaction. Same outer try/except wraps both so a
1578
+ # 5h-detection failure logs but does not regress the rest
1579
+ # of cmd_record_usage.
1580
+ #
1581
+ # Diverges from weekly in three places:
1582
+ # - Threshold: 5.0pp (constant on cctally module), not 25.0pp.
1583
+ # The 5h envelope is smaller so a 5pp move is
1584
+ # proportionally larger.
1585
+ # - Effective-iso floor: 10-min (matches
1586
+ # ``_canonical_5h_window_key``'s 600s floor), not hour.
1587
+ # Up to ~30 distinct slots per 5h block; same-slot
1588
+ # collisions absorbed by UNIQUE per spec §2.3.
1589
+ # - Pre-check: pair-checks the latest event's
1590
+ # ``(prior_percent, post_percent)`` against this tick's
1591
+ # ``(prior_5h_pct, five_hour_percent)``, not
1592
+ # ``new_week_end_at`` equality. A genuine replay matches
1593
+ # BOTH fields; a NEW credit-with-idle (prior_pct equals
1594
+ # the prior credit's post_pct because the user didn't
1595
+ # move between credits) matches only one field and
1596
+ # correctly proceeds to write a second event row.
1597
+ try:
1598
+ if (
1599
+ five_hour_window_key is not None
1600
+ and five_hour_percent is not None
1601
+ ):
1602
+ prior_5h_row = conn.execute(
1603
+ "SELECT five_hour_window_key, five_hour_percent, "
1604
+ " five_hour_resets_at "
1605
+ " FROM weekly_usage_snapshots "
1606
+ " WHERE five_hour_window_key IS NOT NULL "
1607
+ " AND five_hour_percent IS NOT NULL "
1608
+ " ORDER BY captured_at_utc DESC, id DESC LIMIT 1"
1609
+ ).fetchone()
1610
+ if (
1611
+ prior_5h_row is not None
1612
+ and int(prior_5h_row["five_hour_window_key"])
1613
+ == int(five_hour_window_key)
1614
+ and prior_5h_row["five_hour_resets_at"] is not None
1615
+ ):
1616
+ prior_5h_pct = float(prior_5h_row["five_hour_percent"])
1617
+ prior_5h_resets_dt = parse_iso_datetime(
1618
+ prior_5h_row["five_hour_resets_at"],
1619
+ "prior.five_hour_resets_at",
1620
+ )
1621
+ # ``now_utc`` was bound earlier in this same
1622
+ # outer try block from
1623
+ # ``dt.datetime.now(dt.timezone.utc)``; reuse it
1624
+ # so both branches see the same instant.
1625
+ if (
1626
+ prior_5h_resets_dt > now_utc
1627
+ and (prior_5h_pct - float(five_hour_percent))
1628
+ >= c._FIVE_HOUR_RESET_PCT_DROP_THRESHOLD
1629
+ ):
1630
+ # Pair-check dedup pre-check (spec §2.2;
1631
+ # refined by Codex r4 P1 finding). The
1632
+ # round-1 predicate compared only the
1633
+ # latest event's ``post_percent`` against
1634
+ # this tick's ``prior_5h_pct``; that
1635
+ # false-positived on a legitimate 2nd
1636
+ # credit where the user was idle between
1637
+ # credits (Credit 1 lands prior=20/post=5;
1638
+ # user does nothing; Credit 2 arrives with
1639
+ # CLI percent=0 so prior_5h_pct=5 reads
1640
+ # equal to stored post_percent=5 →
1641
+ # silently swallowed). Pair-checking
1642
+ # against BOTH fields disambiguates: a
1643
+ # genuine replay matches BOTH; a new
1644
+ # credit-with-idle matches at most ONE
1645
+ # (the prior side coincides but
1646
+ # post_percent differs).
1647
+ most_recent = conn.execute(
1648
+ "SELECT prior_percent, post_percent "
1649
+ " FROM five_hour_reset_events "
1650
+ " WHERE five_hour_window_key = ? "
1651
+ " ORDER BY id DESC LIMIT 1",
1652
+ (int(five_hour_window_key),),
1653
+ ).fetchone()
1654
+ is_dup = (
1655
+ most_recent is not None
1656
+ and round(prior_5h_pct, 1)
1657
+ == round(float(most_recent["prior_percent"]), 1)
1658
+ and round(float(five_hour_percent), 1)
1659
+ == round(float(most_recent["post_percent"]), 1)
1660
+ )
1661
+ # 10-min floor (spec §2.3 — bounded
1662
+ # stacked-credit resolution; one event per
1663
+ # 10-min slot per block). Resolved BEFORE
1664
+ # the ``if not is_dup`` branch so it's in
1665
+ # scope for the pivots below (per memory
1666
+ # ``project_dedup_must_not_gate_side_effects.md``:
1667
+ # the recovery-tick path must still force
1668
+ # HWM + DELETE even when the INSERT is
1669
+ # absorbed by the pre-check or by
1670
+ # UNIQUE — see comment below for the
1671
+ # crash scenario). ``_floor_to_ten_minutes``
1672
+ # is a cctally module attribute; the
1673
+ # ``c.X`` accessor resolves at call time
1674
+ # so test ``monkeypatch.setitem(ns,
1675
+ # "_floor_to_ten_minutes", …)``
1676
+ # propagates.
1677
+ effective_dt = c._floor_to_ten_minutes(now_utc)
1678
+ effective_iso = effective_dt.isoformat(
1679
+ timespec="seconds"
1680
+ )
1681
+ if not is_dup:
1682
+ conn.execute(
1683
+ "INSERT OR IGNORE INTO five_hour_reset_events "
1684
+ "(detected_at_utc, five_hour_window_key, "
1685
+ " prior_percent, post_percent, "
1686
+ " effective_reset_at_utc) "
1687
+ "VALUES (?, ?, ?, ?, ?)",
1688
+ (
1689
+ now_utc_iso(),
1690
+ int(five_hour_window_key),
1691
+ prior_5h_pct,
1692
+ float(five_hour_percent),
1693
+ effective_iso,
1694
+ ),
1695
+ )
1696
+ conn.commit()
1697
+ # Pivots fire UNCONDITIONALLY whenever a
1698
+ # credit is detected — NOT gated on
1699
+ # ``not is_dup`` and NOT on
1700
+ # ``rowcount == 1``. Memory
1701
+ # ``project_dedup_must_not_gate_side_effects.md``:
1702
+ # "Skipping a no-op INSERT must NOT skip
1703
+ # milestones/rollups/alerts; prior run may
1704
+ # have died mid-flight." Crash scenario A:
1705
+ # tick N committed the event row, then died
1706
+ # before HWM + DELETE. Tick N+1's
1707
+ # INSERT OR IGNORE returns rowcount == 0
1708
+ # (UNIQUE absorbs) but the system is still
1709
+ # wedged on the pre-credit HWM + stale-
1710
+ # replica rows. Crash scenario B (the
1711
+ # Codex r4 finding): a recovery tick where
1712
+ # ``(prior, post)`` pair-matches the
1713
+ # already-stored event row also takes the
1714
+ # ``is_dup`` branch; without the hoist the
1715
+ # pivots would be skipped and the system
1716
+ # would stay wedged. The pivots are
1717
+ # individually idempotent (file overwrite
1718
+ # + DELETE on a stable predicate), so
1719
+ # re-running them on the recovery tick is
1720
+ # always safe. Mirrors the weekly hoist at
1721
+ # ``_cctally_record.py`` after the
1722
+ # ``if already is None`` block (grep
1723
+ # ``Force-write hwm-7d``).
1724
+ #
1725
+ # Force-write hwm-5h: bypasses the
1726
+ # monotonic guard at the normal hwm-5h
1727
+ # writer below. Lands AFTER
1728
+ # ``conn.commit()`` so a concurrent reader
1729
+ # doesn't see the new HWM before the
1730
+ # event row is durable. File format
1731
+ # matches the canonical writer:
1732
+ # ``<key> <percent>\n``.
1432
1733
  try:
1433
- (c.APP_DIR / "hwm-7d").write_text(
1434
- f"{week_start_date} {weekly_percent}\n"
1734
+ (c.APP_DIR / "hwm-5h").write_text(
1735
+ f"{int(five_hour_window_key)} "
1736
+ f"{float(five_hour_percent)}\n"
1435
1737
  )
1436
1738
  except OSError:
1437
1739
  pass
1438
-
1439
- # Race-defensive cleanup. Between the moment
1440
- # Anthropic credited the user (effective_iso)
1441
- # and this code firing, the EXTERNAL
1442
- # claude-statusline tool can replay stale
1443
- # pre-credit `--percent` values (it has its
1444
- # own in-memory HWM cache and re-runs us once
1445
- # per status-line tick). Those replays land
1446
- # captured_at_utc >= effective_iso with
1447
- # weekly_percent == prior_pct (the pre-credit
1448
- # value), and they dominate the reset-aware
1449
- # clamp's MAX over the post-credit segment so
1450
- # legitimate fresh OAuth values are rejected.
1451
- # Strict equality (round(.,1)) keeps this
1452
- # narrow: we only delete rows whose percent
1453
- # exactly matches the pre-credit value we just
1454
- # observed — legitimate post-credit climbs
1455
- # past `prior_pct` (rare, but possible if the
1456
- # credit is small + activity is heavy) stay.
1740
+ # Stale-replica DELETE (spec §4.3).
1741
+ # Defends against claude-statusline
1742
+ # replaying the pre-credit
1743
+ # ``--five-hour-percent`` value past the
1744
+ # credit moment from its own in-memory
1745
+ # HWM cache. 1.0pp tolerance band (issue
1746
+ # #48 symmetric follow-up to weekly #45)
1747
+ # around the observed pre-credit baseline
1748
+ # absorbs any rounding drift between
1749
+ # cctally's OAuth read and statusline's
1750
+ # ``--five-hour-percent`` payload (today
1751
+ # they match byte-identically, but the
1752
+ # band future-proofs against Anthropic or
1753
+ # statusline changing 5h rounding). The
1754
+ # band stays well below the 5.0pp 5h
1755
+ # in-place credit detection threshold
1756
+ # (``_FIVE_HOUR_RESET_PCT_DROP_THRESHOLD``)
1757
+ # 4pp safety margin so legitimate
1758
+ # post-credit values are never caught.
1759
+ # ``unixepoch()`` on both sides for offset
1760
+ # robustness (Z vs +00:00). Bind is the
1761
+ # in-scope ``prior_5h_pct``, which equals
1762
+ # the just-stamped
1763
+ # ``five_hour_reset_events.prior_percent``
1764
+ # on the event row.
1457
1765
  try:
1458
1766
  conn.execute(
1459
1767
  "DELETE FROM weekly_usage_snapshots "
1460
- "WHERE week_start_date = ? "
1461
- " AND unixepoch(captured_at_utc) >= "
1462
- " unixepoch(?) "
1463
- " AND round(weekly_percent, 1) = "
1464
- " round(?, 1)",
1465
- (week_start_date, effective_iso,
1466
- float(prior_pct)),
1768
+ " WHERE five_hour_window_key = ? "
1769
+ " AND unixepoch(captured_at_utc) "
1770
+ " >= unixepoch(?) "
1771
+ " AND ABS(five_hour_percent - ?) "
1772
+ " < 1.0",
1773
+ (
1774
+ int(five_hour_window_key),
1775
+ effective_iso,
1776
+ prior_5h_pct,
1777
+ ),
1467
1778
  )
1468
1779
  conn.commit()
1469
1780
  except sqlite3.DatabaseError as exc:
1470
1781
  eprint(
1471
- "[record-usage] post-credit cleanup "
1472
- f"failed: {exc}"
1782
+ "[record-usage] 5h post-credit "
1783
+ f"cleanup failed: {exc}"
1473
1784
  )
1785
+ except (sqlite3.DatabaseError, ValueError, TypeError) as exc:
1786
+ eprint(
1787
+ f"[record-usage] 5h in-place-credit detection "
1788
+ f"failed: {exc}"
1789
+ )
1474
1790
  except (sqlite3.DatabaseError, ValueError) as exc:
1475
1791
  eprint(f"[record-usage] reset-event detection failed: {exc}")
1476
1792
 
@@ -1511,19 +1827,54 @@ def cmd_record_usage(args: argparse.Namespace) -> int:
1511
1827
  if max_row and max_row["v"] is not None and round(weekly_percent, 1) < round(float(max_row["v"]), 1):
1512
1828
  should_insert = False
1513
1829
  else:
1514
- # 5-hour usage is monotonically non-decreasing within a window.
1515
- # A lower value means stale API data; clamp to existing max.
1516
- # Joining on five_hour_window_key (canonical 10-min-floored
1830
+ # 5-hour usage is monotonically non-decreasing within a window
1831
+ # UNTIL an in-place 5h credit fires. When a
1832
+ # ``five_hour_reset_events`` row exists for THIS
1833
+ # ``five_hour_window_key``, the MAX query filters to samples
1834
+ # captured at-or-after the event's ``effective_reset_at_utc``
1835
+ # so a fresh post-credit OAuth value (e.g. 4%) lands instead
1836
+ # of being re-clamped to the pre-credit max (e.g. 28%). When
1837
+ # no event row exists, ``COALESCE`` defaults to epoch-zero so
1838
+ # the filter is a no-op and legacy clamp behavior is preserved
1839
+ # byte-identically.
1840
+ #
1841
+ # ``unixepoch()`` on BOTH sides for offset robustness — stored
1842
+ # ``captured_at_utc`` carries ``Z`` while
1843
+ # ``effective_reset_at_utc`` carries ``+00:00``. Lex compare
1844
+ # would silently mis-order moments for non-UTC hosts (same
1845
+ # gotcha as the weekly clamp / 5h-block cross-reset flag).
1846
+ #
1847
+ # Joining on ``five_hour_window_key`` (canonical 10-min-floored
1517
1848
  # epoch) absorbs Anthropic's seconds-level jitter on
1518
- # resets_at; an ISO-string equality at this site silently
1849
+ # ``resets_at``; an ISO-string equality at this site silently
1519
1850
  # skipped the clamp every time a jittered fetch landed in
1520
1851
  # the same physical 5h window (spec Bug B).
1852
+ #
1853
+ # Spec §4.1 of
1854
+ # docs/superpowers/specs/2026-05-16-5h-in-place-credit-detection.md.
1521
1855
  if five_hour_percent is not None and five_hour_window_key is not None:
1522
1856
  max_5h_row = conn.execute(
1523
- "SELECT MAX(five_hour_percent) AS v FROM weekly_usage_snapshots WHERE five_hour_window_key = ?",
1524
- (five_hour_window_key,),
1857
+ """
1858
+ SELECT MAX(five_hour_percent) AS v
1859
+ FROM weekly_usage_snapshots
1860
+ WHERE five_hour_window_key = ?
1861
+ AND unixepoch(captured_at_utc) >= unixepoch(COALESCE(
1862
+ (SELECT effective_reset_at_utc
1863
+ FROM five_hour_reset_events
1864
+ WHERE five_hour_window_key = ?
1865
+ ORDER BY id DESC
1866
+ LIMIT 1),
1867
+ '1970-01-01T00:00:00Z'
1868
+ ))
1869
+ """,
1870
+ (int(five_hour_window_key), int(five_hour_window_key)),
1525
1871
  ).fetchone()
1526
- if max_5h_row and max_5h_row["v"] is not None and round(five_hour_percent, 1) < round(float(max_5h_row["v"]), 1):
1872
+ if (
1873
+ max_5h_row
1874
+ and max_5h_row["v"] is not None
1875
+ and round(five_hour_percent, 1)
1876
+ < round(float(max_5h_row["v"]), 1)
1877
+ ):
1527
1878
  five_hour_percent = float(max_5h_row["v"])
1528
1879
 
1529
1880
  # Dedup vs last snapshot: if BOTH weekly_percent and
@@ -1627,6 +1978,16 @@ def cmd_record_usage(args: argparse.Namespace) -> int:
1627
1978
  # last_observed_at_utc is stale relative to the latest
1628
1979
  # snapshot's captured_at_utc (the kill landed between
1629
1980
  # insert_usage_snapshot and maybe_update_five_hour_block).
1981
+ #
1982
+ # Round-3: ALSO scope the milestone-coverage half of
1983
+ # this probe by ACTIVE 5h SEGMENT. Without this, a
1984
+ # credited block's MAX over the whole milestone ledger
1985
+ # would still read the pre-credit ceiling (e.g. 28%) and
1986
+ # silently suppress the post-credit ledger's heal even
1987
+ # though it has zero rows. Mirrors weekly Probe 1's
1988
+ # segment-aware fix above. Uses
1989
+ # ``_resolve_active_five_hour_reset_event_id`` to find
1990
+ # the active segment for the latest snapshot's window.
1630
1991
  need_5h_heal = False
1631
1992
  window_key = latest_row["five_hour_window_key"]
1632
1993
  if window_key is not None:
@@ -1643,6 +2004,45 @@ def cmd_record_usage(args: argparse.Namespace) -> int:
1643
2004
  < latest_row["captured_at_utc"]
1644
2005
  ):
1645
2006
  need_5h_heal = True
2007
+ else:
2008
+ # Block row exists AND last_observed is fresh
2009
+ # — but the post-credit milestone segment may
2010
+ # still owe rows. Scope MAX(percent_threshold)
2011
+ # by the active reset_event_id segment so
2012
+ # post-credit climbs from threshold 1 trigger
2013
+ # heal even when the pre-credit segment already
2014
+ # crossed higher thresholds. Probe shape mirrors
2015
+ # weekly Probe 1 (lines 1922-1956).
2016
+ five_hour_percent_for_probe = latest_row[
2017
+ "five_hour_percent"
2018
+ ]
2019
+ if five_hour_percent_for_probe is not None:
2020
+ latest_5h_floor = math.floor(
2021
+ float(five_hour_percent_for_probe) + 1e-9
2022
+ )
2023
+ if latest_5h_floor >= 1:
2024
+ heal_5h_segment = (
2025
+ _resolve_active_five_hour_reset_event_id(
2026
+ heal_conn, int(window_key)
2027
+ )
2028
+ )
2029
+ max_5h_existing = heal_conn.execute(
2030
+ "SELECT MAX(percent_threshold) AS m "
2031
+ "FROM five_hour_milestones "
2032
+ "WHERE five_hour_window_key = ? "
2033
+ " AND reset_event_id = ?",
2034
+ (int(window_key), heal_5h_segment),
2035
+ ).fetchone()
2036
+ if (
2037
+ max_5h_existing is None
2038
+ or max_5h_existing["m"] is None
2039
+ ):
2040
+ need_5h_heal = True
2041
+ elif (
2042
+ int(max_5h_existing["m"])
2043
+ < latest_5h_floor
2044
+ ):
2045
+ need_5h_heal = True
1646
2046
  finally:
1647
2047
  heal_conn.close()
1648
2048