cctally 1.100.0 → 1.102.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 (53) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +8 -2
  3. package/bin/_cctally_alerts.py +13 -2
  4. package/bin/_cctally_cache.py +3 -1
  5. package/bin/_cctally_cache_report.py +103 -6
  6. package/bin/_cctally_dashboard.py +1140 -282
  7. package/bin/_cctally_dashboard_conversation.py +12 -0
  8. package/bin/_cctally_dashboard_envelope.py +53 -61
  9. package/bin/_cctally_dashboard_share.py +101 -29
  10. package/bin/_cctally_dashboard_sources.py +663 -192
  11. package/bin/_cctally_diagnosis.py +1172 -0
  12. package/bin/_cctally_diagnosis_sources.py +4054 -0
  13. package/bin/_cctally_diff.py +20 -0
  14. package/bin/_cctally_forecast.py +329 -111
  15. package/bin/_cctally_milestone_history.py +10 -2
  16. package/bin/_cctally_parser.py +84 -0
  17. package/bin/_cctally_project.py +155 -47
  18. package/bin/_cctally_quota.py +14 -0
  19. package/bin/_cctally_record.py +151 -71
  20. package/bin/_cctally_refresh.py +105 -93
  21. package/bin/_cctally_share.py +9 -2
  22. package/bin/_cctally_source_analytics.py +40 -4
  23. package/bin/_cctally_statusline.py +8 -1
  24. package/bin/_cctally_tui.py +425 -234
  25. package/bin/_lib_alert_scope.py +685 -0
  26. package/bin/_lib_alerts_payload.py +112 -7
  27. package/bin/_lib_blocks.py +12 -0
  28. package/bin/_lib_cache_report.py +110 -1
  29. package/bin/_lib_codex_conversation.py +14 -0
  30. package/bin/_lib_codex_conversation_query.py +22 -8
  31. package/bin/_lib_codex_pools.py +20 -8
  32. package/bin/_lib_conversation.py +6 -3
  33. package/bin/_lib_conversation_query.py +256 -69
  34. package/bin/_lib_dashboard_sources.py +212 -24
  35. package/bin/_lib_diagnosis.py +1261 -0
  36. package/bin/_lib_forecast.py +62 -4
  37. package/bin/_lib_perf.py +12 -0
  38. package/bin/_lib_pricing.py +8 -7
  39. package/bin/_lib_readme_refresh.py +26 -5
  40. package/bin/_lib_render.py +31 -3
  41. package/bin/_lib_share_templates.py +150 -55
  42. package/bin/_lib_snapshot_cache.py +71 -13
  43. package/bin/_lib_source_identity.py +50 -2
  44. package/bin/_lib_subscription_weeks.py +65 -0
  45. package/bin/cctally +103 -16
  46. package/bin/cctally-explain +5 -0
  47. package/dashboard/static/assets/dashboardStream.shared-worker-1XTMV3nr.js +1 -0
  48. package/dashboard/static/assets/index-Di2hljvB.css +1 -0
  49. package/dashboard/static/assets/index-XYCIWjVG.js +97 -0
  50. package/dashboard/static/dashboard.html +2 -2
  51. package/package.json +6 -1
  52. package/dashboard/static/assets/index-B5YfQEtn.css +0 -1
  53. package/dashboard/static/assets/index-Bt59nMMO.js +0 -97
@@ -42,6 +42,7 @@ from _lib_dashboard_sources import (
42
42
  canonical_alerted_at,
43
43
  canonical_alerted_at_sql,
44
44
  claude_stats_digest,
45
+ combined_accounting_version,
45
46
  codex_stats_digest,
46
47
  dashboard_resource_key,
47
48
  )
@@ -72,6 +73,7 @@ from _lib_source_analytics import (
72
73
  build_codex_project_result,
73
74
  collision_safe_project_label_map,
74
75
  )
76
+ from _lib_source_identity import identity_path
75
77
  from _lib_view_models import (
76
78
  CodexWeeklyView,
77
79
  build_codex_daily_view,
@@ -2258,7 +2260,9 @@ def _session_wire(
2258
2260
  # session paths from distinct $CODEX_HOME roots. The opaque detail
2259
2261
  # key must use that same grouping identity or two visible rows route
2260
2262
  # to one another's detail payload.
2261
- 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
+ )
2262
2266
  row_metadata = (metadata or {}).get((str(row.codex_root or ""), str(row.session_id_path)))
2263
2267
  if row_metadata is None and metadata is not None:
2264
2268
  row_metadata = next((
@@ -2705,6 +2709,86 @@ def _codex_account_admits(scope_key: str | None, row_key: object) -> bool:
2705
2709
  return key in (scope_key, _lib_accounts.UNATTRIBUTED)
2706
2710
 
2707
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
+
2708
2792
  def _quota_read_model(
2709
2793
  context: DashboardReadContext,
2710
2794
  observations: Iterable[object],
@@ -2712,6 +2796,7 @@ def _quota_read_model(
2712
2796
  accounting_entries: Iterable[object] = (),
2713
2797
  account_key: str | None = None,
2714
2798
  decorated: bool,
2799
+ captured_breakdowns: "Mapping[tuple[object, ...], tuple[tuple, tuple]] | None" = None,
2715
2800
  ) -> dict[str, object]:
2716
2801
  """Use S2's pure history/block/forecast kernels over cache evidence.
2717
2802
 
@@ -2824,43 +2909,52 @@ def _quota_read_model(
2824
2909
  < block.resets_at
2825
2910
  )
2826
2911
  canonical_rows = ()
2912
+ correlated_five_hour = ()
2827
2913
  if identity.window_minutes == 10_080 and block.resets_at > context.now_utc:
2828
- try:
2829
- canonical_rows = codex_quota_breakdown(
2830
- identity,
2831
- block.resets_at,
2832
- speed=context.speed,
2833
- cache_conn=context.cache_conn,
2834
- stats_conn=context.stats_conn,
2835
- 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
+ ((), ()),
2836
2919
  )
2837
- except sqlite3.Error:
2838
- # Older or partially migrated stores retain the bounded
2839
- # observation-derived fallback below. A coherent current
2840
- # store always has the durable projection used by the CLI.
2841
- canonical_rows = ()
2842
- if canonical_rows:
2843
- try:
2844
- # #416 Slice 3A review F4: this load is bounded by root, slot
2845
- # and `limit_id` only, so under focus the crossing was annotated
2846
- # with whichever ACCOUNT's 5h observation happened to sort last.
2847
- correlated_five_hour = tuple(
2848
- observation
2849
- for observation in _cached_codex_quota_observations(
2850
- 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,
2851
2926
  cache_conn=context.cache_conn,
2852
2927
  stats_conn=context.stats_conn,
2853
- stats_identity=context.stats_identity,
2854
- captured_at_or_after=block.nominal_start_at,
2928
+ account_key=account_key,
2855
2929
  )
2856
- if observation.identity.window_minutes == 300
2857
- and observation.identity.observed_slot == identity.observed_slot
2858
- and observation.identity.limit_id == identity.limit_id
2859
- and _codex_account_admits(
2860
- account_key, observation.identity.account_key)
2861
- )
2862
- except sqlite3.Error:
2863
- 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 = ()
2864
2958
 
2865
2959
  for row in canonical_rows:
2866
2960
  milestone_rows.append({
@@ -3359,6 +3453,7 @@ def refresh_codex_source_clock(
3359
3453
  # omission silently drops the authoritative account count and makes the
3360
3454
  # combined figure fail closed on an idle tick that changed nothing else.
3361
3455
  account_scope=state.account_scope,
3456
+ combined_accounting=state.combined_accounting,
3362
3457
  private_session_labels=state.private_session_labels,
3363
3458
  # #556 S2 §3.6: the aggregate carrier travels with the rows it
3364
3459
  # describes. This clock refreshes presentation axes only and publishes
@@ -4103,8 +4198,10 @@ def _codex_accounts_wire(
4103
4198
  accounting_start: dt.datetime,
4104
4199
  accounting_end: dt.datetime,
4105
4200
  population: tuple[object, ...],
4106
- ) -> tuple[list[dict[str, object]], list[dict[str, object]]]:
4107
- """Return ``(accounts_wire, hero_cycles_wire)`` for a decorated Codex source.
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.
4108
4205
 
4109
4206
  ``population`` is the merged, encounter-ordered accounting read every card
4110
4207
  is derived from (#583 S5 change 2). This function issues no accounting SQL
@@ -4270,6 +4367,7 @@ def _codex_accounts_wire(
4270
4367
 
4271
4368
  accounts_wire: list[dict[str, object]] = []
4272
4369
  hero_cycles_wire: list[dict[str, object]] = []
4370
+ contributions: list[dict[str, object]] = []
4273
4371
  for key in ordered_keys:
4274
4372
  cyc = cycle_by_account.get(key)
4275
4373
  is_unattributed = key == _lib_accounts.UNATTRIBUTED
@@ -4319,6 +4417,23 @@ def _codex_accounts_wire(
4319
4417
  # which is true of several unrelated states (#564 D3).
4320
4418
  card["spendWindow"] = fallback_window
4321
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
+ })
4322
4437
  if cyc is not None and not is_unattributed:
4323
4438
  hero_cycles_wire.append({
4324
4439
  "accountKey": key,
@@ -4329,7 +4444,11 @@ def _codex_accounts_wire(
4329
4444
  "cost_usd": totals["spendUsd"],
4330
4445
  "total_tokens": totals["totalTokens"],
4331
4446
  })
4332
- 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
+ }
4333
4452
 
4334
4453
 
4335
4454
  def _codex_partition_by_account(
@@ -4447,6 +4566,7 @@ def _codex_account_scopes_wire(
4447
4566
  changed_new_by_account: Mapping[str, tuple[CodexEntry, ...]] | None = None,
4448
4567
  changed_old_rows_by_account: Mapping[str, tuple[object, ...]] | None = None,
4449
4568
  changed_new_rows_by_account: Mapping[str, tuple[object, ...]] | None = None,
4569
+ captured_breakdowns: "Mapping[tuple[object, ...], tuple[tuple, tuple]] | None" = None,
4450
4570
  ) -> dict[str, dict[str, object]]:
4451
4571
  """The per-account CHILDREN of the merged Codex read model (spec §5.3).
4452
4572
 
@@ -4526,6 +4646,7 @@ def _codex_account_scopes_wire(
4526
4646
  # #429 §4.4: this wire is only ever reached under decoration — the
4527
4647
  # caller gates the whole `account_scopes` surface on it.
4528
4648
  decorated=True,
4649
+ captured_breakdowns=captured_breakdowns,
4529
4650
  )
4530
4651
  # Each decorated child owns the only honest cycle index for that
4531
4652
  # account. Reusing a merged parent index here would render account A's
@@ -4755,7 +4876,7 @@ def _claude_accounts_wire(
4755
4876
  stats_conn: sqlite3.Connection,
4756
4877
  *,
4757
4878
  now_utc: dt.datetime,
4758
- ) -> list[dict[str, object]]:
4879
+ ) -> tuple[list[dict[str, object]], dict[str, object]]:
4759
4880
  """Per-account Claude hero cards (#341 Task 4, Ruling C).
4760
4881
 
4761
4882
  Symmetric with ``_codex_accounts_wire``: the caller gates on
@@ -4784,13 +4905,38 @@ def _claude_accounts_wire(
4784
4905
  (key,),
4785
4906
  ).fetchone()
4786
4907
 
4787
- def _latest_cost(key: str) -> float:
4908
+ def _latest_cost(key: str):
4788
4909
  row = stats_conn.execute(
4789
- "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=? "
4790
4912
  "ORDER BY captured_at_utc DESC LIMIT 1",
4791
4913
  (key,),
4792
4914
  ).fetchone()
4793
- 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
4794
4940
 
4795
4941
  _claude_label_map = _cctally_account.display_label_map(stats_conn, "claude")
4796
4942
 
@@ -4804,12 +4950,20 @@ def _claude_accounts_wire(
4804
4950
  ordered_keys.append(_lib_accounts.UNATTRIBUTED)
4805
4951
 
4806
4952
  cards: list[dict[str, object]] = []
4953
+ contributions: list[dict[str, object]] = []
4954
+ unresolved_causes: set[str] = set()
4807
4955
  for key in ordered_keys:
4808
4956
  is_unattributed = key == _lib_accounts.UNATTRIBUTED
4809
4957
  usage = _latest_usage(key)
4810
4958
  weekly_pct = usage[0] if usage is not None else None
4811
4959
  five_hour_pct = usage[1] if usage is not None else None
4812
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)
4813
4967
  card: dict[str, object] = {
4814
4968
  "accountKey": key,
4815
4969
  "label": _claude_label_map.get(key) or _cctally_account.account_label(stats_conn, key),
@@ -4818,12 +4972,30 @@ def _claude_accounts_wire(
4818
4972
  "weeklyPercent": None if is_unattributed else weekly_pct,
4819
4973
  "fiveHourPercent": None if is_unattributed else five_hour_pct,
4820
4974
  "resetsAt": None if is_unattributed else resets_at,
4821
- "spendUsd": _latest_cost(key),
4975
+ "spendUsd": spend_usd,
4822
4976
  }
4823
4977
  if is_unattributed:
4824
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
+ }
4825
4986
  cards.append(card)
4826
- 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
4827
4999
 
4828
5000
 
4829
5001
  def _codex_ingest_backlog_wire(
@@ -4904,6 +5076,297 @@ def reset_codex_source_caches() -> None:
4904
5076
  _lib_snapshot_cache.reset_codex_accounting_cache_state()
4905
5077
 
4906
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(
5090
+ context: DashboardReadContext,
5091
+ *,
5092
+ accounting_start: dt.datetime,
5093
+ accounting_end: dt.datetime,
5094
+ active_roots: tuple[str, ...],
5095
+ path_scope: object,
5096
+ metadata_incomplete: bool,
5097
+ ) -> _CodexAccountingCapture:
5098
+ """Read the incremental accounting carrier without folding public views."""
5099
+ qualified_entries: tuple[object, ...] = ()
5100
+ dirty_accounts: tuple[str, ...] = ()
5101
+ changed_old: tuple[object, ...] = ()
5102
+ changed_new: tuple[object, ...] = ()
5103
+ if not metadata_incomplete:
5104
+ try:
5105
+ cached_accounting = _lib_snapshot_cache.build_cached_codex_accounting(
5106
+ cache_conn=context.cache_conn,
5107
+ range_start=accounting_start,
5108
+ range_end=accounting_end,
5109
+ extra_signature=(
5110
+ context.speed,
5111
+ tuple(str(root) for root in path_scope.roots),
5112
+ active_roots,
5113
+ ),
5114
+ load_all=lambda: load_qualified_codex_entries(
5115
+ accounting_start,
5116
+ accounting_end,
5117
+ speed=context.speed,
5118
+ sync=False,
5119
+ cache_conn=context.cache_conn,
5120
+ ),
5121
+ load_paths=lambda identities: load_qualified_codex_entries(
5122
+ accounting_start,
5123
+ accounting_end,
5124
+ speed=context.speed,
5125
+ sync=False,
5126
+ cache_conn=context.cache_conn,
5127
+ source_identities=identities,
5128
+ ),
5129
+ path_of=lambda entry: (
5130
+ str(entry.source_root_key), str(entry.source_path),
5131
+ ),
5132
+ account_of=lambda entry: str(entry.account_key),
5133
+ order_key=lambda entry: (
5134
+ entry.timestamp,
5135
+ str(entry.source_root_key),
5136
+ str(entry.conversation_key),
5137
+ int(entry.cache_entry_id),
5138
+ ),
5139
+ identity_of=lambda entry: int(entry.cache_entry_id),
5140
+ )
5141
+ qualified_entries = cached_accounting.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
5146
+ except QualifiedMetadataUnavailable:
5147
+ _lib_log.get_logger("dashboard").warning(
5148
+ "Codex qualified metadata read became unavailable; using cache-only accounting fallback"
5149
+ )
5150
+ metadata_incomplete = True
5151
+ _lib_snapshot_cache.reset_codex_accounting_cache_state()
5152
+ entries = load_cached_rooted_codex_accounting_entries(
5153
+ accounting_start,
5154
+ accounting_end,
5155
+ speed=context.speed,
5156
+ cache_conn=context.cache_conn,
5157
+ )
5158
+ dirty_accounts = tuple(sorted({
5159
+ str(getattr(entry, "account_key", "") or
5160
+ _lib_accounts.UNATTRIBUTED)
5161
+ for entry in entries
5162
+ }))
5163
+ else:
5164
+ _lib_snapshot_cache.reset_codex_accounting_cache_state()
5165
+ entries = load_cached_rooted_codex_accounting_entries(
5166
+ accounting_start,
5167
+ accounting_end,
5168
+ speed=context.speed,
5169
+ cache_conn=context.cache_conn,
5170
+ )
5171
+ dirty_accounts = tuple(sorted({
5172
+ str(getattr(entry, "account_key", "") or
5173
+ _lib_accounts.UNATTRIBUTED)
5174
+ for entry in entries
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
+
4907
5370
  def build_codex_source_state(
4908
5371
  context: DashboardReadContext,
4909
5372
  *,
@@ -4927,23 +5390,29 @@ def build_codex_source_state(
4927
5390
  # `_codex_quota_reuse_identity`. The clear that used to stand here existed
4928
5391
  # because a decoration or account-registry change advances no cache.db leg
4929
5392
  # and so could not move a cache-only key. Do not narrow that identity.
4930
- caches = _codex_source_caches()
4931
- cache_checkpoint = tuple(dict(cache) for cache in caches)
4932
- accounting_checkpoint = (
4933
- _lib_snapshot_cache.checkpoint_codex_accounting_cache_state()
4934
- )
4935
- try:
4936
- with codex_path_scope() as path_scope:
4937
- return _build_codex_source_state(
4938
- context, data_version=data_version, path_scope=path_scope,
4939
- )
4940
- except Exception:
4941
- for cache, prior in zip(caches, cache_checkpoint):
4942
- cache.clear()
4943
- cache.update(prior)
4944
- _lib_snapshot_cache.restore_codex_accounting_cache_state(
4945
- accounting_checkpoint)
4946
- raise
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
4947
5416
 
4948
5417
 
4949
5418
  def _build_codex_source_state(
@@ -4951,12 +5420,17 @@ def _build_codex_source_state(
4951
5420
  *,
4952
5421
  data_version: str,
4953
5422
  path_scope: object,
5423
+ capture: "CodexSourceCapture | None" = None,
4954
5424
  ) -> SourceDashboardState:
4955
- active_roots = tuple(sorted(
4956
- str(row[0]) for row in context.cache_conn.execute(
4957
- "SELECT source_root_key FROM codex_source_roots"
4958
- )
4959
- ))
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
+ )
4960
5434
  # This read is a function of the tick instant, so it does NOT reuse across
4961
5435
  # ticks and the cross-build memo above cannot help it. Both bounds move
4962
5436
  # with `context.now_utc`, which in production is the tick's wall clock
@@ -4987,34 +5461,51 @@ def _build_codex_source_state(
4987
5461
  # superset fix, which is why the design conclusion above stands. The
4988
5462
  # doctor's read is about 73% of the un-profiled total and this memo never
4989
5463
  # sees it.
4990
- quota_observations = _cached_codex_quota_observations(
4991
- source_root_keys=active_roots,
4992
- cache_conn=context.cache_conn,
4993
- stats_conn=context.stats_conn,
4994
- stats_identity=context.stats_identity,
4995
- # A key carrying the tick instant cannot match a retained entry, so the
4996
- # memo is bypassed rather than asked. On the share path
4997
- # `stats_identity` is None as well, and establishing an identity for an
4998
- # unreachable key cost 35 ms of stats.db work per render.
4999
- memoize=False,
5000
- captured_at_or_after=(
5001
- context.now_utc - dt.timedelta(days=DASHBOARD_QUOTA_RECENT_DAYS)
5002
- ),
5003
- active_at=context.now_utc,
5004
- max_rows=DASHBOARD_QUOTA_OBSERVATION_LIMIT,
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
+ )
5005
5483
  )
5006
- coherence = codex_projection_coherence(
5007
- context,
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)
5008
5493
  )
5009
- projection_incoherent = not coherence.coherent
5010
- ingest_backlog = _codex_ingest_backlog_wire(context.cache_conn)
5011
5494
  # The cache reader's established report surface treats the ``now`` instant
5012
5495
  # as inclusive. The qualified adapter is half-open, so extend only its
5013
5496
  # query/result boundary by one microsecond and keep all live budget sums
5014
5497
  # explicitly half-open at ``now`` below.
5015
- accounting_end = context.now_utc + dt.timedelta(microseconds=1)
5016
- accounting_start = context.range_start
5017
- if context.codex_budget is not None:
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:
5018
5509
  # #556 S5 Unit 2 (Unit 1 review R6, widened) — this is the SECOND
5019
5510
  # unguarded call to the window resolver, and unlike
5020
5511
  # `_codex_budget_cost_events` it had no boundary of its own. It sits
@@ -5032,10 +5523,14 @@ def _build_codex_source_state(
5032
5523
  _warn_codex_budget_window_once("accounting_range")
5033
5524
  else:
5034
5525
  accounting_start = min(accounting_start, budget_start)
5035
- health = load_codex_project_metadata_health(
5036
- cache_conn=context.cache_conn,
5037
- start=accounting_start,
5038
- end=accounting_end,
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
+ )
5039
5534
  )
5040
5535
  metadata_incomplete = health.incomplete_rows > 0
5041
5536
  metadata_warning_message = (
@@ -5045,85 +5540,24 @@ def _build_codex_source_state(
5045
5540
  else "Codex project metadata could not be read; "
5046
5541
  "run `cctally cache-sync --source codex --rebuild`."
5047
5542
  )
5048
- qualified_entries: tuple[object, ...] = ()
5049
- accounting_dirty_accounts: tuple[str, ...] = ()
5050
- accounting_changed_old: tuple[object, ...] = ()
5051
- accounting_changed_new: tuple[object, ...] = ()
5052
- if not metadata_incomplete:
5053
- try:
5054
- cached_accounting = _lib_snapshot_cache.build_cached_codex_accounting(
5055
- cache_conn=context.cache_conn,
5056
- range_start=accounting_start,
5057
- range_end=accounting_end,
5058
- extra_signature=(
5059
- context.speed,
5060
- tuple(str(root) for root in path_scope.roots),
5061
- active_roots,
5062
- ),
5063
- load_all=lambda: load_qualified_codex_entries(
5064
- accounting_start,
5065
- accounting_end,
5066
- speed=context.speed,
5067
- sync=False,
5068
- cache_conn=context.cache_conn,
5069
- ),
5070
- load_paths=lambda identities: load_qualified_codex_entries(
5071
- accounting_start,
5072
- accounting_end,
5073
- speed=context.speed,
5074
- sync=False,
5075
- cache_conn=context.cache_conn,
5076
- source_identities=identities,
5077
- ),
5078
- path_of=lambda entry: (
5079
- str(entry.source_root_key), str(entry.source_path),
5080
- ),
5081
- account_of=lambda entry: str(entry.account_key),
5082
- order_key=lambda entry: (
5083
- entry.timestamp,
5084
- str(entry.source_root_key),
5085
- str(entry.conversation_key),
5086
- int(entry.cache_entry_id),
5087
- ),
5088
- identity_of=lambda entry: int(entry.cache_entry_id),
5089
- )
5090
- qualified_entries = cached_accounting.entries
5091
- accounting_dirty_accounts = cached_accounting.dirty_accounts
5092
- accounting_changed_old = cached_accounting.changed_old
5093
- accounting_changed_new = cached_accounting.changed_new
5094
- accounting_entries: tuple[object, ...] = qualified_entries
5095
- except QualifiedMetadataUnavailable:
5096
- # A cached read must be internally coherent, but retain accounting
5097
- # once if a defensive race or malformed row violates that premise.
5098
- _lib_log.get_logger("dashboard").warning(
5099
- "Codex qualified metadata read became unavailable; using cache-only accounting fallback"
5100
- )
5101
- metadata_incomplete = True
5102
- _lib_snapshot_cache.reset_codex_accounting_cache_state()
5103
- accounting_entries = load_cached_rooted_codex_accounting_entries(
5104
- accounting_start,
5105
- accounting_end,
5106
- speed=context.speed,
5107
- cache_conn=context.cache_conn,
5108
- )
5109
- accounting_dirty_accounts = tuple(sorted({
5110
- str(getattr(entry, "account_key", "") or
5111
- _lib_accounts.UNATTRIBUTED)
5112
- for entry in accounting_entries
5113
- }))
5114
- else:
5115
- _lib_snapshot_cache.reset_codex_accounting_cache_state()
5116
- accounting_entries = load_cached_rooted_codex_accounting_entries(
5117
- accounting_start,
5118
- accounting_end,
5119
- speed=context.speed,
5120
- cache_conn=context.cache_conn,
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,
5121
5553
  )
5122
- accounting_dirty_accounts = tuple(sorted({
5123
- str(getattr(entry, "account_key", "") or
5124
- _lib_accounts.UNATTRIBUTED)
5125
- for entry in accounting_entries
5126
- }))
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
5127
5561
  budget_entries = _codex_entries_from_accounting(accounting_entries)
5128
5562
  cycles_all: list[CodexCycleBoundary] = []
5129
5563
  try:
@@ -5141,29 +5575,43 @@ def _build_codex_source_state(
5141
5575
  # here leaves `cycle` unresolved, which `cycle_failure` below turns
5142
5576
  # into the hero failure the accounting axis actually reports.
5143
5577
  cycle = None
5144
- cycle_failure = cycle is None and has_cached_codex_accounting_entries(
5145
- cache_conn=context.cache_conn,
5146
- )
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
5147
5588
  hero_failure = projection_incoherent or cycle_failure
5148
5589
  if cycle is None or hero_failure:
5149
5590
  cycle_entries: list[CodexEntry] = []
5150
5591
  cycle_cost_usd: float | None = None if hero_failure else 0.0
5151
5592
  else:
5152
5593
  cycle_end = min(accounting_end, cycle.resets_at)
5153
- cycle_rows = load_cached_rooted_codex_accounting_entries(
5154
- cycle.start_at,
5155
- cycle_end,
5156
- speed=context.speed,
5157
- cache_conn=context.cache_conn,
5158
- 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
+ )
5159
5608
  )
5160
- cycle_entries = _codex_entries_from_accounting(cycle_rows)
5161
- cycle_cost_usd = build_codex_daily_view(
5162
- cycle_entries,
5609
+ cycle_entries, cycle_cost_usd = _build_codex_cycle_accounting(
5610
+ cycle_rows,
5163
5611
  now_utc=context.now_utc,
5164
- tz_name=context.display_tz_name,
5612
+ display_tz_name=context.display_tz_name,
5165
5613
  speed=context.speed,
5166
- ).total_cost_usd
5614
+ )
5167
5615
  visible_accounting_entries = tuple(
5168
5616
  entry for entry in accounting_entries
5169
5617
  if context.range_start <= getattr(entry, "timestamp").astimezone(UTC) < accounting_end
@@ -5232,12 +5680,15 @@ def _build_codex_source_state(
5232
5680
  # R8 gate, resolved once before the parent weekly projection so that only
5233
5681
  # a decorated merged row gains the additive account axis. Focused children
5234
5682
  # and <=1-real-account providers remain byte-identical.
5235
- try:
5236
- import _cctally_account
5237
- _codex_decorated = _cctally_account.provider_is_decorated(
5238
- context.stats_conn, "codex")
5239
- except Exception:
5240
- _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
5241
5692
  weekly = _cached_codex_native_weekly_view(
5242
5693
  context.stats_conn,
5243
5694
  visible_accounting_entries,
@@ -5271,6 +5722,8 @@ def _build_codex_source_state(
5271
5722
  quota_observations,
5272
5723
  accounting_entries=visible_accounting_entries,
5273
5724
  decorated=_codex_decorated,
5725
+ captured_breakdowns=(
5726
+ capture.breakdowns if capture is not None else None),
5274
5727
  )
5275
5728
  # R8 gate, resolved ONCE and threaded (#341 Task 4 / #416 §5.8). Every
5276
5729
  # per-account decoration below — block/alert/budget `account_key`, the
@@ -5309,7 +5762,11 @@ def _build_codex_source_state(
5309
5762
  context, budget_entries, cost_events=budget_cost_events,
5310
5763
  )
5311
5764
  configured_budget = configured_budget_domain["status"]
5312
- 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
+ )
5313
5770
  cache_report = _codex_cache_report_wire(
5314
5771
  visible_accounting_entries,
5315
5772
  metadata=conversation_metadata,
@@ -5379,6 +5836,7 @@ def _build_codex_source_state(
5379
5836
  # projection (client-side chip filter); the hero renders per-account cards.
5380
5837
  accounts_wire: list[dict[str, object]] = []
5381
5838
  hero_cycles_wire: list[dict[str, object]] = []
5839
+ combined_accounting: dict[str, object] | None = None
5382
5840
  account_scopes: dict[str, dict[str, object]] = {}
5383
5841
  # #556 S5 §3.8: bound OUTSIDE the try, because the degrade path below has to
5384
5842
  # be able to clear it, and the retained `clock_data` reads it either way.
@@ -5405,7 +5863,9 @@ def _build_codex_source_state(
5405
5863
  minutes=ACCOUNT_WEEKLY_WINDOW_MINUTES),
5406
5864
  accounting_start,
5407
5865
  )
5408
- if card_population_start == accounting_start:
5866
+ if capture is not None:
5867
+ card_population = capture.card_population
5868
+ elif card_population_start == accounting_start:
5409
5869
  # The build already holds this exact population, so reading it
5410
5870
  # again would be one extra full-range pass over every retained
5411
5871
  # Codex accounting row, per tick. The equality is the ordinary
@@ -5436,7 +5896,7 @@ def _build_codex_source_state(
5436
5896
  speed=context.speed,
5437
5897
  cache_conn=context.cache_conn,
5438
5898
  )
5439
- accounts_wire, hero_cycles_wire = _codex_accounts_wire(
5899
+ accounts_wire, hero_cycles_wire, combined_accounting = _codex_accounts_wire(
5440
5900
  context,
5441
5901
  quota_observations=quota_observations,
5442
5902
  cycles=cycles_all,
@@ -5490,6 +5950,8 @@ def _build_codex_source_state(
5490
5950
  changed_new_by_account=changed_new_by_account,
5491
5951
  changed_old_rows_by_account=changed_old_rows_by_account,
5492
5952
  changed_new_rows_by_account=changed_new_rows_by_account,
5953
+ captured_breakdowns=(
5954
+ capture.breakdowns if capture is not None else None),
5493
5955
  # Quota/stat generations are already represented by each
5494
5956
  # child's quota observations, cycle and alert/budget rows in
5495
5957
  # `_codex_account_scopes_wire`'s outer signature. Reuse the
@@ -5557,6 +6019,12 @@ def _build_codex_source_state(
5557
6019
  # degrade to the byte-stable undecorated shape.
5558
6020
  accounts_wire = []
5559
6021
  hero_cycles_wire = []
6022
+ combined_accounting = {
6023
+ "scope": "account_cycles",
6024
+ "status": "unresolved",
6025
+ "cause": "account_cost_unresolved",
6026
+ "contributions": (),
6027
+ }
5560
6028
  account_scopes = {}
5561
6029
  budget_events_by_account = {}
5562
6030
  # #416 QA P0-A — the "All accounts" headline is the MERGED spend and tokens
@@ -5593,7 +6061,9 @@ def _build_codex_source_state(
5593
6061
  # observation age and weekly-cycle evidence live on their own axes.
5594
6062
  freshness="fresh",
5595
6063
  warnings=tuple(warnings),
5596
- data_version=data_version,
6064
+ data_version=combined_accounting_version(
6065
+ data_version, combined_accounting,
6066
+ ),
5597
6067
  last_success_at=context.now_utc,
5598
6068
  capabilities={
5599
6069
  "hero": (
@@ -5717,4 +6187,5 @@ def _build_codex_source_state(
5717
6187
  ),
5718
6188
  },
5719
6189
  private_session_labels=private_session_labels,
6190
+ combined_accounting=combined_accounting,
5720
6191
  )