cctally 1.7.2 → 1.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/bin/_cctally_dashboard.py +64 -3
- package/bin/_cctally_db.py +182 -21
- package/bin/_cctally_record.py +457 -58
- package/bin/_cctally_tui.py +100 -1
- package/bin/_lib_render.py +20 -0
- package/bin/cctally +191 -6
- package/dashboard/static/assets/index-DhCnIFq9.js +18 -0
- package/dashboard/static/assets/index-Dv5Dzag5.css +1 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +1 -1
- package/dashboard/static/assets/index-BgpoazlS.js +0 -18
- package/dashboard/static/assets/index-nJdUaGys.css +0 -1
package/bin/_cctally_record.py
CHANGED
|
@@ -372,6 +372,47 @@ def _normalize_percent(value: "float | int | None") -> "float | None":
|
|
|
372
372
|
return round(float(value), _PERCENT_NORMALIZE_DECIMALS)
|
|
373
373
|
|
|
374
374
|
|
|
375
|
+
def _resolve_active_five_hour_reset_event_id(
|
|
376
|
+
conn: "sqlite3.Connection",
|
|
377
|
+
five_hour_window_key: int,
|
|
378
|
+
) -> int:
|
|
379
|
+
"""Return ``id`` of the most-recent ``five_hour_reset_events`` row for
|
|
380
|
+
``five_hour_window_key``, else 0 (pre-credit / no-event sentinel).
|
|
381
|
+
|
|
382
|
+
Mirrors the weekly active-segment resolution pattern used by
|
|
383
|
+
``maybe_record_milestone`` for ``percent_milestones.reset_event_id``.
|
|
384
|
+
Called once per ``maybe_update_five_hour_block`` invocation and the
|
|
385
|
+
return value is threaded through every read/write site that keys on
|
|
386
|
+
``(five_hour_window_key, percent_threshold)`` so post-credit threshold
|
|
387
|
+
crossings land as a distinct row from any pre-credit one at the same
|
|
388
|
+
threshold. See spec
|
|
389
|
+
docs/superpowers/specs/2026-05-16-5h-in-place-credit-detection.md §3.3.
|
|
390
|
+
|
|
391
|
+
Returns ``0`` when:
|
|
392
|
+
- The window has no ``five_hour_reset_events`` row (most blocks).
|
|
393
|
+
- The table doesn't exist yet (DB predates this feature).
|
|
394
|
+
|
|
395
|
+
Returns the largest ``id`` matching the window otherwise; the
|
|
396
|
+
``ORDER BY id DESC LIMIT 1`` clause is what *defines* "active" in
|
|
397
|
+
the stacked-credit case (spec §2.3 — multiple events across distinct
|
|
398
|
+
10-min slots): pre-credit milestones key on ``seg=0``, milestones
|
|
399
|
+
between credit 1 and credit 2 key on event-1's id, and milestones
|
|
400
|
+
after credit 2 key on event-2's id.
|
|
401
|
+
"""
|
|
402
|
+
try:
|
|
403
|
+
row = conn.execute(
|
|
404
|
+
"SELECT id FROM five_hour_reset_events "
|
|
405
|
+
"WHERE five_hour_window_key = ? "
|
|
406
|
+
"ORDER BY id DESC LIMIT 1",
|
|
407
|
+
(int(five_hour_window_key),),
|
|
408
|
+
).fetchone()
|
|
409
|
+
except sqlite3.DatabaseError:
|
|
410
|
+
return 0
|
|
411
|
+
if row is None:
|
|
412
|
+
return 0
|
|
413
|
+
return int(row["id"])
|
|
414
|
+
|
|
415
|
+
|
|
375
416
|
def maybe_record_milestone(
|
|
376
417
|
saved: dict[str, Any],
|
|
377
418
|
) -> None:
|
|
@@ -963,7 +1004,23 @@ def maybe_update_five_hour_block(saved: dict[str, Any]) -> None:
|
|
|
963
1004
|
# Snap-up-by-1e-9 per the gotcha: 0.50 * 100 == 49.99...9 in
|
|
964
1005
|
# IEEE-754, so bare math.floor would miss the 50 threshold.
|
|
965
1006
|
current_floor = math.floor(float(five_hour_percent) + 1e-9)
|
|
1007
|
+
|
|
1008
|
+
# Resolve active segment ONCE so every per-site read + write
|
|
1009
|
+
# below sees the same value within this transaction. Spec
|
|
1010
|
+
# §3.3 & §3.4 of
|
|
1011
|
+
# docs/superpowers/specs/2026-05-16-5h-in-place-credit-detection.md:
|
|
1012
|
+
# the active segment is the latest five_hour_reset_events row
|
|
1013
|
+
# for this window_key, else sentinel 0 (pre-credit).
|
|
1014
|
+
active_reset_event_id = _resolve_active_five_hour_reset_event_id(
|
|
1015
|
+
conn, int(five_hour_window_key)
|
|
1016
|
+
)
|
|
1017
|
+
|
|
966
1018
|
if current_floor >= 1:
|
|
1019
|
+
# Site A — MAX(percent_threshold) scoped to active segment.
|
|
1020
|
+
# Without the reset_event_id filter, MAX returns the
|
|
1021
|
+
# pre-credit max and post-credit milestones from 1..max
|
|
1022
|
+
# are silently never emitted.
|
|
1023
|
+
#
|
|
967
1024
|
# Use max(percent_threshold) directly (not prior block's
|
|
968
1025
|
# final_pct) so first-observation already-mid-stream doesn't
|
|
969
1026
|
# synthesize crossings 1..(current_floor - 1) we never had
|
|
@@ -971,8 +1028,8 @@ def maybe_update_five_hour_block(saved: dict[str, Any]) -> None:
|
|
|
971
1028
|
# maybe_record_milestone's max_existing path.
|
|
972
1029
|
row = conn.execute(
|
|
973
1030
|
"SELECT MAX(percent_threshold) AS m FROM five_hour_milestones "
|
|
974
|
-
"WHERE five_hour_window_key = ?",
|
|
975
|
-
(int(five_hour_window_key),),
|
|
1031
|
+
"WHERE five_hour_window_key = ? AND reset_event_id = ?",
|
|
1032
|
+
(int(five_hour_window_key), active_reset_event_id),
|
|
976
1033
|
).fetchone()
|
|
977
1034
|
max_existing = row["m"] if row and row["m"] is not None else None
|
|
978
1035
|
|
|
@@ -985,14 +1042,21 @@ def maybe_update_five_hour_block(saved: dict[str, Any]) -> None:
|
|
|
985
1042
|
# block_id was resolved above (before the children writes) and
|
|
986
1043
|
# is still in scope here.
|
|
987
1044
|
|
|
1045
|
+
# Site B — prior-cost lookup scoped to active segment.
|
|
988
1046
|
# Marginal-cost lookup for the start_threshold milestone
|
|
989
1047
|
# (only when there's a prior milestone in this block).
|
|
1048
|
+
# Without the reset_event_id filter, marginal could be
|
|
1049
|
+
# computed against a pre-credit row whose block_cost is
|
|
1050
|
+
# unrelated to the post-credit segment's totals.
|
|
990
1051
|
prior_cost: float | None = None
|
|
991
1052
|
if max_existing is not None:
|
|
992
1053
|
prev_row = conn.execute(
|
|
993
1054
|
"SELECT block_cost_usd FROM five_hour_milestones "
|
|
994
|
-
"WHERE five_hour_window_key = ?
|
|
995
|
-
|
|
1055
|
+
"WHERE five_hour_window_key = ? "
|
|
1056
|
+
" AND percent_threshold = ? "
|
|
1057
|
+
" AND reset_event_id = ?",
|
|
1058
|
+
(int(five_hour_window_key), int(max_existing),
|
|
1059
|
+
active_reset_event_id),
|
|
996
1060
|
).fetchone()
|
|
997
1061
|
if prev_row is not None:
|
|
998
1062
|
prior_cost = float(prev_row["block_cost_usd"])
|
|
@@ -1002,6 +1066,12 @@ def maybe_update_five_hour_block(saved: dict[str, Any]) -> None:
|
|
|
1002
1066
|
marginal: float | None = totals["cost_usd"] - prior_cost
|
|
1003
1067
|
else:
|
|
1004
1068
|
marginal = None
|
|
1069
|
+
# Site C — INSERT stamps the resolved
|
|
1070
|
+
# ``active_reset_event_id`` (0 = pre-credit, else
|
|
1071
|
+
# the latest five_hour_reset_events.id). UNIQUE
|
|
1072
|
+
# is now (window_key, threshold, reset_event_id)
|
|
1073
|
+
# so post-credit threshold crossings re-fire
|
|
1074
|
+
# fresh — not absorbed into the pre-credit row.
|
|
1005
1075
|
cur = conn.execute(
|
|
1006
1076
|
"""
|
|
1007
1077
|
INSERT OR IGNORE INTO five_hour_milestones (
|
|
@@ -1016,9 +1086,10 @@ def maybe_update_five_hour_block(saved: dict[str, Any]) -> None:
|
|
|
1016
1086
|
block_cache_read_tokens,
|
|
1017
1087
|
block_cost_usd,
|
|
1018
1088
|
marginal_cost_usd,
|
|
1019
|
-
seven_day_pct_at_crossing
|
|
1089
|
+
seven_day_pct_at_crossing,
|
|
1090
|
+
reset_event_id
|
|
1020
1091
|
)
|
|
1021
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1092
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1022
1093
|
""",
|
|
1023
1094
|
(
|
|
1024
1095
|
block_id,
|
|
@@ -1033,6 +1104,7 @@ def maybe_update_five_hour_block(saved: dict[str, Any]) -> None:
|
|
|
1033
1104
|
totals["cost_usd"],
|
|
1034
1105
|
marginal,
|
|
1035
1106
|
weekly_percent,
|
|
1107
|
+
active_reset_event_id,
|
|
1036
1108
|
),
|
|
1037
1109
|
)
|
|
1038
1110
|
# ── Threshold-actions dispatch (set-then-dispatch, spec §3.2) ──
|
|
@@ -1061,11 +1133,19 @@ def maybe_update_five_hour_block(saved: dict[str, Any]) -> None:
|
|
|
1061
1133
|
and pct in alerts_cfg["five_hour_thresholds"]
|
|
1062
1134
|
):
|
|
1063
1135
|
crossed_at = now_utc_iso()
|
|
1136
|
+
# Site D — alerted_at UPDATE scoped to the
|
|
1137
|
+
# active segment, so the post-credit row
|
|
1138
|
+
# gets stamped without overwriting an
|
|
1139
|
+
# already-alerted pre-credit row at the
|
|
1140
|
+
# same threshold.
|
|
1064
1141
|
conn.execute(
|
|
1065
1142
|
"UPDATE five_hour_milestones SET alerted_at = ? "
|
|
1066
|
-
"WHERE five_hour_window_key = ?
|
|
1067
|
-
"AND
|
|
1068
|
-
|
|
1143
|
+
"WHERE five_hour_window_key = ? "
|
|
1144
|
+
" AND percent_threshold = ? "
|
|
1145
|
+
" AND reset_event_id = ? "
|
|
1146
|
+
" AND alerted_at IS NULL",
|
|
1147
|
+
(crossed_at, int(five_hour_window_key),
|
|
1148
|
+
int(pct), active_reset_event_id),
|
|
1069
1149
|
)
|
|
1070
1150
|
# Cheap re-reads inside BEGIN are SELECT-only and
|
|
1071
1151
|
# safe; values reflect post-INSERT state. We
|
|
@@ -1073,10 +1153,17 @@ def maybe_update_five_hour_block(saved: dict[str, Any]) -> None:
|
|
|
1073
1153
|
# are in scope) and defer ONLY the Popen-side
|
|
1074
1154
|
# _dispatch_alert_notification to after the outer
|
|
1075
1155
|
# commit.
|
|
1156
|
+
# Site E — alert-payload reread scoped to
|
|
1157
|
+
# the active segment so the dispatch shows
|
|
1158
|
+
# post-credit cost, not the pre-credit
|
|
1159
|
+
# row's stale value at the same threshold.
|
|
1076
1160
|
cost_row = conn.execute(
|
|
1077
1161
|
"SELECT block_cost_usd FROM five_hour_milestones "
|
|
1078
|
-
"WHERE five_hour_window_key = ?
|
|
1079
|
-
|
|
1162
|
+
"WHERE five_hour_window_key = ? "
|
|
1163
|
+
" AND percent_threshold = ? "
|
|
1164
|
+
" AND reset_event_id = ?",
|
|
1165
|
+
(int(five_hour_window_key), int(pct),
|
|
1166
|
+
active_reset_event_id),
|
|
1080
1167
|
).fetchone()
|
|
1081
1168
|
block_row = conn.execute(
|
|
1082
1169
|
"SELECT block_start_at FROM five_hour_blocks "
|
|
@@ -1393,9 +1480,9 @@ def cmd_record_usage(args: argparse.Namespace) -> int:
|
|
|
1393
1480
|
"WHERE new_week_end_at = ? LIMIT 1",
|
|
1394
1481
|
(cur_end_canon,),
|
|
1395
1482
|
).fetchone()
|
|
1483
|
+
effective_dt = _floor_to_hour(now_utc)
|
|
1484
|
+
effective_iso = effective_dt.isoformat(timespec="seconds")
|
|
1396
1485
|
if already is None:
|
|
1397
|
-
effective_dt = _floor_to_hour(now_utc)
|
|
1398
|
-
effective_iso = effective_dt.isoformat(timespec="seconds")
|
|
1399
1486
|
# Row shape: old=effective_iso, new=cur_end_canon
|
|
1400
1487
|
# (distinct values). The previous shape stored
|
|
1401
1488
|
# old==new==cur_end_canon, which let BOTH
|
|
@@ -1421,56 +1508,284 @@ def cmd_record_usage(args: argparse.Namespace) -> int:
|
|
|
1421
1508
|
effective_iso),
|
|
1422
1509
|
)
|
|
1423
1510
|
conn.commit()
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1511
|
+
# Pivots fire UNCONDITIONALLY whenever a credit
|
|
1512
|
+
# is detected — they're NOT gated on
|
|
1513
|
+
# ``already is None``. Memory
|
|
1514
|
+
# ``project_dedup_must_not_gate_side_effects.md``:
|
|
1515
|
+
# "Skipping a no-op INSERT must NOT skip
|
|
1516
|
+
# milestones/rollups/alerts; prior run may have
|
|
1517
|
+
# died mid-flight." Crash scenario: tick N
|
|
1518
|
+
# committed the event row, then died before
|
|
1519
|
+
# HWM + DELETE. Tick N+1's pre-check sees
|
|
1520
|
+
# ``already`` non-None (the row IS in the
|
|
1521
|
+
# table) and would skip the pivots, leaving
|
|
1522
|
+
# the system wedged on pre-credit HWM + stale-
|
|
1523
|
+
# replica rows. Pivots are individually
|
|
1524
|
+
# idempotent (file overwrite + DELETE on stable
|
|
1525
|
+
# predicate), so re-running them is safe.
|
|
1526
|
+
# ``effective_iso`` is resolved above; on a
|
|
1527
|
+
# recovery tick it lands on the SAME 10-min
|
|
1528
|
+
# slot as the original (now_utc has drifted
|
|
1529
|
+
# only seconds), so the DELETE predicate's
|
|
1530
|
+
# ``unixepoch(captured_at_utc) >= unixepoch(?)``
|
|
1531
|
+
# still matches every stale-replica row.
|
|
1532
|
+
#
|
|
1533
|
+
# Force-write hwm-7d so the next status-line
|
|
1534
|
+
# render reflects the post-credit value. The
|
|
1535
|
+
# monotonic guard at the normal write site
|
|
1536
|
+
# (below) would refuse to decrease the file;
|
|
1537
|
+
# this write is the credit-only escape hatch.
|
|
1538
|
+
# Lands AFTER the conn.commit() so a concurrent
|
|
1539
|
+
# record-usage reader doesn't see the new HWM
|
|
1540
|
+
# before the event row is durable.
|
|
1541
|
+
try:
|
|
1542
|
+
(c.APP_DIR / "hwm-7d").write_text(
|
|
1543
|
+
f"{week_start_date} {weekly_percent}\n"
|
|
1544
|
+
)
|
|
1545
|
+
except OSError:
|
|
1546
|
+
pass
|
|
1547
|
+
|
|
1548
|
+
# Race-defensive cleanup. Between the moment
|
|
1549
|
+
# Anthropic credited the user (effective_iso)
|
|
1550
|
+
# and this code firing, the EXTERNAL
|
|
1551
|
+
# claude-statusline tool can replay stale
|
|
1552
|
+
# pre-credit `--percent` values (it has its
|
|
1553
|
+
# own in-memory HWM cache and re-runs us once
|
|
1554
|
+
# per status-line tick). Those replays land
|
|
1555
|
+
# captured_at_utc >= effective_iso with
|
|
1556
|
+
# weekly_percent == prior_pct (the pre-credit
|
|
1557
|
+
# value), and they dominate the reset-aware
|
|
1558
|
+
# clamp's MAX over the post-credit segment so
|
|
1559
|
+
# legitimate fresh OAuth values are rejected.
|
|
1560
|
+
# Strict equality (round(.,1)) keeps this
|
|
1561
|
+
# narrow: we only delete rows whose percent
|
|
1562
|
+
# exactly matches the pre-credit value we just
|
|
1563
|
+
# observed — legitimate post-credit climbs
|
|
1564
|
+
# past `prior_pct` (rare, but possible if the
|
|
1565
|
+
# credit is small + activity is heavy) stay.
|
|
1566
|
+
try:
|
|
1567
|
+
conn.execute(
|
|
1568
|
+
"DELETE FROM weekly_usage_snapshots "
|
|
1569
|
+
"WHERE week_start_date = ? "
|
|
1570
|
+
" AND unixepoch(captured_at_utc) >= "
|
|
1571
|
+
" unixepoch(?) "
|
|
1572
|
+
" AND round(weekly_percent, 1) = "
|
|
1573
|
+
" round(?, 1)",
|
|
1574
|
+
(week_start_date, effective_iso,
|
|
1575
|
+
float(prior_pct)),
|
|
1576
|
+
)
|
|
1577
|
+
conn.commit()
|
|
1578
|
+
except sqlite3.DatabaseError as exc:
|
|
1579
|
+
eprint(
|
|
1580
|
+
"[record-usage] post-credit cleanup "
|
|
1581
|
+
f"failed: {exc}"
|
|
1582
|
+
)
|
|
1583
|
+
|
|
1584
|
+
# ── 5h in-place credit detection (parallel to weekly above) ──
|
|
1585
|
+
# Spec §2.2 of
|
|
1586
|
+
# docs/superpowers/specs/2026-05-16-5h-in-place-credit-detection.md.
|
|
1587
|
+
# Slot SECOND so the weekly branch retains control-flow
|
|
1588
|
+
# priority — both branches are independent (they touch
|
|
1589
|
+
# different tables) and the order has no behavioral
|
|
1590
|
+
# interaction. Same outer try/except wraps both so a
|
|
1591
|
+
# 5h-detection failure logs but does not regress the rest
|
|
1592
|
+
# of cmd_record_usage.
|
|
1593
|
+
#
|
|
1594
|
+
# Diverges from weekly in three places:
|
|
1595
|
+
# - Threshold: 5.0pp (constant on cctally module), not 25.0pp.
|
|
1596
|
+
# The 5h envelope is smaller so a 5pp move is
|
|
1597
|
+
# proportionally larger.
|
|
1598
|
+
# - Effective-iso floor: 10-min (matches
|
|
1599
|
+
# ``_canonical_5h_window_key``'s 600s floor), not hour.
|
|
1600
|
+
# Up to ~30 distinct slots per 5h block; same-slot
|
|
1601
|
+
# collisions absorbed by UNIQUE per spec §2.3.
|
|
1602
|
+
# - Pre-check: pair-checks the latest event's
|
|
1603
|
+
# ``(prior_percent, post_percent)`` against this tick's
|
|
1604
|
+
# ``(prior_5h_pct, five_hour_percent)``, not
|
|
1605
|
+
# ``new_week_end_at`` equality. A genuine replay matches
|
|
1606
|
+
# BOTH fields; a NEW credit-with-idle (prior_pct equals
|
|
1607
|
+
# the prior credit's post_pct because the user didn't
|
|
1608
|
+
# move between credits) matches only one field and
|
|
1609
|
+
# correctly proceeds to write a second event row.
|
|
1610
|
+
try:
|
|
1611
|
+
if (
|
|
1612
|
+
five_hour_window_key is not None
|
|
1613
|
+
and five_hour_percent is not None
|
|
1614
|
+
):
|
|
1615
|
+
prior_5h_row = conn.execute(
|
|
1616
|
+
"SELECT five_hour_window_key, five_hour_percent, "
|
|
1617
|
+
" five_hour_resets_at "
|
|
1618
|
+
" FROM weekly_usage_snapshots "
|
|
1619
|
+
" WHERE five_hour_window_key IS NOT NULL "
|
|
1620
|
+
" AND five_hour_percent IS NOT NULL "
|
|
1621
|
+
" ORDER BY captured_at_utc DESC, id DESC LIMIT 1"
|
|
1622
|
+
).fetchone()
|
|
1623
|
+
if (
|
|
1624
|
+
prior_5h_row is not None
|
|
1625
|
+
and int(prior_5h_row["five_hour_window_key"])
|
|
1626
|
+
== int(five_hour_window_key)
|
|
1627
|
+
and prior_5h_row["five_hour_resets_at"] is not None
|
|
1628
|
+
):
|
|
1629
|
+
prior_5h_pct = float(prior_5h_row["five_hour_percent"])
|
|
1630
|
+
prior_5h_resets_dt = parse_iso_datetime(
|
|
1631
|
+
prior_5h_row["five_hour_resets_at"],
|
|
1632
|
+
"prior.five_hour_resets_at",
|
|
1633
|
+
)
|
|
1634
|
+
# ``now_utc`` was bound earlier in this same
|
|
1635
|
+
# outer try block from
|
|
1636
|
+
# ``dt.datetime.now(dt.timezone.utc)``; reuse it
|
|
1637
|
+
# so both branches see the same instant.
|
|
1638
|
+
if (
|
|
1639
|
+
prior_5h_resets_dt > now_utc
|
|
1640
|
+
and (prior_5h_pct - float(five_hour_percent))
|
|
1641
|
+
>= c._FIVE_HOUR_RESET_PCT_DROP_THRESHOLD
|
|
1642
|
+
):
|
|
1643
|
+
# Pair-check dedup pre-check (spec §2.2;
|
|
1644
|
+
# refined by Codex r4 P1 finding). The
|
|
1645
|
+
# round-1 predicate compared only the
|
|
1646
|
+
# latest event's ``post_percent`` against
|
|
1647
|
+
# this tick's ``prior_5h_pct``; that
|
|
1648
|
+
# false-positived on a legitimate 2nd
|
|
1649
|
+
# credit where the user was idle between
|
|
1650
|
+
# credits (Credit 1 lands prior=20/post=5;
|
|
1651
|
+
# user does nothing; Credit 2 arrives with
|
|
1652
|
+
# CLI percent=0 so prior_5h_pct=5 reads
|
|
1653
|
+
# equal to stored post_percent=5 →
|
|
1654
|
+
# silently swallowed). Pair-checking
|
|
1655
|
+
# against BOTH fields disambiguates: a
|
|
1656
|
+
# genuine replay matches BOTH; a new
|
|
1657
|
+
# credit-with-idle matches at most ONE
|
|
1658
|
+
# (the prior side coincides but
|
|
1659
|
+
# post_percent differs).
|
|
1660
|
+
most_recent = conn.execute(
|
|
1661
|
+
"SELECT prior_percent, post_percent "
|
|
1662
|
+
" FROM five_hour_reset_events "
|
|
1663
|
+
" WHERE five_hour_window_key = ? "
|
|
1664
|
+
" ORDER BY id DESC LIMIT 1",
|
|
1665
|
+
(int(five_hour_window_key),),
|
|
1666
|
+
).fetchone()
|
|
1667
|
+
is_dup = (
|
|
1668
|
+
most_recent is not None
|
|
1669
|
+
and round(prior_5h_pct, 1)
|
|
1670
|
+
== round(float(most_recent["prior_percent"]), 1)
|
|
1671
|
+
and round(float(five_hour_percent), 1)
|
|
1672
|
+
== round(float(most_recent["post_percent"]), 1)
|
|
1673
|
+
)
|
|
1674
|
+
# 10-min floor (spec §2.3 — bounded
|
|
1675
|
+
# stacked-credit resolution; one event per
|
|
1676
|
+
# 10-min slot per block). Resolved BEFORE
|
|
1677
|
+
# the ``if not is_dup`` branch so it's in
|
|
1678
|
+
# scope for the pivots below (per memory
|
|
1679
|
+
# ``project_dedup_must_not_gate_side_effects.md``:
|
|
1680
|
+
# the recovery-tick path must still force
|
|
1681
|
+
# HWM + DELETE even when the INSERT is
|
|
1682
|
+
# absorbed by the pre-check or by
|
|
1683
|
+
# UNIQUE — see comment below for the
|
|
1684
|
+
# crash scenario). ``_floor_to_ten_minutes``
|
|
1685
|
+
# is a cctally module attribute; the
|
|
1686
|
+
# ``c.X`` accessor resolves at call time
|
|
1687
|
+
# so test ``monkeypatch.setitem(ns,
|
|
1688
|
+
# "_floor_to_ten_minutes", …)``
|
|
1689
|
+
# propagates.
|
|
1690
|
+
effective_dt = c._floor_to_ten_minutes(now_utc)
|
|
1691
|
+
effective_iso = effective_dt.isoformat(
|
|
1692
|
+
timespec="seconds"
|
|
1693
|
+
)
|
|
1694
|
+
if not is_dup:
|
|
1695
|
+
conn.execute(
|
|
1696
|
+
"INSERT OR IGNORE INTO five_hour_reset_events "
|
|
1697
|
+
"(detected_at_utc, five_hour_window_key, "
|
|
1698
|
+
" prior_percent, post_percent, "
|
|
1699
|
+
" effective_reset_at_utc) "
|
|
1700
|
+
"VALUES (?, ?, ?, ?, ?)",
|
|
1701
|
+
(
|
|
1702
|
+
now_utc_iso(),
|
|
1703
|
+
int(five_hour_window_key),
|
|
1704
|
+
prior_5h_pct,
|
|
1705
|
+
float(five_hour_percent),
|
|
1706
|
+
effective_iso,
|
|
1707
|
+
),
|
|
1708
|
+
)
|
|
1709
|
+
conn.commit()
|
|
1710
|
+
# Pivots fire UNCONDITIONALLY whenever a
|
|
1711
|
+
# credit is detected — NOT gated on
|
|
1712
|
+
# ``not is_dup`` and NOT on
|
|
1713
|
+
# ``rowcount == 1``. Memory
|
|
1714
|
+
# ``project_dedup_must_not_gate_side_effects.md``:
|
|
1715
|
+
# "Skipping a no-op INSERT must NOT skip
|
|
1716
|
+
# milestones/rollups/alerts; prior run may
|
|
1717
|
+
# have died mid-flight." Crash scenario A:
|
|
1718
|
+
# tick N committed the event row, then died
|
|
1719
|
+
# before HWM + DELETE. Tick N+1's
|
|
1720
|
+
# INSERT OR IGNORE returns rowcount == 0
|
|
1721
|
+
# (UNIQUE absorbs) but the system is still
|
|
1722
|
+
# wedged on the pre-credit HWM + stale-
|
|
1723
|
+
# replica rows. Crash scenario B (the
|
|
1724
|
+
# Codex r4 finding): a recovery tick where
|
|
1725
|
+
# ``(prior, post)`` pair-matches the
|
|
1726
|
+
# already-stored event row also takes the
|
|
1727
|
+
# ``is_dup`` branch; without the hoist the
|
|
1728
|
+
# pivots would be skipped and the system
|
|
1729
|
+
# would stay wedged. The pivots are
|
|
1730
|
+
# individually idempotent (file overwrite
|
|
1731
|
+
# + DELETE on a stable predicate), so
|
|
1732
|
+
# re-running them on the recovery tick is
|
|
1733
|
+
# always safe. Mirrors the weekly hoist at
|
|
1734
|
+
# ``_cctally_record.py`` after the
|
|
1735
|
+
# ``if already is None`` block (grep
|
|
1736
|
+
# ``Force-write hwm-7d``).
|
|
1737
|
+
#
|
|
1738
|
+
# Force-write hwm-5h: bypasses the
|
|
1739
|
+
# monotonic guard at the normal hwm-5h
|
|
1740
|
+
# writer below. Lands AFTER
|
|
1741
|
+
# ``conn.commit()`` so a concurrent reader
|
|
1742
|
+
# doesn't see the new HWM before the
|
|
1743
|
+
# event row is durable. File format
|
|
1744
|
+
# matches the canonical writer:
|
|
1745
|
+
# ``<key> <percent>\n``.
|
|
1432
1746
|
try:
|
|
1433
|
-
(c.APP_DIR / "hwm-
|
|
1434
|
-
f"{
|
|
1747
|
+
(c.APP_DIR / "hwm-5h").write_text(
|
|
1748
|
+
f"{int(five_hour_window_key)} "
|
|
1749
|
+
f"{float(five_hour_percent)}\n"
|
|
1435
1750
|
)
|
|
1436
1751
|
except OSError:
|
|
1437
1752
|
pass
|
|
1438
|
-
|
|
1439
|
-
#
|
|
1440
|
-
#
|
|
1441
|
-
#
|
|
1442
|
-
#
|
|
1443
|
-
#
|
|
1444
|
-
#
|
|
1445
|
-
#
|
|
1446
|
-
#
|
|
1447
|
-
#
|
|
1448
|
-
#
|
|
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.
|
|
1753
|
+
# Stale-replica DELETE (spec §4.3).
|
|
1754
|
+
# Defends against claude-statusline
|
|
1755
|
+
# replaying the pre-credit
|
|
1756
|
+
# ``--five-hour-percent`` value past the
|
|
1757
|
+
# credit moment from its own in-memory
|
|
1758
|
+
# HWM cache. Strict round-1 equality
|
|
1759
|
+
# keeps the scope narrow — only rows
|
|
1760
|
+
# whose five_hour_percent exactly matches
|
|
1761
|
+
# the just-observed pre-credit value are
|
|
1762
|
+
# removed. ``unixepoch()`` on both sides
|
|
1763
|
+
# for offset robustness (Z vs +00:00).
|
|
1457
1764
|
try:
|
|
1458
1765
|
conn.execute(
|
|
1459
1766
|
"DELETE FROM weekly_usage_snapshots "
|
|
1460
|
-
"WHERE
|
|
1461
|
-
"
|
|
1462
|
-
"
|
|
1463
|
-
"
|
|
1464
|
-
"
|
|
1465
|
-
(
|
|
1466
|
-
|
|
1767
|
+
" WHERE five_hour_window_key = ? "
|
|
1768
|
+
" AND unixepoch(captured_at_utc) "
|
|
1769
|
+
" >= unixepoch(?) "
|
|
1770
|
+
" AND round(five_hour_percent, 1) "
|
|
1771
|
+
" = round(?, 1)",
|
|
1772
|
+
(
|
|
1773
|
+
int(five_hour_window_key),
|
|
1774
|
+
effective_iso,
|
|
1775
|
+
prior_5h_pct,
|
|
1776
|
+
),
|
|
1467
1777
|
)
|
|
1468
1778
|
conn.commit()
|
|
1469
1779
|
except sqlite3.DatabaseError as exc:
|
|
1470
1780
|
eprint(
|
|
1471
|
-
"[record-usage] post-credit
|
|
1472
|
-
f"failed: {exc}"
|
|
1781
|
+
"[record-usage] 5h post-credit "
|
|
1782
|
+
f"cleanup failed: {exc}"
|
|
1473
1783
|
)
|
|
1784
|
+
except (sqlite3.DatabaseError, ValueError, TypeError) as exc:
|
|
1785
|
+
eprint(
|
|
1786
|
+
f"[record-usage] 5h in-place-credit detection "
|
|
1787
|
+
f"failed: {exc}"
|
|
1788
|
+
)
|
|
1474
1789
|
except (sqlite3.DatabaseError, ValueError) as exc:
|
|
1475
1790
|
eprint(f"[record-usage] reset-event detection failed: {exc}")
|
|
1476
1791
|
|
|
@@ -1511,19 +1826,54 @@ def cmd_record_usage(args: argparse.Namespace) -> int:
|
|
|
1511
1826
|
if max_row and max_row["v"] is not None and round(weekly_percent, 1) < round(float(max_row["v"]), 1):
|
|
1512
1827
|
should_insert = False
|
|
1513
1828
|
else:
|
|
1514
|
-
# 5-hour usage is monotonically non-decreasing within a window
|
|
1515
|
-
#
|
|
1516
|
-
#
|
|
1829
|
+
# 5-hour usage is monotonically non-decreasing within a window
|
|
1830
|
+
# — UNTIL an in-place 5h credit fires. When a
|
|
1831
|
+
# ``five_hour_reset_events`` row exists for THIS
|
|
1832
|
+
# ``five_hour_window_key``, the MAX query filters to samples
|
|
1833
|
+
# captured at-or-after the event's ``effective_reset_at_utc``
|
|
1834
|
+
# so a fresh post-credit OAuth value (e.g. 4%) lands instead
|
|
1835
|
+
# of being re-clamped to the pre-credit max (e.g. 28%). When
|
|
1836
|
+
# no event row exists, ``COALESCE`` defaults to epoch-zero so
|
|
1837
|
+
# the filter is a no-op and legacy clamp behavior is preserved
|
|
1838
|
+
# byte-identically.
|
|
1839
|
+
#
|
|
1840
|
+
# ``unixepoch()`` on BOTH sides for offset robustness — stored
|
|
1841
|
+
# ``captured_at_utc`` carries ``Z`` while
|
|
1842
|
+
# ``effective_reset_at_utc`` carries ``+00:00``. Lex compare
|
|
1843
|
+
# would silently mis-order moments for non-UTC hosts (same
|
|
1844
|
+
# gotcha as the weekly clamp / 5h-block cross-reset flag).
|
|
1845
|
+
#
|
|
1846
|
+
# Joining on ``five_hour_window_key`` (canonical 10-min-floored
|
|
1517
1847
|
# epoch) absorbs Anthropic's seconds-level jitter on
|
|
1518
|
-
# resets_at
|
|
1848
|
+
# ``resets_at``; an ISO-string equality at this site silently
|
|
1519
1849
|
# skipped the clamp every time a jittered fetch landed in
|
|
1520
1850
|
# the same physical 5h window (spec Bug B).
|
|
1851
|
+
#
|
|
1852
|
+
# Spec §4.1 of
|
|
1853
|
+
# docs/superpowers/specs/2026-05-16-5h-in-place-credit-detection.md.
|
|
1521
1854
|
if five_hour_percent is not None and five_hour_window_key is not None:
|
|
1522
1855
|
max_5h_row = conn.execute(
|
|
1523
|
-
"
|
|
1524
|
-
(
|
|
1856
|
+
"""
|
|
1857
|
+
SELECT MAX(five_hour_percent) AS v
|
|
1858
|
+
FROM weekly_usage_snapshots
|
|
1859
|
+
WHERE five_hour_window_key = ?
|
|
1860
|
+
AND unixepoch(captured_at_utc) >= unixepoch(COALESCE(
|
|
1861
|
+
(SELECT effective_reset_at_utc
|
|
1862
|
+
FROM five_hour_reset_events
|
|
1863
|
+
WHERE five_hour_window_key = ?
|
|
1864
|
+
ORDER BY id DESC
|
|
1865
|
+
LIMIT 1),
|
|
1866
|
+
'1970-01-01T00:00:00Z'
|
|
1867
|
+
))
|
|
1868
|
+
""",
|
|
1869
|
+
(int(five_hour_window_key), int(five_hour_window_key)),
|
|
1525
1870
|
).fetchone()
|
|
1526
|
-
if
|
|
1871
|
+
if (
|
|
1872
|
+
max_5h_row
|
|
1873
|
+
and max_5h_row["v"] is not None
|
|
1874
|
+
and round(five_hour_percent, 1)
|
|
1875
|
+
< round(float(max_5h_row["v"]), 1)
|
|
1876
|
+
):
|
|
1527
1877
|
five_hour_percent = float(max_5h_row["v"])
|
|
1528
1878
|
|
|
1529
1879
|
# Dedup vs last snapshot: if BOTH weekly_percent and
|
|
@@ -1627,6 +1977,16 @@ def cmd_record_usage(args: argparse.Namespace) -> int:
|
|
|
1627
1977
|
# last_observed_at_utc is stale relative to the latest
|
|
1628
1978
|
# snapshot's captured_at_utc (the kill landed between
|
|
1629
1979
|
# insert_usage_snapshot and maybe_update_five_hour_block).
|
|
1980
|
+
#
|
|
1981
|
+
# Round-3: ALSO scope the milestone-coverage half of
|
|
1982
|
+
# this probe by ACTIVE 5h SEGMENT. Without this, a
|
|
1983
|
+
# credited block's MAX over the whole milestone ledger
|
|
1984
|
+
# would still read the pre-credit ceiling (e.g. 28%) and
|
|
1985
|
+
# silently suppress the post-credit ledger's heal even
|
|
1986
|
+
# though it has zero rows. Mirrors weekly Probe 1's
|
|
1987
|
+
# segment-aware fix above. Uses
|
|
1988
|
+
# ``_resolve_active_five_hour_reset_event_id`` to find
|
|
1989
|
+
# the active segment for the latest snapshot's window.
|
|
1630
1990
|
need_5h_heal = False
|
|
1631
1991
|
window_key = latest_row["five_hour_window_key"]
|
|
1632
1992
|
if window_key is not None:
|
|
@@ -1643,6 +2003,45 @@ def cmd_record_usage(args: argparse.Namespace) -> int:
|
|
|
1643
2003
|
< latest_row["captured_at_utc"]
|
|
1644
2004
|
):
|
|
1645
2005
|
need_5h_heal = True
|
|
2006
|
+
else:
|
|
2007
|
+
# Block row exists AND last_observed is fresh
|
|
2008
|
+
# — but the post-credit milestone segment may
|
|
2009
|
+
# still owe rows. Scope MAX(percent_threshold)
|
|
2010
|
+
# by the active reset_event_id segment so
|
|
2011
|
+
# post-credit climbs from threshold 1 trigger
|
|
2012
|
+
# heal even when the pre-credit segment already
|
|
2013
|
+
# crossed higher thresholds. Probe shape mirrors
|
|
2014
|
+
# weekly Probe 1 (lines 1922-1956).
|
|
2015
|
+
five_hour_percent_for_probe = latest_row[
|
|
2016
|
+
"five_hour_percent"
|
|
2017
|
+
]
|
|
2018
|
+
if five_hour_percent_for_probe is not None:
|
|
2019
|
+
latest_5h_floor = math.floor(
|
|
2020
|
+
float(five_hour_percent_for_probe) + 1e-9
|
|
2021
|
+
)
|
|
2022
|
+
if latest_5h_floor >= 1:
|
|
2023
|
+
heal_5h_segment = (
|
|
2024
|
+
_resolve_active_five_hour_reset_event_id(
|
|
2025
|
+
heal_conn, int(window_key)
|
|
2026
|
+
)
|
|
2027
|
+
)
|
|
2028
|
+
max_5h_existing = heal_conn.execute(
|
|
2029
|
+
"SELECT MAX(percent_threshold) AS m "
|
|
2030
|
+
"FROM five_hour_milestones "
|
|
2031
|
+
"WHERE five_hour_window_key = ? "
|
|
2032
|
+
" AND reset_event_id = ?",
|
|
2033
|
+
(int(window_key), heal_5h_segment),
|
|
2034
|
+
).fetchone()
|
|
2035
|
+
if (
|
|
2036
|
+
max_5h_existing is None
|
|
2037
|
+
or max_5h_existing["m"] is None
|
|
2038
|
+
):
|
|
2039
|
+
need_5h_heal = True
|
|
2040
|
+
elif (
|
|
2041
|
+
int(max_5h_existing["m"])
|
|
2042
|
+
< latest_5h_floor
|
|
2043
|
+
):
|
|
2044
|
+
need_5h_heal = True
|
|
1646
2045
|
finally:
|
|
1647
2046
|
heal_conn.close()
|
|
1648
2047
|
|