cctally 1.73.0 → 1.75.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.
@@ -4,25 +4,36 @@ from __future__ import annotations
4
4
  import datetime as dt
5
5
  import hashlib
6
6
  import json
7
+ import pathlib
7
8
  import sqlite3
8
9
  import sys
9
10
  from collections.abc import Mapping
10
- from dataclasses import dataclass
11
- from types import MappingProxyType
11
+ from dataclasses import dataclass, replace
12
+ from types import MappingProxyType, SimpleNamespace
12
13
  from typing import Any, Iterable
13
14
  from zoneinfo import ZoneInfo
14
15
 
15
16
  from _cctally_core import get_week_start_name
16
17
  from _cctally_quota import (
18
+ codex_five_hour_percent_at_crossing,
19
+ codex_quota_breakdown,
17
20
  codex_physical_mutation_seq,
18
21
  load_codex_quota_observations,
19
22
  load_codex_quota_projection_certificate,
20
23
  )
21
- from _cctally_source_analytics import load_qualified_codex_entries
24
+ from _cctally_source_analytics import (
25
+ QualifiedMetadataUnavailable,
26
+ has_cached_codex_accounting_entries,
27
+ load_cached_rooted_codex_accounting_entries,
28
+ load_codex_project_metadata_health,
29
+ load_qualified_codex_entries,
30
+ )
31
+ import _lib_log
22
32
  from _lib_dashboard_sources import (
23
33
  CapabilityRecord,
24
34
  ProjectionCoherence,
25
35
  SourceDashboardState,
36
+ SourceDashboardWarning,
26
37
  assess_codex_projection_coherence,
27
38
  dashboard_resource_key,
28
39
  )
@@ -35,12 +46,16 @@ from _lib_quota import (
35
46
  select_baseline,
36
47
  )
37
48
  from _lib_jsonl import CodexEntry
49
+ from _lib_fmt import stable_sum
50
+ from _lib_aggregators import _aggregate_codex_buckets
51
+ from _lib_five_hour import _FIVE_HOUR_JITTER_FLOOR_SECONDS
38
52
  from _lib_source_analytics import build_codex_project_result
39
53
  from _lib_view_models import (
54
+ CodexWeeklyView,
40
55
  build_codex_daily_view,
41
56
  build_codex_monthly_view,
57
+ build_rooted_codex_session_view,
42
58
  build_codex_session_view,
43
- build_codex_weekly_view,
44
59
  )
45
60
 
46
61
 
@@ -50,12 +65,12 @@ DASHBOARD_QUOTA_OBSERVATION_LIMIT = 1000
50
65
  DASHBOARD_QUOTA_RECENT_DAYS = 35
51
66
 
52
67
 
53
- class CodexProjectionIncoherent(RuntimeError):
54
- """Physical cache and S2 interpreted quota state cannot be mixed."""
68
+ class CodexCycleUnavailable(RuntimeError):
69
+ """No single active native seven-day boundary can bound hero accounting."""
55
70
 
56
- def __init__(self, reason: str | None) -> None:
71
+ def __init__(self, reason: str) -> None:
57
72
  self.reason = reason
58
- super().__init__("Codex quota projection is incoherent")
73
+ super().__init__(reason)
59
74
 
60
75
 
61
76
  class SourceCapabilityUnavailable(ValueError):
@@ -66,6 +81,173 @@ class SourceResourceNotFound(LookupError):
66
81
  """A valid opaque resource key has no row in its provider state."""
67
82
 
68
83
 
84
+ @dataclass(frozen=True)
85
+ class CodexCycleBoundary:
86
+ """The one active native subscription cycle usable for hero accounting."""
87
+
88
+ window_minutes: int
89
+ start_at: dt.datetime
90
+ resets_at: dt.datetime
91
+ # Root provenance is server-only accounting input, never public wire data.
92
+ source_root_keys: tuple[str, ...]
93
+ used_percent: float | None = None
94
+
95
+
96
+ @dataclass(frozen=True)
97
+ class CodexWeeklyPeriod:
98
+ """One non-overlapping observed native seven-day quota cycle segment."""
99
+
100
+ start_at: dt.datetime
101
+ end_at: dt.datetime
102
+ source_root_keys: tuple[str, ...]
103
+ used_percent: float | None = None
104
+
105
+
106
+ def _resolve_codex_weekly_cycle(
107
+ observations: Iterable[object],
108
+ now_utc: dt.datetime,
109
+ ) -> CodexCycleBoundary:
110
+ """Select exactly one active account-level 10,080-minute native cycle."""
111
+ boundaries: dict[tuple[int, dt.datetime], dict[str, object]] = {}
112
+ stale_weekly_evidence = False
113
+ for history in build_history(tuple(observations)):
114
+ if history.identity.window_minutes != 10_080:
115
+ continue
116
+ baseline = select_baseline(history.observations, now_utc)
117
+ if baseline is None or baseline.resets_at <= now_utc:
118
+ continue
119
+ if quota_freshness(history.physical_observations, now_utc).state != "fresh":
120
+ stale_weekly_evidence = True
121
+ continue
122
+ boundary = (history.identity.window_minutes, baseline.resets_at)
123
+ item = boundaries.setdefault(boundary, {"roots": set(), "used": []})
124
+ item["roots"].add(history.identity.source_root_key)
125
+ if baseline.used_percent is not None:
126
+ item["used"].append(float(baseline.used_percent))
127
+ if len(boundaries) != 1:
128
+ if not boundaries:
129
+ raise CodexCycleUnavailable("stale" if stale_weekly_evidence else "missing")
130
+ raise CodexCycleUnavailable("conflicting")
131
+ (window_minutes, resets_at), boundary_data = next(iter(boundaries.items()))
132
+ root_keys = boundary_data["roots"]
133
+ used_values = boundary_data["used"]
134
+ return CodexCycleBoundary(
135
+ window_minutes=window_minutes,
136
+ start_at=resets_at - dt.timedelta(minutes=window_minutes),
137
+ resets_at=resets_at,
138
+ source_root_keys=tuple(sorted(root_keys)),
139
+ used_percent=max(used_values) if used_values else None,
140
+ )
141
+
142
+
143
+ def _codex_weekly_periods(
144
+ stats_conn: sqlite3.Connection,
145
+ *,
146
+ source_root_keys: Iterable[str],
147
+ active_cycle: CodexCycleBoundary | None,
148
+ ) -> tuple[CodexWeeklyPeriod, ...]:
149
+ """Read durable 10,080-minute boundaries and clip early re-anchors.
150
+
151
+ A provider-granted reset changes the native window's nominal start before
152
+ the prior seven-day deadline. Sorting those nominal starts and ending the
153
+ prior segment at the next start preserves the actual quota-cycle boundary
154
+ without double-counting the overlapping nominal windows.
155
+ """
156
+ roots = tuple(sorted({
157
+ root for root in source_root_keys if isinstance(root, str) and root
158
+ }))
159
+ if not roots:
160
+ return ()
161
+ placeholders = ",".join("?" for _ in roots)
162
+ try:
163
+ rows = stats_conn.execute(
164
+ "SELECT source_root_key, resets_at_utc, nominal_start_at_utc, current_percent "
165
+ "FROM quota_window_blocks "
166
+ "WHERE source='codex' AND window_minutes=10080 "
167
+ f"AND source_root_key IN ({placeholders}) AND orphaned_at IS NULL "
168
+ "ORDER BY nominal_start_at_utc DESC, resets_at_utc DESC, source_root_key "
169
+ "LIMIT ?",
170
+ (*roots, SOURCE_HISTORY_LIMIT),
171
+ ).fetchall()
172
+ except sqlite3.Error:
173
+ rows = ()
174
+
175
+ raw_boundaries: list[tuple[dt.datetime, dt.datetime, set[str], list[float]]] = []
176
+
177
+ for root_key, resets_at_raw, start_at_raw, current_percent in rows:
178
+ try:
179
+ start_at = dt.datetime.fromisoformat(str(start_at_raw).replace("Z", "+00:00"))
180
+ resets_at = dt.datetime.fromisoformat(str(resets_at_raw).replace("Z", "+00:00"))
181
+ except (TypeError, ValueError):
182
+ continue
183
+ if start_at.tzinfo is None or resets_at.tzinfo is None:
184
+ continue
185
+ start_at = start_at.astimezone(UTC)
186
+ resets_at = resets_at.astimezone(UTC)
187
+ if resets_at <= start_at:
188
+ continue
189
+ used_values = []
190
+ if isinstance(current_percent, (int, float)) and not isinstance(current_percent, bool):
191
+ used_values.append(float(current_percent))
192
+ raw_boundaries.append((start_at, resets_at, {str(root_key)}, used_values))
193
+
194
+ if active_cycle is not None:
195
+ raw_boundaries.append((
196
+ active_cycle.start_at.astimezone(UTC),
197
+ active_cycle.resets_at.astimezone(UTC),
198
+ set(active_cycle.source_root_keys),
199
+ [active_cycle.used_percent] if active_cycle.used_percent is not None else [],
200
+ ))
201
+
202
+ ordered: list[tuple[dt.datetime, dt.datetime, set[str], list[float]]] = []
203
+ for start_at, resets_at, period_roots, used_values in sorted(
204
+ raw_boundaries, key=lambda item: (item[0], item[1]),
205
+ ):
206
+ if (
207
+ ordered
208
+ and (start_at - ordered[-1][0]).total_seconds()
209
+ < _FIVE_HOUR_JITTER_FLOOR_SECONDS
210
+ ):
211
+ first_start, latest_reset, existing_roots, existing_used = ordered[-1]
212
+ existing_roots.update(period_roots)
213
+ existing_used.extend(used_values)
214
+ ordered[-1] = (
215
+ first_start, max(latest_reset, resets_at), existing_roots, existing_used,
216
+ )
217
+ else:
218
+ ordered.append((start_at, resets_at, set(period_roots), list(used_values)))
219
+ periods: list[CodexWeeklyPeriod] = []
220
+ for index, (start_at, resets_at, period_roots, used_values) in enumerate(ordered):
221
+ next_start = ordered[index + 1][0] if index + 1 < len(ordered) else None
222
+ end_at = min(resets_at, next_start) if next_start is not None else resets_at
223
+ if end_at <= start_at:
224
+ continue
225
+ periods.append(CodexWeeklyPeriod(
226
+ start_at=start_at,
227
+ end_at=end_at,
228
+ source_root_keys=tuple(sorted(period_roots)),
229
+ used_percent=max(used_values) if used_values else None,
230
+ ))
231
+ return tuple(periods)
232
+
233
+
234
+ def _native_limit_label(limit_name: object, window_minutes: object) -> str:
235
+ """Prefer provider label text, deriving duration copy only when absent."""
236
+ if isinstance(limit_name, str) and limit_name.strip():
237
+ return limit_name.strip()
238
+ if window_minutes == 300:
239
+ return "5-hour limit"
240
+ if window_minutes == 10_080:
241
+ return "7-day limit"
242
+ if not isinstance(window_minutes, int) or isinstance(window_minutes, bool) or window_minutes <= 0:
243
+ return "Codex quota"
244
+ if window_minutes % 1_440 == 0:
245
+ return f"{window_minutes // 1_440}-day limit"
246
+ if window_minutes % 60 == 0:
247
+ return f"{window_minutes // 60}-hour limit"
248
+ return f"{window_minutes}-minute limit"
249
+
250
+
69
251
  @dataclass(frozen=True)
70
252
  class DashboardSourceSemantics:
71
253
  """One canonical CLI configuration resolution for a dashboard read.
@@ -81,6 +263,9 @@ class DashboardSourceSemantics:
81
263
  week_start_idx: int
82
264
  speed: str
83
265
  codex_budget: Mapping[str, object] | None
266
+ codex_quota_actual_thresholds: tuple[int, ...]
267
+ codex_quota_projected_thresholds: tuple[int, ...]
268
+ cache_report_anomaly_threshold_pp: int
84
269
  claude_identity: str
85
270
  codex_identity: str
86
271
 
@@ -103,6 +288,17 @@ def resolve_dashboard_source_semantics(
103
288
  week_start_idx = c.WEEKDAY_MAP[week_start_name]
104
289
  speed = c._resolve_codex_speed("auto")
105
290
  budget_config = c._get_budget_config(raw_config)
291
+ quota_alerts = c._get_quota_alerts_config(raw_config)
292
+ raw_cache_report = raw_config.get("cache_report")
293
+ raw_cache_threshold = (
294
+ raw_cache_report.get("anomaly_threshold_pp", 15)
295
+ if isinstance(raw_cache_report, Mapping) else 15
296
+ )
297
+ cache_threshold = (
298
+ int(raw_cache_threshold)
299
+ if isinstance(raw_cache_threshold, int) and not isinstance(raw_cache_threshold, bool)
300
+ and 1 <= raw_cache_threshold <= 100 else 15
301
+ )
106
302
  raw_codex_budget = budget_config.get("codex")
107
303
  codex_budget = (
108
304
  MappingProxyType(dict(raw_codex_budget))
@@ -126,6 +322,8 @@ def resolve_dashboard_source_semantics(
126
322
  }
127
323
  codex_identity_payload = {
128
324
  "codex_budget": dict(codex_budget) if codex_budget is not None else None,
325
+ "codex_quota_alerts": quota_alerts,
326
+ "cache_report_anomaly_threshold_pp": cache_threshold,
129
327
  "display_tz_name": display_tz_name,
130
328
  "speed": speed,
131
329
  "week_start_name": week_start_name,
@@ -142,6 +340,9 @@ def resolve_dashboard_source_semantics(
142
340
  week_start_idx=week_start_idx,
143
341
  speed=speed,
144
342
  codex_budget=codex_budget,
343
+ codex_quota_actual_thresholds=tuple(quota_alerts["actual_thresholds"]),
344
+ codex_quota_projected_thresholds=tuple(quota_alerts["projected_thresholds"]),
345
+ cache_report_anomaly_threshold_pp=cache_threshold,
145
346
  claude_identity=claude_identity,
146
347
  codex_identity=codex_identity,
147
348
  )
@@ -160,6 +361,9 @@ class DashboardReadContext:
160
361
  week_start_name: str = "monday"
161
362
  speed: str = "standard"
162
363
  codex_budget: Mapping[str, object] | None = None
364
+ codex_quota_actual_thresholds: tuple[int, ...] = ()
365
+ codex_quota_projected_thresholds: tuple[int, ...] = ()
366
+ cache_report_anomaly_threshold_pp: int = 15
163
367
 
164
368
  def __post_init__(self) -> None:
165
369
  for name in ("range_start", "now_utc"):
@@ -289,7 +493,7 @@ def _codex_budget_cost_events(
289
493
 
290
494
 
291
495
  def _bucket_wire(bucket: Any) -> dict[str, object]:
292
- return {
496
+ result = {
293
497
  "label": bucket.bucket,
294
498
  "cost_usd": bucket.cost_usd,
295
499
  "input_tokens": bucket.input_tokens,
@@ -298,7 +502,17 @@ def _bucket_wire(bucket: Any) -> dict[str, object]:
298
502
  "reasoning_output_tokens": bucket.reasoning_output_tokens,
299
503
  "total_tokens": bucket.total_tokens,
300
504
  "models": tuple(bucket.models),
505
+ "model_breakdowns": tuple(dict(row) for row in bucket.model_breakdowns),
301
506
  }
507
+ for name in ("period_start_at", "period_end_at"):
508
+ value = getattr(bucket, name, None)
509
+ if isinstance(value, dt.datetime):
510
+ result[name.replace("period_", "")] = value.astimezone(UTC).isoformat()
511
+ for name in ("used_pct", "dollar_per_pct"):
512
+ value = getattr(bucket, name, None)
513
+ if value is not None:
514
+ result[name] = value
515
+ return result
302
516
 
303
517
 
304
518
  def _period_wire(view: Any) -> dict[str, object]:
@@ -310,20 +524,361 @@ def _period_wire(view: Any) -> dict[str, object]:
310
524
  }
311
525
 
312
526
 
313
- def _session_wire(view: Any) -> dict[str, object]:
527
+ def _codex_cache_report_wire(
528
+ entries: Iterable[object],
529
+ *,
530
+ metadata: Mapping[tuple[str, str], Mapping[str, object]],
531
+ now_utc: dt.datetime,
532
+ display_tz_name: str | None,
533
+ speed: str,
534
+ anomaly_threshold_pp: int = 15,
535
+ window_days: int = 14,
536
+ ) -> dict[str, object]:
537
+ """Compute the canonical cache report from Codex's inclusive counters.
538
+
539
+ Codex input is cache-inclusive, so the shared cache-report kernel receives
540
+ uncached input plus cached input as two disjoint counters. OpenAI does not
541
+ charge a cache-write premium; the counterfactual is therefore the exact
542
+ uncached-vs-cached input price difference for each token-count event.
543
+ """
544
+ c = sys.modules["cctally"]
545
+ crk = c._load_sibling("_lib_cache_report")
546
+ display_tz = ZoneInfo(display_tz_name) if display_tz_name else None
547
+ cutoff = now_utc - dt.timedelta(days=window_days)
548
+
549
+ def _tiered_cost(tokens: int, pricing: Mapping[str, object], base: str, above: str) -> float:
550
+ if tokens <= 0:
551
+ return 0.0
552
+ base_rate = float(pricing.get(base, 0.0) or 0.0)
553
+ above_rate = pricing.get(above)
554
+ threshold = int(c.CODEX_TIERED_THRESHOLD)
555
+ if tokens > threshold and above_rate is not None:
556
+ return threshold * base_rate + (tokens - threshold) * float(above_rate)
557
+ return tokens * base_rate
558
+
559
+ wrapped = []
560
+ for entry in entries:
561
+ timestamp = getattr(entry, "timestamp", None)
562
+ if not isinstance(timestamp, dt.datetime) or timestamp < cutoff:
563
+ continue
564
+ model = str(getattr(entry, "model", "") or "unknown")
565
+ input_tokens = int(getattr(entry, "input_tokens", 0))
566
+ cached_tokens = min(input_tokens, int(getattr(entry, "cached_input_tokens", 0)))
567
+ uncached_tokens = max(0, input_tokens - cached_tokens)
568
+ pricing, _is_fallback = c._resolve_codex_pricing(model)
569
+ pricing = pricing or {}
570
+ uncached_counterfactual = _tiered_cost(
571
+ cached_tokens, pricing,
572
+ "input_cost_per_token", "input_cost_per_token_above_272k_tokens",
573
+ )
574
+ cached_actual = _tiered_cost(
575
+ cached_tokens, pricing,
576
+ "cache_read_input_token_cost", "cache_read_input_token_cost_above_272k_tokens",
577
+ )
578
+ multiplier = c._codex_fast_multiplier(model) if speed == "fast" else 1.0
579
+ saved = max(0.0, uncached_counterfactual - cached_actual) * multiplier
580
+ identity = (
581
+ str(getattr(entry, "source_root_key", "") or ""),
582
+ str(getattr(entry, "source_path", "") or ""),
583
+ )
584
+ item_metadata = metadata.get(identity) or {}
585
+ project = (
586
+ str(getattr(entry, "project_label", "") or "").strip()
587
+ or str(item_metadata.get("project_label") or "").strip()
588
+ or "(unknown)"
589
+ )
590
+ wrapped.append(SimpleNamespace(
591
+ timestamp=timestamp,
592
+ model=model,
593
+ cost_usd=float(getattr(entry, "cost_usd", 0.0)),
594
+ project_path=project,
595
+ input_tokens=uncached_tokens,
596
+ output_tokens=int(getattr(entry, "output_tokens", 0)),
597
+ cache_creation_tokens=0,
598
+ cache_read_tokens=cached_tokens,
599
+ cache_saved_usd=saved,
600
+ cache_wasted_usd=0.0,
601
+ cache_net_usd=saved,
602
+ usage={
603
+ "input_tokens": uncached_tokens,
604
+ "output_tokens": int(getattr(entry, "output_tokens", 0)),
605
+ "cache_creation_input_tokens": 0,
606
+ "cache_read_input_tokens": cached_tokens,
607
+ },
608
+ ))
609
+
610
+ today_iso = now_utc.astimezone(display_tz or UTC).strftime("%Y-%m-%d")
611
+ if not wrapped:
612
+ return {
613
+ "window_days": window_days,
614
+ "anomaly_threshold_pp": anomaly_threshold_pp,
615
+ "anomaly_window_days": window_days,
616
+ "today": {
617
+ "date": today_iso, "cache_hit_percent": 0.0,
618
+ "baseline_median_percent": None, "delta_pp": None,
619
+ "net_usd": 0.0, "saved_usd": 0.0, "wasted_usd": 0.0,
620
+ "anomaly_triggered": False, "anomaly_reasons": (),
621
+ "baseline_daily_row_count": 0,
622
+ },
623
+ "days": (), "by_project": (), "by_model": (),
624
+ "seven_day_net_usd": 0.0, "seven_day_anomaly_count": 0,
625
+ "fourteen_day_counterfactual_usd": 0.0,
626
+ "fourteen_day_efficiency_ratio": 0.0, "is_empty": True,
627
+ }
628
+
629
+ result = crk._build_cache_report(
630
+ wrapped,
631
+ now_utc=now_utc,
632
+ window_days=window_days,
633
+ anomaly_threshold_pp=anomaly_threshold_pp,
634
+ anomaly_window_days=window_days,
635
+ display_tz=display_tz,
636
+ pricing=c.CODEX_MODEL_PRICING,
637
+ cost_calculator=lambda _model, _usage, _mode, cost: float(cost or 0.0),
638
+ )
639
+ raw_rows = sorted(result.rows, key=lambda row: row.date or "", reverse=True)
640
+ days = tuple({
641
+ "date": row.date or "",
642
+ "cache_hit_percent": row.cache_hit_percent,
643
+ "input_tokens": row.input_tokens,
644
+ "output_tokens": row.output_tokens,
645
+ "cache_creation_tokens": row.cache_creation_tokens,
646
+ "cache_read_tokens": row.cache_read_tokens,
647
+ "saved_usd": row.saved_usd,
648
+ "wasted_usd": row.wasted_usd,
649
+ "net_usd": row.net_usd,
650
+ "anomaly_triggered": row.anomaly_triggered,
651
+ "anomaly_reasons": tuple(row.anomaly_reasons),
652
+ } for row in raw_rows[:window_days])
653
+ today_row = next((row for row in raw_rows if row.date == today_iso), None)
654
+ baseline_count = sum(1 for row in raw_rows if row.date != today_iso)
655
+ baseline = result.today_baseline_median
656
+ today_hit = today_row.cache_hit_percent if today_row else 0.0
657
+ kept_dates = {row["date"] for row in days}
658
+ kept_entries = [
659
+ entry for entry in wrapped
660
+ if entry.timestamp.astimezone(display_tz or UTC).strftime("%Y-%m-%d") in kept_dates
661
+ ]
662
+ by_project = crk._aggregate_cache_breakdown(
663
+ kept_entries, key_fn=lambda entry: entry.project_path,
664
+ pricing=c.CODEX_MODEL_PRICING,
665
+ )
666
+ by_model = crk._aggregate_cache_breakdown(
667
+ kept_entries, key_fn=lambda entry: entry.model,
668
+ pricing=c.CODEX_MODEL_PRICING,
669
+ )
670
+ seven = days[:7]
671
+ saved_total = stable_sum(float(row["saved_usd"]) for row in days)
672
+ wasted_total = stable_sum(float(row["wasted_usd"]) for row in days)
673
+ efficiency_denom = saved_total + abs(wasted_total)
674
+ return {
675
+ "window_days": window_days,
676
+ "anomaly_threshold_pp": anomaly_threshold_pp,
677
+ "anomaly_window_days": window_days,
678
+ "today": {
679
+ "date": today_iso,
680
+ "cache_hit_percent": today_hit,
681
+ "baseline_median_percent": baseline,
682
+ "delta_pp": today_hit - baseline if baseline is not None else None,
683
+ "net_usd": today_row.net_usd if today_row else 0.0,
684
+ "saved_usd": today_row.saved_usd if today_row else 0.0,
685
+ "wasted_usd": today_row.wasted_usd if today_row else 0.0,
686
+ "anomaly_triggered": today_row.anomaly_triggered if today_row else False,
687
+ "anomaly_reasons": tuple(today_row.anomaly_reasons) if today_row else (),
688
+ "baseline_daily_row_count": baseline_count,
689
+ },
690
+ "days": days,
691
+ "by_project": tuple({
692
+ "key": row.key, "cache_hit_percent": row.cache_hit_percent,
693
+ "net_usd": row.net_usd,
694
+ } for row in by_project),
695
+ "by_model": tuple({
696
+ "key": row.key, "cache_hit_percent": row.cache_hit_percent,
697
+ "net_usd": row.net_usd,
698
+ } for row in by_model),
699
+ "seven_day_net_usd": stable_sum(float(row["net_usd"]) for row in seven),
700
+ "seven_day_anomaly_count": sum(bool(row["anomaly_triggered"]) for row in seven),
701
+ "fourteen_day_counterfactual_usd": saved_total,
702
+ "fourteen_day_efficiency_ratio": (
703
+ saved_total / efficiency_denom if efficiency_denom > 1e-9 else 0.0
704
+ ),
705
+ "is_empty": False,
706
+ }
707
+
708
+
709
+ def _codex_conversation_metadata(
710
+ cache_conn: sqlite3.Connection,
711
+ ) -> dict[tuple[str, str], dict[str, object]]:
712
+ """Read task short names and cached project metadata by rooted rollout.
713
+
714
+ ``state_5.sqlite.threads.title`` is Codex's persisted user-facing task name.
715
+ Conversation rollup titles are derived from prompt text and therefore must
716
+ never be substituted for that name on the dashboard. Accounting remains
717
+ authoritative for totals; conversation rollups only decorate projects.
718
+ """
719
+ metadata: dict[tuple[str, str], dict[str, object]] = {}
720
+ try:
721
+ rows = tuple(cache_conn.execute(
722
+ "SELECT t.source_root_key, t.source_path, t.native_thread_id, "
723
+ "(SELECT e.session_id FROM codex_session_entries AS e "
724
+ " WHERE e.source_root_key=t.source_root_key AND e.source_path=t.source_path "
725
+ " ORDER BY e.id LIMIT 1) AS accounting_session_id, "
726
+ "r.project_key, r.project_label, r.started_utc "
727
+ "FROM codex_conversation_threads AS t "
728
+ "LEFT JOIN codex_conversation_rollups AS r "
729
+ "ON r.conversation_key=t.conversation_key "
730
+ "ORDER BY r.last_activity_utc DESC, t.conversation_key DESC"
731
+ ))
732
+ file_aliases = tuple(cache_conn.execute(
733
+ "SELECT source_root_key, path, last_native_thread_id, last_session_id "
734
+ "FROM codex_session_files "
735
+ "WHERE last_native_thread_id IS NOT NULL AND last_native_thread_id != '' "
736
+ "ORDER BY last_ingested_at DESC, path DESC"
737
+ ))
738
+ native_ids = tuple(sorted({
739
+ str(native_thread_id) for _, _, native_thread_id, *_ in rows
740
+ if isinstance(native_thread_id, str) and native_thread_id
741
+ } | {
742
+ str(native_thread_id) for _, _, native_thread_id, _ in file_aliases
743
+ if isinstance(native_thread_id, str) and native_thread_id
744
+ }))
745
+ provider_roots = {
746
+ str(root_key): pathlib.Path(root_path)
747
+ for root_key, root_path in cache_conn.execute(
748
+ "SELECT source_root_key, canonical_root_path FROM codex_source_roots "
749
+ "ORDER BY source_root_key"
750
+ )
751
+ if isinstance(root_key, str) and root_key
752
+ and isinstance(root_path, str) and root_path
753
+ }
754
+ short_names: dict[str, str] = {}
755
+ for provider_root in provider_roots.values():
756
+ state_path = provider_root / "state_5.sqlite"
757
+ if not state_path.is_file():
758
+ continue
759
+ state_conn: sqlite3.Connection | None = None
760
+ try:
761
+ state_conn = sqlite3.connect(
762
+ f"{state_path.resolve().as_uri()}?mode=ro",
763
+ uri=True,
764
+ timeout=0.05,
765
+ )
766
+ for offset in range(0, len(native_ids), 500):
767
+ batch = native_ids[offset:offset + 500]
768
+ if not batch:
769
+ continue
770
+ placeholders = ",".join("?" for _ in batch)
771
+ for thread_id, title in state_conn.execute(
772
+ f"SELECT id, title FROM threads WHERE id IN ({placeholders})",
773
+ batch,
774
+ ):
775
+ clean_title = " ".join(str(title or "").split())
776
+ if clean_title:
777
+ short_names[str(thread_id)] = clean_title
778
+ except (OSError, sqlite3.Error):
779
+ continue
780
+ finally:
781
+ if state_conn is not None:
782
+ state_conn.close()
783
+
784
+ metadata_by_native: dict[tuple[str, str], dict[str, object]] = {}
785
+ for (
786
+ root_key, source_path, native_thread_id, accounting_session_id,
787
+ project_key, project_label, started_at,
788
+ ) in rows:
789
+ identity = (str(root_key or ""), str(source_path or ""))
790
+ if not all(identity) or identity in metadata:
791
+ continue
792
+ item = {
793
+ "title": short_names.get(str(native_thread_id or "")),
794
+ "native_thread_id": native_thread_id,
795
+ "accounting_session_id": accounting_session_id,
796
+ "root_path": str(provider_roots.get(identity[0]) or ""),
797
+ "project_key": project_key,
798
+ "project_label": project_label,
799
+ "started_at": started_at,
800
+ }
801
+ metadata[identity] = item
802
+ native_identity = (identity[0], str(native_thread_id or ""))
803
+ existing = metadata_by_native.get(native_identity)
804
+ if existing is None or (not existing.get("project_key") and project_key):
805
+ metadata_by_native[native_identity] = item
806
+
807
+ # A child rollout can be accounting-complete while its own historical
808
+ # conversation-thread row is absent (for example, a file first cached
809
+ # before conversation normalization was introduced). The cursor still
810
+ # persists the rooted native thread id. Inherit only presentation
811
+ # metadata from that rooted task; the child's accounting path and
812
+ # session id remain its own identity and totals are never merged.
813
+ for root_key, source_path, native_thread_id, accounting_session_id in file_aliases:
814
+ identity = (str(root_key or ""), str(source_path or ""))
815
+ if not all(identity) or identity in metadata:
816
+ continue
817
+ native_identity = (identity[0], str(native_thread_id or ""))
818
+ inherited = metadata_by_native.get(native_identity)
819
+ metadata[identity] = {
820
+ "title": short_names.get(native_identity[1]) or (inherited or {}).get("title"),
821
+ "native_thread_id": native_thread_id,
822
+ "accounting_session_id": accounting_session_id,
823
+ "root_path": str(provider_roots.get(identity[0]) or ""),
824
+ "project_key": (inherited or {}).get("project_key"),
825
+ "project_label": (inherited or {}).get("project_label"),
826
+ "started_at": (inherited or {}).get("started_at"),
827
+ }
828
+ except sqlite3.Error:
829
+ return {}
830
+ return metadata
831
+
832
+
833
+ def _session_wire(
834
+ view: Any,
835
+ *,
836
+ metadata: Mapping[tuple[str, str], Mapping[str, object]] | None = None,
837
+ ) -> dict[str, object]:
314
838
  rows = []
315
- for ordinal, row in enumerate(view.rows, start=1):
839
+ for row in view.rows:
316
840
  # The Codex session aggregator intentionally splits equal relative
317
841
  # session paths from distinct $CODEX_HOME roots. The opaque detail
318
842
  # key must use that same grouping identity or two visible rows route
319
843
  # to one another's detail payload.
320
844
  root_identity = row.codex_root or "single-root"
845
+ row_metadata = (metadata or {}).get((str(row.codex_root or ""), str(row.session_id_path)))
846
+ if row_metadata is None and metadata is not None:
847
+ row_metadata = next((
848
+ value for (root_key, source_path), value in metadata.items()
849
+ if (
850
+ str(value.get("native_thread_id") or "") == str(row.session_id or "")
851
+ or str(value.get("accounting_session_id") or "") == str(row.session_id or "")
852
+ or source_path == str(row.session_id_path)
853
+ )
854
+ and (
855
+ not row.codex_root
856
+ or str(row.codex_root) in (
857
+ root_key,
858
+ str(value.get("root_path") or ""),
859
+ )
860
+ )
861
+ ), None)
862
+ title = str(row_metadata.get("title") or "").strip() if row_metadata else ""
863
+ project = str(row_metadata.get("project_label") or "").strip() if row_metadata else ""
864
+ started_at = row_metadata.get("started_at") if row_metadata else None
865
+ duration_min = None
866
+ if isinstance(started_at, str):
867
+ try:
868
+ started_dt = dt.datetime.fromisoformat(started_at.replace("Z", "+00:00"))
869
+ duration_min = max(0, round((row.last_activity.astimezone(UTC) - started_dt.astimezone(UTC)).total_seconds() / 60))
870
+ except (TypeError, ValueError):
871
+ started_at = None
321
872
  rows.append({
322
873
  "key": dashboard_resource_key(
323
874
  "session", "codex", root_identity, row.session_id_path,
324
875
  ),
325
876
  "source": "codex",
326
- "label": f"Session {ordinal}",
877
+ "label": title or None,
878
+ "project": project or None,
879
+ "project_key": row_metadata.get("project_key") if row_metadata else None,
880
+ "started_at": started_at,
881
+ "duration_min": duration_min,
327
882
  "last_activity": row.last_activity.astimezone(UTC).isoformat(),
328
883
  "cost_usd": row.cost_usd,
329
884
  "input_tokens": row.input_tokens,
@@ -332,6 +887,9 @@ def _session_wire(view: Any) -> dict[str, object]:
332
887
  "reasoning_output_tokens": row.reasoning_output_tokens,
333
888
  "total_tokens": row.total_tokens,
334
889
  "models": tuple(row.models),
890
+ "model_breakdowns": tuple(
891
+ dict(item) for item in getattr(row, "model_breakdowns", ())
892
+ ),
335
893
  })
336
894
  return {
337
895
  "rows": tuple(rows),
@@ -341,31 +899,111 @@ def _session_wire(view: Any) -> dict[str, object]:
341
899
  }
342
900
 
343
901
 
344
- def _quota_wire(stats_conn: sqlite3.Connection) -> tuple[dict[str, object], ...]:
902
+ def _quota_wire(
903
+ stats_conn: sqlite3.Connection,
904
+ *,
905
+ accounting_entries: Iterable[object] = (),
906
+ cycle: CodexCycleBoundary | None = None,
907
+ now_utc: dt.datetime | None = None,
908
+ display_tz_name: str | None = None,
909
+ ) -> tuple[dict[str, object], ...]:
910
+ """Build current-cycle Codex 5-hour activity rows from durable windows.
911
+
912
+ The durable projection supplies the truthful native block boundaries. Cost,
913
+ tokens, and model splits come from root-qualified accounting inside each
914
+ half-open 300-minute interval. Weekly quota summaries are deliberately not
915
+ activity blocks and never enter this wire.
916
+ """
917
+ if cycle is None or now_utc is None:
918
+ return ()
345
919
  try:
346
920
  rows = stats_conn.execute(
347
921
  "SELECT source_root_key, logical_limit_key, observed_slot, window_minutes, "
348
- "limit_name, resets_at_utc, current_percent, orphaned_at "
349
- "FROM quota_window_blocks WHERE source='codex' "
922
+ "limit_name, resets_at_utc, nominal_start_at_utc, current_percent, orphaned_at "
923
+ "FROM quota_window_blocks WHERE source='codex' AND window_minutes=300 "
350
924
  "ORDER BY resets_at_utc DESC, source_root_key, logical_limit_key, observed_slot "
351
925
  "LIMIT ?",
352
926
  (SOURCE_HISTORY_LIMIT,),
353
927
  ).fetchall()
354
928
  except sqlite3.Error:
355
929
  return ()
356
- return tuple({
357
- "key": dashboard_resource_key(
358
- "block", "codex", root_key, logical_limit_key, observed_slot, window_minutes, resets_at,
359
- ),
360
- "source": "codex",
361
- "label": limit_name or "Codex quota",
362
- "resets_at": resets_at,
363
- "current_percent": current_percent,
364
- "orphaned": orphaned_at is not None,
365
- } for (
930
+ entries = tuple(accounting_entries)
931
+ display_tz = ZoneInfo(display_tz_name) if display_tz_name else None
932
+ c = sys.modules["cctally"]
933
+ wired: list[dict[str, object]] = []
934
+ seen_windows: set[tuple[str, dt.datetime, dt.datetime]] = set()
935
+ for (
366
936
  root_key, logical_limit_key, observed_slot, window_minutes,
367
- limit_name, resets_at, current_percent, orphaned_at,
368
- ) in rows)
937
+ _limit_name, resets_at_raw, nominal_start_raw, current_percent, orphaned_at,
938
+ ) in rows:
939
+ if orphaned_at is not None or str(root_key) not in cycle.source_root_keys:
940
+ continue
941
+ try:
942
+ start_at = dt.datetime.fromisoformat(str(nominal_start_raw).replace("Z", "+00:00"))
943
+ resets_at = dt.datetime.fromisoformat(str(resets_at_raw).replace("Z", "+00:00"))
944
+ except (TypeError, ValueError):
945
+ continue
946
+ if start_at.tzinfo is None or resets_at.tzinfo is None:
947
+ continue
948
+ start_at = start_at.astimezone(UTC)
949
+ resets_at = resets_at.astimezone(UTC)
950
+ if resets_at <= cycle.start_at or start_at >= cycle.resets_at:
951
+ continue
952
+ physical_key = (str(root_key), start_at, resets_at)
953
+ if physical_key in seen_windows:
954
+ continue
955
+ seen_windows.add(physical_key)
956
+ block_entries = tuple(
957
+ entry for entry in entries
958
+ if str(getattr(entry, "source_root_key", "")) == str(root_key)
959
+ and start_at <= getattr(entry, "timestamp").astimezone(UTC) < resets_at
960
+ )
961
+ if not block_entries:
962
+ continue
963
+ by_model: dict[str, dict[str, object]] = {}
964
+ for entry in block_entries:
965
+ model = str(getattr(entry, "model", "") or "unknown")
966
+ aggregate = by_model.setdefault(model, {
967
+ "modelName": model,
968
+ "inputTokens": 0,
969
+ "cachedInputTokens": 0,
970
+ "outputTokens": 0,
971
+ "reasoningOutputTokens": 0,
972
+ "totalTokens": 0,
973
+ "costParts": [],
974
+ })
975
+ aggregate["inputTokens"] += int(getattr(entry, "input_tokens", 0))
976
+ aggregate["cachedInputTokens"] += int(getattr(entry, "cached_input_tokens", 0))
977
+ aggregate["outputTokens"] += int(getattr(entry, "output_tokens", 0))
978
+ aggregate["reasoningOutputTokens"] += int(getattr(entry, "reasoning_output_tokens", 0))
979
+ aggregate["totalTokens"] += int(getattr(entry, "total_tokens", 0))
980
+ aggregate["costParts"].append(float(getattr(entry, "cost_usd", 0.0)))
981
+ breakdowns: list[dict[str, object]] = []
982
+ for aggregate in by_model.values():
983
+ cost = stable_sum(aggregate.pop("costParts"))
984
+ breakdowns.append({**aggregate, "cost": cost})
985
+ breakdowns.sort(key=lambda row: (-float(row["cost"]), str(row["modelName"])))
986
+ cost_usd = stable_sum(float(row["cost"]) for row in breakdowns)
987
+ wired.append({
988
+ "key": dashboard_resource_key(
989
+ "block", "codex", root_key, logical_limit_key,
990
+ observed_slot, window_minutes, resets_at_raw,
991
+ ),
992
+ "source": "codex",
993
+ "label": c.format_display_dt(
994
+ start_at, display_tz, fmt="%H:%M %b %d", suffix=True,
995
+ ),
996
+ "window_minutes": window_minutes,
997
+ "start_at": start_at.isoformat(),
998
+ "end_at": resets_at.isoformat(),
999
+ "resets_at": resets_at_raw,
1000
+ "current_percent": current_percent,
1001
+ "orphaned": False,
1002
+ "is_active": start_at <= now_utc < resets_at,
1003
+ "cost_usd": cost_usd,
1004
+ "model_breakdowns": tuple(breakdowns),
1005
+ })
1006
+ return tuple(wired)
369
1007
 
370
1008
 
371
1009
  def _budget_wire(stats_conn: sqlite3.Connection) -> tuple[dict[str, object], ...]:
@@ -489,9 +1127,12 @@ def _configured_codex_budget_window(
489
1127
  def _quota_read_model(
490
1128
  context: DashboardReadContext,
491
1129
  observations: Iterable[object],
1130
+ *,
1131
+ accounting_entries: Iterable[object] = (),
492
1132
  ) -> dict[str, object]:
493
1133
  """Use S2's pure history/block/forecast kernels over cache evidence."""
494
1134
  quota_observations = tuple(observations)
1135
+ cost_entries = tuple(accounting_entries)
495
1136
  histories = build_history(quota_observations)
496
1137
  blocks = build_blocks(quota_observations)
497
1138
  history_rows: list[dict[str, object]] = []
@@ -511,7 +1152,7 @@ def _quota_read_model(
511
1152
  history_rows.append({
512
1153
  "key": dashboard_resource_key("quota", "codex", *key_parts),
513
1154
  "source": "codex",
514
- "label": identity.limit_name or "Codex quota",
1155
+ "label": _native_limit_label(identity.limit_name, identity.window_minutes),
515
1156
  "observed_slot": identity.observed_slot,
516
1157
  "window_minutes": identity.window_minutes,
517
1158
  "current_percent": baseline.used_percent if baseline is not None else None,
@@ -551,7 +1192,82 @@ def _quota_read_model(
551
1192
  identity.window_minutes,
552
1193
  block.resets_at.astimezone(UTC).isoformat(),
553
1194
  )
1195
+ quota_key = dashboard_resource_key(
1196
+ "quota", "codex", identity.source_root_key,
1197
+ identity.logical_limit_key, identity.observed_slot,
1198
+ identity.window_minutes,
1199
+ )
1200
+ block_cost_entries = tuple(
1201
+ entry for entry in cost_entries
1202
+ if str(getattr(entry, "source_root_key", "")) == identity.source_root_key
1203
+ and block.nominal_start_at
1204
+ <= getattr(entry, "timestamp").astimezone(UTC)
1205
+ < block.resets_at
1206
+ )
1207
+ canonical_rows = ()
1208
+ if identity.window_minutes == 10_080 and block.resets_at > context.now_utc:
1209
+ try:
1210
+ canonical_rows = codex_quota_breakdown(
1211
+ identity,
1212
+ block.resets_at,
1213
+ speed=context.speed,
1214
+ cache_conn=context.cache_conn,
1215
+ stats_conn=context.stats_conn,
1216
+ )
1217
+ except sqlite3.Error:
1218
+ # Older or partially migrated stores retain the bounded
1219
+ # observation-derived fallback below. A coherent current
1220
+ # store always has the durable projection used by the CLI.
1221
+ canonical_rows = ()
1222
+ if canonical_rows:
1223
+ try:
1224
+ correlated_five_hour = tuple(
1225
+ observation
1226
+ for observation in load_codex_quota_observations(
1227
+ source_root_keys={identity.source_root_key},
1228
+ cache_conn=context.cache_conn,
1229
+ captured_at_or_after=block.nominal_start_at,
1230
+ )
1231
+ if observation.identity.window_minutes == 300
1232
+ and observation.identity.observed_slot == identity.observed_slot
1233
+ and observation.identity.limit_id == identity.limit_id
1234
+ )
1235
+ except sqlite3.Error:
1236
+ correlated_five_hour = ()
1237
+
1238
+ for row in canonical_rows:
1239
+ milestone_rows.append({
1240
+ "key": dashboard_resource_key(
1241
+ "quota_milestone", "codex", *block_parts,
1242
+ row.percent, row.captured_at.astimezone(UTC).isoformat(),
1243
+ ),
1244
+ "source": "codex",
1245
+ "block_key": dashboard_resource_key("block", "codex", *block_parts),
1246
+ "quota_key": quota_key,
1247
+ "window_minutes": identity.window_minutes,
1248
+ "resets_at": block.resets_at.astimezone(UTC).isoformat(),
1249
+ "percent": row.percent,
1250
+ "captured_at": row.captured_at.astimezone(UTC).isoformat(),
1251
+ "cumulative_usd": row.cost_usd,
1252
+ "marginal_usd": row.marginal_cost_usd,
1253
+ "input_tokens": row.input_tokens,
1254
+ "cached_input_tokens": row.cached_input_tokens,
1255
+ "output_tokens": row.output_tokens,
1256
+ "reasoning_output_tokens": row.reasoning_output_tokens,
1257
+ "total_tokens": row.total_tokens,
1258
+ "five_hour_percent": codex_five_hour_percent_at_crossing(
1259
+ identity, row.captured_at, correlated_five_hour,
1260
+ ),
1261
+ })
1262
+ continue
1263
+
1264
+ previous_cumulative = 0.0
554
1265
  for milestone in percent_milestones(block):
1266
+ cumulative_usd = stable_sum(
1267
+ float(getattr(entry, "cost_usd", 0.0))
1268
+ for entry in block_cost_entries
1269
+ if getattr(entry, "timestamp").astimezone(UTC) <= milestone.captured_at
1270
+ )
555
1271
  milestone_rows.append({
556
1272
  "key": dashboard_resource_key(
557
1273
  "quota_milestone", "codex", *block_parts,
@@ -559,9 +1275,15 @@ def _quota_read_model(
559
1275
  ),
560
1276
  "source": "codex",
561
1277
  "block_key": dashboard_resource_key("block", "codex", *block_parts),
1278
+ "quota_key": quota_key,
1279
+ "window_minutes": identity.window_minutes,
1280
+ "resets_at": block.resets_at.astimezone(UTC).isoformat(),
562
1281
  "percent": milestone.percent,
563
1282
  "captured_at": milestone.captured_at.astimezone(UTC).isoformat(),
1283
+ "cumulative_usd": cumulative_usd,
1284
+ "marginal_usd": max(0.0, cumulative_usd - previous_cumulative),
564
1285
  })
1286
+ previous_cumulative = cumulative_usd
565
1287
  latest_percent = max(
566
1288
  (float(row["current_percent"]) for row in active_rows), default=None,
567
1289
  )
@@ -607,6 +1329,41 @@ def _clock_freshness(
607
1329
  return "stale" if age_seconds > stale_after else "fresh"
608
1330
 
609
1331
 
1332
+ def _clock_cycle_validity(
1333
+ histories: Iterable[object],
1334
+ now_utc: dt.datetime,
1335
+ ) -> tuple[bool, str]:
1336
+ """Re-evaluate frozen weekly evidence without touching cache or rollouts."""
1337
+ boundaries: set[dt.datetime] = set()
1338
+ stale_weekly_evidence = False
1339
+ for raw_history in histories:
1340
+ if not isinstance(raw_history, Mapping):
1341
+ continue
1342
+ if raw_history.get("window_minutes") != 10_080:
1343
+ continue
1344
+ current = raw_history.get("current_percent")
1345
+ forecast = raw_history.get("forecast")
1346
+ if current is None or not isinstance(forecast, Mapping):
1347
+ continue
1348
+ try:
1349
+ resets_at = dt.datetime.fromisoformat(
1350
+ str(forecast.get("resets_at")).replace("Z", "+00:00")
1351
+ ).astimezone(UTC)
1352
+ except (TypeError, ValueError):
1353
+ continue
1354
+ if resets_at <= now_utc:
1355
+ continue
1356
+ if raw_history.get("freshness") != "fresh":
1357
+ stale_weekly_evidence = True
1358
+ continue
1359
+ boundaries.add(resets_at)
1360
+ if len(boundaries) == 1:
1361
+ return True, "ok"
1362
+ if not boundaries and stale_weekly_evidence:
1363
+ return False, "stale"
1364
+ return False, "missing" if not boundaries else "conflicting"
1365
+
1366
+
610
1367
  def _refresh_budget_status_clock(
611
1368
  status: Mapping[str, object] | None,
612
1369
  now_utc: dt.datetime,
@@ -676,6 +1433,11 @@ def refresh_codex_source_clock(
676
1433
  data = dict(state.data)
677
1434
  quota = data.get("quota")
678
1435
  quota_changed = False
1436
+ cycle_changed = False
1437
+ capabilities = state.capabilities
1438
+ warnings = state.warnings
1439
+ availability = state.availability
1440
+ freshness = state.freshness
679
1441
  if isinstance(quota, Mapping):
680
1442
  quota = dict(quota)
681
1443
  refreshed_histories: list[dict[str, object]] = []
@@ -757,6 +1519,40 @@ def refresh_codex_source_clock(
757
1519
  quota["summary"] = summary
758
1520
  data["quota"] = quota
759
1521
  quota_changed = bool(refreshed_histories)
1522
+ hero = data.get("hero")
1523
+ hero_capability = state.capabilities.get("hero")
1524
+ if (
1525
+ isinstance(hero, Mapping)
1526
+ and isinstance(hero.get("cycle"), Mapping)
1527
+ and hero_capability is not None
1528
+ and hero_capability.status == "supported"
1529
+ ):
1530
+ cycle_valid, cycle_reason = _clock_cycle_validity(refreshed_histories, now_utc)
1531
+ if not cycle_valid:
1532
+ hero = dict(hero)
1533
+ for field in (
1534
+ "cost_usd", "input_tokens", "cached_input_tokens", "output_tokens",
1535
+ "reasoning_output_tokens", "total_tokens", "cycle",
1536
+ ):
1537
+ hero[field] = None
1538
+ data["hero"] = hero
1539
+ refreshed_capabilities = dict(state.capabilities)
1540
+ refreshed_capabilities["hero"] = CapabilityRecord(
1541
+ "unavailable", "missing-or-conflicting-native-cycle",
1542
+ )
1543
+ capabilities = refreshed_capabilities
1544
+ warnings = tuple(
1545
+ warning for warning in state.warnings
1546
+ if warning.code != "codex_cycle_unavailable"
1547
+ ) + (SourceDashboardWarning(
1548
+ "codex_cycle_unavailable",
1549
+ "Codex native reset cycle is unavailable.",
1550
+ "hero",
1551
+ ),)
1552
+ availability = "partial"
1553
+ if cycle_reason == "stale":
1554
+ freshness = "stale"
1555
+ cycle_changed = True
760
1556
  budget_domain = data.get("budget")
761
1557
  budget_changed = False
762
1558
  if isinstance(budget_domain, Mapping):
@@ -778,16 +1574,16 @@ def refresh_codex_source_clock(
778
1574
  hero["budget"] = refreshed_budget
779
1575
  data["hero"] = hero
780
1576
  budget_changed = True
781
- if not (quota_changed or budget_changed):
1577
+ if not (quota_changed or budget_changed or cycle_changed):
782
1578
  return state
783
1579
  refreshed_state = SourceDashboardState(
784
1580
  source=state.source,
785
- availability=state.availability,
786
- freshness=state.freshness,
787
- warnings=state.warnings,
1581
+ availability=availability,
1582
+ freshness=freshness,
1583
+ warnings=warnings,
788
1584
  data_version=state.data_version,
789
1585
  last_success_at=state.last_success_at,
790
- capabilities=state.capabilities,
1586
+ capabilities=capabilities,
791
1587
  data=data,
792
1588
  clock_data=state.clock_data,
793
1589
  )
@@ -888,14 +1684,114 @@ def _projects_wire(
888
1684
  }
889
1685
 
890
1686
 
891
- def _codex_entries_from_qualified(entries: Iterable[object]) -> list[CodexEntry]:
892
- """Adapt the one root-qualified accounting read for shipped view kernels."""
1687
+ def _partial_projects_wire(
1688
+ entries: Iterable[object],
1689
+ metadata: Mapping[tuple[str, str], Mapping[str, object]],
1690
+ ) -> dict[str, object]:
1691
+ """Aggregate the qualified subset when older accounting metadata is mixed.
1692
+
1693
+ Rows without a cached conversation/project identity are omitted and remain
1694
+ covered by the Projects-domain warning. Valid projects stay visible; their
1695
+ totals never include an unqualified accounting row.
1696
+ """
1697
+ groups: dict[tuple[str, str], dict[str, object]] = {}
1698
+ for entry in entries:
1699
+ identity = (
1700
+ str(getattr(entry, "source_root_key", "") or ""),
1701
+ str(getattr(entry, "source_path", "") or ""),
1702
+ )
1703
+ row_metadata = metadata.get(identity)
1704
+ project_key = str(row_metadata.get("project_key") or "").strip() if row_metadata else ""
1705
+ project_label = str(row_metadata.get("project_label") or "").strip() if row_metadata else ""
1706
+ if not project_key or not project_label:
1707
+ continue
1708
+ group_key = (identity[0], project_key)
1709
+ group = groups.setdefault(group_key, {
1710
+ "project_key": project_key,
1711
+ "label": project_label,
1712
+ "sessions": set(),
1713
+ "first_seen": getattr(entry, "timestamp"),
1714
+ "last_seen": getattr(entry, "timestamp"),
1715
+ "cost_usd": 0.0,
1716
+ "input_tokens": 0,
1717
+ "cached_input_tokens": 0,
1718
+ "output_tokens": 0,
1719
+ "reasoning_output_tokens": 0,
1720
+ "total_tokens": 0,
1721
+ "models": {},
1722
+ "session_rows": {},
1723
+ })
1724
+ timestamp = getattr(entry, "timestamp")
1725
+ group["first_seen"] = min(group["first_seen"], timestamp)
1726
+ group["last_seen"] = max(group["last_seen"], timestamp)
1727
+ group["sessions"].add(identity)
1728
+ for field in (
1729
+ "cost_usd", "input_tokens", "cached_input_tokens", "output_tokens",
1730
+ "reasoning_output_tokens", "total_tokens",
1731
+ ):
1732
+ group[field] += getattr(entry, field)
1733
+ model = str(getattr(entry, "model", "") or "unknown")
1734
+ model_totals = group["models"].setdefault(model, {
1735
+ "model": model, "cost_usd": 0.0, "input_tokens": 0,
1736
+ "cached_input_tokens": 0, "output_tokens": 0,
1737
+ "reasoning_output_tokens": 0, "total_tokens": 0,
1738
+ })
1739
+ session_totals = group["session_rows"].setdefault(identity, {
1740
+ "label": str(row_metadata.get("title") or "Session"),
1741
+ "last_activity": timestamp.astimezone(UTC).isoformat(),
1742
+ "cost_usd": 0.0, "input_tokens": 0, "cached_input_tokens": 0,
1743
+ "output_tokens": 0, "reasoning_output_tokens": 0, "total_tokens": 0,
1744
+ })
1745
+ if timestamp.astimezone(UTC).isoformat() > session_totals["last_activity"]:
1746
+ session_totals["last_activity"] = timestamp.astimezone(UTC).isoformat()
1747
+ for field in (
1748
+ "cost_usd", "input_tokens", "cached_input_tokens", "output_tokens",
1749
+ "reasoning_output_tokens", "total_tokens",
1750
+ ):
1751
+ value = getattr(entry, field)
1752
+ model_totals[field] += value
1753
+ session_totals[field] += value
1754
+
1755
+ rows = []
1756
+ for (root_key, _project_key), group in groups.items():
1757
+ rows.append({
1758
+ "key": dashboard_resource_key("project", "codex", root_key, group["project_key"]),
1759
+ "source": "codex",
1760
+ "label": group["label"],
1761
+ "session_count": len(group["sessions"]),
1762
+ "first_seen": group["first_seen"].astimezone(UTC).isoformat(),
1763
+ "last_seen": group["last_seen"].astimezone(UTC).isoformat(),
1764
+ "cost_usd": group["cost_usd"],
1765
+ "input_tokens": group["input_tokens"],
1766
+ "cached_input_tokens": group["cached_input_tokens"],
1767
+ "output_tokens": group["output_tokens"],
1768
+ "reasoning_output_tokens": group["reasoning_output_tokens"],
1769
+ "total_tokens": group["total_tokens"],
1770
+ "models": tuple(sorted(
1771
+ group["models"].values(),
1772
+ key=lambda item: (-float(item["cost_usd"]), str(item["model"])),
1773
+ )),
1774
+ "sessions": tuple(sorted(
1775
+ group["session_rows"].values(),
1776
+ key=lambda item: str(item["last_activity"]), reverse=True,
1777
+ )),
1778
+ })
1779
+ rows.sort(key=lambda row: (-float(row["cost_usd"]), str(row["label"]), str(row["key"])))
1780
+ return {
1781
+ "rows": tuple(rows),
1782
+ "total_cost_usd": stable_sum(float(row["cost_usd"]) for row in rows),
1783
+ "total_tokens": sum(int(row["total_tokens"]) for row in rows),
1784
+ }
1785
+
1786
+
1787
+ def _codex_entries_from_accounting(entries: Iterable[object]) -> list[CodexEntry]:
1788
+ """Adapt coordinated accounting rows for the shipped non-project kernels."""
893
1789
  converted: list[CodexEntry] = []
894
1790
  for entry in entries:
895
1791
  source_path = str(getattr(entry, "source_path", "") or "")
896
1792
  session_id = str(getattr(entry, "session_id", "") or "")
897
1793
  if not source_path or not session_id:
898
- raise SourceCapabilityUnavailable("qualified accounting lacks session identity")
1794
+ raise SourceCapabilityUnavailable("Codex accounting lacks session identity")
899
1795
  converted.append(CodexEntry(
900
1796
  timestamp=getattr(entry, "timestamp"),
901
1797
  session_id=session_id,
@@ -910,6 +1806,84 @@ def _codex_entries_from_qualified(entries: Iterable[object]) -> list[CodexEntry]
910
1806
  return converted
911
1807
 
912
1808
 
1809
+ def _codex_entries_from_qualified(entries: Iterable[object]) -> list[CodexEntry]:
1810
+ """Compatibility name retained for the source-detail reader."""
1811
+ return _codex_entries_from_accounting(entries)
1812
+
1813
+
1814
+ def _build_codex_native_weekly_view(
1815
+ stats_conn: sqlite3.Connection,
1816
+ entries: Iterable[object],
1817
+ *,
1818
+ source_root_keys: Iterable[str],
1819
+ active_cycle: CodexCycleBoundary | None,
1820
+ now_utc: dt.datetime,
1821
+ display_tz_name: str | None,
1822
+ speed: str,
1823
+ ) -> CodexWeeklyView:
1824
+ """Aggregate Codex cost into observed native quota-cycle segments."""
1825
+ periods = _codex_weekly_periods(
1826
+ stats_conn,
1827
+ source_root_keys=source_root_keys,
1828
+ active_cycle=active_cycle,
1829
+ )
1830
+ converted: list[CodexEntry] = []
1831
+ bucket_by_entry: dict[int, str] = {}
1832
+ display_tz = ZoneInfo(display_tz_name) if display_tz_name else None
1833
+ labels: dict[str, str] = {}
1834
+ periods_by_bucket: dict[str, CodexWeeklyPeriod] = {}
1835
+ for entry in entries:
1836
+ timestamp = getattr(entry, "timestamp").astimezone(UTC)
1837
+ root_key = str(getattr(entry, "source_root_key", "") or "")
1838
+ period = next((
1839
+ candidate for candidate in periods
1840
+ if root_key in candidate.source_root_keys
1841
+ and candidate.start_at <= timestamp < candidate.end_at
1842
+ ), None)
1843
+ if period is None:
1844
+ continue
1845
+ converted_entry = _codex_entries_from_accounting((entry,))[0]
1846
+ bucket = period.start_at.isoformat()
1847
+ converted.append(converted_entry)
1848
+ bucket_by_entry[id(converted_entry)] = bucket
1849
+ local_start = (
1850
+ period.start_at.astimezone(display_tz)
1851
+ if display_tz is not None else period.start_at.astimezone()
1852
+ )
1853
+ labels[bucket] = local_start.strftime("%m-%d %H:%M")
1854
+ periods_by_bucket[bucket] = period
1855
+
1856
+ rows = _aggregate_codex_buckets(
1857
+ converted,
1858
+ key_fn=lambda entry: bucket_by_entry[id(entry)],
1859
+ speed=speed,
1860
+ )
1861
+ display_rows = tuple(
1862
+ replace(
1863
+ row,
1864
+ bucket=labels[row.bucket],
1865
+ period_start_at=periods_by_bucket[row.bucket].start_at,
1866
+ period_end_at=periods_by_bucket[row.bucket].end_at,
1867
+ used_pct=periods_by_bucket[row.bucket].used_percent,
1868
+ dollar_per_pct=(
1869
+ row.cost_usd / periods_by_bucket[row.bucket].used_percent
1870
+ if periods_by_bucket[row.bucket].used_percent is not None
1871
+ and periods_by_bucket[row.bucket].used_percent > 0
1872
+ else None
1873
+ ),
1874
+ )
1875
+ for row in rows
1876
+ )
1877
+ return CodexWeeklyView(
1878
+ rows=display_rows,
1879
+ total_cost_usd=stable_sum(row.cost_usd for row in display_rows),
1880
+ total_tokens=sum(row.total_tokens for row in display_rows),
1881
+ period_start=(periods[0].start_at if periods else None),
1882
+ period_end=now_utc,
1883
+ display_tz_label=display_tz_name or str(dt.datetime.now().astimezone().tzinfo),
1884
+ )
1885
+
1886
+
913
1887
  def build_codex_source_state(
914
1888
  context: DashboardReadContext,
915
1889
  *,
@@ -938,8 +1912,7 @@ def build_codex_source_state(
938
1912
  coherence = codex_projection_coherence(
939
1913
  context,
940
1914
  )
941
- if not coherence.coherent:
942
- raise CodexProjectionIncoherent(coherence.reason)
1915
+ projection_incoherent = not coherence.coherent
943
1916
  # The cache reader's established report surface treats the ``now`` instant
944
1917
  # as inclusive. The qualified adapter is half-open, so extend only its
945
1918
  # query/result boundary by one microsecond and keep all live budget sums
@@ -949,37 +1922,123 @@ def build_codex_source_state(
949
1922
  if context.codex_budget is not None:
950
1923
  _period, budget_start, _budget_end = _configured_codex_budget_window(context)
951
1924
  accounting_start = min(accounting_start, budget_start)
952
- qualified_entries = load_qualified_codex_entries(
953
- accounting_start,
954
- accounting_end,
955
- speed=context.speed,
956
- sync=False,
1925
+ health = load_codex_project_metadata_health(
957
1926
  cache_conn=context.cache_conn,
1927
+ start=accounting_start,
1928
+ end=accounting_end,
1929
+ )
1930
+ metadata_incomplete = health.incomplete_rows > 0
1931
+ metadata_warning_message = (
1932
+ f"{health.incomplete_rows} Codex accounting row(s) lack project metadata; "
1933
+ "run `cctally cache-sync --source codex --rebuild`."
1934
+ if metadata_incomplete
1935
+ else "Codex project metadata could not be read; "
1936
+ "run `cctally cache-sync --source codex --rebuild`."
958
1937
  )
959
- budget_entries = _codex_entries_from_qualified(qualified_entries)
960
- visible_qualified_entries = tuple(
961
- entry for entry in qualified_entries
1938
+ qualified_entries: tuple[object, ...] = ()
1939
+ if not metadata_incomplete:
1940
+ try:
1941
+ qualified_entries = load_qualified_codex_entries(
1942
+ accounting_start,
1943
+ accounting_end,
1944
+ speed=context.speed,
1945
+ sync=False,
1946
+ cache_conn=context.cache_conn,
1947
+ )
1948
+ accounting_entries: tuple[object, ...] = qualified_entries
1949
+ except QualifiedMetadataUnavailable:
1950
+ # A cached read must be internally coherent, but retain accounting
1951
+ # once if a defensive race or malformed row violates that premise.
1952
+ _lib_log.get_logger("dashboard").warning(
1953
+ "Codex qualified metadata read became unavailable; using cache-only accounting fallback"
1954
+ )
1955
+ metadata_incomplete = True
1956
+ accounting_entries = load_cached_rooted_codex_accounting_entries(
1957
+ accounting_start,
1958
+ accounting_end,
1959
+ speed=context.speed,
1960
+ cache_conn=context.cache_conn,
1961
+ )
1962
+ else:
1963
+ accounting_entries = load_cached_rooted_codex_accounting_entries(
1964
+ accounting_start,
1965
+ accounting_end,
1966
+ speed=context.speed,
1967
+ cache_conn=context.cache_conn,
1968
+ )
1969
+ budget_entries = _codex_entries_from_accounting(accounting_entries)
1970
+ cycle_reason: str | None = None
1971
+ try:
1972
+ cycle = _resolve_codex_weekly_cycle(quota_observations, context.now_utc)
1973
+ except CodexCycleUnavailable as exc:
1974
+ cycle = None
1975
+ cycle_reason = exc.reason
1976
+ cycle_failure = cycle is None and has_cached_codex_accounting_entries(
1977
+ cache_conn=context.cache_conn,
1978
+ )
1979
+ hero_failure = projection_incoherent or cycle_failure
1980
+ if cycle is None or hero_failure:
1981
+ cycle_entries: list[CodexEntry] = []
1982
+ cycle_cost_usd: float | None = None if hero_failure else 0.0
1983
+ else:
1984
+ cycle_end = min(accounting_end, cycle.resets_at)
1985
+ cycle_rows = load_cached_rooted_codex_accounting_entries(
1986
+ cycle.start_at,
1987
+ cycle_end,
1988
+ speed=context.speed,
1989
+ cache_conn=context.cache_conn,
1990
+ source_root_keys=cycle.source_root_keys,
1991
+ )
1992
+ cycle_entries = _codex_entries_from_accounting(cycle_rows)
1993
+ cycle_cost_usd = build_codex_daily_view(
1994
+ cycle_entries,
1995
+ now_utc=context.now_utc,
1996
+ tz_name=context.display_tz_name,
1997
+ speed=context.speed,
1998
+ ).total_cost_usd
1999
+ visible_accounting_entries = tuple(
2000
+ entry for entry in accounting_entries
962
2001
  if context.range_start <= getattr(entry, "timestamp").astimezone(UTC) < accounting_end
963
2002
  )
964
- entries = _codex_entries_from_qualified(visible_qualified_entries)
2003
+ entries = _codex_entries_from_accounting(visible_accounting_entries)
965
2004
  daily = build_codex_daily_view(
966
2005
  entries, now_utc=context.now_utc, tz_name=context.display_tz_name, speed=context.speed,
967
2006
  )
968
2007
  monthly = build_codex_monthly_view(
969
2008
  entries, now_utc=context.now_utc, tz_name=context.display_tz_name, speed=context.speed,
970
2009
  )
971
- weekly = build_codex_weekly_view(
972
- entries,
2010
+ weekly = _build_codex_native_weekly_view(
2011
+ context.stats_conn,
2012
+ visible_accounting_entries,
2013
+ source_root_keys=active_roots,
2014
+ active_cycle=cycle,
973
2015
  now_utc=context.now_utc,
974
- tz_name=context.display_tz_name,
975
- week_start_idx=context.week_start_idx,
2016
+ display_tz_name=context.display_tz_name,
976
2017
  speed=context.speed,
977
2018
  )
978
- sessions = build_codex_session_view(
979
- entries, now_utc=context.now_utc, tz_name=context.display_tz_name, speed=context.speed,
2019
+ sessions = (
2020
+ build_rooted_codex_session_view(
2021
+ visible_accounting_entries,
2022
+ now_utc=context.now_utc,
2023
+ tz_name=context.display_tz_name,
2024
+ speed=context.speed,
2025
+ )
2026
+ if metadata_incomplete else build_codex_session_view(
2027
+ entries, now_utc=context.now_utc, tz_name=context.display_tz_name, speed=context.speed,
2028
+ )
2029
+ )
2030
+ quota = _quota_read_model(
2031
+ context,
2032
+ quota_observations,
2033
+ accounting_entries=visible_accounting_entries,
2034
+ )
2035
+ quota_blocks = _quota_wire(
2036
+ context.stats_conn,
2037
+ accounting_entries=visible_accounting_entries,
2038
+ cycle=cycle,
2039
+ now_utc=context.now_utc,
2040
+ display_tz_name=context.display_tz_name,
980
2041
  )
981
- quota = _quota_read_model(context, quota_observations)
982
- quota_blocks = _quota_wire(context.stats_conn)
983
2042
  quota = {**quota, "blocks": quota_blocks}
984
2043
  budget_rows = _budget_wire(context.stats_conn)
985
2044
  projected_budget_rows = _projected_budget_wire(context.stats_conn)
@@ -987,45 +2046,102 @@ def build_codex_source_state(
987
2046
  configured_budget = _configured_codex_budget_status(
988
2047
  context, budget_entries, cost_events=budget_cost_events,
989
2048
  )
990
- projects = _projects_wire(
991
- context,
992
- quota_observations,
993
- visible_qualified_entries,
994
- accounting_end=accounting_end,
2049
+ conversation_metadata = _codex_conversation_metadata(context.cache_conn)
2050
+ cache_report = _codex_cache_report_wire(
2051
+ visible_accounting_entries,
2052
+ metadata=conversation_metadata,
2053
+ now_utc=context.now_utc,
2054
+ display_tz_name=context.display_tz_name,
2055
+ speed=context.speed,
2056
+ anomaly_threshold_pp=context.cache_report_anomaly_threshold_pp,
2057
+ )
2058
+ projects = (
2059
+ _partial_projects_wire(visible_accounting_entries, conversation_metadata)
2060
+ if metadata_incomplete else _projects_wire(
2061
+ context,
2062
+ quota_observations,
2063
+ visible_accounting_entries,
2064
+ accounting_end=accounting_end,
2065
+ )
995
2066
  )
996
2067
  alerts = _alerts_wire(context.stats_conn)
997
- availability = "ok" if (entries or quota_blocks or budget_rows) else "empty"
998
- total_input = sum(entry.input_tokens for entry in entries)
999
- total_cached = sum(entry.cached_input_tokens for entry in entries)
1000
- total_output = sum(entry.output_tokens for entry in entries)
1001
- total_reasoning = sum(entry.reasoning_output_tokens for entry in entries)
2068
+ availability = (
2069
+ "partial" if metadata_incomplete or hero_failure
2070
+ else ("ok" if (entries or quota_blocks or budget_rows) else "empty")
2071
+ )
2072
+ hero_input = None if hero_failure else sum(entry.input_tokens for entry in cycle_entries)
2073
+ hero_cached = None if hero_failure else sum(entry.cached_input_tokens for entry in cycle_entries)
2074
+ hero_output = None if hero_failure else sum(entry.output_tokens for entry in cycle_entries)
2075
+ hero_reasoning = None if hero_failure else sum(entry.reasoning_output_tokens for entry in cycle_entries)
2076
+ hero_total = None if hero_failure else sum(entry.total_tokens for entry in cycle_entries)
2077
+ warnings: list[SourceDashboardWarning] = []
2078
+ if metadata_incomplete:
2079
+ warnings.append(SourceDashboardWarning(
2080
+ "codex_metadata_incomplete",
2081
+ metadata_warning_message,
2082
+ "projects",
2083
+ ))
2084
+ if projection_incoherent:
2085
+ warnings.append(SourceDashboardWarning(
2086
+ "codex_projection_incoherent",
2087
+ "Codex quota projection is unavailable.",
2088
+ "hero",
2089
+ ))
2090
+ if cycle_failure:
2091
+ warnings.append(SourceDashboardWarning(
2092
+ "codex_cycle_unavailable",
2093
+ "Codex native reset cycle is unavailable.",
2094
+ "hero",
2095
+ ))
1002
2096
  return SourceDashboardState(
1003
2097
  source="codex",
1004
2098
  availability=availability,
1005
- freshness="fresh",
1006
- warnings=(),
2099
+ freshness=("stale" if cycle_reason == "stale" else "fresh"),
2100
+ warnings=tuple(warnings),
1007
2101
  data_version=data_version,
1008
2102
  last_success_at=context.now_utc,
1009
2103
  capabilities={
1010
- "hero": CapabilityRecord("supported", "calendar-accounting"),
2104
+ "hero": (
2105
+ CapabilityRecord(
2106
+ "unavailable",
2107
+ (
2108
+ "projection-incoherent" if projection_incoherent
2109
+ else "missing-or-conflicting-native-cycle"
2110
+ ),
2111
+ )
2112
+ if hero_failure
2113
+ else CapabilityRecord("supported", "native-reset-cycle")
2114
+ ),
1011
2115
  "daily": CapabilityRecord("supported", "calendar-day"),
1012
2116
  "monthly": CapabilityRecord("supported", "calendar-month"),
1013
- "weekly": CapabilityRecord("supported", "calendar-week"),
2117
+ "weekly": CapabilityRecord("derived", "native-reset-cycles"),
1014
2118
  "sessions": CapabilityRecord("supported", "inclusive-input-tokens"),
1015
2119
  "forensics": CapabilityRecord("supported", "inclusive-input-token-reuse"),
1016
2120
  "quota": CapabilityRecord("derived", "native-windows"),
1017
2121
  "budget": CapabilityRecord("supported", "calendar-period"),
1018
- "projects": CapabilityRecord("supported", "qualified-attribution"),
2122
+ "projects": (
2123
+ CapabilityRecord("supported", "conversation-metadata-partial")
2124
+ if metadata_incomplete
2125
+ else CapabilityRecord("supported", "qualified-attribution")
2126
+ ),
1019
2127
  "alerts": CapabilityRecord("supported", "provider-native"),
1020
2128
  },
1021
2129
  data={
1022
2130
  "hero": {
1023
- "cost_usd": daily.total_cost_usd,
1024
- "input_tokens": total_input,
1025
- "cached_input_tokens": total_cached,
1026
- "output_tokens": total_output,
1027
- "reasoning_output_tokens": total_reasoning,
1028
- "total_tokens": daily.total_tokens,
2131
+ "cost_usd": cycle_cost_usd,
2132
+ "input_tokens": hero_input,
2133
+ "cached_input_tokens": hero_cached,
2134
+ "output_tokens": hero_output,
2135
+ "reasoning_output_tokens": hero_reasoning,
2136
+ "total_tokens": hero_total,
2137
+ "cycle": (
2138
+ {
2139
+ "window_minutes": cycle.window_minutes,
2140
+ "start_at": cycle.start_at.astimezone(UTC).isoformat(),
2141
+ "resets_at": cycle.resets_at.astimezone(UTC).isoformat(),
2142
+ }
2143
+ if cycle is not None and not hero_failure else None
2144
+ ),
1029
2145
  "quota": quota["summary"],
1030
2146
  "budget": configured_budget,
1031
2147
  "alerts": {"count": len(alerts)},
@@ -1035,7 +2151,7 @@ def build_codex_source_state(
1035
2151
  "monthly": _period_wire(monthly),
1036
2152
  "weekly": _period_wire(weekly),
1037
2153
  },
1038
- "sessions": _session_wire(sessions),
2154
+ "sessions": _session_wire(sessions, metadata=conversation_metadata),
1039
2155
  "quota": quota,
1040
2156
  "budget": {
1041
2157
  "status": configured_budget,
@@ -1043,7 +2159,12 @@ def build_codex_source_state(
1043
2159
  "projected": projected_budget_rows,
1044
2160
  },
1045
2161
  "projects": projects,
1046
- "alerts": {"rows": alerts},
2162
+ "alerts": {
2163
+ "rows": alerts,
2164
+ "actual_thresholds": context.codex_quota_actual_thresholds,
2165
+ "projected_thresholds": context.codex_quota_projected_thresholds,
2166
+ },
2167
+ "cache_report": cache_report,
1047
2168
  },
1048
2169
  clock_data={"codex_budget_cost_events": budget_cost_events},
1049
2170
  )