cctally 1.82.1 → 1.83.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +63 -0
- package/README.md +12 -5
- package/bin/_cctally_alerts.py +8 -1
- package/bin/_cctally_cache.py +912 -149
- package/bin/_cctally_config.py +43 -4
- package/bin/_cctally_core.py +933 -759
- package/bin/_cctally_dashboard.py +157 -47
- package/bin/_cctally_dashboard_cache_report.py +13 -6
- package/bin/_cctally_dashboard_conversation.py +1 -0
- package/bin/_cctally_dashboard_envelope.py +116 -8
- package/bin/_cctally_dashboard_share.py +50 -19
- package/bin/_cctally_dashboard_sources.py +223 -48
- package/bin/_cctally_db.py +605 -128
- package/bin/_cctally_doctor.py +417 -28
- package/bin/_cctally_five_hour.py +12 -5
- package/bin/_cctally_journal.py +2050 -156
- package/bin/_cctally_journal_repair.py +519 -0
- package/bin/_cctally_milestone_history.py +142 -56
- package/bin/_cctally_milestones.py +179 -111
- package/bin/_cctally_parser.py +42 -0
- package/bin/_cctally_project.py +24 -18
- package/bin/_cctally_quota.py +139 -25
- package/bin/_cctally_record.py +279 -108
- package/bin/_cctally_rederive.py +1052 -0
- package/bin/_cctally_reporting.py +58 -53
- package/bin/_cctally_setup.py +1 -0
- package/bin/_cctally_source_analytics.py +4 -1
- package/bin/_cctally_statusline.py +11 -11
- package/bin/_cctally_store.py +1039 -31
- package/bin/_cctally_sync_week.py +17 -8
- package/bin/_cctally_tui.py +350 -44
- package/bin/_cctally_update.py +133 -8
- package/bin/_cctally_weekrefs.py +14 -0
- package/bin/_lib_aggregators.py +10 -6
- package/bin/_lib_cache_report.py +101 -9
- package/bin/_lib_codex_pools.py +82 -0
- package/bin/_lib_conversation_query.py +81 -33
- package/bin/_lib_dashboard_sources.py +75 -0
- package/bin/_lib_diff_kernel.py +28 -15
- package/bin/_lib_doctor.py +342 -4
- package/bin/_lib_journal.py +924 -2
- package/bin/_lib_jsonl.py +43 -14
- package/bin/_lib_pricing.py +140 -21
- package/bin/_lib_rederive.py +395 -0
- package/bin/_lib_share.py +58 -2
- package/bin/cctally +56 -8
- package/dashboard/static/assets/{index-BKM43pxK.js → index-3bgCMVHb.js} +52 -52
- package/dashboard/static/assets/index-D27EIHEI.css +1 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +5 -1
- package/dashboard/static/assets/index-Dk1nplOz.css +0 -1
package/bin/_cctally_tui.py
CHANGED
|
@@ -1030,6 +1030,100 @@ def _tui_build_five_hour_milestones(
|
|
|
1030
1030
|
return out
|
|
1031
1031
|
|
|
1032
1032
|
|
|
1033
|
+
@dataclass(frozen=True)
|
|
1034
|
+
class SyncFailureAttribution:
|
|
1035
|
+
"""Database ownership retained at the dashboard leg catch boundary.
|
|
1036
|
+
|
|
1037
|
+
The raw compatibility string remains on ``DataSnapshot.last_sync_error``;
|
|
1038
|
+
this compact sidecar carries only the facts needed for truthful,
|
|
1039
|
+
privacy-safe envelope classification.
|
|
1040
|
+
"""
|
|
1041
|
+
|
|
1042
|
+
leg: str
|
|
1043
|
+
database: str
|
|
1044
|
+
corruption: bool
|
|
1045
|
+
|
|
1046
|
+
|
|
1047
|
+
class _StatsSnapshotCorruption(Exception):
|
|
1048
|
+
"""Internal control signal for one post-query stats heal attempt."""
|
|
1049
|
+
|
|
1050
|
+
def __init__(self, cause: Exception) -> None:
|
|
1051
|
+
self.cause = cause
|
|
1052
|
+
super().__init__(str(cause))
|
|
1053
|
+
|
|
1054
|
+
|
|
1055
|
+
def _tui_attribute_corruption(
|
|
1056
|
+
conn: sqlite3.Connection,
|
|
1057
|
+
exc: Exception,
|
|
1058
|
+
*,
|
|
1059
|
+
database: str,
|
|
1060
|
+
) -> tuple[str, bool]:
|
|
1061
|
+
"""Classify corruption against the actual database at the catch site.
|
|
1062
|
+
|
|
1063
|
+
Mixed stats/cache builders can surface the same SQLite message from either
|
|
1064
|
+
family. Only after a corruption-shaped exception do we run the expensive
|
|
1065
|
+
stats ``quick_check``: a failed/non-ok result positively attributes stats;
|
|
1066
|
+
an intact stats family leaves the failure attributed to cache. No path or
|
|
1067
|
+
exception-text parsing is used for database identity.
|
|
1068
|
+
"""
|
|
1069
|
+
|
|
1070
|
+
corruption = bool(_cctally()._is_sqlite_corruption_error(exc))
|
|
1071
|
+
attributed = database
|
|
1072
|
+
if corruption and database == "stats_or_cache":
|
|
1073
|
+
try:
|
|
1074
|
+
rows = conn.execute("PRAGMA quick_check").fetchall()
|
|
1075
|
+
stats_ok = len(rows) == 1 and tuple(rows[0]) == ("ok",)
|
|
1076
|
+
attributed = "cache" if stats_ok else "stats"
|
|
1077
|
+
except sqlite3.DatabaseError:
|
|
1078
|
+
attributed = "stats"
|
|
1079
|
+
return attributed, corruption
|
|
1080
|
+
|
|
1081
|
+
|
|
1082
|
+
def _tui_capture_sync_failure(
|
|
1083
|
+
conn: sqlite3.Connection,
|
|
1084
|
+
errors: list[str],
|
|
1085
|
+
failures: list[SyncFailureAttribution],
|
|
1086
|
+
*,
|
|
1087
|
+
leg: str,
|
|
1088
|
+
database: str,
|
|
1089
|
+
exc: Exception,
|
|
1090
|
+
stats_heal_attempted: bool,
|
|
1091
|
+
) -> None:
|
|
1092
|
+
"""Record one attributed leg failure or request the single stats retry."""
|
|
1093
|
+
|
|
1094
|
+
attributed_database, corruption = _tui_attribute_corruption(
|
|
1095
|
+
conn, exc, database=database
|
|
1096
|
+
)
|
|
1097
|
+
failure = SyncFailureAttribution(
|
|
1098
|
+
leg=leg,
|
|
1099
|
+
database=attributed_database,
|
|
1100
|
+
corruption=corruption,
|
|
1101
|
+
)
|
|
1102
|
+
if (
|
|
1103
|
+
failure.database == "stats"
|
|
1104
|
+
and failure.corruption
|
|
1105
|
+
and not stats_heal_attempted
|
|
1106
|
+
):
|
|
1107
|
+
raise _StatsSnapshotCorruption(exc)
|
|
1108
|
+
failures.append(failure)
|
|
1109
|
+
errors.append(f"{leg}: {exc}")
|
|
1110
|
+
|
|
1111
|
+
|
|
1112
|
+
def _tui_heal_post_query_stats(exc: Exception) -> bool:
|
|
1113
|
+
"""Invoke the existing stats replacement engine after all handles close."""
|
|
1114
|
+
|
|
1115
|
+
import _cctally_store
|
|
1116
|
+
|
|
1117
|
+
heal = getattr(_cctally_store, "HEAL_HOOK", None)
|
|
1118
|
+
if heal is None:
|
|
1119
|
+
return False
|
|
1120
|
+
try:
|
|
1121
|
+
return bool(heal("stats", exc, post_query=True))
|
|
1122
|
+
except Exception as heal_exc: # noqa: BLE001 — snapshot must still degrade
|
|
1123
|
+
eprint(f"[heal] dashboard stats auto-heal failed: {heal_exc}")
|
|
1124
|
+
return False
|
|
1125
|
+
|
|
1126
|
+
|
|
1033
1127
|
@dataclass
|
|
1034
1128
|
class DataSnapshot:
|
|
1035
1129
|
"""All data needed to render one TUI frame. Produced by sync thread,
|
|
@@ -1041,6 +1135,7 @@ class DataSnapshot:
|
|
|
1041
1135
|
last_sync_at: float | None # monotonic (time.monotonic())
|
|
1042
1136
|
last_sync_error: str | None
|
|
1043
1137
|
generated_at: dt.datetime # wall-clock UTC for displayed timestamps
|
|
1138
|
+
sync_failures: tuple[SyncFailureAttribution, ...] = ()
|
|
1044
1139
|
# ---- v2 additions (spec §4.5) ----
|
|
1045
1140
|
percent_milestones: list[TuiPercentMilestone] = field(default_factory=list)
|
|
1046
1141
|
weekly_history: list[TuiTrendRow] = field(default_factory=list)
|
|
@@ -1922,7 +2017,8 @@ def _fetch_affected_session_entries(
|
|
|
1922
2017
|
sql = (
|
|
1923
2018
|
"SELECT se.timestamp_utc, se.model, se.input_tokens, se.output_tokens, "
|
|
1924
2019
|
" se.cache_create_tokens, se.cache_read_tokens, se.source_path, "
|
|
1925
|
-
" sf.session_id, sf.project_path, se.cost_usd_raw, se.speed "
|
|
2020
|
+
" sf.session_id, sf.project_path, se.cost_usd_raw, se.speed, "
|
|
2021
|
+
" se.cache_create_1h_tokens "
|
|
1926
2022
|
"FROM session_entries se "
|
|
1927
2023
|
"LEFT JOIN session_files sf ON sf.path = se.source_path "
|
|
1928
2024
|
"WHERE se.timestamp_utc >= ? AND se.timestamp_utc <= ? "
|
|
@@ -1958,6 +2054,7 @@ def _fetch_affected_session_entries(
|
|
|
1958
2054
|
project_path=row[8],
|
|
1959
2055
|
cost_usd=row[9],
|
|
1960
2056
|
usage_extra=({"speed": row[10]} if row[10] is not None else None),
|
|
2057
|
+
cache_1h_tokens=row[11], # #195
|
|
1961
2058
|
)
|
|
1962
2059
|
for row in rows
|
|
1963
2060
|
]
|
|
@@ -2038,7 +2135,7 @@ def _tui_build_session_detail_indexed(
|
|
|
2038
2135
|
f" se.input_tokens, se.output_tokens, "
|
|
2039
2136
|
f" se.cache_create_tokens, se.cache_read_tokens, "
|
|
2040
2137
|
f" se.source_path, sf.session_id, sf.project_path, "
|
|
2041
|
-
f" se.cost_usd_raw "
|
|
2138
|
+
f" se.cost_usd_raw, se.speed, se.cache_create_1h_tokens "
|
|
2042
2139
|
f"FROM session_entries se "
|
|
2043
2140
|
f"LEFT JOIN session_files sf ON sf.path = se.source_path "
|
|
2044
2141
|
f"WHERE se.timestamp_utc >= ? AND se.timestamp_utc <= ? "
|
|
@@ -2058,6 +2155,8 @@ def _tui_build_session_detail_indexed(
|
|
|
2058
2155
|
session_id=row[7],
|
|
2059
2156
|
project_path=row[8],
|
|
2060
2157
|
cost_usd=row[9],
|
|
2158
|
+
usage_extra=({"speed": row[10]} if row[10] is not None else None),
|
|
2159
|
+
cache_1h_tokens=row[11], # #195
|
|
2061
2160
|
)
|
|
2062
2161
|
for row in cur
|
|
2063
2162
|
]
|
|
@@ -2384,6 +2483,73 @@ def _tui_project_claude_source_data(legacy_envelope: object) -> dict[str, object
|
|
|
2384
2483
|
}
|
|
2385
2484
|
|
|
2386
2485
|
|
|
2486
|
+
def _tui_claude_domain_freshness(
|
|
2487
|
+
source_data: dict[str, object] | None,
|
|
2488
|
+
) -> dict[str, str]:
|
|
2489
|
+
"""Derive Claude axes from its selected weekly snapshot evidence.
|
|
2490
|
+
|
|
2491
|
+
The legacy current-week label has a third presentation-only ``aging``
|
|
2492
|
+
state. The source contract deliberately keeps the frozen fresh/stale
|
|
2493
|
+
vocabulary: only the exact stale label moves the weekly hero/quota axes.
|
|
2494
|
+
Missing evidence remains a capability/availability concern.
|
|
2495
|
+
"""
|
|
2496
|
+
data = source_data if isinstance(source_data, dict) else {}
|
|
2497
|
+
hero = data.get("hero")
|
|
2498
|
+
current_week = hero.get("current_week") if isinstance(hero, dict) else None
|
|
2499
|
+
freshness = (
|
|
2500
|
+
current_week.get("freshness")
|
|
2501
|
+
if isinstance(current_week, dict) else None
|
|
2502
|
+
)
|
|
2503
|
+
weekly = (
|
|
2504
|
+
"stale"
|
|
2505
|
+
if isinstance(freshness, dict) and freshness.get("label") == "stale"
|
|
2506
|
+
else "fresh"
|
|
2507
|
+
)
|
|
2508
|
+
return {"hero": weekly, "quota": weekly, "sessions": "fresh"}
|
|
2509
|
+
|
|
2510
|
+
|
|
2511
|
+
def _refresh_claude_source_clock(
|
|
2512
|
+
state: SourceDashboardState,
|
|
2513
|
+
*,
|
|
2514
|
+
current_week: object,
|
|
2515
|
+
now_utc: dt.datetime,
|
|
2516
|
+
raw_config: dict[str, object],
|
|
2517
|
+
) -> SourceDashboardState:
|
|
2518
|
+
"""Advance Claude's weekly axes from frozen snapshot evidence only."""
|
|
2519
|
+
if state.source != "claude":
|
|
2520
|
+
return state
|
|
2521
|
+
if now_utc.tzinfo is None or now_utc.utcoffset() is None:
|
|
2522
|
+
raise ValueError("now_utc must be timezone-aware")
|
|
2523
|
+
captured = getattr(current_week, "latest_snapshot_at", None)
|
|
2524
|
+
if not isinstance(captured, dt.datetime):
|
|
2525
|
+
return state
|
|
2526
|
+
if captured.tzinfo is None or captured.utcoffset() is None:
|
|
2527
|
+
captured = captured.replace(tzinfo=dt.timezone.utc)
|
|
2528
|
+
age_seconds = max(
|
|
2529
|
+
0.0,
|
|
2530
|
+
(
|
|
2531
|
+
now_utc.astimezone(dt.timezone.utc)
|
|
2532
|
+
- captured.astimezone(dt.timezone.utc)
|
|
2533
|
+
).total_seconds(),
|
|
2534
|
+
)
|
|
2535
|
+
try:
|
|
2536
|
+
freshness_config = _get_oauth_usage_config(raw_config)
|
|
2537
|
+
except Exception:
|
|
2538
|
+
freshness_config = _OAUTH_USAGE_DEFAULTS
|
|
2539
|
+
weekly = (
|
|
2540
|
+
"stale"
|
|
2541
|
+
if _freshness_label(age_seconds, freshness_config) == "stale"
|
|
2542
|
+
else "fresh"
|
|
2543
|
+
)
|
|
2544
|
+
domain_freshness = dict(state.domain_freshness or {})
|
|
2545
|
+
domain_freshness.update({"hero": weekly, "quota": weekly})
|
|
2546
|
+
refreshed = dataclasses.replace(
|
|
2547
|
+
state,
|
|
2548
|
+
domain_freshness=domain_freshness,
|
|
2549
|
+
)
|
|
2550
|
+
return state if refreshed == state else refreshed
|
|
2551
|
+
|
|
2552
|
+
|
|
2387
2553
|
def _tui_build_source_bundle(
|
|
2388
2554
|
*,
|
|
2389
2555
|
stats_conn,
|
|
@@ -2409,18 +2575,18 @@ def _tui_build_source_bundle(
|
|
|
2409
2575
|
c = _cctally()
|
|
2410
2576
|
cache_conn = c.open_cache_db()
|
|
2411
2577
|
cache_read_tx = False
|
|
2412
|
-
stats_read_tx = False
|
|
2413
2578
|
try:
|
|
2414
|
-
#
|
|
2415
|
-
#
|
|
2416
|
-
#
|
|
2417
|
-
#
|
|
2579
|
+
# Keep cache.db on one stable snapshot, but leave stats.db in
|
|
2580
|
+
# statement-scoped autocommit. A dashboard source build can spend
|
|
2581
|
+
# seconds folding provider rows; holding one stats read transaction
|
|
2582
|
+
# across that CPU work pins every intervening WAL frame and defeats
|
|
2583
|
+
# SQLite's default 1,000-page autocheckpoint (#393). The before/after
|
|
2584
|
+
# composite signature below is already the cross-database consistency
|
|
2585
|
+
# gate: any stats generation movement rejects this build, so a long
|
|
2586
|
+
# stats snapshot buys no correctness and creates unbounded WAL growth.
|
|
2418
2587
|
if not cache_conn.in_transaction:
|
|
2419
2588
|
cache_conn.execute("BEGIN")
|
|
2420
2589
|
cache_read_tx = True
|
|
2421
|
-
if not stats_conn.in_transaction:
|
|
2422
|
-
stats_conn.execute("BEGIN")
|
|
2423
|
-
stats_read_tx = True
|
|
2424
2590
|
if common_range_start is None:
|
|
2425
2591
|
common_range_start = now_utc - dt.timedelta(days=30)
|
|
2426
2592
|
if common_range_start.tzinfo is None or common_range_start.utcoffset() is None:
|
|
@@ -2547,6 +2713,7 @@ def _tui_build_source_bundle(
|
|
|
2547
2713
|
),
|
|
2548
2714
|
**({"accounts": claude_accounts} if claude_accounts else {}),
|
|
2549
2715
|
},
|
|
2716
|
+
domain_freshness=_tui_claude_domain_freshness(claude_data),
|
|
2550
2717
|
)
|
|
2551
2718
|
if codex_ingest_failed:
|
|
2552
2719
|
warning = SourceDashboardWarning(
|
|
@@ -2643,9 +2810,6 @@ def _tui_build_source_bundle(
|
|
|
2643
2810
|
if cache_read_tx:
|
|
2644
2811
|
cache_conn.rollback()
|
|
2645
2812
|
cache_read_tx = False
|
|
2646
|
-
if stats_read_tx:
|
|
2647
|
-
stats_conn.rollback()
|
|
2648
|
-
stats_read_tx = False
|
|
2649
2813
|
post_stats_digest = codex_stats_digest(stats_conn)
|
|
2650
2814
|
post_signature = c.compute_signature(
|
|
2651
2815
|
cache_conn,
|
|
@@ -2662,8 +2826,6 @@ def _tui_build_source_bundle(
|
|
|
2662
2826
|
finally:
|
|
2663
2827
|
if cache_read_tx:
|
|
2664
2828
|
cache_conn.rollback()
|
|
2665
|
-
if stats_read_tx:
|
|
2666
|
-
stats_conn.rollback()
|
|
2667
2829
|
cache_conn.close()
|
|
2668
2830
|
|
|
2669
2831
|
|
|
@@ -2685,6 +2847,7 @@ def _tui_hydrating_source_bundle() -> SourceDashboardBundle:
|
|
|
2685
2847
|
last_success_at=None,
|
|
2686
2848
|
capabilities={},
|
|
2687
2849
|
data=None,
|
|
2850
|
+
domain_freshness={"hero": "stale", "quota": "stale", "sessions": "stale"},
|
|
2688
2851
|
)
|
|
2689
2852
|
codex = SourceDashboardState(
|
|
2690
2853
|
source="codex",
|
|
@@ -2695,6 +2858,7 @@ def _tui_hydrating_source_bundle() -> SourceDashboardBundle:
|
|
|
2695
2858
|
last_success_at=None,
|
|
2696
2859
|
capabilities={},
|
|
2697
2860
|
data=None,
|
|
2861
|
+
domain_freshness={"hero": "stale", "quota": "stale", "sessions": "stale"},
|
|
2698
2862
|
)
|
|
2699
2863
|
return SourceDashboardBundle(
|
|
2700
2864
|
source_schema_version=1,
|
|
@@ -2719,6 +2883,8 @@ def _tui_source_bundle_can_idle(bundle: SourceDashboardBundle | None) -> bool:
|
|
|
2719
2883
|
state = bundle.sources.get(source)
|
|
2720
2884
|
if not isinstance(state, SourceDashboardState):
|
|
2721
2885
|
return False
|
|
2886
|
+
# Idle eligibility is provider-generation coherence, deliberately not a
|
|
2887
|
+
# hero/quota/sessions age aggregate.
|
|
2722
2888
|
if (state.availability not in ("ok", "empty")
|
|
2723
2889
|
or state.freshness != "fresh"
|
|
2724
2890
|
or state.data is None):
|
|
@@ -2753,6 +2919,55 @@ def _tui_build_snapshot(
|
|
|
2753
2919
|
display_tz_pref_override: "str | None" = None,
|
|
2754
2920
|
precompute_envelope: bool = False,
|
|
2755
2921
|
runtime_bind: "str | None" = None,
|
|
2922
|
+
) -> DataSnapshot:
|
|
2923
|
+
"""Build once, then perform at most one post-query stats heal/reopen."""
|
|
2924
|
+
|
|
2925
|
+
try:
|
|
2926
|
+
return _tui_build_snapshot_once(
|
|
2927
|
+
now_utc=now_utc,
|
|
2928
|
+
skip_sync=skip_sync,
|
|
2929
|
+
display_tz_pref_override=display_tz_pref_override,
|
|
2930
|
+
precompute_envelope=precompute_envelope,
|
|
2931
|
+
runtime_bind=runtime_bind,
|
|
2932
|
+
stats_heal_attempted=False,
|
|
2933
|
+
)
|
|
2934
|
+
except _StatsSnapshotCorruption as fault:
|
|
2935
|
+
# ``_tui_build_snapshot_once`` closes its live stats connection before
|
|
2936
|
+
# this boundary. The replacement-capable hook can therefore satisfy
|
|
2937
|
+
# the whole-family drain gate, and the retry opens the published family.
|
|
2938
|
+
_tui_heal_post_query_stats(fault.cause)
|
|
2939
|
+
try:
|
|
2940
|
+
return _tui_build_snapshot_once(
|
|
2941
|
+
now_utc=now_utc,
|
|
2942
|
+
# The first attempt already completed the one cache ingest plan.
|
|
2943
|
+
skip_sync=True,
|
|
2944
|
+
display_tz_pref_override=display_tz_pref_override,
|
|
2945
|
+
precompute_envelope=precompute_envelope,
|
|
2946
|
+
runtime_bind=runtime_bind,
|
|
2947
|
+
stats_heal_attempted=True,
|
|
2948
|
+
)
|
|
2949
|
+
except Exception as retry_exc:
|
|
2950
|
+
# A fresh opener can still lose a race to damage/maintenance after
|
|
2951
|
+
# the heal returns. Corruption on this one retry is a stable,
|
|
2952
|
+
# typed degraded frame—not a third attempt or a dashboard crash.
|
|
2953
|
+
if not _cctally()._is_sqlite_corruption_error(retry_exc):
|
|
2954
|
+
raise
|
|
2955
|
+
return _tui_stats_retry_degraded_snapshot(
|
|
2956
|
+
now_utc=now_utc or dt.datetime.now(dt.timezone.utc),
|
|
2957
|
+
exc=retry_exc,
|
|
2958
|
+
precompute_envelope=precompute_envelope,
|
|
2959
|
+
runtime_bind=runtime_bind,
|
|
2960
|
+
)
|
|
2961
|
+
|
|
2962
|
+
|
|
2963
|
+
def _tui_build_snapshot_once(
|
|
2964
|
+
*,
|
|
2965
|
+
now_utc: dt.datetime | None = None,
|
|
2966
|
+
skip_sync: bool = False,
|
|
2967
|
+
display_tz_pref_override: "str | None" = None,
|
|
2968
|
+
precompute_envelope: bool = False,
|
|
2969
|
+
runtime_bind: "str | None" = None,
|
|
2970
|
+
stats_heal_attempted: bool,
|
|
2756
2971
|
) -> DataSnapshot:
|
|
2757
2972
|
"""Single-shot build of a DataSnapshot from the DB + cache.
|
|
2758
2973
|
|
|
@@ -2803,6 +3018,23 @@ def _tui_build_snapshot(
|
|
|
2803
3018
|
conn = open_db()
|
|
2804
3019
|
try:
|
|
2805
3020
|
errors: list[str] = []
|
|
3021
|
+
sync_failures: list[SyncFailureAttribution] = []
|
|
3022
|
+
|
|
3023
|
+
def capture_failure(
|
|
3024
|
+
leg: str,
|
|
3025
|
+
database: str,
|
|
3026
|
+
exc: Exception,
|
|
3027
|
+
) -> None:
|
|
3028
|
+
_tui_capture_sync_failure(
|
|
3029
|
+
conn,
|
|
3030
|
+
errors,
|
|
3031
|
+
sync_failures,
|
|
3032
|
+
leg=leg,
|
|
3033
|
+
database=database,
|
|
3034
|
+
exc=exc,
|
|
3035
|
+
stats_heal_attempted=stats_heal_attempted,
|
|
3036
|
+
)
|
|
3037
|
+
|
|
2806
3038
|
cw: TuiCurrentWeek | None = None
|
|
2807
3039
|
fc: Any | None = None
|
|
2808
3040
|
trend: list[TuiTrendRow] = []
|
|
@@ -2851,6 +3083,7 @@ def _tui_build_snapshot(
|
|
|
2851
3083
|
return None, exc
|
|
2852
3084
|
|
|
2853
3085
|
operations = [_claude_leg]
|
|
3086
|
+
operation_origins = ["view_model.claude.sync"]
|
|
2854
3087
|
if precompute_envelope:
|
|
2855
3088
|
def _codex_leg(active_conn):
|
|
2856
3089
|
try:
|
|
@@ -2865,10 +3098,13 @@ def _tui_build_snapshot(
|
|
|
2865
3098
|
return None, exc
|
|
2866
3099
|
|
|
2867
3100
|
operations.append(_codex_leg)
|
|
3101
|
+
operation_origins.append("view_model.codex.sync")
|
|
2868
3102
|
|
|
2869
3103
|
results, cache_conn = (
|
|
2870
3104
|
cache_mod._run_cache_plan_with_recovery(
|
|
2871
|
-
cache_conn,
|
|
3105
|
+
cache_conn,
|
|
3106
|
+
tuple(operations),
|
|
3107
|
+
origins=tuple(operation_origins),
|
|
2872
3108
|
)
|
|
2873
3109
|
)
|
|
2874
3110
|
claude_ingest, claude_error = results[0]
|
|
@@ -2878,7 +3114,9 @@ def _tui_build_snapshot(
|
|
|
2878
3114
|
)
|
|
2879
3115
|
else:
|
|
2880
3116
|
claude_ingest_failed = True
|
|
2881
|
-
|
|
3117
|
+
capture_failure(
|
|
3118
|
+
"sync-cache", "cache", claude_error
|
|
3119
|
+
)
|
|
2882
3120
|
if precompute_envelope:
|
|
2883
3121
|
codex_ingest, codex_error = results[1]
|
|
2884
3122
|
if codex_error is None:
|
|
@@ -2887,14 +3125,16 @@ def _tui_build_snapshot(
|
|
|
2887
3125
|
)
|
|
2888
3126
|
else:
|
|
2889
3127
|
codex_ingest_failed = True
|
|
2890
|
-
|
|
3128
|
+
capture_failure(
|
|
3129
|
+
"sync-codex-cache", "cache", codex_error
|
|
3130
|
+
)
|
|
2891
3131
|
finally:
|
|
2892
3132
|
cache_conn.close()
|
|
2893
3133
|
except Exception as exc:
|
|
2894
3134
|
claude_ingest_failed = True
|
|
2895
3135
|
if precompute_envelope:
|
|
2896
3136
|
codex_ingest_failed = True
|
|
2897
|
-
|
|
3137
|
+
capture_failure("sync-cache-open", "cache", exc)
|
|
2898
3138
|
# Force pure reads for every view builder below, independent of the
|
|
2899
3139
|
# caller's flag: the single ingest above is the only glob per tick.
|
|
2900
3140
|
skip_sync = True
|
|
@@ -2963,13 +3203,13 @@ def _tui_build_snapshot(
|
|
|
2963
3203
|
if prior_snap is not None:
|
|
2964
3204
|
prior_source_bundle = getattr(prior_snap, "source_bundle", None)
|
|
2965
3205
|
except Exception as exc:
|
|
2966
|
-
|
|
3206
|
+
capture_failure("prior-source-bundle", "other", exc)
|
|
2967
3207
|
dispatch_sig = None
|
|
2968
3208
|
with _perf.phase("signature"):
|
|
2969
3209
|
try:
|
|
2970
3210
|
dispatch_sig = _tui_compute_dispatch_signature(conn)
|
|
2971
3211
|
except Exception as exc:
|
|
2972
|
-
|
|
3212
|
+
capture_failure("dispatch-signature", "stats_or_cache", exc)
|
|
2973
3213
|
dispatch_sig = None
|
|
2974
3214
|
if dispatch_sig is not None:
|
|
2975
3215
|
# The idle decision keys on the DB signature AND a render key
|
|
@@ -2993,6 +3233,12 @@ def _tui_build_snapshot(
|
|
|
2993
3233
|
data_version = _snapshot_data_version(dispatch_sig)
|
|
2994
3234
|
if (prior_snap is not None and prior_key is not None
|
|
2995
3235
|
and dispatch_key == prior_key
|
|
3236
|
+
and not any(
|
|
3237
|
+
failure.database == "stats" and failure.corruption
|
|
3238
|
+
for failure in getattr(
|
|
3239
|
+
prior_snap, "sync_failures", ()
|
|
3240
|
+
)
|
|
3241
|
+
)
|
|
2996
3242
|
and not _snapshot_period_rolled_over(
|
|
2997
3243
|
prior_snap, now_utc, _build_display_tz)):
|
|
2998
3244
|
with _perf.phase("idle-decision"):
|
|
@@ -3109,12 +3355,12 @@ def _tui_build_snapshot(
|
|
|
3109
3355
|
finally:
|
|
3110
3356
|
_rc_cache_conn.close()
|
|
3111
3357
|
except Exception as exc:
|
|
3112
|
-
|
|
3358
|
+
capture_failure("snapshot-cache-reconcile", "cache", exc)
|
|
3113
3359
|
with _perf.phase("build.current_week"):
|
|
3114
3360
|
try:
|
|
3115
3361
|
cw = _tui_build_current_week(conn, now_utc, skip_sync=skip_sync)
|
|
3116
3362
|
except Exception as exc:
|
|
3117
|
-
|
|
3363
|
+
capture_failure("current-week", "stats_or_cache", exc)
|
|
3118
3364
|
fc_view = None
|
|
3119
3365
|
with _perf.phase("build.forecast"):
|
|
3120
3366
|
try:
|
|
@@ -3128,7 +3374,7 @@ def _tui_build_snapshot(
|
|
|
3128
3374
|
)
|
|
3129
3375
|
fc = fc_view.output if fc_view is not None else None
|
|
3130
3376
|
except Exception as exc:
|
|
3131
|
-
|
|
3377
|
+
capture_failure("forecast", "stats_or_cache", exc)
|
|
3132
3378
|
# Trend: source from build_trend_view so we capture the 3-sample
|
|
3133
3379
|
# avg_dollars_per_pct alongside the rows. The TUI build path
|
|
3134
3380
|
# historically called _tui_build_trend (which now wraps the
|
|
@@ -3146,7 +3392,7 @@ def _tui_build_snapshot(
|
|
|
3146
3392
|
trend = list(_trend_view.rows)
|
|
3147
3393
|
trend_avg_dpp = _trend_view.avg_dollars_per_pct
|
|
3148
3394
|
except Exception as exc:
|
|
3149
|
-
|
|
3395
|
+
capture_failure("trend", "stats_or_cache", exc)
|
|
3150
3396
|
with _perf.phase("build.sessions"):
|
|
3151
3397
|
try:
|
|
3152
3398
|
# The sessions aggregator goes through
|
|
@@ -3164,14 +3410,14 @@ def _tui_build_snapshot(
|
|
|
3164
3410
|
with_titles=precompute_envelope,
|
|
3165
3411
|
)
|
|
3166
3412
|
except Exception as exc:
|
|
3167
|
-
|
|
3413
|
+
capture_failure("sessions", "cache", exc)
|
|
3168
3414
|
# ---- v2 additions ----
|
|
3169
3415
|
with _perf.phase("build.milestones"):
|
|
3170
3416
|
try:
|
|
3171
3417
|
if cw is not None:
|
|
3172
3418
|
milestones = _tui_build_percent_milestones(conn)
|
|
3173
3419
|
except Exception as exc:
|
|
3174
|
-
|
|
3420
|
+
capture_failure("milestones", "stats", exc)
|
|
3175
3421
|
history: list = []
|
|
3176
3422
|
history_median_dpp: "float | None" = None
|
|
3177
3423
|
with _perf.phase("build.weekly_history"):
|
|
@@ -3188,7 +3434,7 @@ def _tui_build_snapshot(
|
|
|
3188
3434
|
history = list(history_view.rows)
|
|
3189
3435
|
history_median_dpp = history_view.median_dpp_non_current_4w
|
|
3190
3436
|
except Exception as exc:
|
|
3191
|
-
|
|
3437
|
+
capture_failure("weekly-history", "stats_or_cache", exc)
|
|
3192
3438
|
# ---- v2.1 additions: dashboard Weekly / Monthly panels ----
|
|
3193
3439
|
# Sync-thread view-model totals (spec §6.6): sum directly over
|
|
3194
3440
|
# the panel rows the dashboard ACTUALLY renders. The previous
|
|
@@ -3221,7 +3467,7 @@ def _tui_build_snapshot(
|
|
|
3221
3467
|
(r.total_tokens for r in weekly_periods), 0,
|
|
3222
3468
|
)
|
|
3223
3469
|
except Exception as exc:
|
|
3224
|
-
|
|
3470
|
+
capture_failure("weekly-periods", "stats_or_cache", exc)
|
|
3225
3471
|
# Sync-thread view-model totals (spec §6.6): sum-over-visible-rows
|
|
3226
3472
|
# (same invariant as weekly above). Monthly has no Bug-K analogue,
|
|
3227
3473
|
# but coupling the footer total to the panel-row source of truth
|
|
@@ -3243,7 +3489,7 @@ def _tui_build_snapshot(
|
|
|
3243
3489
|
(r.total_tokens for r in monthly_periods), 0,
|
|
3244
3490
|
)
|
|
3245
3491
|
except Exception as exc:
|
|
3246
|
-
|
|
3492
|
+
capture_failure("monthly-periods", "cache", exc)
|
|
3247
3493
|
# ---- v2.2 additions: dashboard Blocks / Daily panels ----
|
|
3248
3494
|
# Issue #56: build the BlocksView once and read both rows
|
|
3249
3495
|
# (presentation) and totals (envelope scalars) from the same
|
|
@@ -3266,7 +3512,7 @@ def _tui_build_snapshot(
|
|
|
3266
3512
|
blocks_total_cost_usd = _blocks_view.total_cost_usd
|
|
3267
3513
|
blocks_total_tokens = _blocks_view.total_tokens
|
|
3268
3514
|
except Exception as exc:
|
|
3269
|
-
|
|
3515
|
+
capture_failure("blocks-panel", "stats_or_cache", exc)
|
|
3270
3516
|
# Sync-thread view-model totals (Bundle 1 / spec §6.6):
|
|
3271
3517
|
# sum-over-visible-rows (same invariant as weekly/monthly above).
|
|
3272
3518
|
# Gap days in the materialized panel carry ``cost_usd=0.0`` /
|
|
@@ -3289,7 +3535,7 @@ def _tui_build_snapshot(
|
|
|
3289
3535
|
(r.total_tokens for r in daily_panel), 0,
|
|
3290
3536
|
)
|
|
3291
3537
|
except Exception as exc:
|
|
3292
|
-
|
|
3538
|
+
capture_failure("daily-panel", "cache", exc)
|
|
3293
3539
|
# ---- threshold-actions T5: alerts envelope array ----
|
|
3294
3540
|
# Precomputed at sync time so `snapshot_to_envelope` stays a pure
|
|
3295
3541
|
# renderer (no DB I/O on the dashboard hot path; mirrors how
|
|
@@ -3299,7 +3545,7 @@ def _tui_build_snapshot(
|
|
|
3299
3545
|
try:
|
|
3300
3546
|
alerts = _build_alerts_envelope_array(conn)
|
|
3301
3547
|
except Exception as exc:
|
|
3302
|
-
|
|
3548
|
+
capture_failure("alerts", "stats", exc)
|
|
3303
3549
|
# ---- 5h in-place credit (v1.7.x) ----
|
|
3304
3550
|
# Load 5h milestones (pre + post credit) for the current
|
|
3305
3551
|
# block's window so CurrentWeekModal can render a merged
|
|
@@ -3313,7 +3559,7 @@ def _tui_build_snapshot(
|
|
|
3313
3559
|
win_key = cw.five_hour_block.get("five_hour_window_key")
|
|
3314
3560
|
fh_milestones = _tui_build_five_hour_milestones(conn, win_key)
|
|
3315
3561
|
except Exception as exc:
|
|
3316
|
-
|
|
3562
|
+
capture_failure("five-hour-milestones", "stats", exc)
|
|
3317
3563
|
# ---- hero-modal historical milestones week index (spec §1a/§3) ----
|
|
3318
3564
|
# Built ONLY here on the non-idle rebuild (the idle short-circuit
|
|
3319
3565
|
# returns before this phase and carries the prior index forward via
|
|
@@ -3325,7 +3571,7 @@ def _tui_build_snapshot(
|
|
|
3325
3571
|
try:
|
|
3326
3572
|
week_index = sys.modules["cctally"].build_claude_week_index(conn)
|
|
3327
3573
|
except Exception as exc:
|
|
3328
|
-
|
|
3574
|
+
capture_failure("week-index", "stats", exc)
|
|
3329
3575
|
# ---- Projects panel + modal envelope (spec §5.2, plan Task 1) -----
|
|
3330
3576
|
# Per-tick aggregation lives on the sync thread; the dashboard's
|
|
3331
3577
|
# pure ``snapshot_to_envelope`` reads ``snap.projects_envelope``
|
|
@@ -3375,7 +3621,7 @@ def _tui_build_snapshot(
|
|
|
3375
3621
|
use_projects_env_cache=use_projects_env_cache,
|
|
3376
3622
|
)
|
|
3377
3623
|
except Exception as exc:
|
|
3378
|
-
|
|
3624
|
+
capture_failure("projects-envelope", "stats_or_cache", exc)
|
|
3379
3625
|
finally:
|
|
3380
3626
|
try:
|
|
3381
3627
|
conn.execute("DROP VIEW IF EXISTS session_entries")
|
|
@@ -3435,7 +3681,9 @@ def _tui_build_snapshot(
|
|
|
3435
3681
|
)
|
|
3436
3682
|
sessions = annotated
|
|
3437
3683
|
except Exception as exc:
|
|
3438
|
-
|
|
3684
|
+
capture_failure(
|
|
3685
|
+
"projects-cross-nav-bind", "stats_or_cache", exc
|
|
3686
|
+
)
|
|
3439
3687
|
|
|
3440
3688
|
# Cache-report panel + modal envelope block (spec
|
|
3441
3689
|
# 2026-05-21-cache-report-panel-design.md §5.2). Per-tick build
|
|
@@ -3470,7 +3718,7 @@ def _tui_build_snapshot(
|
|
|
3470
3718
|
use_cache_report_cache=use_cache_report_cache,
|
|
3471
3719
|
)
|
|
3472
3720
|
except Exception as exc:
|
|
3473
|
-
|
|
3721
|
+
capture_failure("cache-report", "cache", exc)
|
|
3474
3722
|
|
|
3475
3723
|
# ---- #268 M4: doctor / config / update-state precompute (spec §6) ----
|
|
3476
3724
|
# Precompute the envelope's doctor / config / update-state reads ONCE
|
|
@@ -3488,14 +3736,14 @@ def _tui_build_snapshot(
|
|
|
3488
3736
|
now_utc, runtime_bind,
|
|
3489
3737
|
)
|
|
3490
3738
|
except Exception as exc:
|
|
3491
|
-
|
|
3739
|
+
capture_failure("doctor-precompute", "other", exc)
|
|
3492
3740
|
with _perf.phase("envelope.precompute"):
|
|
3493
3741
|
try:
|
|
3494
3742
|
envelope_precompute_block = _tui_precompute_envelope_config(
|
|
3495
3743
|
raw_config,
|
|
3496
3744
|
)
|
|
3497
3745
|
except Exception as exc:
|
|
3498
|
-
|
|
3746
|
+
capture_failure("envelope-precompute", "other", exc)
|
|
3499
3747
|
|
|
3500
3748
|
# Determine the shared visible interval before publishing either source.
|
|
3501
3749
|
# The actual source bundle is built after ``snap`` exists, so Claude's
|
|
@@ -3514,6 +3762,7 @@ def _tui_build_snapshot(
|
|
|
3514
3762
|
last_sync_at=time.monotonic(),
|
|
3515
3763
|
last_sync_error=("; ".join(errors) if errors else None),
|
|
3516
3764
|
generated_at=now_utc,
|
|
3765
|
+
sync_failures=tuple(sync_failures),
|
|
3517
3766
|
percent_milestones=milestones,
|
|
3518
3767
|
weekly_history=history,
|
|
3519
3768
|
weekly_periods=weekly_periods,
|
|
@@ -3590,11 +3839,12 @@ def _tui_build_snapshot(
|
|
|
3590
3839
|
except Exception as exc:
|
|
3591
3840
|
# Public source warnings are stable/sanitized; the detailed
|
|
3592
3841
|
# exception remains only on the internal rebuild-error string.
|
|
3593
|
-
|
|
3842
|
+
capture_failure("source-bundle", "stats_or_cache", exc)
|
|
3594
3843
|
source_bundle = prior_source_bundle
|
|
3595
3844
|
snap = dataclasses.replace(
|
|
3596
3845
|
snap,
|
|
3597
3846
|
last_sync_error=("; ".join(errors) if errors else None),
|
|
3847
|
+
sync_failures=tuple(sync_failures),
|
|
3598
3848
|
source_bundle=source_bundle,
|
|
3599
3849
|
)
|
|
3600
3850
|
# #268 M5.1: record the (signature+render key, snapshot) so the next
|
|
@@ -3616,6 +3866,9 @@ def _tui_build_snapshot(
|
|
|
3616
3866
|
generated_at=now_utc.isoformat(),
|
|
3617
3867
|
)
|
|
3618
3868
|
return snap
|
|
3869
|
+
except _StatsSnapshotCorruption:
|
|
3870
|
+
_p_snapshot.__exit__(*sys.exc_info())
|
|
3871
|
+
raise
|
|
3619
3872
|
finally:
|
|
3620
3873
|
conn.close()
|
|
3621
3874
|
|
|
@@ -3835,16 +4088,22 @@ def _tui_build_idle_snapshot(prior, *, now_utc, precompute_envelope,
|
|
|
3835
4088
|
_tui_source_bundle_can_idle(source_bundle)
|
|
3836
4089
|
and not codex_decision_deadline_passed(prior_codex, now_utc)
|
|
3837
4090
|
):
|
|
4091
|
+
claude = _refresh_claude_source_clock(
|
|
4092
|
+
prior_claude,
|
|
4093
|
+
current_week=prior.current_week,
|
|
4094
|
+
now_utc=now_utc,
|
|
4095
|
+
raw_config=raw_config,
|
|
4096
|
+
)
|
|
3838
4097
|
codex = refresh_codex_source_clock(prior_codex, now_utc=now_utc)
|
|
3839
|
-
if codex is not prior_codex:
|
|
4098
|
+
if claude is not prior_claude or codex is not prior_codex:
|
|
3840
4099
|
source_bundle = SourceDashboardBundle(
|
|
3841
4100
|
source_schema_version=source_bundle.source_schema_version,
|
|
3842
4101
|
default_source=source_bundle.default_source,
|
|
3843
4102
|
source_order=source_bundle.source_order,
|
|
3844
4103
|
sources={
|
|
3845
|
-
"claude":
|
|
4104
|
+
"claude": claude,
|
|
3846
4105
|
"codex": codex,
|
|
3847
|
-
"all": compose_all_state(
|
|
4106
|
+
"all": compose_all_state(claude, codex),
|
|
3848
4107
|
},
|
|
3849
4108
|
)
|
|
3850
4109
|
elif source_stats_conn is not None:
|
|
@@ -3896,6 +4155,7 @@ def _tui_build_idle_snapshot(prior, *, now_utc, precompute_envelope,
|
|
|
3896
4155
|
generated_at=now_utc,
|
|
3897
4156
|
last_sync_at=time.monotonic(),
|
|
3898
4157
|
last_sync_error=("; ".join(errors) if errors else None),
|
|
4158
|
+
sync_failures=(),
|
|
3899
4159
|
doctor_payload=doctor_payload,
|
|
3900
4160
|
envelope_precompute=envelope_precompute,
|
|
3901
4161
|
source_bundle=source_bundle,
|
|
@@ -3917,6 +4177,44 @@ def _tui_empty_snapshot(now_utc: dt.datetime) -> DataSnapshot:
|
|
|
3917
4177
|
)
|
|
3918
4178
|
|
|
3919
4179
|
|
|
4180
|
+
def _tui_stats_retry_degraded_snapshot(
|
|
4181
|
+
*,
|
|
4182
|
+
now_utc: dt.datetime,
|
|
4183
|
+
exc: Exception,
|
|
4184
|
+
precompute_envelope: bool,
|
|
4185
|
+
runtime_bind: "str | None",
|
|
4186
|
+
) -> DataSnapshot:
|
|
4187
|
+
"""Return a stable typed frame when the one fresh retry cannot open."""
|
|
4188
|
+
|
|
4189
|
+
errors = [f"stats-open: {exc}"]
|
|
4190
|
+
doctor_payload = None
|
|
4191
|
+
envelope_precompute = None
|
|
4192
|
+
if precompute_envelope:
|
|
4193
|
+
try:
|
|
4194
|
+
envelope_precompute = _tui_precompute_envelope_config(load_config())
|
|
4195
|
+
except Exception as precompute_exc: # noqa: BLE001
|
|
4196
|
+
errors.append(f"envelope-precompute: {precompute_exc}")
|
|
4197
|
+
try:
|
|
4198
|
+
doctor_payload = _tui_precompute_doctor_payload(now_utc, runtime_bind)
|
|
4199
|
+
except Exception as doctor_exc: # noqa: BLE001
|
|
4200
|
+
errors.append(f"doctor-precompute: {doctor_exc}")
|
|
4201
|
+
return dataclasses.replace(
|
|
4202
|
+
_tui_empty_snapshot(now_utc),
|
|
4203
|
+
last_sync_at=time.monotonic(),
|
|
4204
|
+
last_sync_error="; ".join(errors),
|
|
4205
|
+
sync_failures=(
|
|
4206
|
+
SyncFailureAttribution(
|
|
4207
|
+
leg="stats-open",
|
|
4208
|
+
database="stats",
|
|
4209
|
+
corruption=True,
|
|
4210
|
+
),
|
|
4211
|
+
),
|
|
4212
|
+
doctor_payload=doctor_payload,
|
|
4213
|
+
envelope_precompute=envelope_precompute,
|
|
4214
|
+
hydrating=False,
|
|
4215
|
+
)
|
|
4216
|
+
|
|
4217
|
+
|
|
3920
4218
|
class TuiKeyReader:
|
|
3921
4219
|
"""Context manager for raw-mode stdin reads.
|
|
3922
4220
|
|
|
@@ -6248,6 +6546,10 @@ def _make_run_sync_now_locked(*, ref, hub, pinned_now, display_tz_pref_override,
|
|
|
6248
6546
|
),
|
|
6249
6547
|
lambda active_conn: sync_codex_cache(active_conn),
|
|
6250
6548
|
),
|
|
6549
|
+
origins=(
|
|
6550
|
+
"dashboard.refresh.claude_sync",
|
|
6551
|
+
"dashboard.refresh.codex_sync",
|
|
6552
|
+
),
|
|
6251
6553
|
)
|
|
6252
6554
|
except Exception as exc: # noqa: BLE001 — surfaced on the snap
|
|
6253
6555
|
sync_error = f"sync-cache: {exc}"
|
|
@@ -6281,6 +6583,10 @@ def _make_run_sync_now_locked(*, ref, hub, pinned_now, display_tz_pref_override,
|
|
|
6281
6583
|
crashed = dataclasses.replace(
|
|
6282
6584
|
prev,
|
|
6283
6585
|
last_sync_error=f"sync crashed: {exc}",
|
|
6586
|
+
# The new crash supersedes the prior typed leg failures. Keeping
|
|
6587
|
+
# them would let a stale stats attribution win this unrelated
|
|
6588
|
+
# failure in the privacy-safe envelope classifier.
|
|
6589
|
+
sync_failures=(),
|
|
6284
6590
|
generated_at=dt.datetime.now(dt.timezone.utc),
|
|
6285
6591
|
# #278 §1.4.1: a crash-carry snapshot is stable (not mid-
|
|
6286
6592
|
# assembly); clear the latch even if ``prev`` was a hydrating
|