cctally 1.7.1 → 1.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/bin/_cctally_dashboard.py +192 -7
- package/bin/_cctally_db.py +108 -0
- package/bin/_cctally_record.py +199 -14
- package/bin/_cctally_tui.py +117 -11
- package/bin/_lib_blocks.py +33 -6
- package/bin/_lib_doctor.py +79 -0
- package/bin/cctally +650 -23
- package/package.json +1 -1
package/bin/_cctally_record.py
CHANGED
|
@@ -398,7 +398,31 @@ def maybe_record_milestone(
|
|
|
398
398
|
|
|
399
399
|
conn = open_db()
|
|
400
400
|
try:
|
|
401
|
-
|
|
401
|
+
# Resolve the active segment for THIS captured moment. The segment
|
|
402
|
+
# is the latest week_reset_events row keyed on week_end_at whose
|
|
403
|
+
# effective_reset_at_utc <= captured_at; 0 = pre-credit / no-event
|
|
404
|
+
# sentinel. ``unixepoch()`` normalizes the comparison across mixed
|
|
405
|
+
# +00:00 / Z offsets (see precedent at bin/cctally:_compute_block_totals
|
|
406
|
+
# cross-reset detection; also project gotcha
|
|
407
|
+
# ``unixepoch_for_cross_offset_compare``).
|
|
408
|
+
captured_at_iso = saved.get("capturedAt") or now_utc_iso()
|
|
409
|
+
reset_event_id = 0
|
|
410
|
+
if week_end_at:
|
|
411
|
+
seg_row = conn.execute(
|
|
412
|
+
"""
|
|
413
|
+
SELECT id FROM week_reset_events
|
|
414
|
+
WHERE new_week_end_at = ?
|
|
415
|
+
AND unixepoch(effective_reset_at_utc) <= unixepoch(?)
|
|
416
|
+
ORDER BY id DESC LIMIT 1
|
|
417
|
+
""",
|
|
418
|
+
(week_end_at, captured_at_iso),
|
|
419
|
+
).fetchone()
|
|
420
|
+
if seg_row is not None:
|
|
421
|
+
reset_event_id = int(seg_row["id"])
|
|
422
|
+
|
|
423
|
+
max_existing = get_max_milestone_for_week(
|
|
424
|
+
conn, week_start_date, reset_event_id=reset_event_id,
|
|
425
|
+
)
|
|
402
426
|
if max_existing is not None and current_floor <= max_existing:
|
|
403
427
|
return
|
|
404
428
|
|
|
@@ -482,7 +506,10 @@ def maybe_record_milestone(
|
|
|
482
506
|
pending_alerts: list[dict[str, Any]] = []
|
|
483
507
|
for pct in range(start_threshold, current_floor + 1):
|
|
484
508
|
if pct == start_threshold and max_existing is not None:
|
|
485
|
-
prev_cost = get_milestone_cost_for_week(
|
|
509
|
+
prev_cost = get_milestone_cost_for_week(
|
|
510
|
+
conn, week_start_date, max_existing,
|
|
511
|
+
reset_event_id=reset_event_id,
|
|
512
|
+
)
|
|
486
513
|
marginal = (cumulative_cost - prev_cost) if prev_cost is not None else None
|
|
487
514
|
else:
|
|
488
515
|
marginal = None
|
|
@@ -499,6 +526,7 @@ def maybe_record_milestone(
|
|
|
499
526
|
cost_snapshot_id=cost_snapshot_id,
|
|
500
527
|
five_hour_percent_at_crossing=five_hour_percent,
|
|
501
528
|
commit=False,
|
|
529
|
+
reset_event_id=reset_event_id,
|
|
502
530
|
)
|
|
503
531
|
# ── Threshold-actions dispatch (set-then-dispatch, spec §3.2) ──
|
|
504
532
|
# Only the genuine-new-crossing winner (rowcount==1) reaches this
|
|
@@ -523,17 +551,22 @@ def maybe_record_milestone(
|
|
|
523
551
|
conn.execute(
|
|
524
552
|
"UPDATE percent_milestones SET alerted_at = ? "
|
|
525
553
|
"WHERE week_start_date = ? AND percent_threshold = ? "
|
|
526
|
-
"AND
|
|
527
|
-
|
|
554
|
+
" AND reset_event_id = ? "
|
|
555
|
+
" AND alerted_at IS NULL",
|
|
556
|
+
(crossed_at, week_start_date, pct, reset_event_id),
|
|
528
557
|
)
|
|
529
558
|
# Cheap re-read for payload context (cumulative_cost_usd
|
|
530
559
|
# reflects the value persisted on insert, immune to any
|
|
531
560
|
# subsequent recompute drift). SELECT inside the open
|
|
532
561
|
# transaction is fine; values reflect post-INSERT state.
|
|
562
|
+
# Filter by reset_event_id so a credited week's
|
|
563
|
+
# alert payload reads the post-credit row, not a
|
|
564
|
+
# stale pre-credit row at the same (week, threshold).
|
|
533
565
|
row = conn.execute(
|
|
534
566
|
"SELECT cumulative_cost_usd FROM percent_milestones "
|
|
535
|
-
"WHERE week_start_date = ? AND percent_threshold = ?"
|
|
536
|
-
|
|
567
|
+
"WHERE week_start_date = ? AND percent_threshold = ? "
|
|
568
|
+
" AND reset_event_id = ?",
|
|
569
|
+
(week_start_date, pct, reset_event_id),
|
|
537
570
|
).fetchone()
|
|
538
571
|
if row is not None:
|
|
539
572
|
cum = float(row["cumulative_cost_usd"])
|
|
@@ -1305,9 +1338,9 @@ def cmd_record_usage(args: argparse.Namespace) -> int:
|
|
|
1305
1338
|
prior["week_end_at"], "record.prior"
|
|
1306
1339
|
)
|
|
1307
1340
|
prior_pct = prior["weekly_percent"]
|
|
1341
|
+
now_utc = dt.datetime.now(dt.timezone.utc)
|
|
1308
1342
|
if prior_end_canon and prior_end_canon != cur_end_canon:
|
|
1309
1343
|
prior_end_dt = parse_iso_datetime(prior_end_canon, "prior.week_end_at")
|
|
1310
|
-
now_utc = dt.datetime.now(dt.timezone.utc)
|
|
1311
1344
|
# Fire only when (a) prior window was still in the FUTURE
|
|
1312
1345
|
# (Anthropic shifted the boundary before natural expiration),
|
|
1313
1346
|
# AND (b) weekly_percent dropped by RESET_PCT_DROP_THRESHOLD
|
|
@@ -1331,15 +1364,149 @@ def cmd_record_usage(args: argparse.Namespace) -> int:
|
|
|
1331
1364
|
effective_iso),
|
|
1332
1365
|
)
|
|
1333
1366
|
conn.commit()
|
|
1367
|
+
elif prior_end_canon and prior_end_canon == cur_end_canon:
|
|
1368
|
+
# In-place credit branch (v1.7.2). When `resets_at` stays
|
|
1369
|
+
# unchanged but `weekly_percent` drops by RESET_PCT_DROP_THRESHOLD
|
|
1370
|
+
# or more, Anthropic has issued a goodwill in-place weekly
|
|
1371
|
+
# credit. Emit one week_reset_events row keyed on the
|
|
1372
|
+
# current end_at (old == new) so the reset-aware clamp
|
|
1373
|
+
# above and the milestone segment writer can pivot to
|
|
1374
|
+
# the post-credit segment. The seed snapshot lands via
|
|
1375
|
+
# the now-reset-aware clamp on this same call.
|
|
1376
|
+
prior_end_dt = parse_iso_datetime(prior_end_canon, "prior.week_end_at")
|
|
1377
|
+
if (
|
|
1378
|
+
prior_end_dt > now_utc
|
|
1379
|
+
and prior_pct is not None
|
|
1380
|
+
and (float(prior_pct) - float(weekly_percent)) >= c._RESET_PCT_DROP_THRESHOLD
|
|
1381
|
+
):
|
|
1382
|
+
# Pre-check (Q5 belt-and-suspenders): suppress duplicate
|
|
1383
|
+
# event rows for the same new_week_end_at across
|
|
1384
|
+
# consecutive ticks. UNIQUE(old, new) at the DDL
|
|
1385
|
+
# also catches the duplicate in the (old == new) case,
|
|
1386
|
+
# but the pre-check avoids a useless write attempt
|
|
1387
|
+
# and keeps the log clean. After the seed lands at
|
|
1388
|
+
# post-credit %, the next tick's `prior_pct` will be
|
|
1389
|
+
# the post-credit value so the drop predicate alone
|
|
1390
|
+
# also suffices — pre-check is belt-and-suspenders.
|
|
1391
|
+
already = conn.execute(
|
|
1392
|
+
"SELECT 1 FROM week_reset_events "
|
|
1393
|
+
"WHERE new_week_end_at = ? LIMIT 1",
|
|
1394
|
+
(cur_end_canon,),
|
|
1395
|
+
).fetchone()
|
|
1396
|
+
if already is None:
|
|
1397
|
+
effective_dt = _floor_to_hour(now_utc)
|
|
1398
|
+
effective_iso = effective_dt.isoformat(timespec="seconds")
|
|
1399
|
+
# Row shape: old=effective_iso, new=cur_end_canon
|
|
1400
|
+
# (distinct values). The previous shape stored
|
|
1401
|
+
# old==new==cur_end_canon, which let BOTH
|
|
1402
|
+
# _apply_reset_events_to_weekrefs maps
|
|
1403
|
+
# (pre_map[old] and post_map[new]) fire on the
|
|
1404
|
+
# SAME WeekRef — pre_map rewrote week_end_at to
|
|
1405
|
+
# effective, post_map rewrote week_start_at to
|
|
1406
|
+
# effective, collapsing the credited week to a
|
|
1407
|
+
# zero-width window in downstream renders. With
|
|
1408
|
+
# old==effective and new==cur_end_canon, only
|
|
1409
|
+
# post_map fires on the credited week (setting
|
|
1410
|
+
# week_start_at = effective, the intended
|
|
1411
|
+
# behavior); pre_map keys on effective_iso and
|
|
1412
|
+
# finds no matching WeekRef in practice. The
|
|
1413
|
+
# UNIQUE(old, new) constraint permits this
|
|
1414
|
+
# row, and the pre-check above keys on
|
|
1415
|
+
# new_week_end_at so dedup still works.
|
|
1416
|
+
conn.execute(
|
|
1417
|
+
"INSERT OR IGNORE INTO week_reset_events "
|
|
1418
|
+
"(detected_at_utc, old_week_end_at, new_week_end_at, "
|
|
1419
|
+
" effective_reset_at_utc) VALUES (?, ?, ?, ?)",
|
|
1420
|
+
(now_utc_iso(), effective_iso, cur_end_canon,
|
|
1421
|
+
effective_iso),
|
|
1422
|
+
)
|
|
1423
|
+
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.
|
|
1432
|
+
try:
|
|
1433
|
+
(c.APP_DIR / "hwm-7d").write_text(
|
|
1434
|
+
f"{week_start_date} {weekly_percent}\n"
|
|
1435
|
+
)
|
|
1436
|
+
except OSError:
|
|
1437
|
+
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.
|
|
1457
|
+
try:
|
|
1458
|
+
conn.execute(
|
|
1459
|
+
"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)),
|
|
1467
|
+
)
|
|
1468
|
+
conn.commit()
|
|
1469
|
+
except sqlite3.DatabaseError as exc:
|
|
1470
|
+
eprint(
|
|
1471
|
+
"[record-usage] post-credit cleanup "
|
|
1472
|
+
f"failed: {exc}"
|
|
1473
|
+
)
|
|
1334
1474
|
except (sqlite3.DatabaseError, ValueError) as exc:
|
|
1335
1475
|
eprint(f"[record-usage] reset-event detection failed: {exc}")
|
|
1336
1476
|
|
|
1337
|
-
# 7-day usage is monotonically non-decreasing within a billing week
|
|
1338
|
-
#
|
|
1339
|
-
#
|
|
1477
|
+
# 7-day usage is monotonically non-decreasing within a billing week
|
|
1478
|
+
# — UNTIL Anthropic issues an in-place weekly credit. When a
|
|
1479
|
+
# week_reset_events row exists for THIS week_end_at, the MAX query
|
|
1480
|
+
# filters to samples captured at-or-after the segment's
|
|
1481
|
+
# effective_reset_at_utc so a fresh post-credit OAuth value (e.g.
|
|
1482
|
+
# 2%) lands instead of being held back by stale pre-credit history
|
|
1483
|
+
# (e.g. 67%). When no event row exists, COALESCE defaults to
|
|
1484
|
+
# epoch-zero so the filter is a no-op and legacy clamp behavior
|
|
1485
|
+
# is preserved byte-identically.
|
|
1486
|
+
# NB: comparison wrapped with ``unixepoch()`` on BOTH sides.
|
|
1487
|
+
# ``captured_at_utc`` is stored with `Z` suffix, but
|
|
1488
|
+
# ``effective_reset_at_utc`` may have a non-UTC offset on
|
|
1489
|
+
# historical backfill rows written before Bug 3 was fixed
|
|
1490
|
+
# (parse_iso_datetime returned host-local). Lex string compare
|
|
1491
|
+
# on mixed offsets silently mis-orders moments for non-UTC
|
|
1492
|
+
# hosts (CLAUDE.md gotcha: 5h-block cross-reset flag — "all
|
|
1493
|
+
# comparisons go through unixepoch(), NOT lex
|
|
1494
|
+
# BETWEEN/`<`/`>`"). Same rule applies here.
|
|
1340
1495
|
max_row = conn.execute(
|
|
1341
|
-
"
|
|
1342
|
-
(
|
|
1496
|
+
"""
|
|
1497
|
+
SELECT MAX(weekly_percent) AS v
|
|
1498
|
+
FROM weekly_usage_snapshots
|
|
1499
|
+
WHERE week_start_date = ?
|
|
1500
|
+
AND unixepoch(captured_at_utc) >= unixepoch(COALESCE(
|
|
1501
|
+
(SELECT effective_reset_at_utc
|
|
1502
|
+
FROM week_reset_events
|
|
1503
|
+
WHERE new_week_end_at = ?
|
|
1504
|
+
ORDER BY id DESC
|
|
1505
|
+
LIMIT 1),
|
|
1506
|
+
'1970-01-01T00:00:00Z'
|
|
1507
|
+
))
|
|
1508
|
+
""",
|
|
1509
|
+
(week_start_date, week_end_at),
|
|
1343
1510
|
).fetchone()
|
|
1344
1511
|
if max_row and max_row["v"] is not None and round(weekly_percent, 1) < round(float(max_row["v"]), 1):
|
|
1345
1512
|
should_insert = False
|
|
@@ -1426,11 +1593,29 @@ def cmd_record_usage(args: argparse.Namespace) -> int:
|
|
|
1426
1593
|
)
|
|
1427
1594
|
need_milestone_heal = False
|
|
1428
1595
|
if latest_floor >= 1:
|
|
1596
|
+
# v1.7.2: scope the heal probe to the ACTIVE segment.
|
|
1597
|
+
# Without this, a credited week's MAX over the whole
|
|
1598
|
+
# ledger would still read the pre-credit ceiling
|
|
1599
|
+
# (e.g. 67%) and silently suppress the post-credit
|
|
1600
|
+
# ledger's heal even though it has zero rows.
|
|
1601
|
+
captured_at_for_probe = latest_row["captured_at_utc"]
|
|
1602
|
+
week_end_at_for_probe = latest_row["week_end_at"]
|
|
1603
|
+
heal_segment = 0
|
|
1604
|
+
if week_end_at_for_probe and captured_at_for_probe:
|
|
1605
|
+
seg = heal_conn.execute(
|
|
1606
|
+
"SELECT id FROM week_reset_events "
|
|
1607
|
+
"WHERE new_week_end_at = ? "
|
|
1608
|
+
" AND unixepoch(effective_reset_at_utc) <= unixepoch(?) "
|
|
1609
|
+
"ORDER BY id DESC LIMIT 1",
|
|
1610
|
+
(week_end_at_for_probe, captured_at_for_probe),
|
|
1611
|
+
).fetchone()
|
|
1612
|
+
if seg is not None:
|
|
1613
|
+
heal_segment = int(seg["id"])
|
|
1429
1614
|
max_existing = heal_conn.execute(
|
|
1430
1615
|
"SELECT MAX(percent_threshold) AS m "
|
|
1431
1616
|
"FROM percent_milestones "
|
|
1432
|
-
"WHERE week_start_date = ?",
|
|
1433
|
-
(week_start_date,),
|
|
1617
|
+
"WHERE week_start_date = ? AND reset_event_id = ?",
|
|
1618
|
+
(week_start_date, heal_segment),
|
|
1434
1619
|
).fetchone()
|
|
1435
1620
|
if max_existing is None or max_existing["m"] is None:
|
|
1436
1621
|
need_milestone_heal = True
|
package/bin/_cctally_tui.py
CHANGED
|
@@ -317,6 +317,10 @@ def get_milestones_for_week(*args, **kwargs):
|
|
|
317
317
|
return sys.modules["cctally"].get_milestones_for_week(*args, **kwargs)
|
|
318
318
|
|
|
319
319
|
|
|
320
|
+
def _canonicalize_optional_iso(*args, **kwargs):
|
|
321
|
+
return sys.modules["cctally"]._canonicalize_optional_iso(*args, **kwargs)
|
|
322
|
+
|
|
323
|
+
|
|
320
324
|
def get_recent_weeks(*args, **kwargs):
|
|
321
325
|
return sys.modules["cctally"].get_recent_weeks(*args, **kwargs)
|
|
322
326
|
|
|
@@ -949,7 +953,8 @@ class TuiPercentMilestone:
|
|
|
949
953
|
def _tui_build_percent_milestones(
|
|
950
954
|
conn: sqlite3.Connection,
|
|
951
955
|
) -> list[TuiPercentMilestone]:
|
|
952
|
-
"""Return per-percent crossings for the current week
|
|
956
|
+
"""Return per-percent crossings for the current week's ACTIVE
|
|
957
|
+
segment, ascending by percent.
|
|
953
958
|
|
|
954
959
|
Resolves `week_start_date` from the latest `weekly_usage_snapshots` row
|
|
955
960
|
— the same path `cmd_percent_breakdown` takes. The post-override
|
|
@@ -957,15 +962,55 @@ def _tui_build_percent_milestones(
|
|
|
957
962
|
reset, `_apply_midweek_reset_override` shifts that datetime forward to
|
|
958
963
|
the reset instant, whose `.date()` no longer matches the `week_start_date`
|
|
959
964
|
under which milestones were recorded.
|
|
960
|
-
|
|
965
|
+
|
|
966
|
+
v1.7.2: when a `week_reset_events` row exists for the snapshot's
|
|
967
|
+
`week_end_at`, narrow to the active segment so the dashboard /
|
|
968
|
+
TUI milestone panel stays coherent with the already-credit-aware
|
|
969
|
+
header. ``active_segment = 0`` (sentinel) preserves legacy
|
|
970
|
+
behavior on un-credited weeks.
|
|
971
|
+
|
|
972
|
+
Returns [] if no usage snapshot exists, OR if the active segment
|
|
973
|
+
has no milestone rows yet (post-credit "fresh" state).
|
|
961
974
|
"""
|
|
962
975
|
latest = conn.execute(
|
|
963
|
-
"SELECT week_start_date FROM weekly_usage_snapshots "
|
|
976
|
+
"SELECT week_start_date, week_end_at FROM weekly_usage_snapshots "
|
|
977
|
+
"WHERE week_end_at IS NOT NULL "
|
|
964
978
|
"ORDER BY captured_at_utc DESC, id DESC LIMIT 1"
|
|
965
979
|
).fetchone()
|
|
966
980
|
if latest is None:
|
|
967
|
-
|
|
968
|
-
|
|
981
|
+
# Legacy fallback: a snapshot without week_end_at can still have
|
|
982
|
+
# milestones — keep the prior behavior in that path.
|
|
983
|
+
latest = conn.execute(
|
|
984
|
+
"SELECT week_start_date, NULL AS week_end_at "
|
|
985
|
+
"FROM weekly_usage_snapshots "
|
|
986
|
+
"ORDER BY captured_at_utc DESC, id DESC LIMIT 1"
|
|
987
|
+
).fetchone()
|
|
988
|
+
if latest is None:
|
|
989
|
+
return []
|
|
990
|
+
|
|
991
|
+
# Resolve active segment via the canonical end_at.
|
|
992
|
+
active_segment = 0
|
|
993
|
+
if latest["week_end_at"]:
|
|
994
|
+
try:
|
|
995
|
+
canon_end = _canonicalize_optional_iso(
|
|
996
|
+
latest["week_end_at"], "tui.pm.cur"
|
|
997
|
+
)
|
|
998
|
+
except (AttributeError, ValueError):
|
|
999
|
+
canon_end = None
|
|
1000
|
+
if canon_end:
|
|
1001
|
+
seg_row = conn.execute(
|
|
1002
|
+
"SELECT id FROM week_reset_events "
|
|
1003
|
+
"WHERE new_week_end_at = ? "
|
|
1004
|
+
"ORDER BY id DESC LIMIT 1",
|
|
1005
|
+
(canon_end,),
|
|
1006
|
+
).fetchone()
|
|
1007
|
+
if seg_row is not None:
|
|
1008
|
+
active_segment = int(seg_row["id"])
|
|
1009
|
+
|
|
1010
|
+
rows = [
|
|
1011
|
+
r for r in get_milestones_for_week(conn, latest["week_start_date"])
|
|
1012
|
+
if int(r["reset_event_id"] or 0) == active_segment
|
|
1013
|
+
]
|
|
969
1014
|
out: list[TuiPercentMilestone] = []
|
|
970
1015
|
for r in rows:
|
|
971
1016
|
try:
|
|
@@ -1423,27 +1468,71 @@ def _tui_build_trend(
|
|
|
1423
1468
|
columns (`week_start_at`, `used_pct`, `dollars_per_percent`) matches
|
|
1424
1469
|
`cmd_report` byte-for-byte — verified in the bundle regression diff.
|
|
1425
1470
|
"""
|
|
1426
|
-
# `get_recent_weeks` returns WeekRef rows DESC by week_start_date
|
|
1471
|
+
# `get_recent_weeks` returns WeekRef rows DESC by week_start_date,
|
|
1472
|
+
# already routed through `_apply_reset_events_to_weekrefs` so credited
|
|
1473
|
+
# weeks come back as TWO refs (pre-credit + post-credit) sharing the
|
|
1474
|
+
# same `WeekRef.key`.
|
|
1427
1475
|
week_refs = get_recent_weeks(conn, max(1, count))
|
|
1428
1476
|
|
|
1429
1477
|
# Figure out which week_ref corresponds to the current subscription week.
|
|
1430
|
-
#
|
|
1431
|
-
#
|
|
1478
|
+
# Mirrors `cmd_report`'s Bug D pattern: build a current_ref from the
|
|
1479
|
+
# latest usage snapshot, route it through `_apply_reset_events_to_weekrefs`
|
|
1480
|
+
# so its `week_start_at` reflects the post-credit segment (or the
|
|
1481
|
+
# original start for non-credit weeks), then disambiguate the
|
|
1482
|
+
# synthesized pre-credit ref from the live post-credit ref via BOTH
|
|
1483
|
+
# `key` AND `week_start_at`. Key-only equality marks both segments
|
|
1484
|
+
# as current, which is why the dashboard's trend panel previously
|
|
1485
|
+
# showed two adjacent rows both highlighted as "current" with
|
|
1486
|
+
# identical 4.0% values on the user's live in-place credit data.
|
|
1432
1487
|
latest_usage = conn.execute(
|
|
1433
1488
|
"SELECT week_start_date, week_end_date "
|
|
1434
1489
|
"FROM weekly_usage_snapshots "
|
|
1435
1490
|
"ORDER BY captured_at_utc DESC, id DESC LIMIT 1"
|
|
1436
1491
|
).fetchone()
|
|
1437
1492
|
current_key: str | None = None
|
|
1438
|
-
|
|
1493
|
+
current_week_start_at: str | None = None
|
|
1494
|
+
if latest_usage is not None and latest_usage["week_start_date"] is not None:
|
|
1439
1495
|
current_key = latest_usage["week_start_date"]
|
|
1496
|
+
try:
|
|
1497
|
+
c = _cctally()
|
|
1498
|
+
canon_start, canon_end = c._get_canonical_boundary_for_date(
|
|
1499
|
+
conn, latest_usage["week_start_date"]
|
|
1500
|
+
)
|
|
1501
|
+
current_ref = c.make_week_ref(
|
|
1502
|
+
week_start_date=latest_usage["week_start_date"],
|
|
1503
|
+
week_end_date=latest_usage["week_end_date"],
|
|
1504
|
+
week_start_at=canon_start,
|
|
1505
|
+
week_end_at=canon_end,
|
|
1506
|
+
)
|
|
1507
|
+
_adjusted = c._apply_reset_events_to_weekrefs(conn, [current_ref])
|
|
1508
|
+
if _adjusted:
|
|
1509
|
+
current_week_start_at = _adjusted[0].week_start_at
|
|
1510
|
+
except (ValueError, sqlite3.DatabaseError, AttributeError):
|
|
1511
|
+
current_week_start_at = None
|
|
1440
1512
|
|
|
1441
1513
|
# Build an intermediate list of (week_ref, used_pct, dpp) in oldest-first
|
|
1442
1514
|
# chronological order.
|
|
1443
1515
|
chrono = list(reversed(week_refs))
|
|
1516
|
+
# Split-key set (Bug D): credited weeks appear twice in `week_refs`
|
|
1517
|
+
# with identical `WeekRef.key`. For those keys ONLY, pin
|
|
1518
|
+
# `as_of_utc=week_ref.week_end_at` so each segment finds its own
|
|
1519
|
+
# latest snapshot — without this both segments collapse to the
|
|
1520
|
+
# post-credit snapshot's weekly_percent. Non-credit weeks (single
|
|
1521
|
+
# ref per key) keep the legacy unfiltered lookup.
|
|
1522
|
+
_split_keys = {
|
|
1523
|
+
r.key
|
|
1524
|
+
for r in week_refs
|
|
1525
|
+
if sum(1 for x in week_refs if x.key == r.key) > 1
|
|
1526
|
+
}
|
|
1444
1527
|
intermediate: list[tuple[Any, float | None, float | None]] = []
|
|
1445
1528
|
for week_ref in chrono:
|
|
1446
|
-
usage = get_latest_usage_for_week(
|
|
1529
|
+
usage = get_latest_usage_for_week(
|
|
1530
|
+
conn,
|
|
1531
|
+
week_ref,
|
|
1532
|
+
as_of_utc=(
|
|
1533
|
+
week_ref.week_end_at if week_ref.key in _split_keys else None
|
|
1534
|
+
),
|
|
1535
|
+
)
|
|
1447
1536
|
# See cmd_report for why reset-affected weeks skip the cost cache
|
|
1448
1537
|
# and live-compute from session_entries over the effective range.
|
|
1449
1538
|
if _week_ref_has_reset_event(conn, week_ref):
|
|
@@ -1491,6 +1580,23 @@ def _tui_build_trend(
|
|
|
1491
1580
|
# localizing midnight-UTC doesn't shift it to the prior day in
|
|
1492
1581
|
# zones west of UTC (e.g. 2026-04-14 → "Apr 13" in America/New_York).
|
|
1493
1582
|
week_label = week_ref.week_start.strftime("%b %d")
|
|
1583
|
+
# Bug G (v1.7.2 round-5): match on BOTH `key` AND `week_start_at`
|
|
1584
|
+
# for credited weeks so the pre-credit synthesized ref doesn't
|
|
1585
|
+
# also light up as "current" — both refs share `key`, only their
|
|
1586
|
+
# `week_start_at` differs (post-credit = effective reset moment,
|
|
1587
|
+
# pre-credit = original API-derived start). Non-credit weeks
|
|
1588
|
+
# have only one ref per key so `week_start_at` matching is
|
|
1589
|
+
# automatic. When `current_week_start_at` is None (no reset
|
|
1590
|
+
# event for the current week, or the resolution above failed),
|
|
1591
|
+
# falls back to legacy key-only matching.
|
|
1592
|
+
is_cur = (
|
|
1593
|
+
current_key is not None
|
|
1594
|
+
and week_ref.key == current_key
|
|
1595
|
+
and (
|
|
1596
|
+
current_week_start_at is None
|
|
1597
|
+
or week_ref.week_start_at == current_week_start_at
|
|
1598
|
+
)
|
|
1599
|
+
)
|
|
1494
1600
|
out.append(TuiTrendRow(
|
|
1495
1601
|
week_label=week_label,
|
|
1496
1602
|
week_start_at=week_start_dt,
|
|
@@ -1502,7 +1608,7 @@ def _tui_build_trend(
|
|
|
1502
1608
|
dollars_per_percent=dpp,
|
|
1503
1609
|
delta_dpp=delta,
|
|
1504
1610
|
spark_height=spark,
|
|
1505
|
-
is_current=
|
|
1611
|
+
is_current=is_cur,
|
|
1506
1612
|
))
|
|
1507
1613
|
if dpp is not None:
|
|
1508
1614
|
prev_dpp = dpp
|
package/bin/_lib_blocks.py
CHANGED
|
@@ -94,6 +94,7 @@ def _group_entries_into_blocks(
|
|
|
94
94
|
mode: str = "auto",
|
|
95
95
|
*,
|
|
96
96
|
recorded_windows: list[dt.datetime] | None = None,
|
|
97
|
+
block_start_overrides: dict[dt.datetime, dt.datetime] | None = None,
|
|
97
98
|
now: dt.datetime | None = None,
|
|
98
99
|
) -> list[Block]:
|
|
99
100
|
"""Group sorted UsageEntry objects into 5-hour blocks with gap detection.
|
|
@@ -106,6 +107,18 @@ def _group_entries_into_blocks(
|
|
|
106
107
|
into per-R buckets and built as 'recorded' blocks. Leftover entries
|
|
107
108
|
run through the existing gap-detection heuristic (anchor='heuristic').
|
|
108
109
|
|
|
110
|
+
`block_start_overrides` (v1.7.2 round-5 / Bug J): an optional
|
|
111
|
+
`{R → block_start_at}` map. When present for a given R, the
|
|
112
|
+
recorded block's displayed ``start_time`` becomes the override
|
|
113
|
+
instead of the default ``R - BLOCK_DURATION``. Used by
|
|
114
|
+
``_load_recorded_five_hour_windows`` to preserve the real
|
|
115
|
+
``five_hour_blocks.block_start_at`` for credit-truncated windows
|
|
116
|
+
(an in-place credit shortens the prior 5h block's effective end
|
|
117
|
+
to the credit moment, but the block's API-derived START is
|
|
118
|
+
unchanged — without an override the renderer would compute
|
|
119
|
+
``start = truncated_R - 5h`` which is hours before the real start
|
|
120
|
+
and confuses the user with an off-by-hours window header).
|
|
121
|
+
|
|
109
122
|
`now` pins the current instant (typically via `_command_as_of()`). When
|
|
110
123
|
omitted, falls back to wall clock so existing callers are unaffected.
|
|
111
124
|
"""
|
|
@@ -117,13 +130,22 @@ def _group_entries_into_blocks(
|
|
|
117
130
|
now = dt.datetime.now(dt.timezone.utc)
|
|
118
131
|
|
|
119
132
|
recorded_windows = sorted(recorded_windows or [])
|
|
133
|
+
block_start_overrides = block_start_overrides or {}
|
|
120
134
|
|
|
121
135
|
# ── Partition entries by recorded windows ──────────────────────────
|
|
122
136
|
# For each R in recorded_windows, entries whose timestamp falls in
|
|
123
|
-
# [
|
|
124
|
-
# (gaps between recorded windows, or fully outside any window)
|
|
125
|
-
# into `leftover` and runs through the existing heuristic
|
|
126
|
-
#
|
|
137
|
+
# [override_start_or_R-5h, R) go into recorded_buckets[R]. Everything
|
|
138
|
+
# else (gaps between recorded windows, or fully outside any window)
|
|
139
|
+
# drops into `leftover` and runs through the existing heuristic
|
|
140
|
+
# grouper.
|
|
141
|
+
#
|
|
142
|
+
# Why override_start_or_R-5h, not always R-5h: a credit-truncated
|
|
143
|
+
# canonical block has R = effective_reset_at_utc (e.g. 17:58Z) but
|
|
144
|
+
# its real ``block_start_at`` is unchanged (e.g. 15:50Z). Using
|
|
145
|
+
# `R - 5h` as the partition floor would pull entries from earlier
|
|
146
|
+
# blocks (e.g. 12:58-15:50Z range) into the truncated bucket. The
|
|
147
|
+
# override keeps the real start so each entry lands in the bucket
|
|
148
|
+
# whose API-defined interval actually contains it.
|
|
127
149
|
recorded_buckets: dict[dt.datetime, list[UsageEntry]] = {
|
|
128
150
|
R: [] for R in recorded_windows
|
|
129
151
|
}
|
|
@@ -132,7 +154,8 @@ def _group_entries_into_blocks(
|
|
|
132
154
|
idx = bisect.bisect_right(recorded_windows, entry.timestamp)
|
|
133
155
|
if idx < len(recorded_windows):
|
|
134
156
|
R = recorded_windows[idx]
|
|
135
|
-
|
|
157
|
+
bucket_start = block_start_overrides.get(R, R - BLOCK_DURATION)
|
|
158
|
+
if bucket_start <= entry.timestamp:
|
|
136
159
|
recorded_buckets[R].append(entry)
|
|
137
160
|
continue
|
|
138
161
|
leftover.append(entry)
|
|
@@ -193,7 +216,11 @@ def _group_entries_into_blocks(
|
|
|
193
216
|
bucket = recorded_buckets[R]
|
|
194
217
|
if not bucket:
|
|
195
218
|
continue
|
|
196
|
-
|
|
219
|
+
# Display start: override when present (credit-truncated
|
|
220
|
+
# canonical blocks need their real block_start_at so the
|
|
221
|
+
# rendered window header matches Anthropic's actual interval);
|
|
222
|
+
# default to R - BLOCK_DURATION for normal canonical anchors.
|
|
223
|
+
start_time = block_start_overrides.get(R, R - BLOCK_DURATION)
|
|
197
224
|
end_time = R
|
|
198
225
|
bucket_sorted = sorted(bucket, key=lambda e: e.timestamp)
|
|
199
226
|
blk = _build_activity_block(
|
package/bin/_lib_doctor.py
CHANGED
|
@@ -60,6 +60,19 @@ class DoctorState:
|
|
|
60
60
|
# skipped/failed/pending or (b) a buggy writer slipped through
|
|
61
61
|
# after the migration ran.
|
|
62
62
|
forked_bucket_counts: Optional[dict]
|
|
63
|
+
# v1.7.2 credited-week tracking. Each entry is a dict with:
|
|
64
|
+
# * ``week_start_date`` — the credited week's bucket key
|
|
65
|
+
# * ``latest_weekly_percent`` — most recent weekly_percent for that
|
|
66
|
+
# week (used to gate the WARN — a credit + 0% means the user
|
|
67
|
+
# hasn't started the new segment yet, which is the EXPECTED
|
|
68
|
+
# state and shouldn't warn)
|
|
69
|
+
# * ``post_credit_milestone_count`` — count of percent_milestones
|
|
70
|
+
# rows with ``reset_event_id`` matching the credit event for
|
|
71
|
+
# this week
|
|
72
|
+
# None means the stats.db couldn't be opened to gather; check
|
|
73
|
+
# degrades to OK rather than FAIL (consistent with the rest of
|
|
74
|
+
# the doctor kernel's degradation posture).
|
|
75
|
+
credited_weeks: Optional[list[dict]]
|
|
63
76
|
codex_entries_count: Optional[int]
|
|
64
77
|
codex_last_entry_at: Optional[dt.datetime]
|
|
65
78
|
codex_jsonl_present: bool
|
|
@@ -559,6 +572,71 @@ def _check_data_forked_buckets(s: DoctorState) -> CheckResult:
|
|
|
559
572
|
|
|
560
573
|
|
|
561
574
|
|
|
575
|
+
def _check_data_post_credit_milestones(s: DoctorState) -> CheckResult:
|
|
576
|
+
"""Invariant: for every week with a ``week_reset_events`` row whose
|
|
577
|
+
``effective_reset_at_utc`` is in the past AND latest_weekly_percent
|
|
578
|
+
>= 1.0, the percent_milestones ledger should have at least one row
|
|
579
|
+
in the credit's segment.
|
|
580
|
+
|
|
581
|
+
Pre-v1.7.2 the milestone writer didn't know about segments, so a
|
|
582
|
+
credited week could have a non-empty pre-credit ledger but zero
|
|
583
|
+
post-credit rows even after the user's usage climbed past 1%. This
|
|
584
|
+
check surfaces that drift as a WARN (informational; no remediation
|
|
585
|
+
— the next ``record-usage`` tick at >=1% will self-heal via the
|
|
586
|
+
segment-aware probe).
|
|
587
|
+
|
|
588
|
+
OK (silent) when:
|
|
589
|
+
* No credited weeks exist (state.credited_weeks is empty/None).
|
|
590
|
+
* Every credited week has at least one post-credit milestone row
|
|
591
|
+
OR latest_weekly_percent < 1.0 (= "new segment not started yet,
|
|
592
|
+
which is expected on a fresh credit").
|
|
593
|
+
"""
|
|
594
|
+
weeks = s.credited_weeks
|
|
595
|
+
if weeks is None:
|
|
596
|
+
# Gather failed (stats.db open error). Don't double-warn; the
|
|
597
|
+
# db.stats.file check already covers DB-open issues.
|
|
598
|
+
return CheckResult(
|
|
599
|
+
id="data.post_credit_milestones",
|
|
600
|
+
title="Post-credit milestones",
|
|
601
|
+
severity="ok",
|
|
602
|
+
summary="no data",
|
|
603
|
+
remediation=None,
|
|
604
|
+
details={"reason": "credited_weeks gather returned None"},
|
|
605
|
+
)
|
|
606
|
+
stuck = [
|
|
607
|
+
w for w in weeks
|
|
608
|
+
if float(w.get("latest_weekly_percent") or 0.0) >= 1.0
|
|
609
|
+
and int(w.get("post_credit_milestone_count") or 0) == 0
|
|
610
|
+
]
|
|
611
|
+
if not stuck:
|
|
612
|
+
return CheckResult(
|
|
613
|
+
id="data.post_credit_milestones",
|
|
614
|
+
title="Post-credit milestones",
|
|
615
|
+
severity="ok",
|
|
616
|
+
summary=(
|
|
617
|
+
f"{len(weeks)} credited week(s); all tracked"
|
|
618
|
+
if weeks else "no credited weeks"
|
|
619
|
+
),
|
|
620
|
+
remediation=None,
|
|
621
|
+
details={"credited_weeks": len(weeks)},
|
|
622
|
+
)
|
|
623
|
+
starts = ", ".join(sorted(w["week_start_date"] for w in stuck))
|
|
624
|
+
return CheckResult(
|
|
625
|
+
id="data.post_credit_milestones",
|
|
626
|
+
title="Post-credit milestones",
|
|
627
|
+
severity="warn",
|
|
628
|
+
summary=(
|
|
629
|
+
f"{len(stuck)} credited week(s) with no post-credit milestone "
|
|
630
|
+
f"crossings yet: {starts}"
|
|
631
|
+
),
|
|
632
|
+
remediation=None,
|
|
633
|
+
details={
|
|
634
|
+
"stuck_week_count": len(stuck),
|
|
635
|
+
"stuck_week_starts": [w["week_start_date"] for w in stuck],
|
|
636
|
+
},
|
|
637
|
+
)
|
|
638
|
+
|
|
639
|
+
|
|
562
640
|
_LOOPBACK_HOSTS = frozenset({"loopback", "127.0.0.1", "::1", "localhost"})
|
|
563
641
|
|
|
564
642
|
|
|
@@ -786,6 +864,7 @@ _CATEGORY_DEFINITIONS: tuple[tuple[str, str, tuple[tuple[str, str], ...]], ...]
|
|
|
786
864
|
("data.cache_sync_state", "_check_data_cache_sync_state"),
|
|
787
865
|
("data.codex_cache", "_check_data_codex_cache"),
|
|
788
866
|
("data.forked_buckets", "_check_data_forked_buckets"),
|
|
867
|
+
("data.post_credit_milestones", "_check_data_post_credit_milestones"),
|
|
789
868
|
)),
|
|
790
869
|
("safety", "Safety", (
|
|
791
870
|
("safety.dashboard_bind", "_check_safety_dashboard_bind"),
|