cctally 1.99.1 → 1.101.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 (49) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/bin/_cctally_alerts.py +13 -2
  3. package/bin/_cctally_cache.py +3 -1
  4. package/bin/_cctally_cache_report.py +103 -6
  5. package/bin/_cctally_dashboard.py +2083 -356
  6. package/bin/_cctally_dashboard_envelope.py +115 -31
  7. package/bin/_cctally_dashboard_perf.py +433 -0
  8. package/bin/_cctally_dashboard_share.py +101 -29
  9. package/bin/_cctally_dashboard_sources.py +1072 -214
  10. package/bin/_cctally_db.py +30 -14
  11. package/bin/_cctally_diff.py +20 -0
  12. package/bin/_cctally_doctor.py +1331 -1148
  13. package/bin/_cctally_forecast.py +304 -96
  14. package/bin/_cctally_milestone_history.py +10 -2
  15. package/bin/_cctally_parser.py +70 -1
  16. package/bin/_cctally_project.py +155 -47
  17. package/bin/_cctally_quota.py +40 -21
  18. package/bin/_cctally_record.py +44 -3
  19. package/bin/_cctally_refresh.py +60 -10
  20. package/bin/_cctally_share.py +9 -2
  21. package/bin/_cctally_source_analytics.py +40 -4
  22. package/bin/_cctally_statusline.py +53 -7
  23. package/bin/_cctally_tui.py +723 -232
  24. package/bin/_cctally_update.py +28 -22
  25. package/bin/_lib_alert_scope.py +685 -0
  26. package/bin/_lib_alerts_payload.py +112 -7
  27. package/bin/_lib_cache_report.py +110 -1
  28. package/bin/_lib_codex_pools.py +20 -8
  29. package/bin/_lib_dashboard_sources.py +237 -30
  30. package/bin/_lib_doctor.py +37 -0
  31. package/bin/_lib_forecast.py +12 -4
  32. package/bin/_lib_jsonl.py +4 -2
  33. package/bin/_lib_perf.py +132 -3
  34. package/bin/_lib_pricing.py +8 -7
  35. package/bin/_lib_render.py +31 -3
  36. package/bin/_lib_share_templates.py +150 -55
  37. package/bin/_lib_snapshot_cache.py +71 -13
  38. package/bin/_lib_source_analytics.py +2 -2
  39. package/bin/_lib_source_identity.py +50 -2
  40. package/bin/_lib_subscription_weeks.py +65 -0
  41. package/bin/_lib_tick_stats.py +538 -0
  42. package/bin/cctally +29 -7
  43. package/dashboard/static/assets/dashboardStream.shared-worker-1XTMV3nr.js +1 -0
  44. package/dashboard/static/assets/index-D6Eb9KDn.js +97 -0
  45. package/dashboard/static/assets/index-i3g7g8zo.css +1 -0
  46. package/dashboard/static/dashboard.html +2 -2
  47. package/package.json +4 -1
  48. package/dashboard/static/assets/index-C5NBB2w9.js +0 -97
  49. package/dashboard/static/assets/index-hJP4wlIO.css +0 -1
@@ -15,6 +15,7 @@ from zoneinfo import ZoneInfo
15
15
 
16
16
  from _cctally_core import get_week_start_name
17
17
  from _cctally_quota import (
18
+ QuotaProjectionIncomplete,
18
19
  assert_projection_readable,
19
20
  codex_five_hour_percent_at_crossing,
20
21
  codex_quota_breakdown,
@@ -40,6 +41,9 @@ from _lib_dashboard_sources import (
40
41
  assess_codex_projection_coherence,
41
42
  canonical_alerted_at,
42
43
  canonical_alerted_at_sql,
44
+ claude_stats_digest,
45
+ combined_accounting_version,
46
+ codex_stats_digest,
43
47
  dashboard_resource_key,
44
48
  )
45
49
  from _lib_quota import (
@@ -69,6 +73,7 @@ from _lib_source_analytics import (
69
73
  build_codex_project_result,
70
74
  collision_safe_project_label_map,
71
75
  )
76
+ from _lib_source_identity import identity_path
72
77
  from _lib_view_models import (
73
78
  CodexWeeklyView,
74
79
  build_codex_daily_view,
@@ -85,44 +90,184 @@ _CODEX_QUOTA_OBSERVATION_CACHE: dict[object, tuple[object, ...]] = {}
85
90
 
86
91
 
87
92
  def reset_codex_quota_observation_cache() -> None:
88
- """Clear #582's value-only quota-read memo."""
93
+ """Discard #582's value-only quota-read memo. TEST HOOK — no production caller.
94
+
95
+ Since #583 S5 removed the per-build clear, nothing in the shipped code path
96
+ calls this: the memo is invalidated by its key moving, never by a caller
97
+ emptying it. It survives as the isolation hook the pytest fixtures and the
98
+ cold-reference comparisons use, and `_codex_source_caches` is what the
99
+ build's own checkpoint/restore goes through.
100
+ """
89
101
  _CODEX_QUOTA_OBSERVATION_CACHE.clear()
90
102
 
91
103
 
92
- def _cached_codex_quota_observations(**kwargs) -> tuple[object, ...]:
93
- """Reuse bounded quota reads while their dedicated mutation stream is idle."""
94
- conn = kwargs.get("cache_conn")
95
- if not isinstance(conn, sqlite3.Connection):
96
- return load_codex_quota_observations(**kwargs)
104
+ def _codex_quota_reuse_identity(
105
+ cache_conn, stats_conn, *, stats_identity: tuple | None = None,
106
+ ) -> tuple | None:
107
+ """The full invalidation basis for the cross-build quota memo (#583 S5).
108
+
109
+ The cache legs alone are not sufficient. ``_build_codex_source_state`` used
110
+ to discard this memo on every build precisely because a stats-side
111
+ decoration or account-registry change advances no cache.db leg, so a key
112
+ built only from cache.db would not move for a mutation that changes the
113
+ answer. Returning ``None`` means "cannot establish identity", and every
114
+ caller must treat that as a cold read rather than as a cache hit.
115
+
116
+ Six legs, in two halves. The cache.db half is the quota change ledger's
117
+ high-water sequence, the window-attribution revision and the ``main``
118
+ database path -- an unreadable ledger or an absent ledger table is not an
119
+ idle ledger, so both return ``None``. The stats.db half is the three
120
+ digests the build already computes for its own composite signature
121
+ (``bin/_cctally_tui.py``), reused rather than replaced by a fourth
122
+ identity.
123
+
124
+ ``stats_identity`` is that reuse, made literal: the caller passes
125
+ ``(codex_stats_digest, accounts_identity_digest, claude_stats_digest)``
126
+ already computed for its own signature and this function does not recompute
127
+ them. Deriving them here is not cheap -- the three helpers issue twelve
128
+ stats.db statements, several of them whole-relation scans with an ``ORDER
129
+ BY``, plus one uncached filesystem read per configured provider root once
130
+ the ``accounts`` registry is non-empty. Measured on a copy of the real store
131
+ (276,391 retained Codex quota rows), warm, five samples after one untimed
132
+ warm-up: 35.2 ms median, range 33.1-35.7 ms per derivation.
133
+
134
+ Without a supplied identity the digests are derived here, and then the
135
+ projection gate applies. ``codex_stats_digest``'s relations include
136
+ ``quota_projection_state`` and ``quota_window_blocks``, whose read site is
137
+ classified ``gate_at_caller`` in ``_cctally_quota.PROJECTION_READ_SITE_
138
+ ACTIONS`` precisely because that kernel may not import the gate; this
139
+ function is therefore named in ``PROJECTION_GATE_CALLERS`` and runs it. An
140
+ incomplete projection returns ``None`` rather than propagating, because the
141
+ honest answer for a REUSE IDENTITY is "cannot establish identity" and the
142
+ fail-safe for that is a cold read. Nothing is suppressed by swallowing it:
143
+ every caller of this memo runs a gated projection read of its own within a
144
+ few statements -- ``codex_projection_coherence`` on the build path -- so the
145
+ retry signal still reaches whoever owns the transaction.
146
+
147
+ A ``stats_identity`` of the wrong shape is the one input that RAISES instead
148
+ of returning ``None``, and the two cases are different in kind. Everything
149
+ above is an environmental inability to establish an identity -- an absent
150
+ ledger table, an unreadable database, an incomplete projection -- for which
151
+ a cold read is the correct degrade. A caller that hands this function a
152
+ two-element tuple has a defect in the caller, and taking the cold path for
153
+ it would leave the memo permanently and silently disabled. It is unreachable
154
+ from the production callers, all of which pass either ``None`` or
155
+ ``DashboardReadContext.stats_identity``.
156
+ """
157
+ if not isinstance(cache_conn, sqlite3.Connection):
158
+ return None
159
+ if stats_identity is not None:
160
+ if not isinstance(stats_identity, tuple) or len(stats_identity) != 3:
161
+ raise ValueError(
162
+ "stats_identity must be the three-digest tuple or None")
163
+ elif not isinstance(stats_conn, sqlite3.Connection):
164
+ return None
97
165
  try:
98
- seq_row = conn.execute(
166
+ seq_row = cache_conn.execute(
99
167
  "SELECT seq FROM sqlite_sequence "
100
168
  "WHERE name='quota_window_change_log'"
101
169
  ).fetchone()
102
- table = conn.execute(
170
+ table = cache_conn.execute(
103
171
  "SELECT 1 FROM sqlite_master WHERE type='table' "
104
172
  "AND name='quota_window_change_log'"
105
173
  ).fetchone()
106
- revision_row = conn.execute(
174
+ revision_row = cache_conn.execute(
107
175
  "SELECT value FROM cache_meta "
108
176
  "WHERE key='codex_window_attribution_revision'"
109
177
  ).fetchone()
110
178
  db_path = next(
111
- str(row[2]) for row in conn.execute("PRAGMA database_list")
179
+ str(row[2]) for row in cache_conn.execute("PRAGMA database_list")
112
180
  if str(row[1]) == "main"
113
181
  )
114
182
  except (sqlite3.Error, StopIteration):
115
- return load_codex_quota_observations(**kwargs)
183
+ return None
116
184
  if table is None:
185
+ return None
186
+ if stats_identity is None:
187
+ try:
188
+ assert_projection_readable(stats_conn)
189
+ stats_identity = (
190
+ codex_stats_digest(stats_conn),
191
+ accounts_identity_digest(stats_conn),
192
+ claude_stats_digest(stats_conn),
193
+ )
194
+ except QuotaProjectionIncomplete:
195
+ return None
196
+ except sqlite3.Error:
197
+ return None
198
+ return (
199
+ db_path,
200
+ 0 if seq_row is None else int(seq_row[0]),
201
+ "" if revision_row is None else str(revision_row[0]),
202
+ *stats_identity,
203
+ )
204
+
205
+
206
+ def _cached_codex_quota_observations(**kwargs) -> tuple[object, ...]:
207
+ """Reuse bounded quota reads while their dedicated mutation stream is idle.
208
+
209
+ #583 S5: the memo now survives across source builds, so the key must carry
210
+ the stats-side identity as well as the cache.db legs. ``stats_conn``,
211
+ ``stats_identity`` and ``memoize`` are memo-only inputs; none is forwarded
212
+ to the loader. A caller that already holds the three digests passes
213
+ ``stats_identity`` so they are not recomputed once per memo call.
214
+
215
+ ``memoize=False`` is the opt-out for a key that CANNOT match, and it is a
216
+ cost decision rather than a correctness one. A caller whose key carries the
217
+ tick instant -- ``active_at=now`` and a ``now``-derived
218
+ ``captured_at_or_after`` -- moves both terms on every call, so it can never
219
+ read a prior entry. Going through the memo anyway would establish an
220
+ identity it cannot use (35 ms of stats.db work when no ``stats_identity`` is
221
+ supplied) and retain a result nobody can hit, which also evicts the
222
+ clock-free entries that DO reuse, because the memo is discarded whole on
223
+ reaching 32 entries.
224
+
225
+ THE GENERATION CONTRACT IS WEAKER THAN IT WAS, and the next reader of
226
+ ``_tui_build_source_bundle``'s pin needs to know exactly how. This dict is a
227
+ plain module global with no ``_assert_owner``, and the per-build clear that
228
+ used to stand in ``_build_codex_source_state`` is gone, so an entry
229
+ populated on a non-builder path -- the share render or the per-request
230
+ cycle-detail route, from an unpinned connection at whatever cache generation
231
+ it saw -- can now be served to the builder INSIDE its pinned read
232
+ transaction. Before #583 S5 every cache-backed read inside that pin was
233
+ physically issued on the pinned connection.
234
+
235
+ What still holds, and why that is safe:
236
+
237
+ * Every read the builder issues ITSELF still runs on the pinned connection
238
+ and sees the frozen generation. Only a memo HIT returns rows another
239
+ caller loaded, and after the ``memoize=False`` sites above the sole read
240
+ that can hit inside a build is the five-hour correlation read.
241
+ * A hit requires the whole key to match, and the key is the invalidation
242
+ basis: the ``main`` database path, ``quota_window_change_log``'s
243
+ high-water sequence, ``codex_window_attribution_revision`` and the three
244
+ stats digests. Every ``quota_window_snapshots`` mutation is recorded by
245
+ the three ledger triggers in ``bin/_cctally_db.py``
246
+ (``trg_qws_ledger_ins`` / ``_del`` / ``_upd``, the last firing on the
247
+ semantic column set the loader interprets), and the attribution overlay
248
+ bumps its revision in the same transaction as the rows it applies
249
+ (``bin/_cctally_journal.py``). A retained value is therefore served only
250
+ while the evidence it was derived from has not moved, whatever generation
251
+ the connection that loaded it was on.
252
+ * The value is a tuple of frozen ``QuotaObservation``s, so a hit hands back
253
+ no cursor, connection or lazily-read state belonging to the other caller.
254
+ """
255
+ stats_conn = kwargs.pop("stats_conn", None)
256
+ stats_identity = kwargs.pop("stats_identity", None)
257
+ memoize = bool(kwargs.pop("memoize", True))
258
+ conn = kwargs.get("cache_conn")
259
+ if not memoize or not isinstance(conn, sqlite3.Connection):
260
+ return load_codex_quota_observations(**kwargs)
261
+ identity = _codex_quota_reuse_identity(
262
+ conn, stats_conn, stats_identity=stats_identity)
263
+ if identity is None:
117
264
  return load_codex_quota_observations(**kwargs)
118
265
  roots = kwargs.get("source_root_keys")
119
266
  physical_signatures = kwargs.get("physical_signatures")
120
267
  physical_groups = kwargs.get("physical_groups")
121
268
  key = (
122
269
  id(load_codex_quota_observations),
123
- db_path,
124
- 0 if seq_row is None else int(seq_row[0]),
125
- "" if revision_row is None else str(revision_row[0]),
270
+ identity,
126
271
  None if roots is None else tuple(sorted(str(root) for root in roots)),
127
272
  kwargs.get("captured_at_or_after"),
128
273
  kwargs.get("active_at"),
@@ -139,6 +284,27 @@ def _cached_codex_quota_observations(**kwargs) -> tuple[object, ...]:
139
284
  if cached is not None:
140
285
  return cached
141
286
  loaded = tuple(load_codex_quota_observations(**kwargs))
287
+ # The written memory bound (#583 S5 spec §2.1 "Memory"). Now that this memo
288
+ # survives a build, "32 entries" is a bound on ENTRIES and says nothing on
289
+ # its own about rows, so the worst-case retained row count is stated here:
290
+ #
291
+ # * at most 32 results are retained, and the whole memo is discarded on
292
+ # reaching that count rather than evicted one entry at a time;
293
+ # * a bounded dashboard read contributes at most
294
+ # `DASHBOARD_QUOTA_OBSERVATION_LIMIT` (1,000) `QuotaObservation`s;
295
+ # * the five-hour correlation read in `_quota_read_model` carries NO
296
+ # `max_rows`. It is bounded only by one root's retained observations
297
+ # captured at or after one weekly block's nominal start, which is the
298
+ # term that actually sizes this cache, and it grows with the block's
299
+ # age because an older block's nominal start reaches further back.
300
+ # Measured on a store holding 276,391 Codex quota rows, over the seven
301
+ # weekly blocks live at that instant: 15,129 observations for the
302
+ # newest and 36,943 for the oldest. The worst case is therefore 32
303
+ # times a population in the tens of thousands, not 32 times 1,000.
304
+ # * the doctor's unbounded `latest_per_identity` read is NOT retained
305
+ # here. `bin/_cctally_doctor.py` calls `load_codex_quota_observations`
306
+ # directly rather than through this memo, so its all-history population
307
+ # never enters this dict.
142
308
  if len(_CODEX_QUOTA_OBSERVATION_CACHE) >= 32:
143
309
  _CODEX_QUOTA_OBSERVATION_CACHE.clear()
144
310
  _CODEX_QUOTA_OBSERVATION_CACHE[key] = loaded
@@ -327,10 +493,10 @@ def _resolve_codex_weekly_cycle(
327
493
 
328
494
  #350 — FRESH-FIRST ranking (spec §3.2). Codex has no background quota poll,
329
495
  so ``stale_after_seconds(10_080) == 3600`` makes an idle weekly observation
330
- stale after exactly one hour. Discarding a stale-but-FUTURE boundary blanked
496
+ stale after exactly one hour. Discarding a stale-but-ACTIVE boundary blanked
331
497
  the hero's backward-looking actuals even though the spend was never lost, so
332
- each account's future weekly boundaries are now collected into a fresh set
333
- and a stale set and ranked:
498
+ each account's weekly boundaries that contain ``now`` are collected into a
499
+ fresh set and a stale set and ranked:
334
500
 
335
501
  1. exactly one FRESH boundary -> valid, cycle fresh;
336
502
  2. else, no fresh boundaries and exactly one STALE boundary -> valid, cycle
@@ -363,7 +529,15 @@ def _resolve_codex_weekly_cycle(
363
529
  # current-cycle milestone filter matches `resets_at` exactly, so the
364
530
  # ladder empties for precisely the jittered cycles canonicalization
365
531
  # exists to collapse. Liveness rides the same instant the hero shows.
366
- if baseline is None or baseline.canonical_resets_at <= now_utc:
532
+ # #599: a future reset is not active when its nominal start is also in
533
+ # the future; admitting it makes the bounded accounting range empty.
534
+ if (
535
+ baseline is None
536
+ or baseline.canonical_resets_at <= now_utc
537
+ or baseline.canonical_resets_at - dt.timedelta(
538
+ minutes=history.identity.window_minutes
539
+ ) >= now_utc
540
+ ):
367
541
  continue
368
542
  state = quota_freshness(history.physical_observations, now_utc).state
369
543
  boundary = (history.identity.window_minutes, baseline.canonical_resets_at)
@@ -435,6 +609,7 @@ def resolve_codex_cycle_detail_identity(
435
609
  source_root_keys: Iterable[str],
436
610
  now_utc: dt.datetime,
437
611
  account_key: str | None = None,
612
+ stats_conn=None,
438
613
  ):
439
614
  """The live-cycle identity for a per-request Codex cycle-DETAIL read (#373).
440
615
 
@@ -488,6 +663,10 @@ def resolve_codex_cycle_detail_identity(
488
663
  observations = _cached_codex_quota_observations(
489
664
  source_root_keys=active_roots,
490
665
  cache_conn=cache_conn,
666
+ stats_conn=stats_conn,
667
+ # Both bounds are derived from this request's own instant, so this
668
+ # key can never match a retained entry. See `memoize` there.
669
+ memoize=False,
491
670
  captured_at_or_after=(
492
671
  now_utc - dt.timedelta(days=DASHBOARD_QUOTA_RECENT_DAYS)
493
672
  ),
@@ -918,6 +1097,14 @@ class DashboardReadContext:
918
1097
  codex_quota_actual_thresholds: tuple[int, ...] = ()
919
1098
  codex_quota_projected_thresholds: tuple[int, ...] = ()
920
1099
  cache_report_anomaly_threshold_pp: int = 15
1100
+ #: ``(codex_stats_digest, accounts_identity_digest, claude_stats_digest)``
1101
+ #: when the caller already computed them for its own composite signature,
1102
+ #: so the quota memo's reuse identity does not derive a second copy once
1103
+ #: per call (#583 S5 spec §2.1 item 1, Preserve 2). ``None`` means the
1104
+ #: identity derives them itself under the projection gate; that is the
1105
+ #: share path and the per-request cycle-detail route, neither of which
1106
+ #: computes a build signature.
1107
+ stats_identity: tuple[str, str, str] | None = None
921
1108
 
922
1109
  def __post_init__(self) -> None:
923
1110
  for name in ("range_start", "now_utc"):
@@ -2073,7 +2260,9 @@ def _session_wire(
2073
2260
  # session paths from distinct $CODEX_HOME roots. The opaque detail
2074
2261
  # key must use that same grouping identity or two visible rows route
2075
2262
  # to one another's detail payload.
2076
- root_identity = row.codex_root or "single-root"
2263
+ root_identity = (
2264
+ identity_path(row.codex_root) if row.codex_root else "single-root"
2265
+ )
2077
2266
  row_metadata = (metadata or {}).get((str(row.codex_root or ""), str(row.session_id_path)))
2078
2267
  if row_metadata is None and metadata is not None:
2079
2268
  row_metadata = next((
@@ -2430,11 +2619,25 @@ def _codex_budget_status_domain(
2430
2619
  ``{"status": None}`` for it made the client render "No budget set." beside
2431
2620
  the command that sets one — to a user who has budgets set. It publishes the
2432
2621
  same ``not_configured.disposition`` the Claude half publishes, which the
2433
- client already handles. An ACCOUNT-SCOPED read is deliberately excluded:
2434
- ``account_budgets_only`` describes the vendor-wide axis, and that account's
2435
- own missing budget is genuinely unset.
2622
+ client already handles. #586 gives the genuinely-unset ACCOUNT-SCOPED read
2623
+ its own ``account_budget_unset`` disposition plus immutable account key, so
2624
+ the card can name ``budget.codex.accounts`` rather than the unrelated
2625
+ vendor-wide command.
2436
2626
  """
2437
2627
  config = context.codex_budget
2628
+ if account_key is not None:
2629
+ per_account = config.get("accounts") if isinstance(config, Mapping) else None
2630
+ if not isinstance(per_account, Mapping) or account_key not in per_account:
2631
+ return {
2632
+ "status": None,
2633
+ "not_configured": {
2634
+ "disposition": "account_budget_unset",
2635
+ "account_key": account_key,
2636
+ # `config set` replaces this map wholesale. Preserve every
2637
+ # sibling in the client-rendered example command (#586).
2638
+ "configured_accounts": dict(per_account or {}),
2639
+ },
2640
+ }
2438
2641
  if (
2439
2642
  account_key is None
2440
2643
  and isinstance(config, Mapping)
@@ -2506,6 +2709,86 @@ def _codex_account_admits(scope_key: str | None, row_key: object) -> bool:
2506
2709
  return key in (scope_key, _lib_accounts.UNATTRIBUTED)
2507
2710
 
2508
2711
 
2712
+ def _quota_breakdown_capture_key(
2713
+ account_key: str | None,
2714
+ identity: QuotaWindowIdentity,
2715
+ resets_at: dt.datetime,
2716
+ ) -> tuple[object, ...]:
2717
+ return (
2718
+ account_key,
2719
+ identity.source_root_key,
2720
+ identity.logical_limit_key,
2721
+ identity.observed_slot,
2722
+ identity.window_minutes,
2723
+ identity.account_key,
2724
+ resets_at.astimezone(UTC).isoformat(),
2725
+ )
2726
+
2727
+
2728
+ def _capture_quota_breakdown_evidence(
2729
+ context: DashboardReadContext,
2730
+ observations: Iterable[object],
2731
+ ) -> Mapping[tuple[object, ...], tuple[tuple, tuple]]:
2732
+ """Capture every fold-dependent weekly read from one cache generation.
2733
+
2734
+ The account partitions and `build_blocks` fold are intentionally cheap and
2735
+ may run inside the pin: their only purpose is to enumerate the bounded
2736
+ identity set whose durable breakdown and 5h-correlation reads cannot be
2737
+ issued after rollback. Expensive row aggregation remains in
2738
+ `_quota_read_model` and consumes these values later.
2739
+ """
2740
+ values = tuple(observations)
2741
+ partitions: list[tuple[str | None, tuple[object, ...]]] = [(None, values)]
2742
+ account_keys = sorted({
2743
+ str(observation.identity.account_key) for observation in values
2744
+ })
2745
+ partitions.extend((key, tuple(
2746
+ observation for observation in values
2747
+ if str(observation.identity.account_key) == key
2748
+ )) for key in account_keys)
2749
+ captured: dict[tuple[object, ...], tuple[tuple, tuple]] = {}
2750
+ for account_key, partition in partitions:
2751
+ for block in build_blocks(partition):
2752
+ identity = block.identity
2753
+ if identity.window_minutes != 10_080 or block.resets_at <= context.now_utc:
2754
+ continue
2755
+ key = _quota_breakdown_capture_key(
2756
+ account_key, identity, block.resets_at)
2757
+ try:
2758
+ canonical_rows = codex_quota_breakdown(
2759
+ identity,
2760
+ block.resets_at,
2761
+ speed=context.speed,
2762
+ cache_conn=context.cache_conn,
2763
+ stats_conn=context.stats_conn,
2764
+ account_key=account_key,
2765
+ )
2766
+ except sqlite3.Error:
2767
+ canonical_rows = ()
2768
+ correlated_five_hour: tuple = ()
2769
+ if canonical_rows:
2770
+ try:
2771
+ correlated_five_hour = tuple(
2772
+ observation
2773
+ for observation in _cached_codex_quota_observations(
2774
+ source_root_keys={identity.source_root_key},
2775
+ cache_conn=context.cache_conn,
2776
+ stats_conn=context.stats_conn,
2777
+ stats_identity=context.stats_identity,
2778
+ captured_at_or_after=block.nominal_start_at,
2779
+ )
2780
+ if observation.identity.window_minutes == 300
2781
+ and observation.identity.observed_slot == identity.observed_slot
2782
+ and observation.identity.limit_id == identity.limit_id
2783
+ and _codex_account_admits(
2784
+ account_key, observation.identity.account_key)
2785
+ )
2786
+ except sqlite3.Error:
2787
+ correlated_five_hour = ()
2788
+ captured[key] = (tuple(canonical_rows), correlated_five_hour)
2789
+ return MappingProxyType(captured)
2790
+
2791
+
2509
2792
  def _quota_read_model(
2510
2793
  context: DashboardReadContext,
2511
2794
  observations: Iterable[object],
@@ -2513,6 +2796,7 @@ def _quota_read_model(
2513
2796
  accounting_entries: Iterable[object] = (),
2514
2797
  account_key: str | None = None,
2515
2798
  decorated: bool,
2799
+ captured_breakdowns: "Mapping[tuple[object, ...], tuple[tuple, tuple]] | None" = None,
2516
2800
  ) -> dict[str, object]:
2517
2801
  """Use S2's pure history/block/forecast kernels over cache evidence.
2518
2802
 
@@ -2625,41 +2909,52 @@ def _quota_read_model(
2625
2909
  < block.resets_at
2626
2910
  )
2627
2911
  canonical_rows = ()
2912
+ correlated_five_hour = ()
2628
2913
  if identity.window_minutes == 10_080 and block.resets_at > context.now_utc:
2629
- try:
2630
- canonical_rows = codex_quota_breakdown(
2631
- identity,
2632
- block.resets_at,
2633
- speed=context.speed,
2634
- cache_conn=context.cache_conn,
2635
- stats_conn=context.stats_conn,
2636
- account_key=account_key,
2914
+ if captured_breakdowns is not None:
2915
+ canonical_rows, correlated_five_hour = captured_breakdowns.get(
2916
+ _quota_breakdown_capture_key(
2917
+ account_key, identity, block.resets_at),
2918
+ ((), ()),
2637
2919
  )
2638
- except sqlite3.Error:
2639
- # Older or partially migrated stores retain the bounded
2640
- # observation-derived fallback below. A coherent current
2641
- # store always has the durable projection used by the CLI.
2642
- canonical_rows = ()
2643
- if canonical_rows:
2644
- try:
2645
- # #416 Slice 3A review F4: this load is bounded by root, slot
2646
- # and `limit_id` only, so under focus the crossing was annotated
2647
- # with whichever ACCOUNT's 5h observation happened to sort last.
2648
- correlated_five_hour = tuple(
2649
- observation
2650
- for observation in _cached_codex_quota_observations(
2651
- source_root_keys={identity.source_root_key},
2920
+ else:
2921
+ try:
2922
+ canonical_rows = codex_quota_breakdown(
2923
+ identity,
2924
+ block.resets_at,
2925
+ speed=context.speed,
2652
2926
  cache_conn=context.cache_conn,
2653
- captured_at_or_after=block.nominal_start_at,
2927
+ stats_conn=context.stats_conn,
2928
+ account_key=account_key,
2654
2929
  )
2655
- if observation.identity.window_minutes == 300
2656
- and observation.identity.observed_slot == identity.observed_slot
2657
- and observation.identity.limit_id == identity.limit_id
2658
- and _codex_account_admits(
2659
- account_key, observation.identity.account_key)
2660
- )
2661
- except sqlite3.Error:
2662
- correlated_five_hour = ()
2930
+ except sqlite3.Error:
2931
+ # Older or partially migrated stores retain the bounded
2932
+ # observation-derived fallback below. A coherent current
2933
+ # store always has the durable projection used by the CLI.
2934
+ canonical_rows = ()
2935
+ if canonical_rows:
2936
+ if captured_breakdowns is None:
2937
+ try:
2938
+ # #416 Slice 3A review F4: this load is bounded by root,
2939
+ # slot and `limit_id` only, so under focus the crossing was
2940
+ # once annotated with whichever account sorted last.
2941
+ correlated_five_hour = tuple(
2942
+ observation
2943
+ for observation in _cached_codex_quota_observations(
2944
+ source_root_keys={identity.source_root_key},
2945
+ cache_conn=context.cache_conn,
2946
+ stats_conn=context.stats_conn,
2947
+ stats_identity=context.stats_identity,
2948
+ captured_at_or_after=block.nominal_start_at,
2949
+ )
2950
+ if observation.identity.window_minutes == 300
2951
+ and observation.identity.observed_slot == identity.observed_slot
2952
+ and observation.identity.limit_id == identity.limit_id
2953
+ and _codex_account_admits(
2954
+ account_key, observation.identity.account_key)
2955
+ )
2956
+ except sqlite3.Error:
2957
+ correlated_five_hour = ()
2663
2958
 
2664
2959
  for row in canonical_rows:
2665
2960
  milestone_rows.append({
@@ -3133,8 +3428,8 @@ def refresh_codex_source_clock(
3133
3428
  availability = "partial"
3134
3429
  # #556 S1 §4.1: an EXPIRED boundary is exactly the state the
3135
3430
  # accounting axis reports as stale. Build time can never see it
3136
- # (`_resolve_codex_weekly_cycle` retains only `resets_at > now`),
3137
- # so this clock is the only writer of that value.
3431
+ # (`_resolve_codex_weekly_cycle` retains only boundaries containing
3432
+ # `now`), so this clock is the only writer of that value.
3138
3433
  domain_freshness["hero"] = "stale"
3139
3434
  cycle_changed = True
3140
3435
  # 3. budget last
@@ -3158,6 +3453,7 @@ def refresh_codex_source_clock(
3158
3453
  # omission silently drops the authoritative account count and makes the
3159
3454
  # combined figure fail closed on an idle tick that changed nothing else.
3160
3455
  account_scope=state.account_scope,
3456
+ combined_accounting=state.combined_accounting,
3161
3457
  private_session_labels=state.private_session_labels,
3162
3458
  # #556 S2 §3.6: the aggregate carrier travels with the rows it
3163
3459
  # describes. This clock refreshes presentation axes only and publishes
@@ -3901,8 +4197,20 @@ def _codex_accounts_wire(
3901
4197
  cycles: list["CodexCycleBoundary"],
3902
4198
  accounting_start: dt.datetime,
3903
4199
  accounting_end: dt.datetime,
3904
- ) -> tuple[list[dict[str, object]], list[dict[str, object]]]:
3905
- """Return ``(accounts_wire, hero_cycles_wire)`` for a decorated Codex source.
4200
+ population: tuple[object, ...],
4201
+ ) -> tuple[
4202
+ list[dict[str, object]], list[dict[str, object]], dict[str, object]
4203
+ ]:
4204
+ """Return cards, hero cycles, and their server-only spend certificate.
4205
+
4206
+ ``population`` is the merged, encounter-ordered accounting read every card
4207
+ is derived from (#583 S5 change 2). This function issues no accounting SQL
4208
+ of its own. It walks the population ONCE, partitioning it by account in
4209
+ encounter order, and then range-filters each card's own partition, so
4210
+ neither the read count nor the in-memory work grows with the account count.
4211
+ The caller loads it over a range that starts no later than any card's own
4212
+ range, which on the share path is EARLIER than the published accounting
4213
+ range.
3906
4214
 
3907
4215
  Caller must gate on ``provider_is_decorated(stats_conn, "codex")`` — this
3908
4216
  builds nothing for a <=1-real-account install (the whole surface is absent,
@@ -3947,11 +4255,79 @@ def _codex_accounts_wire(
3947
4255
  "startAt": fallback_start.astimezone(UTC).isoformat(),
3948
4256
  "endAt": context.now_utc.astimezone(UTC).isoformat(),
3949
4257
  }
4258
+ # ONE encounter-ordered pass over the population, in place of one pass per
4259
+ # card (#583 S5 spec §2.2). `_codex_partition_by_account` applies the same
4260
+ # `or UNATTRIBUTED` normalization each card's filter used to apply itself,
4261
+ # so the buckets are the sets those filters selected from, and it preserves
4262
+ # encounter order, which `_aggregate_codex_buckets` depends on for a
4263
+ # bucket's first-seen model order.
4264
+ by_account = _codex_partition_by_account(population)
4265
+
4266
+ def _slice(
4267
+ account_key: str,
4268
+ start: dt.datetime,
4269
+ end: dt.datetime,
4270
+ source_root_keys: Iterable[str] | None = None,
4271
+ ) -> tuple[object, ...]:
4272
+ """One card's rows, range-filtered from that account's partition.
4273
+
4274
+ The predicates match the reader these filters replaced, member for
4275
+ member: half-open ``[start, end)`` on a UTC-normalized timestamp, the
4276
+ reserved sentinel standing for a NULL stamp, and a root set that selects
4277
+ nothing when it is supplied empty. Encounter order is the partition's,
4278
+ which is the population's, which is the reader's ``ORDER BY`` over a
4279
+ wider range, so a bucket's first-seen model order is unchanged.
4280
+
4281
+ Two comparisons are nevertheless not the SAME comparison, and both are
4282
+ equivalent only because ingest cannot write the row that separates them.
4283
+ Stated here so the next reader does not have to re-derive it, and
4284
+ asserted at the producer by
4285
+ ``test_ingest_writes_only_canonical_account_and_timestamp_values``.
4286
+
4287
+ 1. The replaced per-card query passed ``account_key=<key>`` to
4288
+ ``load_cached_rooted_codex_accounting_entries``, whose sentinel
4289
+ predicate is ``account_key IS NULL OR account_key = 'unattributed'``.
4290
+ A row stored as the EMPTY STRING matched neither that nor any real
4291
+ account's ``account_key = ?``, so it appeared on no card at all,
4292
+ while the reader maps ``''`` to the sentinel and this filter
4293
+ therefore includes it. No shipped writer emits it: the three
4294
+ statements that set this column (`bin/_cctally_cache.py` -- the
4295
+ ingest insert, the window spend-adoption update and the attribution
4296
+ restore) each write NULL or a resolved 32-hex account key, and the
4297
+ ingest value comes from ``codex_file_accounts.account_key``, itself
4298
+ written only by two statements with the same two outcomes. Measured:
4299
+ zero empty strings among 163,424 rows of the maintainer's store.
4300
+ Note also that every OTHER account-partitioned surface already files
4301
+ such a row under the sentinel -- ``_codex_partition_by_account``
4302
+ applies the identical ``or _lib_accounts.UNATTRIBUTED`` -- so the
4303
+ card query was the sole outlier rather than the rule.
4304
+ 2. The reader bounds ``timestamp_utc`` by comparing ISO TEXT inside
4305
+ SQLite; this filter compares parsed UTC datetimes. For the canonical
4306
+ ``...+00:00`` spelling the two orders coincide, including against a
4307
+ bound carrying microseconds, because ``+`` sorts before ``.``. They
4308
+ diverge only for a stamp carrying a different valid offset, and a
4309
+ NAIVE stamp is not a divergence at all: ``_parse_timestamp`` refuses
4310
+ it, so such a row fails the whole read on the old path and the new
4311
+ one alike. Codex ingest writes
4312
+ ``entry.timestamp.astimezone(UTC).isoformat()``, always ``+00:00``.
4313
+ Measured: 163,424 of 163,424 rows canonical.
4314
+ """
4315
+ roots = (
4316
+ None if source_root_keys is None
4317
+ else {key for key in source_root_keys
4318
+ if isinstance(key, str) and key}
4319
+ )
4320
+ if roots is not None and not roots:
4321
+ return ()
4322
+ return tuple(
4323
+ row for row in by_account.get(account_key, ())
4324
+ if start <= row.timestamp < end
4325
+ and (roots is None or row.source_root_key in roots)
4326
+ )
4327
+
3950
4328
  # Include unattributed last iff it has cycle/5h/spend evidence.
3951
- unattributed_rows = load_cached_rooted_codex_accounting_entries(
3952
- accounting_start, accounting_end, speed=context.speed,
3953
- cache_conn=context.cache_conn, account_key=_lib_accounts.UNATTRIBUTED,
3954
- )
4329
+ unattributed_rows = _slice(
4330
+ _lib_accounts.UNATTRIBUTED, accounting_start, accounting_end)
3955
4331
  # Existence is decided over the accounting range so a sentinel holding only
3956
4332
  # older spend keeps its card; the totals below cover the bounded window, so
3957
4333
  # a resolved $0.00 is an honest empty state rather than an absence (#564).
@@ -3991,15 +4367,15 @@ def _codex_accounts_wire(
3991
4367
 
3992
4368
  accounts_wire: list[dict[str, object]] = []
3993
4369
  hero_cycles_wire: list[dict[str, object]] = []
4370
+ contributions: list[dict[str, object]] = []
3994
4371
  for key in ordered_keys:
3995
4372
  cyc = cycle_by_account.get(key)
3996
4373
  is_unattributed = key == _lib_accounts.UNATTRIBUTED
3997
4374
  if cyc is not None and not is_unattributed:
3998
4375
  cycle_end = min(accounting_end, cyc.resets_at)
3999
- rows = load_cached_rooted_codex_accounting_entries(
4000
- cyc.start_at, cycle_end, speed=context.speed,
4001
- cache_conn=context.cache_conn,
4002
- source_root_keys=cyc.source_root_keys, account_key=key,
4376
+ rows = _slice(
4377
+ key, cyc.start_at, cycle_end,
4378
+ source_root_keys=cyc.source_root_keys,
4003
4379
  )
4004
4380
  totals = _totals(rows)
4005
4381
  elif is_unattributed:
@@ -4009,10 +4385,7 @@ def _codex_accounts_wire(
4009
4385
  # native cycle width ending now, so this card can be summed into a
4010
4386
  # week-labelled headline without overstating it (#564). No bars or
4011
4387
  # reset, because there is no live cycle to describe.
4012
- rows = load_cached_rooted_codex_accounting_entries(
4013
- fallback_start, accounting_end, speed=context.speed,
4014
- cache_conn=context.cache_conn, account_key=key,
4015
- )
4388
+ rows = _slice(key, fallback_start, accounting_end)
4016
4389
  totals = _totals(rows)
4017
4390
  card: dict[str, object] = {
4018
4391
  "accountKey": key,
@@ -4044,6 +4417,23 @@ def _codex_accounts_wire(
4044
4417
  # which is true of several unrelated states (#564 D3).
4045
4418
  card["spendWindow"] = fallback_window
4046
4419
  accounts_wire.append(card)
4420
+ if cyc is not None and not is_unattributed:
4421
+ contribution_period = {
4422
+ "kind": "native_7_day_cycle",
4423
+ "start_at": cyc.start_at.astimezone(UTC).isoformat(),
4424
+ "end_at": cyc.resets_at.astimezone(UTC).isoformat(),
4425
+ }
4426
+ else:
4427
+ contribution_period = {
4428
+ "kind": "trailing_7_day_cycle",
4429
+ "start_at": fallback_start.astimezone(UTC).isoformat(),
4430
+ "end_at": context.now_utc.astimezone(UTC).isoformat(),
4431
+ }
4432
+ contributions.append({
4433
+ "account_key": key,
4434
+ "cost_usd": totals["spendUsd"],
4435
+ "period": contribution_period,
4436
+ })
4047
4437
  if cyc is not None and not is_unattributed:
4048
4438
  hero_cycles_wire.append({
4049
4439
  "accountKey": key,
@@ -4054,7 +4444,11 @@ def _codex_accounts_wire(
4054
4444
  "cost_usd": totals["spendUsd"],
4055
4445
  "total_tokens": totals["totalTokens"],
4056
4446
  })
4057
- return accounts_wire, hero_cycles_wire
4447
+ return accounts_wire, hero_cycles_wire, {
4448
+ "scope": "account_cycles",
4449
+ "status": "certified",
4450
+ "contributions": tuple(contributions),
4451
+ }
4058
4452
 
4059
4453
 
4060
4454
  def _codex_partition_by_account(
@@ -4172,6 +4566,7 @@ def _codex_account_scopes_wire(
4172
4566
  changed_new_by_account: Mapping[str, tuple[CodexEntry, ...]] | None = None,
4173
4567
  changed_old_rows_by_account: Mapping[str, tuple[object, ...]] | None = None,
4174
4568
  changed_new_rows_by_account: Mapping[str, tuple[object, ...]] | None = None,
4569
+ captured_breakdowns: "Mapping[tuple[object, ...], tuple[tuple, tuple]] | None" = None,
4175
4570
  ) -> dict[str, dict[str, object]]:
4176
4571
  """The per-account CHILDREN of the merged Codex read model (spec §5.3).
4177
4572
 
@@ -4251,6 +4646,7 @@ def _codex_account_scopes_wire(
4251
4646
  # #429 §4.4: this wire is only ever reached under decoration — the
4252
4647
  # caller gates the whole `account_scopes` surface on it.
4253
4648
  decorated=True,
4649
+ captured_breakdowns=captured_breakdowns,
4254
4650
  )
4255
4651
  # Each decorated child owns the only honest cycle index for that
4256
4652
  # account. Reusing a merged parent index here would render account A's
@@ -4480,7 +4876,7 @@ def _claude_accounts_wire(
4480
4876
  stats_conn: sqlite3.Connection,
4481
4877
  *,
4482
4878
  now_utc: dt.datetime,
4483
- ) -> list[dict[str, object]]:
4879
+ ) -> tuple[list[dict[str, object]], dict[str, object]]:
4484
4880
  """Per-account Claude hero cards (#341 Task 4, Ruling C).
4485
4881
 
4486
4882
  Symmetric with ``_codex_accounts_wire``: the caller gates on
@@ -4509,13 +4905,38 @@ def _claude_accounts_wire(
4509
4905
  (key,),
4510
4906
  ).fetchone()
4511
4907
 
4512
- def _latest_cost(key: str) -> float:
4908
+ def _latest_cost(key: str):
4513
4909
  row = stats_conn.execute(
4514
- "SELECT cost_usd FROM weekly_cost_snapshots WHERE account_key=? "
4910
+ "SELECT cost_usd, week_start_at, week_end_at "
4911
+ "FROM weekly_cost_snapshots WHERE account_key=? "
4515
4912
  "ORDER BY captured_at_utc DESC LIMIT 1",
4516
4913
  (key,),
4517
4914
  ).fetchone()
4518
- return float(row[0]) if row is not None and row[0] is not None else 0.0
4915
+ if row is None or row[0] is None:
4916
+ return 0.0, None, "account_cost_unresolved"
4917
+ try:
4918
+ start = dt.datetime.fromisoformat(str(row[1]).replace("Z", "+00:00"))
4919
+ end = dt.datetime.fromisoformat(str(row[2]).replace("Z", "+00:00"))
4920
+ except (TypeError, ValueError):
4921
+ return float(row[0]), None, "account_cycle_unresolved"
4922
+ if (
4923
+ start.tzinfo is None or start.utcoffset() is None
4924
+ or end.tzinfo is None or end.utcoffset() is None
4925
+ or start >= end
4926
+ ):
4927
+ return float(row[0]), None, "account_cycle_unresolved"
4928
+ start_at = start.astimezone(UTC).isoformat().replace("+00:00", "Z")
4929
+ end_at = end.astimezone(UTC).isoformat().replace("+00:00", "Z")
4930
+ contribution = {
4931
+ "account_key": key,
4932
+ "cost_usd": float(row[0]),
4933
+ "period": {
4934
+ "kind": "subscription_week",
4935
+ "start_at": start_at,
4936
+ "end_at": end_at,
4937
+ },
4938
+ }
4939
+ return float(row[0]), contribution, None
4519
4940
 
4520
4941
  _claude_label_map = _cctally_account.display_label_map(stats_conn, "claude")
4521
4942
 
@@ -4529,12 +4950,20 @@ def _claude_accounts_wire(
4529
4950
  ordered_keys.append(_lib_accounts.UNATTRIBUTED)
4530
4951
 
4531
4952
  cards: list[dict[str, object]] = []
4953
+ contributions: list[dict[str, object]] = []
4954
+ unresolved_causes: set[str] = set()
4532
4955
  for key in ordered_keys:
4533
4956
  is_unattributed = key == _lib_accounts.UNATTRIBUTED
4534
4957
  usage = _latest_usage(key)
4535
4958
  weekly_pct = usage[0] if usage is not None else None
4536
4959
  five_hour_pct = usage[1] if usage is not None else None
4537
4960
  resets_at = usage[2] if usage is not None else None
4961
+ spend_usd, contribution, unresolved_cause = _latest_cost(key)
4962
+ if contribution is None:
4963
+ unresolved_causes.add(
4964
+ unresolved_cause or "account_reconciliation_failed")
4965
+ else:
4966
+ contributions.append(contribution)
4538
4967
  card: dict[str, object] = {
4539
4968
  "accountKey": key,
4540
4969
  "label": _claude_label_map.get(key) or _cctally_account.account_label(stats_conn, key),
@@ -4543,12 +4972,30 @@ def _claude_accounts_wire(
4543
4972
  "weeklyPercent": None if is_unattributed else weekly_pct,
4544
4973
  "fiveHourPercent": None if is_unattributed else five_hour_pct,
4545
4974
  "resetsAt": None if is_unattributed else resets_at,
4546
- "spendUsd": _latest_cost(key),
4975
+ "spendUsd": spend_usd,
4547
4976
  }
4548
4977
  if is_unattributed:
4549
4978
  card["unattributed"] = True
4979
+ if contribution is not None:
4980
+ period = contribution["period"]
4981
+ card["spendWindow"] = {
4982
+ "kind": "subscription-week",
4983
+ "startAt": period["start_at"],
4984
+ "endAt": period["end_at"],
4985
+ }
4550
4986
  cards.append(card)
4551
- return cards
4987
+ certificate: dict[str, object] = {
4988
+ "scope": "account_cycles",
4989
+ "status": "unresolved" if unresolved_causes else "certified",
4990
+ "contributions": tuple(contributions),
4991
+ }
4992
+ if unresolved_causes:
4993
+ certificate["cause"] = (
4994
+ "account_cost_unresolved"
4995
+ if "account_cost_unresolved" in unresolved_causes
4996
+ else "account_cycle_unresolved"
4997
+ )
4998
+ return cards, certificate
4552
4999
 
4553
5000
 
4554
5001
  def _codex_ingest_backlog_wire(
@@ -4595,31 +5042,15 @@ def _codex_ingest_backlog_wire(
4595
5042
  }
4596
5043
 
4597
5044
 
4598
- def build_codex_source_state(
4599
- context: DashboardReadContext,
4600
- *,
4601
- data_version: str,
4602
- ) -> SourceDashboardState:
4603
- """Build Codex data strictly from the coordinated cache/stats reads.
4604
-
4605
- No sync, rollout scan, CLI parser, or fallback is reachable from this
4606
- adapter. Period and session arithmetic remains delegated to the shipped
4607
- S3 view kernels, preserving the CLI's inclusive-token vocabulary.
5045
+ def _codex_source_caches() -> tuple[dict, ...]:
5046
+ """Every process cache a Codex source build reuses across builds.
4608
5047
 
4609
- The whole read runs under ONE ``codex_path_scope`` (#566 §5.1 item 1), so
4610
- the merged parent view and every per-account child share a single session
4611
- root resolution and a single parse per distinct session file. The scope is
4612
- opened here rather than further out because this is the boundary that owns
4613
- every Codex session view in the build, and it is discarded when the read
4614
- returns.
5048
+ ONE definition, so the set the build checkpoints and the set a cold
5049
+ reference discards cannot drift apart. A cold reference that discards only
5050
+ some of them compares two partly-warm builds and calls the second cold
5051
+ (#583 S5 criterion 10).
4615
5052
  """
4616
- # This memo deduplicates the several account/parent consumers inside ONE
4617
- # coordinated source build. It may not cross that boundary: a caller can
4618
- # deliberately request a fresh build after stats/account decoration changes
4619
- # without advancing cache.db's quota ledger, and the established contract
4620
- # requires one bounded physical load for that new build.
4621
- reset_codex_quota_observation_cache()
4622
- caches = (
5053
+ return (
4623
5054
  _CODEX_QUOTA_OBSERVATION_CACHE,
4624
5055
  _CODEX_PERIOD_VIEW_CACHE,
4625
5056
  _CODEX_CACHE_REPORT_ROWS,
@@ -4630,90 +5061,45 @@ def build_codex_source_state(
4630
5061
  _CODEX_WEEKLY_VIEW_CACHE,
4631
5062
  _CODEX_ACCOUNT_SCOPE_CACHE,
4632
5063
  )
4633
- cache_checkpoint = tuple(dict(cache) for cache in caches)
4634
- accounting_checkpoint = (
4635
- _lib_snapshot_cache.checkpoint_codex_accounting_cache_state()
4636
- )
4637
- try:
4638
- with codex_path_scope() as path_scope:
4639
- return _build_codex_source_state(
4640
- context, data_version=data_version, path_scope=path_scope,
4641
- )
4642
- except Exception:
4643
- for cache, prior in zip(caches, cache_checkpoint):
4644
- cache.clear()
4645
- cache.update(prior)
4646
- _lib_snapshot_cache.restore_codex_accounting_cache_state(
4647
- accounting_checkpoint)
4648
- raise
4649
5064
 
4650
5065
 
4651
- def _build_codex_source_state(
5066
+ def reset_codex_source_caches() -> None:
5067
+ """Discard every reuse a Codex source build could draw on.
5068
+
5069
+ The cold reference for a warm-versus-cold equality check: after this, the
5070
+ next build reconstructs from the databases alone. It covers the accounting
5071
+ cache state too, which lives in ``_lib_snapshot_cache`` rather than in a
5072
+ dict here and is the other half of what a build retains.
5073
+ """
5074
+ for cache in _codex_source_caches():
5075
+ cache.clear()
5076
+ _lib_snapshot_cache.reset_codex_accounting_cache_state()
5077
+
5078
+
5079
+ @dataclass(frozen=True)
5080
+ class _CodexAccountingCapture:
5081
+ qualified_entries: tuple[object, ...]
5082
+ dirty_accounts: tuple[str, ...]
5083
+ changed_old: tuple[object, ...]
5084
+ changed_new: tuple[object, ...]
5085
+ entries: tuple[object, ...]
5086
+ metadata_incomplete: bool
5087
+
5088
+
5089
+ def _capture_codex_accounting(
4652
5090
  context: DashboardReadContext,
4653
5091
  *,
4654
- data_version: str,
5092
+ accounting_start: dt.datetime,
5093
+ accounting_end: dt.datetime,
5094
+ active_roots: tuple[str, ...],
4655
5095
  path_scope: object,
4656
- ) -> SourceDashboardState:
4657
- active_roots = tuple(sorted(
4658
- str(row[0]) for row in context.cache_conn.execute(
4659
- "SELECT source_root_key FROM codex_source_roots"
4660
- )
4661
- ))
4662
- quota_observations = _cached_codex_quota_observations(
4663
- source_root_keys=active_roots,
4664
- cache_conn=context.cache_conn,
4665
- captured_at_or_after=(
4666
- context.now_utc - dt.timedelta(days=DASHBOARD_QUOTA_RECENT_DAYS)
4667
- ),
4668
- active_at=context.now_utc,
4669
- max_rows=DASHBOARD_QUOTA_OBSERVATION_LIMIT,
4670
- )
4671
- coherence = codex_projection_coherence(
4672
- context,
4673
- )
4674
- projection_incoherent = not coherence.coherent
4675
- ingest_backlog = _codex_ingest_backlog_wire(context.cache_conn)
4676
- # The cache reader's established report surface treats the ``now`` instant
4677
- # as inclusive. The qualified adapter is half-open, so extend only its
4678
- # query/result boundary by one microsecond and keep all live budget sums
4679
- # explicitly half-open at ``now`` below.
4680
- accounting_end = context.now_utc + dt.timedelta(microseconds=1)
4681
- accounting_start = context.range_start
4682
- if context.codex_budget is not None:
4683
- # #556 S5 Unit 2 (Unit 1 review R6, widened) — this is the SECOND
4684
- # unguarded call to the window resolver, and unlike
4685
- # `_codex_budget_cost_events` it had no boundary of its own. It sits
4686
- # outside every other `try` in this function, so an unresolvable window
4687
- # escaped into `_tui_build_source_bundle`'s `source_build_failed`
4688
- # handler and destroyed the entire Codex provider's data — the exact
4689
- # failure §3.5 exists to prevent, reached from a different line.
4690
- #
4691
- # Degrading to the un-widened accounting range cannot publish a false
4692
- # figure: the same failure reaches `_codex_budget_status_domain`, which
4693
- # nulls the status and names `budget_compute_failed`.
4694
- try:
4695
- _period, budget_start, _budget_end = _configured_codex_budget_window(context)
4696
- except Exception:
4697
- _warn_codex_budget_window_once("accounting_range")
4698
- else:
4699
- accounting_start = min(accounting_start, budget_start)
4700
- health = load_codex_project_metadata_health(
4701
- cache_conn=context.cache_conn,
4702
- start=accounting_start,
4703
- end=accounting_end,
4704
- )
4705
- metadata_incomplete = health.incomplete_rows > 0
4706
- metadata_warning_message = (
4707
- f"{health.incomplete_rows} Codex accounting row(s) lack project metadata; "
4708
- "run `cctally cache-sync --source codex --rebuild`."
4709
- if metadata_incomplete
4710
- else "Codex project metadata could not be read; "
4711
- "run `cctally cache-sync --source codex --rebuild`."
4712
- )
5096
+ metadata_incomplete: bool,
5097
+ ) -> _CodexAccountingCapture:
5098
+ """Read the incremental accounting carrier without folding public views."""
4713
5099
  qualified_entries: tuple[object, ...] = ()
4714
- accounting_dirty_accounts: tuple[str, ...] = ()
4715
- accounting_changed_old: tuple[object, ...] = ()
4716
- accounting_changed_new: tuple[object, ...] = ()
5100
+ dirty_accounts: tuple[str, ...] = ()
5101
+ changed_old: tuple[object, ...] = ()
5102
+ changed_new: tuple[object, ...] = ()
4717
5103
  if not metadata_incomplete:
4718
5104
  try:
4719
5105
  cached_accounting = _lib_snapshot_cache.build_cached_codex_accounting(
@@ -4753,42 +5139,425 @@ def _build_codex_source_state(
4753
5139
  identity_of=lambda entry: int(entry.cache_entry_id),
4754
5140
  )
4755
5141
  qualified_entries = cached_accounting.entries
4756
- accounting_dirty_accounts = cached_accounting.dirty_accounts
4757
- accounting_changed_old = cached_accounting.changed_old
4758
- accounting_changed_new = cached_accounting.changed_new
4759
- accounting_entries: tuple[object, ...] = qualified_entries
5142
+ dirty_accounts = cached_accounting.dirty_accounts
5143
+ changed_old = cached_accounting.changed_old
5144
+ changed_new = cached_accounting.changed_new
5145
+ entries: tuple[object, ...] = qualified_entries
4760
5146
  except QualifiedMetadataUnavailable:
4761
- # A cached read must be internally coherent, but retain accounting
4762
- # once if a defensive race or malformed row violates that premise.
4763
5147
  _lib_log.get_logger("dashboard").warning(
4764
5148
  "Codex qualified metadata read became unavailable; using cache-only accounting fallback"
4765
5149
  )
4766
5150
  metadata_incomplete = True
4767
5151
  _lib_snapshot_cache.reset_codex_accounting_cache_state()
4768
- accounting_entries = load_cached_rooted_codex_accounting_entries(
5152
+ entries = load_cached_rooted_codex_accounting_entries(
4769
5153
  accounting_start,
4770
5154
  accounting_end,
4771
5155
  speed=context.speed,
4772
5156
  cache_conn=context.cache_conn,
4773
5157
  )
4774
- accounting_dirty_accounts = tuple(sorted({
5158
+ dirty_accounts = tuple(sorted({
4775
5159
  str(getattr(entry, "account_key", "") or
4776
5160
  _lib_accounts.UNATTRIBUTED)
4777
- for entry in accounting_entries
5161
+ for entry in entries
4778
5162
  }))
4779
5163
  else:
4780
5164
  _lib_snapshot_cache.reset_codex_accounting_cache_state()
4781
- accounting_entries = load_cached_rooted_codex_accounting_entries(
5165
+ entries = load_cached_rooted_codex_accounting_entries(
4782
5166
  accounting_start,
4783
5167
  accounting_end,
4784
5168
  speed=context.speed,
4785
5169
  cache_conn=context.cache_conn,
4786
5170
  )
4787
- accounting_dirty_accounts = tuple(sorted({
5171
+ dirty_accounts = tuple(sorted({
4788
5172
  str(getattr(entry, "account_key", "") or
4789
5173
  _lib_accounts.UNATTRIBUTED)
4790
- for entry in accounting_entries
5174
+ for entry in entries
4791
5175
  }))
5176
+ return _CodexAccountingCapture(
5177
+ qualified_entries=qualified_entries,
5178
+ dirty_accounts=dirty_accounts,
5179
+ changed_old=changed_old,
5180
+ changed_new=changed_new,
5181
+ entries=tuple(entries),
5182
+ metadata_incomplete=metadata_incomplete,
5183
+ )
5184
+
5185
+
5186
+ @dataclass(frozen=True)
5187
+ class CodexSourceCapture:
5188
+ """Every cache-backed input for a post-transaction Codex source build."""
5189
+
5190
+ context: DashboardReadContext
5191
+ active_roots: tuple[str, ...]
5192
+ quota_observations: tuple[object, ...]
5193
+ projection_incoherent: bool
5194
+ ingest_backlog: object
5195
+ accounting_start: dt.datetime
5196
+ accounting_end: dt.datetime
5197
+ health: object
5198
+ accounting: _CodexAccountingCapture
5199
+ accounting_exists: bool
5200
+ accounting_exists_error: Exception | None
5201
+ conversation_metadata: Mapping
5202
+ decorated: bool
5203
+ card_population: tuple[object, ...]
5204
+ breakdowns: Mapping
5205
+ cache_checkpoint: tuple[dict, ...]
5206
+ accounting_checkpoint: object
5207
+
5208
+
5209
+ def _restore_codex_source_capture(capture: CodexSourceCapture) -> None:
5210
+ for cache, prior in zip(_codex_source_caches(), capture.cache_checkpoint):
5211
+ cache.clear()
5212
+ cache.update(prior)
5213
+ _lib_snapshot_cache.restore_codex_accounting_cache_state(
5214
+ capture.accounting_checkpoint)
5215
+
5216
+
5217
+ def capture_codex_source_state(
5218
+ context: DashboardReadContext,
5219
+ *,
5220
+ path_scope: object,
5221
+ ) -> CodexSourceCapture:
5222
+ """Capture Codex cache evidence without constructing public view models."""
5223
+ caches = _codex_source_caches()
5224
+ cache_checkpoint = tuple(dict(cache) for cache in caches)
5225
+ accounting_checkpoint = (
5226
+ _lib_snapshot_cache.checkpoint_codex_accounting_cache_state()
5227
+ )
5228
+ try:
5229
+ active_roots = tuple(sorted(
5230
+ str(row[0]) for row in context.cache_conn.execute(
5231
+ "SELECT source_root_key FROM codex_source_roots"
5232
+ )
5233
+ ))
5234
+ quota_observations = tuple(_cached_codex_quota_observations(
5235
+ source_root_keys=active_roots,
5236
+ cache_conn=context.cache_conn,
5237
+ stats_conn=context.stats_conn,
5238
+ stats_identity=context.stats_identity,
5239
+ memoize=False,
5240
+ captured_at_or_after=(
5241
+ context.now_utc - dt.timedelta(days=DASHBOARD_QUOTA_RECENT_DAYS)
5242
+ ),
5243
+ active_at=context.now_utc,
5244
+ max_rows=DASHBOARD_QUOTA_OBSERVATION_LIMIT,
5245
+ ))
5246
+ projection_incoherent = not codex_projection_coherence(context).coherent
5247
+ ingest_backlog = _codex_ingest_backlog_wire(context.cache_conn)
5248
+ accounting_end = context.now_utc + dt.timedelta(microseconds=1)
5249
+ accounting_start = context.range_start
5250
+ if context.codex_budget is not None:
5251
+ try:
5252
+ _period, budget_start, _budget_end = (
5253
+ _configured_codex_budget_window(context)
5254
+ )
5255
+ except Exception:
5256
+ _warn_codex_budget_window_once("accounting_range")
5257
+ else:
5258
+ accounting_start = min(accounting_start, budget_start)
5259
+ health = load_codex_project_metadata_health(
5260
+ cache_conn=context.cache_conn,
5261
+ start=accounting_start,
5262
+ end=accounting_end,
5263
+ )
5264
+ accounting = _capture_codex_accounting(
5265
+ context,
5266
+ accounting_start=accounting_start,
5267
+ accounting_end=accounting_end,
5268
+ active_roots=active_roots,
5269
+ path_scope=path_scope,
5270
+ metadata_incomplete=health.incomplete_rows > 0,
5271
+ )
5272
+ try:
5273
+ accounting_exists = has_cached_codex_accounting_entries(
5274
+ cache_conn=context.cache_conn)
5275
+ accounting_exists_error = None
5276
+ except Exception as exc:
5277
+ # This probe is semantically relevant only when quota evidence
5278
+ # cannot resolve a native cycle. Capture the fault beside the
5279
+ # value, then defer it until the build knows that branch is live.
5280
+ accounting_exists = False
5281
+ accounting_exists_error = exc
5282
+ conversation_metadata = MappingProxyType(
5283
+ _codex_conversation_metadata(context.cache_conn))
5284
+ try:
5285
+ import _cctally_account
5286
+ decorated = _cctally_account.provider_is_decorated(
5287
+ context.stats_conn, "codex")
5288
+ except Exception:
5289
+ decorated = False
5290
+ card_population_start = accounting_start
5291
+ if decorated:
5292
+ card_population_start = min(
5293
+ context.range_start,
5294
+ context.now_utc - dt.timedelta(
5295
+ minutes=ACCOUNT_WEEKLY_WINDOW_MINUTES),
5296
+ accounting_start,
5297
+ )
5298
+ else:
5299
+ # The undecorated parent has no account cards. It needs an earlier
5300
+ # population only when a healthy representative cycle actually
5301
+ # resolves; an unresolved narrow share must not pay for a seven-day
5302
+ # read whose rows no published domain can consume.
5303
+ try:
5304
+ captured_cycles = _resolve_codex_weekly_cycle(
5305
+ quota_observations, context.now_utc)
5306
+ except CodexCycleUnavailable:
5307
+ captured_cycles = ()
5308
+ if captured_cycles:
5309
+ card_population_start = min(
5310
+ accounting_start, captured_cycles[0].start_at)
5311
+ card_population = (
5312
+ accounting.entries
5313
+ if card_population_start == accounting_start
5314
+ else load_cached_rooted_codex_accounting_entries(
5315
+ card_population_start,
5316
+ accounting_end,
5317
+ speed=context.speed,
5318
+ cache_conn=context.cache_conn,
5319
+ )
5320
+ )
5321
+ breakdowns = _capture_quota_breakdown_evidence(
5322
+ context, quota_observations)
5323
+ return CodexSourceCapture(
5324
+ context=context,
5325
+ active_roots=active_roots,
5326
+ quota_observations=quota_observations,
5327
+ projection_incoherent=projection_incoherent,
5328
+ ingest_backlog=ingest_backlog,
5329
+ accounting_start=accounting_start,
5330
+ accounting_end=accounting_end,
5331
+ health=health,
5332
+ accounting=accounting,
5333
+ accounting_exists=accounting_exists,
5334
+ accounting_exists_error=accounting_exists_error,
5335
+ conversation_metadata=conversation_metadata,
5336
+ decorated=decorated,
5337
+ card_population=tuple(card_population),
5338
+ breakdowns=breakdowns,
5339
+ cache_checkpoint=cache_checkpoint,
5340
+ accounting_checkpoint=accounting_checkpoint,
5341
+ )
5342
+ except Exception:
5343
+ for cache, prior in zip(caches, cache_checkpoint):
5344
+ cache.clear()
5345
+ cache.update(prior)
5346
+ _lib_snapshot_cache.restore_codex_accounting_cache_state(
5347
+ accounting_checkpoint)
5348
+ raise
5349
+
5350
+
5351
+ def build_codex_source_state_from_capture(
5352
+ capture: CodexSourceCapture,
5353
+ *,
5354
+ data_version: str,
5355
+ path_scope: object,
5356
+ ) -> SourceDashboardState:
5357
+ """Fold one captured Codex generation without issuing cache SQL."""
5358
+ try:
5359
+ return _build_codex_source_state(
5360
+ capture.context,
5361
+ data_version=data_version,
5362
+ path_scope=path_scope,
5363
+ capture=capture,
5364
+ )
5365
+ except Exception:
5366
+ _restore_codex_source_capture(capture)
5367
+ raise
5368
+
5369
+
5370
+ def build_codex_source_state(
5371
+ context: DashboardReadContext,
5372
+ *,
5373
+ data_version: str,
5374
+ ) -> SourceDashboardState:
5375
+ """Build Codex data strictly from the coordinated cache/stats reads.
5376
+
5377
+ No sync, rollout scan, CLI parser, or fallback is reachable from this
5378
+ adapter. Period and session arithmetic remains delegated to the shipped
5379
+ S3 view kernels, preserving the CLI's inclusive-token vocabulary.
5380
+
5381
+ The whole read runs under ONE ``codex_path_scope`` (#566 §5.1 item 1), so
5382
+ the merged parent view and every per-account child share a single session
5383
+ root resolution and a single parse per distinct session file. The scope is
5384
+ opened here rather than further out because this is the boundary that owns
5385
+ every Codex session view in the build, and it is discarded when the read
5386
+ returns.
5387
+ """
5388
+ # #583 S5: this memo now survives across builds. It may do so ONLY because
5389
+ # its key carries the stats-side identity as well as the cache legs -- see
5390
+ # `_codex_quota_reuse_identity`. The clear that used to stand here existed
5391
+ # because a decoration or account-registry change advances no cache.db leg
5392
+ # and so could not move a cache-only key. Do not narrow that identity.
5393
+ with codex_path_scope() as path_scope:
5394
+ capture = capture_codex_source_state(
5395
+ context, path_scope=path_scope)
5396
+ return build_codex_source_state_from_capture(
5397
+ capture, data_version=data_version, path_scope=path_scope)
5398
+
5399
+
5400
+ def _build_codex_cycle_accounting(
5401
+ rows: Iterable[object],
5402
+ *,
5403
+ now_utc: dt.datetime,
5404
+ display_tz_name: str | None,
5405
+ speed: str,
5406
+ ) -> tuple[list[CodexEntry], float]:
5407
+ """Price the hero's already-captured native-cycle accounting rows."""
5408
+ entries = _codex_entries_from_accounting(rows)
5409
+ cost_usd = build_codex_daily_view(
5410
+ entries,
5411
+ now_utc=now_utc,
5412
+ tz_name=display_tz_name,
5413
+ speed=speed,
5414
+ ).total_cost_usd
5415
+ return entries, cost_usd
5416
+
5417
+
5418
+ def _build_codex_source_state(
5419
+ context: DashboardReadContext,
5420
+ *,
5421
+ data_version: str,
5422
+ path_scope: object,
5423
+ capture: "CodexSourceCapture | None" = None,
5424
+ ) -> SourceDashboardState:
5425
+ active_roots = (
5426
+ capture.active_roots
5427
+ if capture is not None
5428
+ else tuple(sorted(
5429
+ str(row[0]) for row in context.cache_conn.execute(
5430
+ "SELECT source_root_key FROM codex_source_roots"
5431
+ )
5432
+ ))
5433
+ )
5434
+ # This read is a function of the tick instant, so it does NOT reuse across
5435
+ # ticks and the cross-build memo above cannot help it. Both bounds move
5436
+ # with `context.now_utc`, which in production is the tick's wall clock
5437
+ # (`bin/_cctally_tui.py`: `now_utc or dt.datetime.now(dt.timezone.utc)`).
5438
+ #
5439
+ # It cannot be made reusable by loading a memoizable superset and filtering
5440
+ # in memory, and the reason is a property of the loader rather than of this
5441
+ # call: `max_rows` is a SQL `LIMIT` under an `ORDER BY` whose leading term
5442
+ # is `resets_at_utc > active_at`, so a superset truncates a DIFFERENT set of
5443
+ # rows; and `_apply_codex_window_attribution_overlay` plus
5444
+ # `adopt_unidentified_observations` are population-dependent folds run after
5445
+ # the bound, so a wider population can change the account a retained
5446
+ # observation carries. Filtering a superset would therefore change the
5447
+ # published answer, not just its cost.
5448
+ #
5449
+ # Measured on a copy of the real store (276,391 retained Codex quota rows),
5450
+ # warm, five samples after one untimed warm-up: 222.5 ms median per tick,
5451
+ # range 210.7-231.3 ms.
5452
+ #
5453
+ # As a share, that is 13.9% of 1.60 s, and the denominator has to be named:
5454
+ # 1.60 s is the sum of the three quota call sites when each is timed ALONE
5455
+ # and UN-profiled on that store (this bounded read 222.5 ms, the five-hour
5456
+ # correlation read 209.6 ms, the doctor's all-history `latest_per_identity`
5457
+ # read 1,164.7 ms). The S5 measurement record's 2.571 s for
5458
+ # `load_codex_quota_observations` is a cProfile total instead, so putting an
5459
+ # un-profiled numerator over it reads about 8% and understates the share.
5460
+ # Either way the site is well under the threshold that would have required a
5461
+ # superset fix, which is why the design conclusion above stands. The
5462
+ # doctor's read is about 73% of the un-profiled total and this memo never
5463
+ # sees it.
5464
+ quota_observations = (
5465
+ capture.quota_observations
5466
+ if capture is not None
5467
+ else _cached_codex_quota_observations(
5468
+ source_root_keys=active_roots,
5469
+ cache_conn=context.cache_conn,
5470
+ stats_conn=context.stats_conn,
5471
+ stats_identity=context.stats_identity,
5472
+ # A key carrying the tick instant cannot match a retained entry, so the
5473
+ # memo is bypassed rather than asked. On the share path
5474
+ # `stats_identity` is None as well, and establishing an identity for an
5475
+ # unreachable key cost 35 ms of stats.db work per render.
5476
+ memoize=False,
5477
+ captured_at_or_after=(
5478
+ context.now_utc - dt.timedelta(days=DASHBOARD_QUOTA_RECENT_DAYS)
5479
+ ),
5480
+ active_at=context.now_utc,
5481
+ max_rows=DASHBOARD_QUOTA_OBSERVATION_LIMIT,
5482
+ )
5483
+ )
5484
+ projection_incoherent = (
5485
+ capture.projection_incoherent
5486
+ if capture is not None
5487
+ else not codex_projection_coherence(context).coherent
5488
+ )
5489
+ ingest_backlog = (
5490
+ capture.ingest_backlog
5491
+ if capture is not None
5492
+ else _codex_ingest_backlog_wire(context.cache_conn)
5493
+ )
5494
+ # The cache reader's established report surface treats the ``now`` instant
5495
+ # as inclusive. The qualified adapter is half-open, so extend only its
5496
+ # query/result boundary by one microsecond and keep all live budget sums
5497
+ # explicitly half-open at ``now`` below.
5498
+ accounting_end = (
5499
+ capture.accounting_end
5500
+ if capture is not None
5501
+ else context.now_utc + dt.timedelta(microseconds=1)
5502
+ )
5503
+ accounting_start = (
5504
+ capture.accounting_start
5505
+ if capture is not None
5506
+ else context.range_start
5507
+ )
5508
+ if capture is None and context.codex_budget is not None:
5509
+ # #556 S5 Unit 2 (Unit 1 review R6, widened) — this is the SECOND
5510
+ # unguarded call to the window resolver, and unlike
5511
+ # `_codex_budget_cost_events` it had no boundary of its own. It sits
5512
+ # outside every other `try` in this function, so an unresolvable window
5513
+ # escaped into `_tui_build_source_bundle`'s `source_build_failed`
5514
+ # handler and destroyed the entire Codex provider's data — the exact
5515
+ # failure §3.5 exists to prevent, reached from a different line.
5516
+ #
5517
+ # Degrading to the un-widened accounting range cannot publish a false
5518
+ # figure: the same failure reaches `_codex_budget_status_domain`, which
5519
+ # nulls the status and names `budget_compute_failed`.
5520
+ try:
5521
+ _period, budget_start, _budget_end = _configured_codex_budget_window(context)
5522
+ except Exception:
5523
+ _warn_codex_budget_window_once("accounting_range")
5524
+ else:
5525
+ accounting_start = min(accounting_start, budget_start)
5526
+ health = (
5527
+ capture.health
5528
+ if capture is not None
5529
+ else load_codex_project_metadata_health(
5530
+ cache_conn=context.cache_conn,
5531
+ start=accounting_start,
5532
+ end=accounting_end,
5533
+ )
5534
+ )
5535
+ metadata_incomplete = health.incomplete_rows > 0
5536
+ metadata_warning_message = (
5537
+ f"{health.incomplete_rows} Codex accounting row(s) lack project metadata; "
5538
+ "run `cctally cache-sync --source codex --rebuild`."
5539
+ if metadata_incomplete
5540
+ else "Codex project metadata could not be read; "
5541
+ "run `cctally cache-sync --source codex --rebuild`."
5542
+ )
5543
+ accounting_capture = (
5544
+ capture.accounting
5545
+ if capture is not None
5546
+ else _capture_codex_accounting(
5547
+ context,
5548
+ accounting_start=accounting_start,
5549
+ accounting_end=accounting_end,
5550
+ active_roots=active_roots,
5551
+ path_scope=path_scope,
5552
+ metadata_incomplete=metadata_incomplete,
5553
+ )
5554
+ )
5555
+ qualified_entries = accounting_capture.qualified_entries
5556
+ accounting_dirty_accounts = accounting_capture.dirty_accounts
5557
+ accounting_changed_old = accounting_capture.changed_old
5558
+ accounting_changed_new = accounting_capture.changed_new
5559
+ accounting_entries = accounting_capture.entries
5560
+ metadata_incomplete = accounting_capture.metadata_incomplete
4792
5561
  budget_entries = _codex_entries_from_accounting(accounting_entries)
4793
5562
  cycles_all: list[CodexCycleBoundary] = []
4794
5563
  try:
@@ -4806,29 +5575,43 @@ def _build_codex_source_state(
4806
5575
  # here leaves `cycle` unresolved, which `cycle_failure` below turns
4807
5576
  # into the hero failure the accounting axis actually reports.
4808
5577
  cycle = None
4809
- cycle_failure = cycle is None and has_cached_codex_accounting_entries(
4810
- cache_conn=context.cache_conn,
4811
- )
5578
+ if cycle is None and capture is not None:
5579
+ if capture.accounting_exists_error is not None:
5580
+ raise capture.accounting_exists_error
5581
+ accounting_exists = capture.accounting_exists
5582
+ elif cycle is None:
5583
+ accounting_exists = has_cached_codex_accounting_entries(
5584
+ cache_conn=context.cache_conn)
5585
+ else:
5586
+ accounting_exists = False
5587
+ cycle_failure = cycle is None and accounting_exists
4812
5588
  hero_failure = projection_incoherent or cycle_failure
4813
5589
  if cycle is None or hero_failure:
4814
5590
  cycle_entries: list[CodexEntry] = []
4815
5591
  cycle_cost_usd: float | None = None if hero_failure else 0.0
4816
5592
  else:
4817
5593
  cycle_end = min(accounting_end, cycle.resets_at)
4818
- cycle_rows = load_cached_rooted_codex_accounting_entries(
4819
- cycle.start_at,
4820
- cycle_end,
4821
- speed=context.speed,
4822
- cache_conn=context.cache_conn,
4823
- source_root_keys=cycle.source_root_keys,
5594
+ cycle_rows = (
5595
+ tuple(
5596
+ row for row in capture.card_population
5597
+ if cycle.start_at <= row.timestamp.astimezone(UTC) < cycle_end
5598
+ and str(row.source_root_key) in cycle.source_root_keys
5599
+ )
5600
+ if capture is not None
5601
+ else load_cached_rooted_codex_accounting_entries(
5602
+ cycle.start_at,
5603
+ cycle_end,
5604
+ speed=context.speed,
5605
+ cache_conn=context.cache_conn,
5606
+ source_root_keys=cycle.source_root_keys,
5607
+ )
4824
5608
  )
4825
- cycle_entries = _codex_entries_from_accounting(cycle_rows)
4826
- cycle_cost_usd = build_codex_daily_view(
4827
- cycle_entries,
5609
+ cycle_entries, cycle_cost_usd = _build_codex_cycle_accounting(
5610
+ cycle_rows,
4828
5611
  now_utc=context.now_utc,
4829
- tz_name=context.display_tz_name,
5612
+ display_tz_name=context.display_tz_name,
4830
5613
  speed=context.speed,
4831
- ).total_cost_usd
5614
+ )
4832
5615
  visible_accounting_entries = tuple(
4833
5616
  entry for entry in accounting_entries
4834
5617
  if context.range_start <= getattr(entry, "timestamp").astimezone(UTC) < accounting_end
@@ -4897,12 +5680,15 @@ def _build_codex_source_state(
4897
5680
  # R8 gate, resolved once before the parent weekly projection so that only
4898
5681
  # a decorated merged row gains the additive account axis. Focused children
4899
5682
  # and <=1-real-account providers remain byte-identical.
4900
- try:
4901
- import _cctally_account
4902
- _codex_decorated = _cctally_account.provider_is_decorated(
4903
- context.stats_conn, "codex")
4904
- except Exception:
4905
- _codex_decorated = False
5683
+ if capture is not None:
5684
+ _codex_decorated = capture.decorated
5685
+ else:
5686
+ try:
5687
+ import _cctally_account
5688
+ _codex_decorated = _cctally_account.provider_is_decorated(
5689
+ context.stats_conn, "codex")
5690
+ except Exception:
5691
+ _codex_decorated = False
4906
5692
  weekly = _cached_codex_native_weekly_view(
4907
5693
  context.stats_conn,
4908
5694
  visible_accounting_entries,
@@ -4936,6 +5722,8 @@ def _build_codex_source_state(
4936
5722
  quota_observations,
4937
5723
  accounting_entries=visible_accounting_entries,
4938
5724
  decorated=_codex_decorated,
5725
+ captured_breakdowns=(
5726
+ capture.breakdowns if capture is not None else None),
4939
5727
  )
4940
5728
  # R8 gate, resolved ONCE and threaded (#341 Task 4 / #416 §5.8). Every
4941
5729
  # per-account decoration below — block/alert/budget `account_key`, the
@@ -4974,7 +5762,11 @@ def _build_codex_source_state(
4974
5762
  context, budget_entries, cost_events=budget_cost_events,
4975
5763
  )
4976
5764
  configured_budget = configured_budget_domain["status"]
4977
- conversation_metadata = _codex_conversation_metadata(context.cache_conn)
5765
+ conversation_metadata = (
5766
+ capture.conversation_metadata
5767
+ if capture is not None
5768
+ else _codex_conversation_metadata(context.cache_conn)
5769
+ )
4978
5770
  cache_report = _codex_cache_report_wire(
4979
5771
  visible_accounting_entries,
4980
5772
  metadata=conversation_metadata,
@@ -5044,18 +5836,73 @@ def _build_codex_source_state(
5044
5836
  # projection (client-side chip filter); the hero renders per-account cards.
5045
5837
  accounts_wire: list[dict[str, object]] = []
5046
5838
  hero_cycles_wire: list[dict[str, object]] = []
5839
+ combined_accounting: dict[str, object] | None = None
5047
5840
  account_scopes: dict[str, dict[str, object]] = {}
5048
5841
  # #556 S5 §3.8: bound OUTSIDE the try, because the degrade path below has to
5049
5842
  # be able to clear it, and the retained `clock_data` reads it either way.
5050
5843
  budget_events_by_account: dict[str, tuple[tuple[dt.datetime, float], ...]] = {}
5051
5844
  if _codex_decorated:
5052
5845
  try:
5053
- accounts_wire, hero_cycles_wire = _codex_accounts_wire(
5846
+ # #583 S5 change 2: ONE population the cards are partitioned from,
5847
+ # in place of one accounting read per card. The PUBLISHED accounting
5848
+ # range is unchanged -- widening it would move the envelope.
5849
+ #
5850
+ # This derivation population starts earlier than the published range
5851
+ # on purpose. On the dashboard path `accounting_start` is already a
5852
+ # superset of every card's own range, because it is the 30-day
5853
+ # shared range and a live cycle's `start_at` is inside
5854
+ # `(now - 7d, now)` by the two filters in
5855
+ # `_resolve_codex_weekly_cycle`. On the SHARE path it is not:
5856
+ # `bin/_cctally_dashboard_share.py` passes a user-selected
5857
+ # `range_start` verbatim, so a one-day custom share would truncate a
5858
+ # card whose cycle began six days earlier -- silently, with no error
5859
+ # and no moved golden.
5860
+ card_population_start = min(
5861
+ context.range_start,
5862
+ context.now_utc - dt.timedelta(
5863
+ minutes=ACCOUNT_WEEKLY_WINDOW_MINUTES),
5864
+ accounting_start,
5865
+ )
5866
+ if capture is not None:
5867
+ card_population = capture.card_population
5868
+ elif card_population_start == accounting_start:
5869
+ # The build already holds this exact population, so reading it
5870
+ # again would be one extra full-range pass over every retained
5871
+ # Codex accounting row, per tick. The equality is the ordinary
5872
+ # dashboard case rather than an edge: `card_population_start` is
5873
+ # a `min` that includes `accounting_start`, so it can only be
5874
+ # earlier or equal, and it is equal whenever `accounting_start`
5875
+ # is already at or before both `range_start` and `now - 7d` --
5876
+ # which the 30-day shared range always is. Only a custom share
5877
+ # narrower than one native cycle takes the other branch.
5878
+ #
5879
+ # `accounting_entries` is `QualifiedCodexEntry` on the normal
5880
+ # path and `RootedCodexAccountingEntry` on the
5881
+ # `metadata_incomplete` fallback. Both carry the three
5882
+ # attributes `_slice` reads (`account_key`, `timestamp`,
5883
+ # `source_root_key`), both normalize a NULL or empty stored
5884
+ # account to the sentinel, both price through
5885
+ # `_calculate_codex_entry_cost` at this `speed`, and both are
5886
+ # ordered by (timestamp, source_root_key, conversation_key, id)
5887
+ # over the same half-open range -- the qualified read adds only
5888
+ # a LEFT JOIN onto `codex_conversation_threads`, whose
5889
+ # `conversation_key` is that table's PRIMARY KEY and so cannot
5890
+ # duplicate a row.
5891
+ card_population = accounting_entries
5892
+ else:
5893
+ card_population = load_cached_rooted_codex_accounting_entries(
5894
+ card_population_start,
5895
+ accounting_end,
5896
+ speed=context.speed,
5897
+ cache_conn=context.cache_conn,
5898
+ )
5899
+ accounts_wire, hero_cycles_wire, combined_accounting = _codex_accounts_wire(
5054
5900
  context,
5055
5901
  quota_observations=quota_observations,
5056
5902
  cycles=cycles_all,
5057
5903
  accounting_start=accounting_start,
5058
5904
  accounting_end=accounting_end,
5905
+ population=card_population,
5059
5906
  )
5060
5907
  # #416 §5.3: the per-account CHILDREN beside the merged parent. The
5061
5908
  # scope set is exactly the card set, so every chip the client can
@@ -5103,6 +5950,8 @@ def _build_codex_source_state(
5103
5950
  changed_new_by_account=changed_new_by_account,
5104
5951
  changed_old_rows_by_account=changed_old_rows_by_account,
5105
5952
  changed_new_rows_by_account=changed_new_rows_by_account,
5953
+ captured_breakdowns=(
5954
+ capture.breakdowns if capture is not None else None),
5106
5955
  # Quota/stat generations are already represented by each
5107
5956
  # child's quota observations, cycle and alert/budget rows in
5108
5957
  # `_codex_account_scopes_wire`'s outer signature. Reuse the
@@ -5170,6 +6019,12 @@ def _build_codex_source_state(
5170
6019
  # degrade to the byte-stable undecorated shape.
5171
6020
  accounts_wire = []
5172
6021
  hero_cycles_wire = []
6022
+ combined_accounting = {
6023
+ "scope": "account_cycles",
6024
+ "status": "unresolved",
6025
+ "cause": "account_cost_unresolved",
6026
+ "contributions": (),
6027
+ }
5173
6028
  account_scopes = {}
5174
6029
  budget_events_by_account = {}
5175
6030
  # #416 QA P0-A — the "All accounts" headline is the MERGED spend and tokens
@@ -5206,7 +6061,9 @@ def _build_codex_source_state(
5206
6061
  # observation age and weekly-cycle evidence live on their own axes.
5207
6062
  freshness="fresh",
5208
6063
  warnings=tuple(warnings),
5209
- data_version=data_version,
6064
+ data_version=combined_accounting_version(
6065
+ data_version, combined_accounting,
6066
+ ),
5210
6067
  last_success_at=context.now_utc,
5211
6068
  capabilities={
5212
6069
  "hero": (
@@ -5305,8 +6162,8 @@ def _build_codex_source_state(
5305
6162
  # while nothing about the accounting changed. The percent age is
5306
6163
  # already carried by `quota` below and by the additive hero-local
5307
6164
  # `cycle_freshness` field. `_resolve_codex_weekly_cycle` retains
5308
- # only boundaries with `resets_at > now`, so a resolved cycle is
5309
- # never expired at build time; the idle clock owns expiry.
6165
+ # only boundaries with `start_at < now < resets_at`, so a resolved
6166
+ # cycle is never expired at build time; the idle clock owns expiry.
5310
6167
  "hero": "stale" if hero_failure else "fresh",
5311
6168
  "quota": (
5312
6169
  "stale"
@@ -5330,4 +6187,5 @@ def _build_codex_source_state(
5330
6187
  ),
5331
6188
  },
5332
6189
  private_session_labels=private_session_labels,
6190
+ combined_accounting=combined_accounting,
5333
6191
  )