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.
Files changed (58) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +7 -3
  3. package/bin/_cctally_cache.py +2783 -175
  4. package/bin/_cctally_completion.py +106 -33
  5. package/bin/_cctally_core.py +69 -4
  6. package/bin/_cctally_dashboard.py +188 -13
  7. package/bin/_cctally_dashboard_conversation.py +5 -1
  8. package/bin/_cctally_dashboard_envelope.py +50 -19
  9. package/bin/_cctally_dashboard_sources.py +23 -1
  10. package/bin/_cctally_db.py +193 -153
  11. package/bin/_cctally_diagnosis_sources.py +63 -7
  12. package/bin/_cctally_doctor.py +865 -93
  13. package/bin/_cctally_five_hour.py +67 -50
  14. package/bin/_cctally_forecast.py +35 -13
  15. package/bin/_cctally_journal.py +186 -18
  16. package/bin/_cctally_milestone_history.py +859 -62
  17. package/bin/_cctally_oauth_samples.py +286 -15
  18. package/bin/_cctally_parser.py +2 -2
  19. package/bin/_cctally_quota.py +104 -12
  20. package/bin/_cctally_record.py +153 -29
  21. package/bin/_cctally_rederive.py +608 -68
  22. package/bin/_cctally_statusline.py +108 -81
  23. package/bin/_cctally_store.py +179 -38
  24. package/bin/_cctally_tui.py +892 -181
  25. package/bin/_cctally_update.py +32 -13
  26. package/bin/_lib_codex_conversation_query.py +52 -14
  27. package/bin/_lib_codex_drain_admission.py +224 -6
  28. package/bin/_lib_codex_metadata.py +39 -0
  29. package/bin/_lib_conversation_anon.py +15 -8
  30. package/bin/_lib_conversation_dispatch.py +7 -0
  31. package/bin/_lib_conversation_query.py +289 -70
  32. package/bin/_lib_dashboard_sources.py +37 -1
  33. package/bin/_lib_detector_access_registry.py +497 -26
  34. package/bin/_lib_doctor.py +85 -20
  35. package/bin/_lib_doctor_scan_index.py +321 -0
  36. package/bin/_lib_ingest_frontier.py +20 -1
  37. package/bin/_lib_oauth_samples.py +23 -7
  38. package/bin/_lib_pricing.py +146 -16
  39. package/bin/_lib_quota.py +19 -0
  40. package/bin/_lib_rederive.py +11 -6
  41. package/bin/_lib_retained_records.py +516 -0
  42. package/bin/_lib_snapshot_cache.py +10 -0
  43. package/bin/_lib_stale_proof.py +272 -43
  44. package/bin/_lib_stats_content_revision.py +831 -0
  45. package/bin/_lib_write_io.py +11 -4
  46. package/bin/cctally +4 -1
  47. package/dashboard/static/assets/ConversationsView-D4Iv4ku0.js +72 -0
  48. package/dashboard/static/assets/{DoctorModal-B0DnU9d9.js → DoctorModal-D4QSq6uz.js} +1 -1
  49. package/dashboard/static/assets/ModalRoot-C9F5dhZi.js +1 -0
  50. package/dashboard/static/assets/{ProjectsDrillPanel-DSxXQSa-.js → ProjectsDrillPanel-WxAdRDMt.js} +1 -1
  51. package/dashboard/static/assets/{SourceDetailModal-BleDpQqG.js → SourceDetailModal-NyyEfXmc.js} +1 -1
  52. package/dashboard/static/assets/{UpdateModal-CMqaAkSK.js → UpdateModal-CFLq9pT1.js} +1 -1
  53. package/dashboard/static/assets/{index-BjQV-0-D.js → index-BKTWH9JE.js} +2 -2
  54. package/dashboard/static/assets/{outlineNavigation-CO-VWBm7.js → outlineNavigation-BnGUwCS3.js} +3 -3
  55. package/dashboard/static/dashboard.html +1 -1
  56. package/package.json +4 -1
  57. package/dashboard/static/assets/ConversationsView-oqNsWzWV.js +0 -72
  58. package/dashboard/static/assets/ModalRoot-CYua2wP_.js +0 -1
@@ -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 _tui_card_freshness(captured, now_utc: dt.datetime):
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; ``(None, None)`` when the
1731
- capture is not a datetime. Mirrors the dashboard envelope's cw_freshness
1732
- derivation in `snapshot_to_envelope`. Refs spec §3.4."""
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
- try:
1739
- _fresh_cfg = _get_oauth_usage_config(load_config())
1740
- except Exception:
1741
- _fresh_cfg = _OAUTH_USAGE_DEFAULTS
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
- owner = _claude_select_current_owner(conn, now_utc)
1793
- if owner is None:
1794
- return None
1795
- scope = _claude_current_cycle_scope(conn, now_utc, account_key=owner)
1796
- if scope is None:
1797
- return None
1798
- fetched = _claude_cycle_scope_samples(conn, scope, now_utc)
1799
- if fetched is None:
1800
- return None
1801
- week_start_at, week_end_at, observations, week_scope = fetched
1802
- week_start_before_override = week_start_at
1803
- if scope.effective_start_at is not None:
1804
- # The scope's effective start may also PRECEDE the nominal start (a
1805
- # same-date early re-anchor, #836 review F7): the card's cycle starts
1806
- # there either way, as its current index entry does.
1807
- effective = parse_iso_datetime(
1808
- scope.effective_start_at, "cycle_scope.effective_start_at")
1809
- if effective != week_start_at:
1810
- week_start_at = effective
1811
- observations = [s for s in observations if s[0] >= effective]
1812
- weekly_samples = [s for s in observations if not s[3]]
1813
- if not weekly_samples:
1814
- return None
1815
- latest = weekly_samples[-1]
1816
- used_pct = float(latest[1])
1817
- five_hour = _current_week_five_hour_reading(
1818
- conn, now_utc, week_scope, account_key=owner,
1819
- week_start_at=week_start_at,
1820
- week_start_before_override=week_start_before_override)
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(latest[0], now_utc)
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
- # #769 S11 (#824): ONE read, held-inclusive, split per axis below.
1879
- # Retain the owner of the exact non-held weekly sample this card displays,
1880
- # including when one real account coexists with unattributed history. R8
1881
- # controls decoration, not the physical population used for computation.
1882
- # Cost and forecast retain their existing merged semantics.
1883
- fetched = _fetch_current_week_snapshots(
1884
- conn, now_utc, include_held=True, include_account=True,
1885
- include_scope=True)
1886
- if fetched is None:
1887
- return None
1888
- week_start_at, week_end_at, observations, week_scope = fetched
1889
- if not observations:
1890
- return None
1891
- week_start_before_override = week_start_at
1892
- # Mirror the reset override applied by `_load_forecast_inputs` so the
1893
- # Current Week card's spent_usd and $/1% reflect the post-reset window.
1894
- week_start_at, observations = _apply_midweek_reset_override(
1895
- conn, week_start_at, week_end_at, observations, now_utc=now_utc
1896
- )
1897
- if not observations:
1898
- return None
1899
- # Held-inclusive sample shape: (captured_at_utc, weekly_percent,
1900
- # five_hour_percent, weekly_observation_held[, account_key]). The optional
1901
- # account key identifies the displayed weekly sample on a decorated store.
1902
- # The helper does not surface five_hour_resets_at; the five-hour reading
1903
- # selected below supplies it, with its percentage.
1904
- #
1905
- # #769 S11 (#824). This card publishes BOTH axes, and the dashboard
1906
- # envelope reads `used_pct` and `latest_snapshot_at` straight off it for
1907
- # `current_week.freshness` and `snapshot_age_seconds`. A held row carries
1908
- # an older row's weekly value under this tick's capture instant, so
1909
- # letting one reach `latest` would present stale weekly evidence as freshly
1910
- # captured — review finding 5. The five-hour percentage takes the newest
1911
- # observation of either kind, because a held row is written exactly when
1912
- # its five-hour reading grew.
1913
- weekly_samples = [s for s in observations if not s[3]]
1914
- if not weekly_samples:
1915
- return None
1916
- latest = weekly_samples[-1]
1917
- used_pct = float(latest[1])
1918
- weekly_account = latest[4]
1919
- # Five-hour readings may be newer held observations, but must remain on
1920
- # the account that supplied the displayed weekly reading. The reading is
1921
- # the newest five-hour READING under this card's own week and as-of scope
1922
- # (#825/#832 spec 4.7 revision 7, the one shared selection): the newest
1923
- # sample can be a retired replica or a weekly-only reading, which carries
1924
- # no five-hour value and used to render as 0 %.
1925
- five_hour = _current_week_five_hour_reading(
1926
- conn, now_utc, week_scope, account_key=weekly_account,
1927
- week_start_at=week_start_at,
1928
- week_start_before_override=week_start_before_override)
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 displayed sample vs. now.
1957
- freshness_label, freshness_age = _tui_card_freshness(latest[0], now_utc)
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
- return out if not retired else f"{out}.r{retired}"
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
- captured = getattr(current_week, "latest_snapshot_at", None)
3051
- if isinstance(captured, dt.datetime):
3052
- if captured.tzinfo is None or captured.utcoffset() is None:
3053
- captured = captured.replace(tzinfo=dt.timezone.utc)
3054
- age_seconds = max(
3055
- 0.0,
3056
- (
3057
- now_utc.astimezone(dt.timezone.utc)
3058
- - captured.astimezone(dt.timezone.utc)
3059
- ).total_seconds(),
3060
- )
3061
- try:
3062
- freshness_config = _get_oauth_usage_config(raw_config)
3063
- except Exception:
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:{signature.max_entry_id}:{signature.entry_mutation_seq}:"
4326
- f"{signature.max_wus_id}:{signature.max_wcs_id}:"
4327
- f"{signature.reset_sig[0]}:{signature.reset_sig[1]}:"
4328
- f"{signature.generation}:{semantics.claude_identity}"
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{claude_digest}"
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 retired_digest:
4337
- claude_version = f"{claude_version}:r{retired_digest}"
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(conn, now_utc, skip_sync=skip_sync)
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
- def _tui_compute_dispatch_signature(stats_conn, *, with_codex_dependency=False):
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=prior.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 = prior
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
- try:
6808
- doctor_payload = _tui_precompute_doctor_payload(now_utc, runtime_bind)
6809
- except Exception as doctor_exc: # noqa: BLE001
6810
- errors.append(f"doctor-precompute: {doctor_exc}")
6811
- return dataclasses.replace(
6812
- _tui_empty_snapshot(now_utc),
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=None,
6816
- last_sync_error="; ".join(errors),
6817
- sync_failures=(
6818
- SyncFailureAttribution(
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=doctor_payload,
6825
- envelope_precompute=envelope_precompute,
6826
- hydrating=False,
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=f"sync crashed: {exc}",
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
- if cw.freshness_label and cw.freshness_label != "fresh":
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
- cw.latest_snapshot_at, runtime.display_tz,
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
- snap = dataclasses.replace(snap, last_sync_at=None, hydrating=False)
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
- prev = ref.get()
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
- generated_at=dt.datetime.now(dt.timezone.utc),
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.