cctally 1.117.2 → 1.118.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/README.md +7 -3
- package/bin/_cctally_cache.py +2783 -175
- package/bin/_cctally_completion.py +106 -33
- package/bin/_cctally_core.py +69 -4
- package/bin/_cctally_dashboard.py +188 -13
- package/bin/_cctally_dashboard_conversation.py +5 -1
- package/bin/_cctally_dashboard_envelope.py +50 -19
- package/bin/_cctally_dashboard_sources.py +23 -1
- package/bin/_cctally_db.py +193 -153
- package/bin/_cctally_diagnosis_sources.py +63 -7
- package/bin/_cctally_doctor.py +865 -93
- package/bin/_cctally_five_hour.py +67 -50
- package/bin/_cctally_forecast.py +35 -13
- package/bin/_cctally_journal.py +186 -18
- package/bin/_cctally_milestone_history.py +859 -62
- package/bin/_cctally_oauth_samples.py +286 -15
- package/bin/_cctally_parser.py +2 -2
- package/bin/_cctally_quota.py +104 -12
- package/bin/_cctally_record.py +153 -29
- package/bin/_cctally_rederive.py +608 -68
- package/bin/_cctally_statusline.py +108 -81
- package/bin/_cctally_store.py +179 -38
- package/bin/_cctally_tui.py +892 -181
- package/bin/_cctally_update.py +32 -13
- package/bin/_lib_codex_conversation_query.py +52 -14
- package/bin/_lib_codex_drain_admission.py +224 -6
- package/bin/_lib_codex_metadata.py +39 -0
- package/bin/_lib_conversation_anon.py +15 -8
- package/bin/_lib_conversation_dispatch.py +7 -0
- package/bin/_lib_conversation_query.py +289 -70
- package/bin/_lib_dashboard_sources.py +37 -1
- package/bin/_lib_detector_access_registry.py +497 -26
- package/bin/_lib_doctor.py +85 -20
- package/bin/_lib_doctor_scan_index.py +321 -0
- package/bin/_lib_ingest_frontier.py +20 -1
- package/bin/_lib_oauth_samples.py +23 -7
- package/bin/_lib_pricing.py +146 -16
- package/bin/_lib_quota.py +19 -0
- package/bin/_lib_rederive.py +11 -6
- package/bin/_lib_retained_records.py +516 -0
- package/bin/_lib_snapshot_cache.py +10 -0
- package/bin/_lib_stale_proof.py +272 -43
- package/bin/_lib_stats_content_revision.py +831 -0
- package/bin/_lib_write_io.py +11 -4
- package/bin/cctally +4 -1
- package/dashboard/static/assets/ConversationsView-D4Iv4ku0.js +72 -0
- package/dashboard/static/assets/{DoctorModal-B0DnU9d9.js → DoctorModal-D4QSq6uz.js} +1 -1
- package/dashboard/static/assets/ModalRoot-C9F5dhZi.js +1 -0
- package/dashboard/static/assets/{ProjectsDrillPanel-DSxXQSa-.js → ProjectsDrillPanel-WxAdRDMt.js} +1 -1
- package/dashboard/static/assets/{SourceDetailModal-BleDpQqG.js → SourceDetailModal-NyyEfXmc.js} +1 -1
- package/dashboard/static/assets/{UpdateModal-CMqaAkSK.js → UpdateModal-CFLq9pT1.js} +1 -1
- package/dashboard/static/assets/{index-BjQV-0-D.js → index-BKTWH9JE.js} +2 -2
- package/dashboard/static/assets/{outlineNavigation-CO-VWBm7.js → outlineNavigation-BnGUwCS3.js} +3 -3
- package/dashboard/static/dashboard.html +1 -1
- package/package.json +4 -1
- package/dashboard/static/assets/ConversationsView-oqNsWzWV.js +0 -72
- package/dashboard/static/assets/ModalRoot-CYua2wP_.js +0 -1
package/bin/_cctally_tui.py
CHANGED
|
@@ -316,6 +316,8 @@ from _lib_source_retry import (
|
|
|
316
316
|
clear_partial_retry,
|
|
317
317
|
plan_partial_retry,
|
|
318
318
|
)
|
|
319
|
+
# #1050: the stats.db Claude content revision (a stdlib-only leaf kernel).
|
|
320
|
+
import _lib_stats_content_revision as _content_revision
|
|
319
321
|
|
|
320
322
|
|
|
321
323
|
# === Module-level back-ref shims for helpers that STAY in bin/cctally ======
|
|
@@ -927,6 +929,20 @@ class TuiCurrentWeek:
|
|
|
927
929
|
account_decorated: bool = False
|
|
928
930
|
account_label: "str | None" = None
|
|
929
931
|
cycle_scope: "Any | None" = None
|
|
932
|
+
# #1018 twin (spec 5.3): the capture instant of the newest genuine reading
|
|
933
|
+
# that confirms the displayed weekly value, and the server-private scope
|
|
934
|
+
# it was selected under (`_cctally_oauth_samples.WeeklyReadingScope`,
|
|
935
|
+
# never serialized), which idle ticks re-run. The freshness label and age
|
|
936
|
+
# above age from THIS instant; `latest_snapshot_at` stays the stored
|
|
937
|
+
# row's capture. Both None when the displayed row has no resolvable
|
|
938
|
+
# originating observation. Trailing defaults, for fixture constructors.
|
|
939
|
+
latest_weekly_reading_at: "dt.datetime | None" = None
|
|
940
|
+
weekly_reading_scope: "Any | None" = None
|
|
941
|
+
# #1018 twin review F1: the scope's inputs (`_TuiWeeklyReadingRequest`,
|
|
942
|
+
# server-private), set only when the build's scope lookup FAILED, so the
|
|
943
|
+
# idle refresh retries it instead of serving no freshness until the next
|
|
944
|
+
# full build. None once a scope is resolved, or when it never failed.
|
|
945
|
+
weekly_reading_retry: "Any | None" = None
|
|
930
946
|
|
|
931
947
|
|
|
932
948
|
# ---- View-model row dataclasses moved to bin/_lib_view_models.py ----
|
|
@@ -1145,6 +1161,46 @@ class SyncFailureAttribution:
|
|
|
1145
1161
|
# branch; `_tui_capture_sync_failure` is the one normal site that supplies
|
|
1146
1162
|
# the real value, and it already receives the exception.
|
|
1147
1163
|
sqlite_busy: bool = False
|
|
1164
|
+
# #932 §4.1. True only where a caught `StatsRebuildDeferred` instance is
|
|
1165
|
+
# turned into a frame (`_tui_deferral_attribution`); never inferred from
|
|
1166
|
+
# text, the corruption flag, hydration or row counts. It is the one signal
|
|
1167
|
+
# a frozen dashboard's settle loop retries on, and the hydration latch
|
|
1168
|
+
# writers respect. `deferral_outcome` is that exception's admission result
|
|
1169
|
+
# (`spawned`, `pending` or `failed`).
|
|
1170
|
+
deferred: bool = False
|
|
1171
|
+
deferral_outcome: "str | None" = None
|
|
1172
|
+
|
|
1173
|
+
|
|
1174
|
+
def _tui_deferral_attribution(exc: BaseException) -> SyncFailureAttribution:
|
|
1175
|
+
"""The attribution for a caught `StatsRebuildDeferred` (#932 §4.1)."""
|
|
1176
|
+
return SyncFailureAttribution(
|
|
1177
|
+
leg="stats-open",
|
|
1178
|
+
database="stats",
|
|
1179
|
+
corruption=_stats_open_failure_is_corruption(exc),
|
|
1180
|
+
deferred=True,
|
|
1181
|
+
deferral_outcome=getattr(exc, "outcome", None),
|
|
1182
|
+
)
|
|
1183
|
+
|
|
1184
|
+
|
|
1185
|
+
def _tui_snapshot_is_stats_deferral(snapshot) -> bool:
|
|
1186
|
+
"""True iff the frame states an explicit stats deferral (#932 §4.1).
|
|
1187
|
+
|
|
1188
|
+
Some attribution must carry ``database == "stats"``, ``leg ==
|
|
1189
|
+
"stats-open"`` and ``deferred``. Tolerates a snapshot without the field and
|
|
1190
|
+
dict-shaped items, as the envelope classifier does.
|
|
1191
|
+
"""
|
|
1192
|
+
for item in getattr(snapshot, "sync_failures", None) or ():
|
|
1193
|
+
if isinstance(item, dict):
|
|
1194
|
+
database = item.get("database")
|
|
1195
|
+
leg = item.get("leg")
|
|
1196
|
+
deferred = item.get("deferred", False)
|
|
1197
|
+
else:
|
|
1198
|
+
database = getattr(item, "database", None)
|
|
1199
|
+
leg = getattr(item, "leg", None)
|
|
1200
|
+
deferred = getattr(item, "deferred", False)
|
|
1201
|
+
if database == "stats" and leg == "stats-open" and bool(deferred):
|
|
1202
|
+
return True
|
|
1203
|
+
return False
|
|
1148
1204
|
|
|
1149
1205
|
|
|
1150
1206
|
class _StatsSnapshotCorruption(Exception):
|
|
@@ -1492,6 +1548,9 @@ class DataSnapshot:
|
|
|
1492
1548
|
freshness_label="fresh",
|
|
1493
1549
|
freshness_age=12,
|
|
1494
1550
|
five_hour_block=None,
|
|
1551
|
+
# #1018 twin: the published freshness ages from the confirming
|
|
1552
|
+
# reading; the synthesized card's reading is its snapshot.
|
|
1553
|
+
latest_weekly_reading_at=as_of,
|
|
1495
1554
|
)
|
|
1496
1555
|
# ---- Forecast: WARN, ~98% projected, fits within modal width.
|
|
1497
1556
|
# The TUI's verdict mapping (`_tui_verdict_of`) reads
|
|
@@ -1725,23 +1784,247 @@ class RuntimeState:
|
|
|
1725
1784
|
)
|
|
1726
1785
|
|
|
1727
1786
|
|
|
1728
|
-
def
|
|
1787
|
+
def _tui_oauth_usage_cfg(raw_config) -> dict:
|
|
1788
|
+
"""The resolved oauth_usage block of ``raw_config`` (the freshness-label
|
|
1789
|
+
thresholds), or the defaults when it does not validate. The one
|
|
1790
|
+
resolution every current-week freshness derivation uses — the card, the
|
|
1791
|
+
source copies and the quota clock, on full and idle ticks alike (#1018
|
|
1792
|
+
twin review F4) — so custom thresholds never flip a label between a full
|
|
1793
|
+
build and the next idle tick."""
|
|
1794
|
+
try:
|
|
1795
|
+
return _get_oauth_usage_config(raw_config)
|
|
1796
|
+
except Exception:
|
|
1797
|
+
return _OAUTH_USAGE_DEFAULTS
|
|
1798
|
+
|
|
1799
|
+
|
|
1800
|
+
def _tui_card_freshness(captured, now_utc: dt.datetime,
|
|
1801
|
+
oauth_usage_cfg: "dict | None" = None):
|
|
1729
1802
|
"""``(freshness_label, freshness_age)`` of the card's displayed sample,
|
|
1730
|
-
against the configured oauth_usage thresholds
|
|
1731
|
-
|
|
1732
|
-
|
|
1803
|
+
against the configured oauth_usage thresholds (``oauth_usage_cfg`` when
|
|
1804
|
+
the caller already resolved them, else read from the config file);
|
|
1805
|
+
``(None, None)`` when the capture is not a datetime. Mirrors the dashboard
|
|
1806
|
+
envelope's cw_freshness derivation in `snapshot_to_envelope`. Refs spec
|
|
1807
|
+
§3.4."""
|
|
1733
1808
|
if not isinstance(captured, dt.datetime):
|
|
1734
1809
|
return None, None
|
|
1735
1810
|
if captured.tzinfo is None:
|
|
1736
1811
|
captured = captured.replace(tzinfo=dt.timezone.utc)
|
|
1737
1812
|
age_s = max(0.0, (now_utc - captured).total_seconds())
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1813
|
+
_fresh_cfg = oauth_usage_cfg
|
|
1814
|
+
if _fresh_cfg is None:
|
|
1815
|
+
try:
|
|
1816
|
+
_fresh_cfg = _get_oauth_usage_config(load_config())
|
|
1817
|
+
except Exception:
|
|
1818
|
+
_fresh_cfg = _OAUTH_USAGE_DEFAULTS
|
|
1742
1819
|
return _freshness_label(age_s, _fresh_cfg), int(age_s)
|
|
1743
1820
|
|
|
1744
1821
|
|
|
1822
|
+
@dataclass(frozen=True)
|
|
1823
|
+
class _TuiWeeklyReadingRequest:
|
|
1824
|
+
"""The inputs of a card's confirming-reading scope (#1018 twin review
|
|
1825
|
+
F1), the keyword arguments of `_tui_weekly_reading_scope` but ``now_utc``.
|
|
1826
|
+
Kept on a card whose scope lookup failed, so the idle refresh can retry
|
|
1827
|
+
the lookup on a later tick; server-private, never serialized."""
|
|
1828
|
+
|
|
1829
|
+
journal_id: object
|
|
1830
|
+
account_key: object
|
|
1831
|
+
weekly_percent: float
|
|
1832
|
+
segment_start: dt.datetime
|
|
1833
|
+
week_start_date: object
|
|
1834
|
+
nominal_start: dt.datetime
|
|
1835
|
+
week_end_at: dt.datetime
|
|
1836
|
+
|
|
1837
|
+
def scope_fields(self) -> dict:
|
|
1838
|
+
return {f.name: getattr(self, f.name)
|
|
1839
|
+
for f in dataclasses.fields(self)}
|
|
1840
|
+
|
|
1841
|
+
|
|
1842
|
+
def _tui_weekly_floor_at_or_before(
|
|
1843
|
+
conn: sqlite3.Connection,
|
|
1844
|
+
*,
|
|
1845
|
+
week_start_date: str,
|
|
1846
|
+
week_start_at: dt.datetime,
|
|
1847
|
+
week_end_at: dt.datetime,
|
|
1848
|
+
account_key: str,
|
|
1849
|
+
now_utc: dt.datetime,
|
|
1850
|
+
) -> "dt.datetime | None":
|
|
1851
|
+
"""The newest weekly floor the fold applies to ``account_key``'s week
|
|
1852
|
+
that takes effect at or before ``now_utc`` (#1018 twin spec 5.3, review
|
|
1853
|
+
F3), or None.
|
|
1854
|
+
|
|
1855
|
+
The two legs and their predicates are `_cctally_core._reset_aware_floor`'s
|
|
1856
|
+
(a reset event effective in ``[week_start_at, week_end_at)``, a credit
|
|
1857
|
+
floor of ``week_start_date``, both of this account). That helper returns
|
|
1858
|
+
only the newest floor, so an as-of or replay build placed between two
|
|
1859
|
+
floors of one week would see a floor later than now and none at all;
|
|
1860
|
+
this reads the week's floors (a handful) and compares parsed instants in
|
|
1861
|
+
Python, which also keeps the sub-second order exact. It is a separate
|
|
1862
|
+
query rather than an option of the shared helper, which the strict
|
|
1863
|
+
detector path pins (`_lib_detector_access_registry`)."""
|
|
1864
|
+
rows = conn.execute(
|
|
1865
|
+
"""
|
|
1866
|
+
SELECT effective_reset_at_utc FROM week_reset_events
|
|
1867
|
+
WHERE unixepoch(effective_reset_at_utc) >= unixepoch(?)
|
|
1868
|
+
AND unixepoch(effective_reset_at_utc) < unixepoch(?)
|
|
1869
|
+
AND account_key = ?
|
|
1870
|
+
UNION ALL
|
|
1871
|
+
SELECT effective_at_utc FROM weekly_credit_floors
|
|
1872
|
+
WHERE week_start_date = ? AND account_key = ?
|
|
1873
|
+
""",
|
|
1874
|
+
(_iso_z(week_start_at), _iso_z(week_end_at), account_key,
|
|
1875
|
+
week_start_date, account_key),
|
|
1876
|
+
).fetchall()
|
|
1877
|
+
best = None
|
|
1878
|
+
for (text,) in rows:
|
|
1879
|
+
if not text:
|
|
1880
|
+
continue
|
|
1881
|
+
try:
|
|
1882
|
+
floor_at = parse_iso_datetime(str(text), "weekly floor")
|
|
1883
|
+
except ValueError:
|
|
1884
|
+
continue
|
|
1885
|
+
if floor_at <= now_utc and (best is None or floor_at > best):
|
|
1886
|
+
best = floor_at
|
|
1887
|
+
return best
|
|
1888
|
+
|
|
1889
|
+
|
|
1890
|
+
def _tui_weekly_reading_scope(
|
|
1891
|
+
conn: sqlite3.Connection,
|
|
1892
|
+
*,
|
|
1893
|
+
journal_id: object,
|
|
1894
|
+
account_key: object,
|
|
1895
|
+
weekly_percent: float,
|
|
1896
|
+
segment_start: dt.datetime,
|
|
1897
|
+
week_start_date: object,
|
|
1898
|
+
nominal_start: dt.datetime,
|
|
1899
|
+
week_end_at: dt.datetime,
|
|
1900
|
+
now_utc: dt.datetime,
|
|
1901
|
+
):
|
|
1902
|
+
"""The confirming-reading scope of the displayed weekly sample (#1018 twin
|
|
1903
|
+
spec 5.3), or None when its originating observation does not resolve.
|
|
1904
|
+
|
|
1905
|
+
The window identity is the originating observation's own normalized
|
|
1906
|
+
provider reset (``journal_id`` ``sa:<observation-id>`` -> the index row
|
|
1907
|
+
by primary key), never the stored ``week_end_at``, which can be a
|
|
1908
|
+
first-established boundary. A synthetic credit snapshot, a direct or
|
|
1909
|
+
bootstrap row, or an observation without a window yields None.
|
|
1910
|
+
|
|
1911
|
+
The lower bound is the card's segment start raised to the newest weekly
|
|
1912
|
+
floor the fold applies at or before now (`_tui_weekly_floor_at_or_before`:
|
|
1913
|
+
reset events and credit floors of this account and week), so a
|
|
1914
|
+
pre-credit reading of the same value never confirms a post-credit one.
|
|
1915
|
+
The upper bound is the earlier of the provider reset and the card's
|
|
1916
|
+
window end."""
|
|
1917
|
+
import _cctally_oauth_samples
|
|
1918
|
+
import _lib_oauth_samples
|
|
1919
|
+
if (not isinstance(journal_id, str) or not journal_id.startswith("sa:")
|
|
1920
|
+
or not isinstance(account_key, str) or not account_key):
|
|
1921
|
+
return None
|
|
1922
|
+
row = conn.execute(
|
|
1923
|
+
"SELECT weekly_resets_at_utc FROM claude_weekly_observation_index"
|
|
1924
|
+
" WHERE observation_id = ?", (journal_id[3:],)).fetchone()
|
|
1925
|
+
if row is None or not isinstance(row[0], str):
|
|
1926
|
+
return None
|
|
1927
|
+
try:
|
|
1928
|
+
reset = parse_iso_datetime(row[0], "weekly_resets_at_utc")
|
|
1929
|
+
except ValueError:
|
|
1930
|
+
return None
|
|
1931
|
+
lower = segment_start
|
|
1932
|
+
if isinstance(week_start_date, str) and week_start_date:
|
|
1933
|
+
floor_at = _tui_weekly_floor_at_or_before(
|
|
1934
|
+
conn, week_start_date=week_start_date,
|
|
1935
|
+
week_start_at=nominal_start, week_end_at=week_end_at,
|
|
1936
|
+
account_key=account_key, now_utc=now_utc)
|
|
1937
|
+
if floor_at is not None and lower < floor_at:
|
|
1938
|
+
lower = floor_at
|
|
1939
|
+
return _cctally_oauth_samples.WeeklyReadingScope(
|
|
1940
|
+
account_key=account_key,
|
|
1941
|
+
weekly_resets_at_utc=row[0],
|
|
1942
|
+
weekly_percent=float(weekly_percent),
|
|
1943
|
+
lower_bound_utc=_lib_oauth_samples.canonical_instant(lower),
|
|
1944
|
+
upper_bound_utc=_lib_oauth_samples.canonical_instant(
|
|
1945
|
+
min(reset, week_end_at)),
|
|
1946
|
+
)
|
|
1947
|
+
|
|
1948
|
+
|
|
1949
|
+
def _tui_card_reading(conn, *, now_utc: dt.datetime,
|
|
1950
|
+
reading_errors: "list | None" = None, **scope_fields):
|
|
1951
|
+
"""``(weekly_reading_scope, latest_weekly_reading_at,
|
|
1952
|
+
weekly_reading_retry)`` for a card being BUILT.
|
|
1953
|
+
|
|
1954
|
+
A ``sqlite3.Error`` never costs the card (#1018 twin review F1): it is
|
|
1955
|
+
built without freshness, the error is appended to ``reading_errors`` for
|
|
1956
|
+
the caller to record through its own failure handling (which classifies
|
|
1957
|
+
stats corruption and requests the heal, exactly as for the card's other
|
|
1958
|
+
reads), and the card keeps what the idle refresh needs to retry — the
|
|
1959
|
+
resolved scope when only the selector failed, or the scope's inputs
|
|
1960
|
+
(`_TuiWeeklyReadingRequest`) when the scope lookup itself failed. A
|
|
1961
|
+
caller passing no list (a direct builder call with no failure surface)
|
|
1962
|
+
gets the same card and retry state, unrecorded."""
|
|
1963
|
+
try:
|
|
1964
|
+
scope = _tui_weekly_reading_scope(
|
|
1965
|
+
conn, now_utc=now_utc, **scope_fields)
|
|
1966
|
+
except sqlite3.Error as exc:
|
|
1967
|
+
if reading_errors is not None:
|
|
1968
|
+
reading_errors.append(exc)
|
|
1969
|
+
return None, None, _TuiWeeklyReadingRequest(**scope_fields)
|
|
1970
|
+
try:
|
|
1971
|
+
return scope, _tui_weekly_reading_at(conn, scope, now_utc), None
|
|
1972
|
+
except sqlite3.Error as exc:
|
|
1973
|
+
if reading_errors is not None:
|
|
1974
|
+
reading_errors.append(exc)
|
|
1975
|
+
return scope, None, None
|
|
1976
|
+
|
|
1977
|
+
|
|
1978
|
+
def _tui_weekly_reading_at(conn, scope, now_utc: dt.datetime):
|
|
1979
|
+
"""`latest_weekly_reading_at` for ``scope`` (None skips the query)."""
|
|
1980
|
+
if scope is None:
|
|
1981
|
+
return None
|
|
1982
|
+
import _cctally_oauth_samples
|
|
1983
|
+
return _cctally_oauth_samples.latest_weekly_reading_at(
|
|
1984
|
+
conn, scope, now_utc)
|
|
1985
|
+
|
|
1986
|
+
|
|
1987
|
+
def refresh_current_week_reading(cw, conn, now_utc: dt.datetime, *,
|
|
1988
|
+
oauth_usage_cfg: "dict | None" = None):
|
|
1989
|
+
"""``cw`` with its confirming-reading freshness re-derived at ``now_utc``
|
|
1990
|
+
(#1018 twin spec 5.5), for the idle and reuse ticks that do not rebuild
|
|
1991
|
+
the card.
|
|
1992
|
+
|
|
1993
|
+
Re-runs the selector on the card's own ``weekly_reading_scope`` (a card
|
|
1994
|
+
with no scope skips the query and keeps its instant), then recomputes the
|
|
1995
|
+
whole freshness tuple, because the label and age advance with the clock
|
|
1996
|
+
even when no reading arrives. The instant may move forward, backwards or
|
|
1997
|
+
to None (a later reviewed hold). A card whose build-time scope lookup
|
|
1998
|
+
failed carries ``weekly_reading_retry`` instead: the lookup is retried
|
|
1999
|
+
here first, and once it resolves (to a scope or to None) the retry state
|
|
2000
|
+
is dropped (review F1). ``oauth_usage_cfg`` is the caller's resolved
|
|
2001
|
+
threshold block (review F4/F9), else the config file is read. Returns
|
|
2002
|
+
``cw`` itself only when nothing changed, else a replaced copy: a
|
|
2003
|
+
published card is never mutated. A ``sqlite3.Error`` propagates; the
|
|
2004
|
+
caller keeps its prior card, retry state included."""
|
|
2005
|
+
if cw is None:
|
|
2006
|
+
return None
|
|
2007
|
+
scope = getattr(cw, "weekly_reading_scope", None)
|
|
2008
|
+
retry = getattr(cw, "weekly_reading_retry", None)
|
|
2009
|
+
if scope is None and retry is not None:
|
|
2010
|
+
scope = _tui_weekly_reading_scope(
|
|
2011
|
+
conn, now_utc=now_utc, **retry.scope_fields())
|
|
2012
|
+
retry = None
|
|
2013
|
+
if scope is None:
|
|
2014
|
+
reading_at = getattr(cw, "latest_weekly_reading_at", None)
|
|
2015
|
+
else:
|
|
2016
|
+
reading_at = _tui_weekly_reading_at(conn, scope, now_utc)
|
|
2017
|
+
label, age = _tui_card_freshness(reading_at, now_utc, oauth_usage_cfg)
|
|
2018
|
+
if (reading_at == getattr(cw, "latest_weekly_reading_at", None)
|
|
2019
|
+
and label == cw.freshness_label and age == cw.freshness_age
|
|
2020
|
+
and retry is getattr(cw, "weekly_reading_retry", None)):
|
|
2021
|
+
return cw
|
|
2022
|
+
return dataclasses.replace(
|
|
2023
|
+
cw, latest_weekly_reading_at=reading_at, freshness_label=label,
|
|
2024
|
+
freshness_age=age, weekly_reading_scope=scope,
|
|
2025
|
+
weekly_reading_retry=retry)
|
|
2026
|
+
|
|
2027
|
+
|
|
1745
2028
|
def _tui_five_hour_resets_at(five_hour, now_utc: dt.datetime):
|
|
1746
2029
|
"""The five-hour reading's reset instant, or None when absent,
|
|
1747
2030
|
unparseable or already elapsed (a stale reset would render as
|
|
@@ -1773,6 +2056,7 @@ def _tui_build_decorated_current_week(
|
|
|
1773
2056
|
now_utc: dt.datetime,
|
|
1774
2057
|
*,
|
|
1775
2058
|
skip_sync: bool = False,
|
|
2059
|
+
reading_errors: "list | None" = None,
|
|
1776
2060
|
) -> TuiCurrentWeek | None:
|
|
1777
2061
|
"""The Current Week card on a decorated store: ONE account (#836,
|
|
1778
2062
|
spec 5.2).
|
|
@@ -1789,35 +2073,47 @@ def _tui_build_decorated_current_week(
|
|
|
1789
2073
|
never falls back to the merged population. `_apply_midweek_reset_override`
|
|
1790
2074
|
is not called here.
|
|
1791
2075
|
"""
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
if
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
2076
|
+
import _lib_snapshot_cache
|
|
2077
|
+
# #1018 twin spec 5.3: the card's evidence reads, the confirming-reading
|
|
2078
|
+
# lookup included, share ONE stats snapshot, released before the cost
|
|
2079
|
+
# aggregation below.
|
|
2080
|
+
with _lib_snapshot_cache.one_read_snapshot(conn):
|
|
2081
|
+
owner = _claude_select_current_owner(conn, now_utc)
|
|
2082
|
+
if owner is None:
|
|
2083
|
+
return None
|
|
2084
|
+
scope = _claude_current_cycle_scope(conn, now_utc, account_key=owner)
|
|
2085
|
+
if scope is None:
|
|
2086
|
+
return None
|
|
2087
|
+
fetched = _claude_cycle_scope_samples(
|
|
2088
|
+
conn, scope, now_utc, include_journal_id=True)
|
|
2089
|
+
if fetched is None:
|
|
2090
|
+
return None
|
|
2091
|
+
week_start_at, week_end_at, observations, week_scope = fetched
|
|
2092
|
+
week_start_before_override = week_start_at
|
|
2093
|
+
if scope.effective_start_at is not None:
|
|
2094
|
+
# The scope's effective start may also PRECEDE the nominal start (a
|
|
2095
|
+
# same-date early re-anchor, #836 review F7): the card's cycle
|
|
2096
|
+
# starts there either way, as its current index entry does.
|
|
2097
|
+
effective = parse_iso_datetime(
|
|
2098
|
+
scope.effective_start_at, "cycle_scope.effective_start_at")
|
|
2099
|
+
if effective != week_start_at:
|
|
2100
|
+
week_start_at = effective
|
|
2101
|
+
observations = [s for s in observations if s[0] >= effective]
|
|
2102
|
+
weekly_samples = [s for s in observations if not s[3]]
|
|
2103
|
+
if not weekly_samples:
|
|
2104
|
+
return None
|
|
2105
|
+
latest = weekly_samples[-1]
|
|
2106
|
+
used_pct = float(latest[1])
|
|
2107
|
+
five_hour = _current_week_five_hour_reading(
|
|
2108
|
+
conn, now_utc, week_scope, account_key=owner,
|
|
2109
|
+
week_start_at=week_start_at,
|
|
2110
|
+
week_start_before_override=week_start_before_override)
|
|
2111
|
+
reading_scope, reading_at, reading_retry = _tui_card_reading(
|
|
2112
|
+
conn, reading_errors=reading_errors, journal_id=latest[5], account_key=owner,
|
|
2113
|
+
weekly_percent=latest[1], segment_start=week_start_at,
|
|
2114
|
+
week_start_date=scope.week_start_date,
|
|
2115
|
+
nominal_start=week_start_before_override,
|
|
2116
|
+
week_end_at=week_end_at, now_utc=now_utc)
|
|
1821
2117
|
spent, total_tokens = _sum_cost_and_tokens_for_range(
|
|
1822
2118
|
week_start_at, now_utc, mode="auto", skip_sync=skip_sync,
|
|
1823
2119
|
account_key=owner,
|
|
@@ -1826,7 +2122,7 @@ def _tui_build_decorated_current_week(
|
|
|
1826
2122
|
used_pct_corrected = corrected_percent_point(used_pct)
|
|
1827
2123
|
dpp_divisor = used_pct if used_pct_corrected is None else used_pct_corrected
|
|
1828
2124
|
dpp = (spent / dpp_divisor) if used_pct > 0 else None
|
|
1829
|
-
freshness_label, freshness_age = _tui_card_freshness(
|
|
2125
|
+
freshness_label, freshness_age = _tui_card_freshness(reading_at, now_utc)
|
|
1830
2126
|
import _cctally_account
|
|
1831
2127
|
return TuiCurrentWeek(
|
|
1832
2128
|
week_start_at=week_start_at,
|
|
@@ -1848,6 +2144,9 @@ def _tui_build_decorated_current_week(
|
|
|
1848
2144
|
account_decorated=True,
|
|
1849
2145
|
account_label=_cctally_account.display_account_label(conn, owner),
|
|
1850
2146
|
cycle_scope=scope,
|
|
2147
|
+
latest_weekly_reading_at=reading_at,
|
|
2148
|
+
weekly_reading_scope=reading_scope,
|
|
2149
|
+
weekly_reading_retry=reading_retry,
|
|
1851
2150
|
)
|
|
1852
2151
|
|
|
1853
2152
|
|
|
@@ -1862,11 +2161,16 @@ def _tui_build_current_week(
|
|
|
1862
2161
|
now_utc: dt.datetime,
|
|
1863
2162
|
*,
|
|
1864
2163
|
skip_sync: bool = False,
|
|
2164
|
+
reading_errors: "list | None" = None,
|
|
1865
2165
|
) -> TuiCurrentWeek | None:
|
|
1866
2166
|
"""Build the TuiCurrentWeek from the latest snapshot + live cost.
|
|
1867
2167
|
|
|
1868
2168
|
Returns None when no current-week usage snapshot exists.
|
|
1869
2169
|
|
|
2170
|
+
``reading_errors`` (#1018 twin review F1) collects a ``sqlite3.Error``
|
|
2171
|
+
met by the confirming-reading lookup, which never costs the card: the
|
|
2172
|
+
caller records each one through its failure handling (`_tui_card_reading`).
|
|
2173
|
+
|
|
1870
2174
|
#836: a decorated store (more than one real Claude account, R8) takes
|
|
1871
2175
|
`_tui_build_decorated_current_week`, the one-account card. Below the
|
|
1872
2176
|
threshold the merged card below is unchanged.
|
|
@@ -1874,58 +2178,70 @@ def _tui_build_current_week(
|
|
|
1874
2178
|
import _cctally_account
|
|
1875
2179
|
if _cctally_account.provider_is_decorated(conn, "claude"):
|
|
1876
2180
|
return _tui_build_decorated_current_week(
|
|
1877
|
-
conn, now_utc, skip_sync=skip_sync)
|
|
1878
|
-
|
|
1879
|
-
#
|
|
1880
|
-
#
|
|
1881
|
-
#
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
2181
|
+
conn, now_utc, skip_sync=skip_sync, reading_errors=reading_errors)
|
|
2182
|
+
import _lib_snapshot_cache
|
|
2183
|
+
# #1018 twin spec 5.3: the card's evidence reads, the confirming-reading
|
|
2184
|
+
# lookup included, share ONE stats snapshot, released before the cost
|
|
2185
|
+
# aggregation below.
|
|
2186
|
+
with _lib_snapshot_cache.one_read_snapshot(conn):
|
|
2187
|
+
# #769 S11 (#824): ONE read, held-inclusive, split per axis below.
|
|
2188
|
+
# Retain the owner of the exact non-held weekly sample this card displays,
|
|
2189
|
+
# including when one real account coexists with unattributed history. R8
|
|
2190
|
+
# controls decoration, not the physical population used for computation.
|
|
2191
|
+
# Cost and forecast retain their existing merged semantics.
|
|
2192
|
+
fetched = _fetch_current_week_snapshots(
|
|
2193
|
+
conn, now_utc, include_held=True, include_account=True,
|
|
2194
|
+
include_scope=True, include_journal_id=True)
|
|
2195
|
+
if fetched is None:
|
|
2196
|
+
return None
|
|
2197
|
+
week_start_at, week_end_at, observations, week_scope = fetched
|
|
2198
|
+
if not observations:
|
|
2199
|
+
return None
|
|
2200
|
+
week_start_before_override = week_start_at
|
|
2201
|
+
# Mirror the reset override applied by `_load_forecast_inputs` so the
|
|
2202
|
+
# Current Week card's spent_usd and $/1% reflect the post-reset window.
|
|
2203
|
+
week_start_at, observations = _apply_midweek_reset_override(
|
|
2204
|
+
conn, week_start_at, week_end_at, observations, now_utc=now_utc
|
|
2205
|
+
)
|
|
2206
|
+
if not observations:
|
|
2207
|
+
return None
|
|
2208
|
+
# Held-inclusive sample shape: (captured_at_utc, weekly_percent,
|
|
2209
|
+
# five_hour_percent, weekly_observation_held, account_key, journal_id).
|
|
2210
|
+
# The account key identifies the displayed weekly sample's owner, and
|
|
2211
|
+
# its journal id the originating observation (#1018 twin).
|
|
2212
|
+
# The helper does not surface five_hour_resets_at; the five-hour reading
|
|
2213
|
+
# selected below supplies it, with its percentage.
|
|
2214
|
+
#
|
|
2215
|
+
# #769 S11 (#824). This card publishes BOTH axes, and the dashboard
|
|
2216
|
+
# envelope reads `used_pct` and `latest_snapshot_at` straight off it for
|
|
2217
|
+
# `current_week.freshness` and `snapshot_age_seconds`. A held row carries
|
|
2218
|
+
# an older row's weekly value under this tick's capture instant, so
|
|
2219
|
+
# letting one reach `latest` would present stale weekly evidence as freshly
|
|
2220
|
+
# captured — review finding 5. The five-hour percentage takes the newest
|
|
2221
|
+
# observation of either kind, because a held row is written exactly when
|
|
2222
|
+
# its five-hour reading grew.
|
|
2223
|
+
weekly_samples = [s for s in observations if not s[3]]
|
|
2224
|
+
if not weekly_samples:
|
|
2225
|
+
return None
|
|
2226
|
+
latest = weekly_samples[-1]
|
|
2227
|
+
used_pct = float(latest[1])
|
|
2228
|
+
weekly_account = latest[4]
|
|
2229
|
+
# Five-hour readings may be newer held observations, but must remain on
|
|
2230
|
+
# the account that supplied the displayed weekly reading. The reading is
|
|
2231
|
+
# the newest five-hour READING under this card's own week and as-of scope
|
|
2232
|
+
# (#825/#832 spec 4.7 revision 7, the one shared selection): the newest
|
|
2233
|
+
# sample can be a retired replica or a weekly-only reading, which carries
|
|
2234
|
+
# no five-hour value and used to render as 0 %.
|
|
2235
|
+
five_hour = _current_week_five_hour_reading(
|
|
2236
|
+
conn, now_utc, week_scope, account_key=weekly_account,
|
|
2237
|
+
week_start_at=week_start_at,
|
|
2238
|
+
week_start_before_override=week_start_before_override)
|
|
2239
|
+
reading_scope, reading_at, reading_retry = _tui_card_reading(
|
|
2240
|
+
conn, reading_errors=reading_errors, journal_id=latest[5], account_key=weekly_account,
|
|
2241
|
+
weekly_percent=latest[1], segment_start=week_start_at,
|
|
2242
|
+
week_start_date=week_scope[0],
|
|
2243
|
+
nominal_start=week_start_before_override,
|
|
2244
|
+
week_end_at=week_end_at, now_utc=now_utc)
|
|
1929
2245
|
five_hr_pct = None if five_hour is None else five_hour.percent
|
|
1930
2246
|
# #556 S1 §3.3: one walk yields both halves. The range is whatever
|
|
1931
2247
|
# `spent_usd` already used — taken AFTER `_apply_midweek_reset_override`
|
|
@@ -1953,8 +2269,11 @@ def _tui_build_current_week(
|
|
|
1953
2269
|
# Suppress stale resets that have already elapsed so renderers don't
|
|
1954
2270
|
# show "resets 0h 00m" or a negative duration at the boundary.
|
|
1955
2271
|
five_hr_resets_at = _tui_five_hour_resets_at(five_hour, now_utc)
|
|
1956
|
-
# Freshness — label/age from the
|
|
1957
|
-
|
|
2272
|
+
# Freshness — label/age from the newest genuine reading confirming the
|
|
2273
|
+
# displayed value (#1018 twin spec 5.3), not the stored row: equal
|
|
2274
|
+
# readings are deduplicated out of `weekly_usage_snapshots`, so a healthy
|
|
2275
|
+
# flat stream leaves that row arbitrarily old.
|
|
2276
|
+
freshness_label, freshness_age = _tui_card_freshness(reading_at, now_utc)
|
|
1958
2277
|
return TuiCurrentWeek(
|
|
1959
2278
|
week_start_at=week_start_at,
|
|
1960
2279
|
week_end_at=week_end_at,
|
|
@@ -1971,6 +2290,9 @@ def _tui_build_current_week(
|
|
|
1971
2290
|
account_key=weekly_account,
|
|
1972
2291
|
),
|
|
1973
2292
|
total_tokens=total_tokens,
|
|
2293
|
+
latest_weekly_reading_at=reading_at,
|
|
2294
|
+
weekly_reading_scope=reading_scope,
|
|
2295
|
+
weekly_reading_retry=reading_retry,
|
|
1974
2296
|
)
|
|
1975
2297
|
|
|
1976
2298
|
|
|
@@ -2600,7 +2922,12 @@ def _snapshot_data_version(sig) -> str:
|
|
|
2600
2922
|
# with no retired snapshot and no unavailable five-hour join, so it is
|
|
2601
2923
|
# byte-neutral there.
|
|
2602
2924
|
retired = getattr(sig, "claude_weekly_retired_digest", "")
|
|
2603
|
-
|
|
2925
|
+
out = out if not retired else f"{out}.r{retired}"
|
|
2926
|
+
# #1050 spec 4.5 item 2: the Claude content revision leg, so an in-place
|
|
2927
|
+
# rewrite of a published Claude row revalidates the browser. `""` on a
|
|
2928
|
+
# never-mutated store, so its bytes are unchanged.
|
|
2929
|
+
revision = getattr(sig, "claude_content_revision", "")
|
|
2930
|
+
return out if not revision else f"{out}.c{revision}"
|
|
2604
2931
|
|
|
2605
2932
|
|
|
2606
2933
|
def _tui_evidence_eligible_at(conn, as_of_utc: dt.datetime) -> "dt.datetime | None":
|
|
@@ -2972,6 +3299,50 @@ def _tui_claude_domain_freshness(
|
|
|
2972
3299
|
return {"hero": accounting, "quota": quota, "sessions": "fresh"}
|
|
2973
3300
|
|
|
2974
3301
|
|
|
3302
|
+
def _tui_claude_with_current_week_freshness(
|
|
3303
|
+
state: SourceDashboardState,
|
|
3304
|
+
freshness: "Mapping[str, object] | None",
|
|
3305
|
+
) -> SourceDashboardState:
|
|
3306
|
+
"""``state`` publishing ``freshness`` as its current-week freshness: both
|
|
3307
|
+
source copies (``hero.current_week.freshness``,
|
|
3308
|
+
``quota.current_week.freshness``) and the quota domain clock, which is
|
|
3309
|
+
`stale` exactly when that label is (`_tui_claude_domain_freshness`'s
|
|
3310
|
+
rule, None -> `fresh`). For the ticks that keep a Claude state without
|
|
3311
|
+
rebuilding it (#1018 twin spec 5.5): the idle clock and exact-version
|
|
3312
|
+
reuse. Returns ``state`` itself when nothing changes; never mutates it."""
|
|
3313
|
+
if state.source != "claude" or not isinstance(state.data, Mapping):
|
|
3314
|
+
return state
|
|
3315
|
+
value = None if freshness is None else _tui_source_copy(dict(freshness))
|
|
3316
|
+
data = dict(state.data)
|
|
3317
|
+
changed = False
|
|
3318
|
+
for domain in ("hero", "quota"):
|
|
3319
|
+
section = data.get(domain)
|
|
3320
|
+
current_week = (
|
|
3321
|
+
section.get("current_week") if isinstance(section, Mapping)
|
|
3322
|
+
else None)
|
|
3323
|
+
if not isinstance(current_week, Mapping):
|
|
3324
|
+
continue
|
|
3325
|
+
if current_week.get("freshness") == value:
|
|
3326
|
+
continue
|
|
3327
|
+
data[domain] = {
|
|
3328
|
+
**dict(section),
|
|
3329
|
+
"current_week": {**dict(current_week), "freshness": value},
|
|
3330
|
+
}
|
|
3331
|
+
changed = True
|
|
3332
|
+
domain_freshness = dict(state.domain_freshness or {})
|
|
3333
|
+
quota = (
|
|
3334
|
+
"stale" if isinstance(value, dict) and value.get("label") == "stale"
|
|
3335
|
+
else "fresh")
|
|
3336
|
+
if domain_freshness.get("quota") != quota:
|
|
3337
|
+
domain_freshness["quota"] = quota
|
|
3338
|
+
changed = True
|
|
3339
|
+
if not changed:
|
|
3340
|
+
return state
|
|
3341
|
+
refreshed = dataclasses.replace(
|
|
3342
|
+
state, data=data, domain_freshness=domain_freshness)
|
|
3343
|
+
return state if refreshed == state else refreshed
|
|
3344
|
+
|
|
3345
|
+
|
|
2975
3346
|
def _refresh_claude_budget_clock(
|
|
2976
3347
|
state: SourceDashboardState,
|
|
2977
3348
|
*,
|
|
@@ -3026,8 +3397,13 @@ def _refresh_claude_source_clock(
|
|
|
3026
3397
|
current_week: object,
|
|
3027
3398
|
now_utc: dt.datetime,
|
|
3028
3399
|
raw_config: dict[str, object],
|
|
3400
|
+
refresh_current_week: bool = True,
|
|
3029
3401
|
) -> SourceDashboardState:
|
|
3030
|
-
"""Advance Claude's weekly axes from frozen snapshot evidence only.
|
|
3402
|
+
"""Advance Claude's weekly axes from frozen snapshot evidence only.
|
|
3403
|
+
|
|
3404
|
+
``refresh_current_week`` False (the idle tick's confirming-reading lookup
|
|
3405
|
+
failed, #1018 twin spec 5.5) keeps the prior current-week freshness
|
|
3406
|
+
copies and quota clock; the accounting and budget legs still run."""
|
|
3031
3407
|
if state.source != "claude":
|
|
3032
3408
|
return state
|
|
3033
3409
|
if now_utc.tzinfo is None or now_utc.utcoffset() is None:
|
|
@@ -3047,30 +3423,20 @@ def _refresh_claude_source_clock(
|
|
|
3047
3423
|
).astimezone(dt.timezone.utc)
|
|
3048
3424
|
else "stale"
|
|
3049
3425
|
)
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
freshness_config = _OAUTH_USAGE_DEFAULTS
|
|
3065
|
-
domain_freshness["quota"] = (
|
|
3066
|
-
"stale"
|
|
3067
|
-
if _freshness_label(age_seconds, freshness_config) == "stale"
|
|
3068
|
-
else "fresh"
|
|
3069
|
-
)
|
|
3070
|
-
refreshed = dataclasses.replace(
|
|
3071
|
-
state,
|
|
3072
|
-
domain_freshness=domain_freshness,
|
|
3073
|
-
)
|
|
3426
|
+
# #1018 twin spec 5.4: the quota axis and the source copies of the
|
|
3427
|
+
# current-week freshness follow the card's newest CONFIRMING reading,
|
|
3428
|
+
# recomputed on every tick — including to None, where the quota axis is
|
|
3429
|
+
# `fresh` (a null age is unknown, not old) rather than the value a prior
|
|
3430
|
+
# tick left. The stored row's `latest_snapshot_at` no longer feeds it.
|
|
3431
|
+
freshness_config = _tui_oauth_usage_cfg(raw_config)
|
|
3432
|
+
refreshed = dataclasses.replace(state, domain_freshness=domain_freshness)
|
|
3433
|
+
if refresh_current_week:
|
|
3434
|
+
import _cctally_dashboard_envelope
|
|
3435
|
+
freshness = _cctally_dashboard_envelope.current_week_freshness(
|
|
3436
|
+
getattr(current_week, "latest_weekly_reading_at", None),
|
|
3437
|
+
now_utc=now_utc, oauth_usage_cfg=freshness_config)
|
|
3438
|
+
refreshed = _tui_claude_with_current_week_freshness(
|
|
3439
|
+
refreshed, freshness)
|
|
3074
3440
|
# #556 S5 §3.7: the budget leg runs on the pure-idle path too. It is the
|
|
3075
3441
|
# SAME helper the bundle builder calls after every other branch, so the two
|
|
3076
3442
|
# paths cannot drift.
|
|
@@ -4106,9 +4472,30 @@ def _tui_build_source_bundle(
|
|
|
4106
4472
|
raw_config: dict[str, object] | None = None,
|
|
4107
4473
|
attempt: dict[str, bool] | None = None,
|
|
4108
4474
|
evidence_eligible_at: dt.datetime | None = None,
|
|
4475
|
+
claude_input_identity,
|
|
4476
|
+
codex_speed: str | None = None,
|
|
4109
4477
|
) -> SourceDashboardBundle:
|
|
4110
4478
|
"""Build one frozen source bundle after the dashboard's coordinated ingest.
|
|
4111
4479
|
|
|
4480
|
+
``claude_input_identity`` (#1050 spec 4.5 items 3-5), REQUIRED with no
|
|
4481
|
+
default, is ``(SnapshotSignature, ContentRevision)``: the dispatch read
|
|
4482
|
+
under which the supplied Claude inputs (``claude_data``,
|
|
4483
|
+
``projects_envelope``, the cost and token totals) were assembled — this
|
|
4484
|
+
tick's dispatch read on the full path, the idle tick's on the bounded
|
|
4485
|
+
adapter. An identity certifies only data read after it, so every
|
|
4486
|
+
signature-derived `claude_version` leg comes from it, never from this
|
|
4487
|
+
builder's own later start read. Movement against it alone never rejects
|
|
4488
|
+
the build (spec 4.5 item 5, revision 6): only the start-read-to-post-build
|
|
4489
|
+
bracket does. ``None`` (no dispatch identity could be
|
|
4490
|
+
read this tick) certifies under the start read with a fresh nonce leg,
|
|
4491
|
+
so that source is never reused by exact version.
|
|
4492
|
+
|
|
4493
|
+
``codex_speed`` (#1050 spec 4.7) is the resolved Codex service tier this
|
|
4494
|
+
tick captured beside its dispatch read (`_tui_resolve_codex_speed`), the
|
|
4495
|
+
fifth element of the dispatch key: Codex construction, ``codex_version``
|
|
4496
|
+
and every Codex cycle stamp use that one value rather than resolving the
|
|
4497
|
+
tier again. ``None`` (a direct caller) resolves it here.
|
|
4498
|
+
|
|
4112
4499
|
``evidence_eligible_at`` (#836, spec 5.7 item 2) is the build's
|
|
4113
4500
|
`DataSnapshot.evidence_eligible_at`; it enters the Claude version only
|
|
4114
4501
|
when set.
|
|
@@ -4218,6 +4605,7 @@ def _tui_build_source_bundle(
|
|
|
4218
4605
|
semantics = resolve_dashboard_source_semantics(
|
|
4219
4606
|
raw_config if raw_config is not None else c.load_config(),
|
|
4220
4607
|
display_tz_name=display_tz_name,
|
|
4608
|
+
speed=codex_speed,
|
|
4221
4609
|
)
|
|
4222
4610
|
# `codex_stats_digest`'s relation table reads `quota_projection_state`
|
|
4223
4611
|
# and `quota_window_blocks` from a pure kernel that may not import
|
|
@@ -4263,6 +4651,16 @@ def _tui_build_source_bundle(
|
|
|
4263
4651
|
claude_stats_digest=claude_digest,
|
|
4264
4652
|
claude_weekly_retired_digest=retired_digest,
|
|
4265
4653
|
)
|
|
4654
|
+
# #1050 spec 4.5 items 3-4: the identity the supplied Claude inputs
|
|
4655
|
+
# were assembled under. `signature` above stays the builder's own
|
|
4656
|
+
# start read: Codex is built from this builder's reads, and the
|
|
4657
|
+
# Claude carriers below are captured inside this pin.
|
|
4658
|
+
if claude_input_identity is None:
|
|
4659
|
+
input_signature = signature._replace(
|
|
4660
|
+
claude_content_revision=_content_revision.revision_leg(
|
|
4661
|
+
_TUI_UNREADABLE_CONTENT_REVISION))
|
|
4662
|
+
else:
|
|
4663
|
+
input_signature, _ = claude_input_identity
|
|
4266
4664
|
# #857: the Codex quota-dependency identity, read on the SAME pinned
|
|
4267
4665
|
# cache snapshot the Codex capture below reads, so a generation built
|
|
4268
4666
|
# this tick is stamped with exactly the inputs it saw — never with a
|
|
@@ -4321,20 +4719,34 @@ def _tui_build_source_bundle(
|
|
|
4321
4719
|
week_start_name=semantics.week_start_name,
|
|
4322
4720
|
)
|
|
4323
4721
|
_budget_suffix = f":b{_budget_identity}" if _budget_identity else ""
|
|
4722
|
+
# #1050 spec 4.5 item 4: every signature-derived leg is the INPUT
|
|
4723
|
+
# identity's. On `main` they came from `signature`, read after the
|
|
4724
|
+
# supplied values were assembled, so an append or a correction landing
|
|
4725
|
+
# in between certified the older values under the newer legs and every
|
|
4726
|
+
# later tick reused that source by exact version.
|
|
4324
4727
|
claude_version = (
|
|
4325
|
-
f"claude:{
|
|
4326
|
-
f"{
|
|
4327
|
-
f"{
|
|
4328
|
-
f"{
|
|
4728
|
+
f"claude:{input_signature.max_entry_id}:"
|
|
4729
|
+
f"{input_signature.entry_mutation_seq}:"
|
|
4730
|
+
f"{input_signature.max_wus_id}:{input_signature.max_wcs_id}:"
|
|
4731
|
+
f"{input_signature.reset_sig[0]}:{input_signature.reset_sig[1]}:"
|
|
4732
|
+
f"{input_signature.generation}:{semantics.claude_identity}"
|
|
4329
4733
|
f":p{_period_identity}{_budget_suffix}{_acct_suffix}"
|
|
4330
|
-
f"{_aggregate_suffix}:x{
|
|
4734
|
+
f"{_aggregate_suffix}:x{input_signature.claude_stats_digest}"
|
|
4331
4735
|
)
|
|
4332
4736
|
# #836 spec 5.7: exact-version reuse compares this string, so a
|
|
4333
4737
|
# retirement or restoration (`:r`) and a tick that admits evidence an
|
|
4334
4738
|
# earlier one excluded (`:e`) both refuse the prior Claude object. Each
|
|
4335
4739
|
# is appended only when set, so other stores keep their version bytes.
|
|
4336
|
-
if
|
|
4337
|
-
claude_version =
|
|
4740
|
+
if input_signature.claude_weekly_retired_digest:
|
|
4741
|
+
claude_version = (
|
|
4742
|
+
f"{claude_version}:r"
|
|
4743
|
+
f"{input_signature.claude_weekly_retired_digest}")
|
|
4744
|
+
# #1050 spec 4.5 item 4: the content revision leg (`""` on a
|
|
4745
|
+
# never-mutated store, so its version bytes are unchanged; a nonce on
|
|
4746
|
+
# an untrusted one, so that source is never reused).
|
|
4747
|
+
if input_signature.claude_content_revision:
|
|
4748
|
+
claude_version = (
|
|
4749
|
+
f"{claude_version}:c{input_signature.claude_content_revision}")
|
|
4338
4750
|
if evidence_eligible_at is not None:
|
|
4339
4751
|
claude_version = f"{claude_version}:e{_iso_z(evidence_eligible_at)}"
|
|
4340
4752
|
# #836 spec 5.7: the decorated-only fragment. A decorated Claude source
|
|
@@ -4404,6 +4816,19 @@ def _tui_build_source_bundle(
|
|
|
4404
4816
|
# the life of the process. Gate 1 is the bundle-level idle guard.
|
|
4405
4817
|
if claude is not None and aggregate_scope_failed(claude):
|
|
4406
4818
|
claude = None
|
|
4819
|
+
# #1018 twin spec 5.5: a reused (or retained) Claude state is the
|
|
4820
|
+
# PRIOR object, whose current-week freshness copies and quota
|
|
4821
|
+
# clock were derived at its build instant. The version deliberately
|
|
4822
|
+
# excludes that freshness (it moves with the clock), so republish
|
|
4823
|
+
# it from this build's freshly derived card instead.
|
|
4824
|
+
if claude is not None and claude is prior_claude and (
|
|
4825
|
+
isinstance(claude_data, Mapping)):
|
|
4826
|
+
_hero = claude_data.get("hero")
|
|
4827
|
+
_cw = (_hero.get("current_week")
|
|
4828
|
+
if isinstance(_hero, Mapping) else None)
|
|
4829
|
+
if isinstance(_cw, Mapping):
|
|
4830
|
+
claude = _tui_claude_with_current_week_freshness(
|
|
4831
|
+
claude, _cw.get("freshness"))
|
|
4407
4832
|
if codex_ingest_failed:
|
|
4408
4833
|
warning = SourceDashboardWarning(
|
|
4409
4834
|
"source_ingest_failed", "Source ingest failed.", "ingest",
|
|
@@ -4854,6 +5279,24 @@ def _tui_build_source_bundle(
|
|
|
4854
5279
|
# and the settle window guarantees the next commit a newer timestamp
|
|
4855
5280
|
# even on a coarse-timestamp filesystem. So these reads rescan and see
|
|
4856
5281
|
# the new generation (#857; `_stats_relations_digest_signal`).
|
|
5282
|
+
#
|
|
5283
|
+
# #1050 spec 4.5 item 5 (revision 6): only this bracket — the
|
|
5284
|
+
# builder's start read against the post-build read, #857's terms —
|
|
5285
|
+
# rejects a build. A movement only against the Claude INPUT identity
|
|
5286
|
+
# (the dispatch read: a cache leg between it and the pinned start
|
|
5287
|
+
# read, or a stats leg, the retired digest or the content revision
|
|
5288
|
+
# between it and the post-build read) publishes the build, warm or
|
|
5289
|
+
# cold. That is safe because the Claude version names the dispatch
|
|
5290
|
+
# read, which precedes every supplied Claude value, and every such leg
|
|
5291
|
+
# is a dispatch-key leg the next tick reads again: whatever moved
|
|
5292
|
+
# makes the next tick non-idle, so it rebuilds and this source is
|
|
5293
|
+
# never reused by exact version. Rejecting instead would discard the
|
|
5294
|
+
# whole warm build, Codex part included, whenever a hook-tick cache
|
|
5295
|
+
# sync or a status-line ingest committed inside the dispatch-to-start
|
|
5296
|
+
# window — continuously under active use, and never rejected by
|
|
5297
|
+
# `main`, whose bracket starts at the start read. Nothing compares
|
|
5298
|
+
# against the input identity here, so the post-build read re-reads
|
|
5299
|
+
# neither the content revision nor the retired digest.
|
|
4857
5300
|
with _perf.phase("source.reconciliation"):
|
|
4858
5301
|
post_stats_digest = codex_stats_digest(stats_conn)
|
|
4859
5302
|
post_accounts_digest = accounts_identity_digest(stats_conn)
|
|
@@ -5427,6 +5870,9 @@ def _tui_build_snapshot_once(
|
|
|
5427
5870
|
# The signature is computed AFTER the top-of-rebuild ingest so a fresh
|
|
5428
5871
|
# tail-ingested row is reflected before the idle decision is made.
|
|
5429
5872
|
dispatch_key = None
|
|
5873
|
+
# #1050: `(dispatch_sig, its ContentRevision)`, set with the dispatch
|
|
5874
|
+
# read below; `None` when no dispatch identity was established.
|
|
5875
|
+
claude_input_identity = None
|
|
5430
5876
|
# #300: the change signal surfaced on the dashboard envelope. Empty
|
|
5431
5877
|
# unless a dispatch signature is computed below (the non-precompute /
|
|
5432
5878
|
# TUI path never consumes it). The idle short-circuit carries the prior
|
|
@@ -5436,6 +5882,9 @@ def _tui_build_snapshot_once(
|
|
|
5436
5882
|
_sc = None
|
|
5437
5883
|
prior_key = None
|
|
5438
5884
|
prior_snap = None
|
|
5885
|
+
# #1050: this tick's resolved Codex tier, captured with the dispatch
|
|
5886
|
+
# read below (`None` off the dashboard path, which builds no sources).
|
|
5887
|
+
codex_speed = None
|
|
5439
5888
|
if precompute_envelope:
|
|
5440
5889
|
# The last published provider generation is independently useful
|
|
5441
5890
|
# when calculating today's signature fails. Read it before the
|
|
@@ -5455,19 +5904,41 @@ def _tui_build_snapshot_once(
|
|
|
5455
5904
|
except Exception as exc:
|
|
5456
5905
|
capture_failure("prior-source-bundle", "other", exc)
|
|
5457
5906
|
dispatch_sig = None
|
|
5907
|
+
# #1050: the content revision read with `dispatch_sig`, or `None`.
|
|
5908
|
+
dispatch_content_revision = None
|
|
5458
5909
|
# #857: the Codex quota-dependency identity, read on the
|
|
5459
5910
|
# signature's own cache handle. An unreadable identity is `None`.
|
|
5460
5911
|
codex_dependency = None
|
|
5461
5912
|
# #1015 spec 5.4: the calibration dependency, read on this tick's
|
|
5462
5913
|
# own stats connection (`_tui_read_calibration_dependency`).
|
|
5463
5914
|
calibration_dependency = None
|
|
5915
|
+
# #1050 spec 4.7 (H5): the resolved Codex service tier, read once
|
|
5916
|
+
# beside the dispatch read. It comes from Codex's own `config.toml`
|
|
5917
|
+
# roots, which neither database nor `raw_config` covers, so it is
|
|
5918
|
+
# the dispatch key's fifth element, and the same value reaches
|
|
5919
|
+
# Codex construction and every Codex cycle stamp below.
|
|
5464
5920
|
with _perf.phase("signature"):
|
|
5921
|
+
codex_speed = _tui_resolve_codex_speed()
|
|
5465
5922
|
try:
|
|
5923
|
+
_revision_sink: list = []
|
|
5466
5924
|
dispatch_sig, codex_dependency = (
|
|
5467
5925
|
_tui_compute_dispatch_signature(
|
|
5468
5926
|
conn, with_codex_dependency=True,
|
|
5927
|
+
revision_sink=_revision_sink,
|
|
5469
5928
|
)
|
|
5470
5929
|
)
|
|
5930
|
+
# #1050 spec 4.5 items 1 and 3: the content revision this
|
|
5931
|
+
# dispatch read certified, the second half of the Claude
|
|
5932
|
+
# input identity every source build below is passed. An
|
|
5933
|
+
# unreadable one is recorded like any failed dispatch leg,
|
|
5934
|
+
# and its nonce leg already keeps the tick off the idle path.
|
|
5935
|
+
if _revision_sink:
|
|
5936
|
+
dispatch_content_revision, _revision_error = (
|
|
5937
|
+
_revision_sink[0])
|
|
5938
|
+
if _revision_error is not None:
|
|
5939
|
+
capture_failure(
|
|
5940
|
+
"dispatch-signature", "stats_or_cache",
|
|
5941
|
+
_revision_error)
|
|
5471
5942
|
calibration_dependency = (
|
|
5472
5943
|
_tui_read_calibration_dependency(conn))
|
|
5473
5944
|
except QuotaProjectionIncomplete as exc:
|
|
@@ -5482,6 +5953,15 @@ def _tui_build_snapshot_once(
|
|
|
5482
5953
|
capture_failure("dispatch-signature", "stats_or_cache", exc)
|
|
5483
5954
|
dispatch_sig = None
|
|
5484
5955
|
codex_dependency = None
|
|
5956
|
+
# #1050 spec 4.5 item 3: the identity under which every Claude
|
|
5957
|
+
# input this tick supplies to a source build is assembled — the
|
|
5958
|
+
# legacy snapshot and Projects envelope below, or on the idle path
|
|
5959
|
+
# the prior snapshot's values, whose stored key equals this read.
|
|
5960
|
+
claude_input_identity = (
|
|
5961
|
+
(dispatch_sig, dispatch_content_revision)
|
|
5962
|
+
if dispatch_sig is not None
|
|
5963
|
+
and dispatch_content_revision is not None else None
|
|
5964
|
+
)
|
|
5485
5965
|
if dispatch_sig is not None:
|
|
5486
5966
|
# The idle decision keys on the DB signature AND a render key
|
|
5487
5967
|
# that captures the config-derived inputs the composite
|
|
@@ -5511,9 +5991,12 @@ def _tui_build_snapshot_once(
|
|
|
5511
5991
|
# recorded attempt, a completed correction batch or a
|
|
5512
5992
|
# supersession leaves the idle path and rebuilds the forecast
|
|
5513
5993
|
# and the rate-change array on the next tick.
|
|
5994
|
+
# #1050 spec 4.7: the resolved Codex tier is the fifth element;
|
|
5995
|
+
# whole-key equality is the idle gate and the snapshot memo
|
|
5996
|
+
# treats the key as opaque.
|
|
5514
5997
|
dispatch_key = (
|
|
5515
5998
|
dispatch_sig, render_key, codex_dependency,
|
|
5516
|
-
calibration_dependency,
|
|
5999
|
+
calibration_dependency, codex_speed,
|
|
5517
6000
|
)
|
|
5518
6001
|
# #300: carry the change signal onto the non-idle snapshot built
|
|
5519
6002
|
# below. (The idle path returns `idle_snap`, which inherits the
|
|
@@ -5552,6 +6035,8 @@ def _tui_build_snapshot_once(
|
|
|
5552
6035
|
claude_ingest_failed=claude_ingest_failed,
|
|
5553
6036
|
errors=errors,
|
|
5554
6037
|
codex_dependency=codex_dependency,
|
|
6038
|
+
claude_input_identity=claude_input_identity,
|
|
6039
|
+
codex_speed=codex_speed,
|
|
5555
6040
|
)
|
|
5556
6041
|
assert _sc is not None
|
|
5557
6042
|
_sc.store_dispatch_state(dispatch_key, idle_snap)
|
|
@@ -5638,10 +6123,19 @@ def _tui_build_snapshot_once(
|
|
|
5638
6123
|
except Exception as exc:
|
|
5639
6124
|
capture_failure("snapshot-cache-reconcile", "cache", exc)
|
|
5640
6125
|
with _perf.phase("build.current_week"):
|
|
6126
|
+
reading_errors: list[Exception] = []
|
|
5641
6127
|
try:
|
|
5642
|
-
cw = _tui_build_current_week(
|
|
6128
|
+
cw = _tui_build_current_week(
|
|
6129
|
+
conn, now_utc, skip_sync=skip_sync,
|
|
6130
|
+
reading_errors=reading_errors)
|
|
5643
6131
|
except Exception as exc:
|
|
5644
6132
|
capture_failure("current-week", "stats_or_cache", exc)
|
|
6133
|
+
# #1018 twin review F1: the confirming-reading lookup's failure is
|
|
6134
|
+
# recorded like the card's other reads (stats corruption raises
|
|
6135
|
+
# the heal signal here, outside the builder's own handler) without
|
|
6136
|
+
# costing the card, which keeps what the idle refresh retries.
|
|
6137
|
+
for exc in reading_errors:
|
|
6138
|
+
capture_failure("current-week-reading", "stats_or_cache", exc)
|
|
5645
6139
|
fc_view = None
|
|
5646
6140
|
with _perf.phase("build.forecast"):
|
|
5647
6141
|
try:
|
|
@@ -5960,6 +6454,11 @@ def _tui_build_snapshot_once(
|
|
|
5960
6454
|
current_week=cw,
|
|
5961
6455
|
weeks_back=12,
|
|
5962
6456
|
use_projects_env_cache=use_projects_env_cache,
|
|
6457
|
+
# #1050 spec 4.5 item 7: the dispatch leg, read before the
|
|
6458
|
+
# memo lookup; `None` (no dispatch read, the TUI) reads it.
|
|
6459
|
+
content_revision_leg=(
|
|
6460
|
+
claude_input_identity[0].claude_content_revision
|
|
6461
|
+
if claude_input_identity is not None else None),
|
|
5963
6462
|
)
|
|
5964
6463
|
except Exception as exc:
|
|
5965
6464
|
capture_failure("projects-envelope", "stats_or_cache", exc)
|
|
@@ -6174,6 +6673,10 @@ def _tui_build_snapshot_once(
|
|
|
6174
6673
|
legacy_envelope = _cctally().snapshot_to_envelope(
|
|
6175
6674
|
source_snapshot,
|
|
6176
6675
|
now_utc=now_utc,
|
|
6676
|
+
# #1018 twin review F4: the configured freshness
|
|
6677
|
+
# thresholds, as the idle clock uses, so the Claude
|
|
6678
|
+
# source copies never flip label between ticks.
|
|
6679
|
+
oauth_usage_cfg=_tui_oauth_usage_cfg(raw_config),
|
|
6177
6680
|
display_tz_pref_override=display_tz_pref_override,
|
|
6178
6681
|
runtime_bind=runtime_bind,
|
|
6179
6682
|
)
|
|
@@ -6197,6 +6700,8 @@ def _tui_build_snapshot_once(
|
|
|
6197
6700
|
prior_bundle=prior_source_bundle,
|
|
6198
6701
|
raw_config=raw_config,
|
|
6199
6702
|
evidence_eligible_at=evidence_eligible_at,
|
|
6703
|
+
claude_input_identity=claude_input_identity,
|
|
6704
|
+
codex_speed=codex_speed,
|
|
6200
6705
|
)
|
|
6201
6706
|
if source_bundle is None:
|
|
6202
6707
|
raise RuntimeError("source bundle builder returned no bundle")
|
|
@@ -6408,7 +6913,62 @@ def _tui_open_observed_cache(c):
|
|
|
6408
6913
|
return opened_file, c.open_cache_db()
|
|
6409
6914
|
|
|
6410
6915
|
|
|
6411
|
-
|
|
6916
|
+
#: The identity a read records when the Claude content revision cannot be read
|
|
6917
|
+
#: at all (`StatsContentRevisionUnreadable`: a malformed store, or more than one
|
|
6918
|
+
#: publication stamp). Untrusted exactly like ``absent``: its published leg is
|
|
6919
|
+
#: a fresh nonce per read, and a bracket compares it as ``("unreadable",)``.
|
|
6920
|
+
_TUI_UNREADABLE_CONTENT_REVISION = _content_revision.ContentRevision(
|
|
6921
|
+
"unreadable", None, None)
|
|
6922
|
+
|
|
6923
|
+
|
|
6924
|
+
def _tui_read_content_revision(stats_conn):
|
|
6925
|
+
"""``(ContentRevision, error)`` for one read of the Claude content
|
|
6926
|
+
revision (#1050 spec 4.4). An unreadable identity is never an empty one:
|
|
6927
|
+
it is `_TUI_UNREADABLE_CONTENT_REVISION`, and the error is returned so the
|
|
6928
|
+
dispatch read can record it in the tick's failure capture."""
|
|
6929
|
+
try:
|
|
6930
|
+
return _content_revision.read_claude_content_revision(stats_conn), None
|
|
6931
|
+
except _content_revision.StatsContentRevisionUnreadable as exc:
|
|
6932
|
+
return _TUI_UNREADABLE_CONTENT_REVISION, exc
|
|
6933
|
+
|
|
6934
|
+
|
|
6935
|
+
def _tui_read_claude_input_identity(stats_conn):
|
|
6936
|
+
"""``(SnapshotSignature, ContentRevision)``: the dispatch read on its
|
|
6937
|
+
own, for a caller of `_tui_build_source_bundle` that has no tick (a bench,
|
|
6938
|
+
a test). It must be read BEFORE the Claude inputs it is passed with are
|
|
6939
|
+
assembled, exactly as the tick's dispatch read is (#1050 spec 4.5 item 3).
|
|
6940
|
+
"""
|
|
6941
|
+
sink: list = []
|
|
6942
|
+
try:
|
|
6943
|
+
signature = _tui_compute_dispatch_signature(
|
|
6944
|
+
stats_conn, revision_sink=sink)
|
|
6945
|
+
except QuotaProjectionIncomplete:
|
|
6946
|
+
# #496 S5b §4.7: a caller with no tick has no failure capture, so the
|
|
6947
|
+
# typed refusal propagates as itself rather than through a broad
|
|
6948
|
+
# handler that would sanitize its cause and remedy away. The typed
|
|
6949
|
+
# handler is also what satisfies
|
|
6950
|
+
# `tests/test_selector_state_496_s5b.py::test_the_tui_catches_the_refusal_before_its_broad_handlers`,
|
|
6951
|
+
# which counts one at every tick-less caller of the dispatch read.
|
|
6952
|
+
raise
|
|
6953
|
+
return signature, sink[0][0]
|
|
6954
|
+
|
|
6955
|
+
|
|
6956
|
+
def _tui_resolve_codex_speed() -> str:
|
|
6957
|
+
"""The resolved Codex service tier for this tick (#1050 spec 4.7).
|
|
6958
|
+
|
|
6959
|
+
``_resolve_codex_speed('auto')`` is the CLI's only tier resolver, and it
|
|
6960
|
+
already treats an absent or unreadable ``config.toml`` as contributing
|
|
6961
|
+
nothing; any other failure takes the same answer, ``standard``, so the
|
|
6962
|
+
dispatch key's fifth element is always a deterministic string.
|
|
6963
|
+
"""
|
|
6964
|
+
try:
|
|
6965
|
+
return str(_cctally()._resolve_codex_speed("auto"))
|
|
6966
|
+
except Exception: # noqa: BLE001 — a tier read must never fail a tick
|
|
6967
|
+
return "standard"
|
|
6968
|
+
|
|
6969
|
+
|
|
6970
|
+
def _tui_compute_dispatch_signature(stats_conn, *, with_codex_dependency=False,
|
|
6971
|
+
revision_sink=None):
|
|
6412
6972
|
"""Composite data-version signature for the three-path dispatch (#268 M5.1).
|
|
6413
6973
|
|
|
6414
6974
|
Cheap ``MAX(id)`` b-tree descents over cache.db + stats.db, the reset-event
|
|
@@ -6427,6 +6987,15 @@ def _tui_compute_dispatch_signature(stats_conn, *, with_codex_dependency=False):
|
|
|
6427
6987
|
an older signature with a newer identity in one dispatch key. The tick
|
|
6428
6988
|
pays no second connection for it. ``None`` when it cannot be established,
|
|
6429
6989
|
which the idle gate treats as "rebuild".
|
|
6990
|
+
|
|
6991
|
+
#1050 spec 4.5 item 1: the stats.db Claude content revision is read here,
|
|
6992
|
+
beside the other stats legs and before every builder, and its published
|
|
6993
|
+
leg is the signature's ``claude_content_revision``. ``revision_sink``, a
|
|
6994
|
+
list, receives ``(ContentRevision, error)``: the identity the tick passes
|
|
6995
|
+
to `_tui_build_source_bundle` as its Claude input identity, and the
|
|
6996
|
+
`StatsContentRevisionUnreadable` (else ``None``) the tick records in its
|
|
6997
|
+
failure capture. An unreadable revision never idles the tick: its leg is a
|
|
6998
|
+
fresh nonce, like every untrusted state's.
|
|
6430
6999
|
"""
|
|
6431
7000
|
c = _cctally()
|
|
6432
7001
|
sc = c._load_sibling("_lib_snapshot_cache")
|
|
@@ -6435,6 +7004,9 @@ def _tui_compute_dispatch_signature(stats_conn, *, with_codex_dependency=False):
|
|
|
6435
7004
|
# projection tables from a pure kernel (#496 S5b section 4.7).
|
|
6436
7005
|
from _cctally_quota import assert_projection_readable
|
|
6437
7006
|
assert_projection_readable(stats_conn)
|
|
7007
|
+
content_revision, revision_error = _tui_read_content_revision(stats_conn)
|
|
7008
|
+
if revision_sink is not None:
|
|
7009
|
+
revision_sink.append((content_revision, revision_error))
|
|
6438
7010
|
if with_codex_dependency:
|
|
6439
7011
|
opened_file, cache_conn = _tui_open_observed_cache(c)
|
|
6440
7012
|
else:
|
|
@@ -6454,6 +7026,8 @@ def _tui_compute_dispatch_signature(stats_conn, *, with_codex_dependency=False):
|
|
|
6454
7026
|
claude_stats_digest=claude_stats_digest(stats_conn),
|
|
6455
7027
|
claude_weekly_retired_digest=(
|
|
6456
7028
|
claude_weekly_retired_digest(stats_conn)),
|
|
7029
|
+
claude_content_revision=_content_revision.revision_leg(
|
|
7030
|
+
content_revision),
|
|
6457
7031
|
)
|
|
6458
7032
|
if not with_codex_dependency:
|
|
6459
7033
|
return signature
|
|
@@ -6562,7 +7136,9 @@ def _tui_build_idle_snapshot(prior, *, now_utc, precompute_envelope,
|
|
|
6562
7136
|
claude_ingest_failed=False,
|
|
6563
7137
|
failures=None,
|
|
6564
7138
|
stats_heal_attempted=False,
|
|
6565
|
-
codex_dependency
|
|
7139
|
+
codex_dependency,
|
|
7140
|
+
claude_input_identity,
|
|
7141
|
+
codex_speed=None):
|
|
6566
7142
|
"""Fresh snapshot reusing ``prior``'s heavy rows, re-patching only the
|
|
6567
7143
|
time-derived fields + the doctor payload / envelope precompute on each idle
|
|
6568
7144
|
tick (spec §3 idle path).
|
|
@@ -6593,6 +7169,13 @@ def _tui_build_idle_snapshot(prior, *, now_utc, precompute_envelope,
|
|
|
6593
7169
|
``codex_dependency`` (#857) is this tick's Codex quota-dependency identity,
|
|
6594
7170
|
REQUIRED with no default for the reason `_tui_source_bundle_can_idle`
|
|
6595
7171
|
states: ``None`` idles no Codex generation.
|
|
7172
|
+
|
|
7173
|
+
``claude_input_identity`` (#1050 spec 4.5 item 3) is this idle tick's
|
|
7174
|
+
dispatch read, ``(SnapshotSignature, ContentRevision)``, REQUIRED with no
|
|
7175
|
+
default: the bounded source adapter below supplies ``prior``'s Claude
|
|
7176
|
+
values, which were built under the stored key this read equals, so this
|
|
7177
|
+
read is the identity that certifies them — never the adapter's own later
|
|
7178
|
+
start read.
|
|
6596
7179
|
"""
|
|
6597
7180
|
import time
|
|
6598
7181
|
doctor_payload = prior.doctor_payload
|
|
@@ -6607,6 +7190,32 @@ def _tui_build_idle_snapshot(prior, *, now_utc, precompute_envelope,
|
|
|
6607
7190
|
except Exception as exc: # noqa: BLE001 — never crash the rebuild
|
|
6608
7191
|
errors.append(f"envelope-precompute: {exc}")
|
|
6609
7192
|
idle_failures = failures if failures is not None else []
|
|
7193
|
+
# #1018 twin spec 5.5: the idle tick re-runs the card's confirming-reading
|
|
7194
|
+
# lookup and re-derives its freshness at this instant, because a flat
|
|
7195
|
+
# reading adds an index row but no stored row and leaves the dispatch
|
|
7196
|
+
# signature unchanged. The card is replaced, never mutated; on a stats
|
|
7197
|
+
# error the prior card (and below, the prior source copies) is kept and
|
|
7198
|
+
# the failure is captured like the source adapter's own.
|
|
7199
|
+
current_week = prior.current_week
|
|
7200
|
+
current_week_refresh_failed = False
|
|
7201
|
+
if current_week is not None and source_stats_conn is not None:
|
|
7202
|
+
try:
|
|
7203
|
+
current_week = refresh_current_week_reading(
|
|
7204
|
+
current_week, source_stats_conn, now_utc,
|
|
7205
|
+
oauth_usage_cfg=_tui_oauth_usage_cfg(raw_config))
|
|
7206
|
+
except sqlite3.Error as exc:
|
|
7207
|
+
current_week = prior.current_week
|
|
7208
|
+
current_week_refresh_failed = True
|
|
7209
|
+
_tui_capture_sync_failure(
|
|
7210
|
+
source_stats_conn,
|
|
7211
|
+
errors,
|
|
7212
|
+
idle_failures,
|
|
7213
|
+
leg="current-week-refresh",
|
|
7214
|
+
database="stats_or_cache",
|
|
7215
|
+
exc=exc,
|
|
7216
|
+
stats_heal_attempted=stats_heal_attempted,
|
|
7217
|
+
)
|
|
7218
|
+
current_week_refreshed = current_week is not prior.current_week
|
|
6610
7219
|
source_bundle = prior.source_bundle
|
|
6611
7220
|
# #857: whether the bounded source adapter below scheduled the Codex pass.
|
|
6612
7221
|
source_attempt: dict[str, bool] = {}
|
|
@@ -6637,9 +7246,10 @@ def _tui_build_idle_snapshot(prior, *, now_utc, precompute_envelope,
|
|
|
6637
7246
|
):
|
|
6638
7247
|
claude = _refresh_claude_source_clock(
|
|
6639
7248
|
prior_claude,
|
|
6640
|
-
current_week=
|
|
7249
|
+
current_week=current_week,
|
|
6641
7250
|
now_utc=now_utc,
|
|
6642
7251
|
raw_config=raw_config,
|
|
7252
|
+
refresh_current_week=not current_week_refresh_failed,
|
|
6643
7253
|
)
|
|
6644
7254
|
codex = refresh_codex_source_clock(prior_codex, now_utc=now_utc)
|
|
6645
7255
|
if claude is not prior_claude or codex is not prior_codex:
|
|
@@ -6658,7 +7268,10 @@ def _tui_build_idle_snapshot(prior, *, now_utc, precompute_envelope,
|
|
|
6658
7268
|
# data signature (notably after its post-projection certificate
|
|
6659
7269
|
# is written). Re-run only the bounded source adapter while
|
|
6660
7270
|
# preserving the already-idle legacy snapshot rows.
|
|
6661
|
-
source_snapshot =
|
|
7271
|
+
source_snapshot = (
|
|
7272
|
+
dataclasses.replace(prior, current_week=current_week)
|
|
7273
|
+
if current_week_refreshed else prior
|
|
7274
|
+
)
|
|
6662
7275
|
if source_snapshot.doctor_payload is None:
|
|
6663
7276
|
source_snapshot = dataclasses.replace(
|
|
6664
7277
|
source_snapshot,
|
|
@@ -6672,6 +7285,10 @@ def _tui_build_idle_snapshot(prior, *, now_utc, precompute_envelope,
|
|
|
6672
7285
|
legacy_envelope = _cctally().snapshot_to_envelope(
|
|
6673
7286
|
source_snapshot,
|
|
6674
7287
|
now_utc=now_utc,
|
|
7288
|
+
# #1018 twin review F4: the configured freshness
|
|
7289
|
+
# thresholds, as the idle clock uses, so the Claude
|
|
7290
|
+
# source copies never flip label between ticks.
|
|
7291
|
+
oauth_usage_cfg=_tui_oauth_usage_cfg(raw_config),
|
|
6675
7292
|
display_tz_pref_override=display_tz_pref_override,
|
|
6676
7293
|
runtime_bind=runtime_bind,
|
|
6677
7294
|
)
|
|
@@ -6697,6 +7314,8 @@ def _tui_build_idle_snapshot(prior, *, now_utc, precompute_envelope,
|
|
|
6697
7314
|
attempt=source_attempt,
|
|
6698
7315
|
evidence_eligible_at=getattr(
|
|
6699
7316
|
prior, "evidence_eligible_at", None),
|
|
7317
|
+
claude_input_identity=claude_input_identity,
|
|
7318
|
+
codex_speed=codex_speed,
|
|
6700
7319
|
)
|
|
6701
7320
|
except _StatsSnapshotCorruption:
|
|
6702
7321
|
raise
|
|
@@ -6748,6 +7367,7 @@ def _tui_build_idle_snapshot(prior, *, now_utc, precompute_envelope,
|
|
|
6748
7367
|
)
|
|
6749
7368
|
return dataclasses.replace(
|
|
6750
7369
|
prior,
|
|
7370
|
+
current_week=current_week,
|
|
6751
7371
|
generated_at=now_utc,
|
|
6752
7372
|
# #583 S2 §6.1: `last_sync_at` means "last SUCCESSFUL validation". A
|
|
6753
7373
|
# clean idle tick IS one — it re-verified through four independent
|
|
@@ -6794,8 +7414,18 @@ def _tui_stats_retry_degraded_snapshot(
|
|
|
6794
7414
|
precompute_envelope: bool,
|
|
6795
7415
|
runtime_bind: "str | None",
|
|
6796
7416
|
) -> DataSnapshot:
|
|
6797
|
-
"""Return a stable typed frame when the one fresh retry cannot open.
|
|
7417
|
+
"""Return a stable typed frame when the one fresh retry cannot open.
|
|
7418
|
+
|
|
7419
|
+
Two cases share this helper and must stay distinguishable (#932 §4.1): a
|
|
7420
|
+
heal that DEFERRED (``exc`` is a `StatsRebuildDeferred`) builds the same
|
|
7421
|
+
explicit deferral frame the thrown path builds — hydrating, the hydrating
|
|
7422
|
+
source bundle, and the doctor placeholder plus the rebuild-worker
|
|
7423
|
+
aggregate, never the full doctor gather, which opens the index the
|
|
7424
|
+
detached heal owns. An exhausted corruption retry keeps the terminal frame
|
|
7425
|
+
(full doctor, ``hydrating=False``, ``deferred`` false).
|
|
7426
|
+
"""
|
|
6798
7427
|
|
|
7428
|
+
deferral = isinstance(exc, _cctally().StatsRebuildDeferred)
|
|
6799
7429
|
errors = [f"stats-open: {exc}"]
|
|
6800
7430
|
doctor_payload = None
|
|
6801
7431
|
envelope_precompute = None
|
|
@@ -6804,26 +7434,53 @@ def _tui_stats_retry_degraded_snapshot(
|
|
|
6804
7434
|
envelope_precompute = _tui_precompute_envelope_config(load_config())
|
|
6805
7435
|
except Exception as precompute_exc: # noqa: BLE001
|
|
6806
7436
|
errors.append(f"envelope-precompute: {precompute_exc}")
|
|
6807
|
-
|
|
6808
|
-
doctor_payload =
|
|
6809
|
-
|
|
6810
|
-
|
|
6811
|
-
|
|
6812
|
-
|
|
7437
|
+
if deferral:
|
|
7438
|
+
doctor_payload = _deferred_doctor_placeholder(now_utc)
|
|
7439
|
+
doctor_payload["rebuild_worker"] = _tui_rebuild_worker_value(now_utc)
|
|
7440
|
+
else:
|
|
7441
|
+
try:
|
|
7442
|
+
doctor_payload = _tui_precompute_doctor_payload(
|
|
7443
|
+
now_utc, runtime_bind)
|
|
7444
|
+
except Exception as doctor_exc: # noqa: BLE001
|
|
7445
|
+
errors.append(f"doctor-precompute: {doctor_exc}")
|
|
7446
|
+
replacements = {
|
|
6813
7447
|
# #583 S2 §6.1: this produced no successful snapshot, and it builds
|
|
6814
7448
|
# from the empty snapshot, so there is no earlier success to preserve.
|
|
6815
|
-
last_sync_at
|
|
6816
|
-
last_sync_error
|
|
6817
|
-
sync_failures
|
|
6818
|
-
|
|
7449
|
+
"last_sync_at": None,
|
|
7450
|
+
"last_sync_error": "; ".join(errors),
|
|
7451
|
+
"sync_failures": (
|
|
7452
|
+
_tui_deferral_attribution(exc) if deferral
|
|
7453
|
+
else SyncFailureAttribution(
|
|
6819
7454
|
leg="stats-open",
|
|
6820
7455
|
database="stats",
|
|
6821
7456
|
corruption=_stats_open_failure_is_corruption(exc),
|
|
6822
7457
|
),
|
|
6823
7458
|
),
|
|
6824
|
-
doctor_payload
|
|
6825
|
-
envelope_precompute
|
|
6826
|
-
hydrating
|
|
7459
|
+
"doctor_payload": doctor_payload,
|
|
7460
|
+
"envelope_precompute": envelope_precompute,
|
|
7461
|
+
"hydrating": deferral,
|
|
7462
|
+
}
|
|
7463
|
+
if deferral:
|
|
7464
|
+
replacements["source_bundle"] = _tui_hydrating_source_bundle()
|
|
7465
|
+
return dataclasses.replace(_tui_empty_snapshot(now_utc), **replacements)
|
|
7466
|
+
|
|
7467
|
+
|
|
7468
|
+
def _tui_is_hydrating_source_bundle(bundle) -> bool:
|
|
7469
|
+
"""True iff ``bundle`` is the bootstrap shape `_tui_hydrating_source_bundle`
|
|
7470
|
+
builds: both PROVIDER states' ``data_version`` starts ``hydrating:``.
|
|
7471
|
+
|
|
7472
|
+
The ``all`` state is a composition of the two (`compose_all_state`), with a
|
|
7473
|
+
hashed version of its own, so it is not part of the test.
|
|
7474
|
+
"""
|
|
7475
|
+
if bundle is None:
|
|
7476
|
+
return False
|
|
7477
|
+
try:
|
|
7478
|
+
states = [bundle.sources[name] for name in ("claude", "codex")]
|
|
7479
|
+
except Exception: # noqa: BLE001 — an unexpected shape is not bootstrap
|
|
7480
|
+
return False
|
|
7481
|
+
return all(
|
|
7482
|
+
str(getattr(state, "data_version", "")).startswith("hydrating:")
|
|
7483
|
+
for state in states
|
|
6827
7484
|
)
|
|
6828
7485
|
|
|
6829
7486
|
|
|
@@ -7208,8 +7865,12 @@ class _TuiSyncThread:
|
|
|
7208
7865
|
display_tz_pref_override=self._display_tz_pref_override,
|
|
7209
7866
|
)
|
|
7210
7867
|
self._ref.set(snap)
|
|
7211
|
-
except Exception as exc:
|
|
7868
|
+
except (Exception, _cctally().StatsRebuildDeferred) as exc:
|
|
7212
7869
|
# Don't crash the thread on unexpected errors — surface in UI.
|
|
7870
|
+
# #932 §4.5: a stats deferral is a `BaseException`, and one
|
|
7871
|
+
# raised by a later rebuild used to end this thread, leaving
|
|
7872
|
+
# the TUI on its last frame forever; it is carried like any
|
|
7873
|
+
# other error and the next interval retries.
|
|
7213
7874
|
# Carry every additive view-model scalar through verbatim so
|
|
7214
7875
|
# the prior frame's panel rows and their envelope totals stay
|
|
7215
7876
|
# consistent. Bundle 1 / #56 / #57 / #59 each added envelope
|
|
@@ -7227,7 +7888,10 @@ class _TuiSyncThread:
|
|
|
7227
7888
|
trend=prev.trend,
|
|
7228
7889
|
sessions=prev.sessions,
|
|
7229
7890
|
last_sync_at=prev.last_sync_at,
|
|
7230
|
-
last_sync_error=
|
|
7891
|
+
last_sync_error=(
|
|
7892
|
+
f"stats-open: {exc}"
|
|
7893
|
+
if isinstance(exc, _cctally().StatsRebuildDeferred)
|
|
7894
|
+
else f"sync crashed: {exc}"),
|
|
7231
7895
|
generated_at=dt.datetime.now(dt.timezone.utc),
|
|
7232
7896
|
percent_milestones=prev.percent_milestones,
|
|
7233
7897
|
# Carry the navigation index forward on a sync crash so the
|
|
@@ -7357,9 +8021,13 @@ def _tui_panel_current_week(
|
|
|
7357
8021
|
# Freshness chip (Task C6 / spec §3.4). Hidden when label is None or
|
|
7358
8022
|
# 'fresh'; rendered dim for 'aging', warn (amber) for 'stale'. Mirrors
|
|
7359
8023
|
# the dashboard CurrentWeekPanel chip in dashboard/web/src/panels/.
|
|
7360
|
-
|
|
8024
|
+
# #1018 twin spec 5.4: the time names the confirming weekly reading the
|
|
8025
|
+
# label ages from, not the stored row (`last snapshot:` above keeps that).
|
|
8026
|
+
reading_at = getattr(cw, "latest_weekly_reading_at", None)
|
|
8027
|
+
if (cw.freshness_label and cw.freshness_label != "fresh"
|
|
8028
|
+
and isinstance(reading_at, dt.datetime)):
|
|
7361
8029
|
captured_hms = format_display_dt(
|
|
7362
|
-
|
|
8030
|
+
reading_at, runtime.display_tz,
|
|
7363
8031
|
fmt="%H:%M:%S", suffix=False,
|
|
7364
8032
|
)
|
|
7365
8033
|
chip_style = "warn" if cw.freshness_label == "stale" else "dim"
|
|
@@ -9275,6 +9943,45 @@ def _make_run_sync_now_locked(*, ref, hub, pinned_now, display_tz_pref_override,
|
|
|
9275
9943
|
data_version="dashboard-publisher")
|
|
9276
9944
|
return snapshot
|
|
9277
9945
|
|
|
9946
|
+
def _publish_stats_deferral(tick, exc) -> None:
|
|
9947
|
+
"""Publish the pending deferral frame for ``exc`` (#453, #932).
|
|
9948
|
+
|
|
9949
|
+
The one construction both deferral entry points share: a thrown
|
|
9950
|
+
`StatsRebuildDeferred`, and a maintenance-busy open while a detached
|
|
9951
|
+
recovery holds maintenance over a held deferral frame. The prior frame
|
|
9952
|
+
is kept; only the rebuild-worker aggregate, the explicit attribution,
|
|
9953
|
+
the latch and the clock change.
|
|
9954
|
+
"""
|
|
9955
|
+
prev = ref.get()
|
|
9956
|
+
# #997 D10: the doctor gather cannot run during the deferral, so
|
|
9957
|
+
# refresh only the rebuild-worker aggregate the chip leads with.
|
|
9958
|
+
# #932 G1: the pinned clock when there is one, as every pinned build
|
|
9959
|
+
# is stamped; a wall-clock stamp would be "newer" than every later
|
|
9960
|
+
# pinned frame, and the client's time-order guard would drop them all.
|
|
9961
|
+
deferred_now = pinned_now or dt.datetime.now(dt.timezone.utc)
|
|
9962
|
+
prev_doctor = getattr(prev, "doctor_payload", None)
|
|
9963
|
+
base_doctor = (prev_doctor if isinstance(prev_doctor, dict)
|
|
9964
|
+
else _deferred_doctor_placeholder(deferred_now))
|
|
9965
|
+
doctor_payload = {
|
|
9966
|
+
**base_doctor,
|
|
9967
|
+
"rebuild_worker": _tui_rebuild_worker_value(
|
|
9968
|
+
deferred_now,
|
|
9969
|
+
fallback=base_doctor.get("rebuild_worker")),
|
|
9970
|
+
}
|
|
9971
|
+
pending = dataclasses.replace(
|
|
9972
|
+
prev,
|
|
9973
|
+
doctor_payload=doctor_payload,
|
|
9974
|
+
last_sync_error=f"stats-open: {exc}",
|
|
9975
|
+
# #932 §4.1: explicit, so a frozen dashboard's settle loop
|
|
9976
|
+
# knows this retry deferred again and keeps going.
|
|
9977
|
+
sync_failures=(_tui_deferral_attribution(exc),),
|
|
9978
|
+
generated_at=deferred_now,
|
|
9979
|
+
hydrating=True,
|
|
9980
|
+
)
|
|
9981
|
+
pending = ref.set(pending) # #583 S2 §6.2: publish what is held
|
|
9982
|
+
tick.mark_degraded()
|
|
9983
|
+
_tui_publish_final(tick, hub, pending, publication="degraded")
|
|
9984
|
+
|
|
9278
9985
|
def _locked(skip_sync: bool) -> None:
|
|
9279
9986
|
# #279 S5 F6.3 (gate P1-1): arm the snapshot-cache owner-thread tripwire
|
|
9280
9987
|
# for whichever thread holds sync_lock for THIS rebuild — the periodic
|
|
@@ -9287,6 +9994,9 @@ def _make_run_sync_now_locked(*, ref, hub, pinned_now, display_tz_pref_override,
|
|
|
9287
9994
|
# all inside it. The A1 pre-bind seed is deliberately outside, because
|
|
9288
9995
|
# it bypasses `_tui_build_snapshot` and writes no dispatch state.
|
|
9289
9996
|
tick = _tick_stats.begin_tick()
|
|
9997
|
+
# #932 F1: the frame held before this tick, which decides whether a
|
|
9998
|
+
# maintenance-busy failure below is a recovery still running.
|
|
9999
|
+
held = ref.get()
|
|
9290
10000
|
try:
|
|
9291
10001
|
if not skip_sync:
|
|
9292
10002
|
# ── Decoupled ingest + build (§2.1) ─────────────────────────
|
|
@@ -9639,7 +10349,13 @@ def _make_run_sync_now_locked(*, ref, hub, pinned_now, display_tz_pref_override,
|
|
|
9639
10349
|
# chip. #278 §1.4.1: force the hydration latch clear (default False
|
|
9640
10350
|
# from _tui_build_snapshot, restated here since this is a replace()
|
|
9641
10351
|
# clone site).
|
|
9642
|
-
|
|
10352
|
+
#
|
|
10353
|
+
# #932 §4.2: except on an explicit stats deferral (a heal deferral
|
|
10354
|
+
# the builder RETURNED as a frame), whose latch must hold until a
|
|
10355
|
+
# build the recovery permits replaces it.
|
|
10356
|
+
snap = dataclasses.replace(
|
|
10357
|
+
snap, last_sync_at=None,
|
|
10358
|
+
hydrating=_tui_snapshot_is_stats_deferral(snap))
|
|
9643
10359
|
# Terminal for this branch too, for the same reason: POST
|
|
9644
10360
|
# /api/settings brackets its rebuild with the same flag pair.
|
|
9645
10361
|
snap = ref.set_final(snap)
|
|
@@ -9655,46 +10371,41 @@ def _make_run_sync_now_locked(*, ref, hub, pinned_now, display_tz_pref_override,
|
|
|
9655
10371
|
# EPOCH is a readable index (`corruption=False`); a deferred heal is
|
|
9656
10372
|
# an index that could not be read, and reporting that as
|
|
9657
10373
|
# non-corruption makes the envelope name the wrong fault.
|
|
9658
|
-
|
|
9659
|
-
# #997 D10: the doctor gather cannot run during the deferral, so
|
|
9660
|
-
# refresh only the rebuild-worker aggregate the chip leads with.
|
|
9661
|
-
deferred_now = pinned_now or dt.datetime.now(dt.timezone.utc)
|
|
9662
|
-
prev_doctor = getattr(prev, "doctor_payload", None)
|
|
9663
|
-
base_doctor = (prev_doctor if isinstance(prev_doctor, dict)
|
|
9664
|
-
else _deferred_doctor_placeholder(deferred_now))
|
|
9665
|
-
doctor_payload = {
|
|
9666
|
-
**base_doctor,
|
|
9667
|
-
"rebuild_worker": _tui_rebuild_worker_value(
|
|
9668
|
-
deferred_now,
|
|
9669
|
-
fallback=base_doctor.get("rebuild_worker")),
|
|
9670
|
-
}
|
|
9671
|
-
pending = dataclasses.replace(
|
|
9672
|
-
prev,
|
|
9673
|
-
doctor_payload=doctor_payload,
|
|
9674
|
-
last_sync_error=f"stats-open: {exc}",
|
|
9675
|
-
sync_failures=(
|
|
9676
|
-
SyncFailureAttribution(
|
|
9677
|
-
leg="stats-open",
|
|
9678
|
-
database="stats",
|
|
9679
|
-
corruption=_stats_open_failure_is_corruption(exc),
|
|
9680
|
-
),
|
|
9681
|
-
),
|
|
9682
|
-
generated_at=dt.datetime.now(dt.timezone.utc),
|
|
9683
|
-
hydrating=True,
|
|
9684
|
-
)
|
|
9685
|
-
pending = ref.set(pending) # #583 S2 §6.2: publish what is held
|
|
9686
|
-
tick.mark_degraded()
|
|
9687
|
-
_tui_publish_final(tick, hub, pending, publication="degraded")
|
|
10374
|
+
_publish_stats_deferral(tick, exc)
|
|
9688
10375
|
except Exception as exc:
|
|
10376
|
+
# #932 F1: a detached recovery holds maintenance EXCLUSIVE for its
|
|
10377
|
+
# whole rebuild, so a retry over a held deferral frame times out
|
|
10378
|
+
# with a plain maintenance-busy error. While that recovery runs it
|
|
10379
|
+
# is the same deferral, not a crash: a crash frame would end a
|
|
10380
|
+
# frozen dashboard's settle loop for good. Over any other held
|
|
10381
|
+
# frame, or with no recovery running, the crash path below is
|
|
10382
|
+
# unchanged.
|
|
10383
|
+
recovery = None
|
|
10384
|
+
if (isinstance(exc, _cctally().StatsDbMaintenanceError)
|
|
10385
|
+
and _tui_snapshot_is_stats_deferral(held)):
|
|
10386
|
+
recovery = _cctally().active_stats_recovery_deferral()
|
|
10387
|
+
if recovery is not None:
|
|
10388
|
+
_publish_stats_deferral(tick, recovery)
|
|
10389
|
+
return
|
|
9689
10390
|
prev = ref.get()
|
|
10391
|
+
# #932 §4.2: a crash after a deferral must not keep the hydrating
|
|
10392
|
+
# BOOTSTRAP bundle, or the cleared latch below still leaves the
|
|
10393
|
+
# client in loading skeletons forever. `None` serializes as
|
|
10394
|
+
# unavailable with a warning; a genuine prior bundle is kept.
|
|
10395
|
+
prev_bundle = getattr(prev, "source_bundle", None)
|
|
9690
10396
|
crashed = dataclasses.replace(
|
|
9691
10397
|
prev,
|
|
10398
|
+
source_bundle=(
|
|
10399
|
+
None if _tui_is_hydrating_source_bundle(prev_bundle)
|
|
10400
|
+
else prev_bundle),
|
|
9692
10401
|
last_sync_error=f"sync crashed: {exc}",
|
|
9693
10402
|
# The new crash supersedes the prior typed leg failures. Keeping
|
|
9694
10403
|
# them would let a stale stats attribution win this unrelated
|
|
9695
10404
|
# failure in the privacy-safe envelope classifier.
|
|
9696
10405
|
sync_failures=(),
|
|
9697
|
-
|
|
10406
|
+
# #932 G1: the pinned clock when there is one (see
|
|
10407
|
+
# `_publish_stats_deferral`).
|
|
10408
|
+
generated_at=pinned_now or dt.datetime.now(dt.timezone.utc),
|
|
9698
10409
|
# #278 §1.4.1: a crash-carry snapshot is stable (not mid-
|
|
9699
10410
|
# assembly); clear the latch even if ``prev`` was a hydrating
|
|
9700
10411
|
# seed/partial, so the client doesn't stay stuck in skeletons.
|