cctally 1.69.0 → 1.69.2

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.
@@ -0,0 +1,1049 @@
1
+ """Dashboard-only, cache-backed provider read models for #294 S4."""
2
+ from __future__ import annotations
3
+
4
+ import datetime as dt
5
+ import hashlib
6
+ import json
7
+ import sqlite3
8
+ import sys
9
+ from collections.abc import Mapping
10
+ from dataclasses import dataclass
11
+ from types import MappingProxyType
12
+ from typing import Any, Iterable
13
+ from zoneinfo import ZoneInfo
14
+
15
+ from _cctally_core import get_week_start_name
16
+ from _cctally_quota import (
17
+ codex_physical_mutation_seq,
18
+ load_codex_quota_observations,
19
+ load_codex_quota_projection_certificate,
20
+ )
21
+ from _cctally_source_analytics import load_qualified_codex_entries
22
+ from _lib_dashboard_sources import (
23
+ CapabilityRecord,
24
+ ProjectionCoherence,
25
+ SourceDashboardState,
26
+ assess_codex_projection_coherence,
27
+ dashboard_resource_key,
28
+ )
29
+ from _lib_quota import (
30
+ build_blocks,
31
+ build_history,
32
+ forecast_quota,
33
+ percent_milestones,
34
+ quota_freshness,
35
+ select_baseline,
36
+ )
37
+ from _lib_jsonl import CodexEntry
38
+ from _lib_source_analytics import build_codex_project_result
39
+ from _lib_view_models import (
40
+ build_codex_daily_view,
41
+ build_codex_monthly_view,
42
+ build_codex_session_view,
43
+ build_codex_weekly_view,
44
+ )
45
+
46
+
47
+ UTC = dt.timezone.utc
48
+ SOURCE_HISTORY_LIMIT = 250
49
+ DASHBOARD_QUOTA_OBSERVATION_LIMIT = 1000
50
+ DASHBOARD_QUOTA_RECENT_DAYS = 35
51
+
52
+
53
+ class CodexProjectionIncoherent(RuntimeError):
54
+ """Physical cache and S2 interpreted quota state cannot be mixed."""
55
+
56
+ def __init__(self, reason: str | None) -> None:
57
+ self.reason = reason
58
+ super().__init__("Codex quota projection is incoherent")
59
+
60
+
61
+ class SourceCapabilityUnavailable(ValueError):
62
+ """A source is not a physical owner or cannot serve a resource domain."""
63
+
64
+
65
+ class SourceResourceNotFound(LookupError):
66
+ """A valid opaque resource key has no row in its provider state."""
67
+
68
+
69
+ @dataclass(frozen=True)
70
+ class DashboardSourceSemantics:
71
+ """One canonical CLI configuration resolution for a dashboard read.
72
+
73
+ The source bundle must use the same effective Codex tier and calendar-week
74
+ anchor as the CLI. Keeping that resolution in one small immutable object
75
+ also makes every render-affecting configuration input explicit in the
76
+ provider identity rather than accidentally treating it as an idle tick.
77
+ """
78
+
79
+ display_tz_name: str | None
80
+ week_start_name: str
81
+ week_start_idx: int
82
+ speed: str
83
+ codex_budget: Mapping[str, object] | None
84
+ claude_identity: str
85
+ codex_identity: str
86
+
87
+
88
+ def resolve_dashboard_source_semantics(
89
+ config: Mapping[str, object] | None,
90
+ *,
91
+ display_tz_name: str | None,
92
+ ) -> DashboardSourceSemantics:
93
+ """Resolve dashboard semantics through the shipped CLI kernels.
94
+
95
+ ``_resolve_codex_speed('auto')`` is intentionally the only tier resolver:
96
+ it preserves the CLI's all-$CODEX_HOME fast-service-tier behavior. The
97
+ weekly index comes from the same ``get_week_start_name``/``WEEKDAY_MAP``
98
+ pair used by the report and budget command surfaces.
99
+ """
100
+ c = sys.modules["cctally"]
101
+ raw_config = dict(config or {})
102
+ week_start_name = get_week_start_name(raw_config)
103
+ week_start_idx = c.WEEKDAY_MAP[week_start_name]
104
+ speed = c._resolve_codex_speed("auto")
105
+ budget_config = c._get_budget_config(raw_config)
106
+ raw_codex_budget = budget_config.get("codex")
107
+ codex_budget = (
108
+ MappingProxyType(dict(raw_codex_budget))
109
+ if isinstance(raw_codex_budget, Mapping) else None
110
+ )
111
+ # The legacy Claude projection owns the non-Codex config surface. Codex
112
+ # budget semantics are explicitly excluded so changing them cannot evict a
113
+ # byte-identical Claude source object.
114
+ claude_config = dict(raw_config)
115
+ raw_budget = claude_config.get("budget")
116
+ if isinstance(raw_budget, Mapping):
117
+ claude_budget = dict(raw_budget)
118
+ claude_budget.pop("codex", None)
119
+ if claude_budget:
120
+ claude_config["budget"] = claude_budget
121
+ else:
122
+ claude_config.pop("budget", None)
123
+ claude_identity_payload = {
124
+ "display_tz_name": display_tz_name,
125
+ "render_config": claude_config,
126
+ }
127
+ codex_identity_payload = {
128
+ "codex_budget": dict(codex_budget) if codex_budget is not None else None,
129
+ "display_tz_name": display_tz_name,
130
+ "speed": speed,
131
+ "week_start_name": week_start_name,
132
+ }
133
+ claude_identity = hashlib.sha256(json.dumps(
134
+ claude_identity_payload, sort_keys=True, separators=(",", ":"), allow_nan=False,
135
+ ).encode("utf-8")).hexdigest()[:24]
136
+ codex_identity = hashlib.sha256(json.dumps(
137
+ codex_identity_payload, sort_keys=True, separators=(",", ":"), allow_nan=False,
138
+ ).encode("utf-8")).hexdigest()[:24]
139
+ return DashboardSourceSemantics(
140
+ display_tz_name=display_tz_name,
141
+ week_start_name=week_start_name,
142
+ week_start_idx=week_start_idx,
143
+ speed=speed,
144
+ codex_budget=codex_budget,
145
+ claude_identity=claude_identity,
146
+ codex_identity=codex_identity,
147
+ )
148
+
149
+
150
+ @dataclass(frozen=True)
151
+ class DashboardReadContext:
152
+ """Already-open, coordinated-ingest database inputs for one provider read."""
153
+
154
+ cache_conn: sqlite3.Connection
155
+ stats_conn: sqlite3.Connection
156
+ range_start: dt.datetime
157
+ now_utc: dt.datetime
158
+ display_tz_name: str | None
159
+ week_start_idx: int = 0
160
+ week_start_name: str = "monday"
161
+ speed: str = "standard"
162
+ codex_budget: Mapping[str, object] | None = None
163
+
164
+ def __post_init__(self) -> None:
165
+ for name in ("range_start", "now_utc"):
166
+ value = getattr(self, name)
167
+ if value.tzinfo is None or value.utcoffset() is None:
168
+ raise ValueError(f"{name} must be timezone-aware")
169
+ object.__setattr__(self, name, value.astimezone(UTC))
170
+ if self.now_utc < self.range_start:
171
+ raise ValueError("now_utc must not precede range_start")
172
+ if not isinstance(self.week_start_name, str) or not self.week_start_name:
173
+ raise ValueError("week_start_name must be a non-empty string")
174
+ if self.codex_budget is not None and not isinstance(self.codex_budget, Mapping):
175
+ raise ValueError("codex_budget must be a mapping or None")
176
+
177
+
178
+ _RESOURCE_ROWS = {
179
+ "session": ("sessions", "rows"),
180
+ "project": ("projects", "rows"),
181
+ "block": ("quota", "blocks"),
182
+ }
183
+
184
+
185
+ def _public_copy(value: object) -> object:
186
+ """Detach a bounded source row from its immutable published state."""
187
+ if isinstance(value, Mapping):
188
+ return {str(key): _public_copy(item) for key, item in value.items()}
189
+ if isinstance(value, tuple):
190
+ return [_public_copy(item) for item in value]
191
+ return value
192
+
193
+
194
+ def source_detail_lookup(bundle: object, source: str, resource: str, key: str) -> dict[str, object]:
195
+ """Find one provider-owned opaque row without I/O, ingest, or fallback.
196
+
197
+ The handler has already parsed the fixed route grammar. This adapter only
198
+ reads the frozen bundle published by the dashboard owner thread, so a
199
+ request cannot accidentally trigger cache sync, rollout parsing, or a
200
+ Claude fallback for a Codex key.
201
+ """
202
+ if source not in ("claude", "codex") or resource not in _RESOURCE_ROWS:
203
+ raise SourceCapabilityUnavailable()
204
+ try:
205
+ state = bundle.sources[source]
206
+ data = state.data
207
+ except (AttributeError, KeyError, TypeError) as exc:
208
+ raise SourceCapabilityUnavailable() from exc
209
+ if state.availability == "unavailable" or not isinstance(data, Mapping):
210
+ raise SourceCapabilityUnavailable()
211
+ domain, rows_key = _RESOURCE_ROWS[resource]
212
+ try:
213
+ rows = data[domain][rows_key]
214
+ except (KeyError, TypeError) as exc:
215
+ raise SourceCapabilityUnavailable() from exc
216
+ for row in rows:
217
+ if isinstance(row, Mapping) and row.get("key") == key:
218
+ return _public_copy(row) # type: ignore[return-value]
219
+ raise SourceResourceNotFound()
220
+
221
+
222
+ def codex_projection_coherence(
223
+ context: DashboardReadContext,
224
+ ) -> ProjectionCoherence:
225
+ """Check every active root against the post-reconciliation certificate.
226
+
227
+ The source adapter is intentionally a reader: it never reconciles or
228
+ mutates either database. The certificate is stamped from S2's exact full
229
+ physical signature only after its stats transaction commits, and its cache
230
+ sequence must still match before presentation can use it.
231
+ """
232
+ try:
233
+ active_roots = tuple(sorted(
234
+ str(row[0]) for row in context.cache_conn.execute(
235
+ "SELECT source_root_key FROM codex_source_roots"
236
+ )
237
+ ))
238
+ if not active_roots:
239
+ return ProjectionCoherence(True)
240
+ certificate = load_codex_quota_projection_certificate(context.cache_conn)
241
+ if certificate is None or certificate[0] != codex_physical_mutation_seq(context.cache_conn):
242
+ return ProjectionCoherence(False, "projection_certificate_stale")
243
+ resolved_physical_signatures = dict(certificate[1])
244
+ projection_signatures = {
245
+ str(root_key): str(signature)
246
+ for root_key, signature in context.stats_conn.execute(
247
+ "SELECT source_root_key, physical_signature "
248
+ "FROM quota_projection_state"
249
+ )
250
+ }
251
+ except (sqlite3.Error, OSError, ValueError, TypeError):
252
+ return ProjectionCoherence(False, "projection_read_failed")
253
+ return assess_codex_projection_coherence(
254
+ active_root_keys=active_roots,
255
+ physical_signatures=resolved_physical_signatures,
256
+ projection_signatures=projection_signatures,
257
+ )
258
+
259
+
260
+ def _codex_budget_cost_events(
261
+ context: DashboardReadContext,
262
+ entries: Iterable[object],
263
+ ) -> tuple[tuple[dt.datetime, float], ...]:
264
+ """Freeze every configured-window cost event for exact idle pace updates."""
265
+ if context.codex_budget is None:
266
+ return ()
267
+ _period, start_at, end_at = _configured_codex_budget_window(context)
268
+ c = sys.modules["cctally"]
269
+ events: list[tuple[dt.datetime, float]] = []
270
+ for entry in entries:
271
+ timestamp = getattr(entry, "timestamp", None)
272
+ if not isinstance(timestamp, dt.datetime):
273
+ continue
274
+ timestamp = timestamp.astimezone(UTC)
275
+ if not start_at <= timestamp < end_at:
276
+ continue
277
+ events.append((
278
+ timestamp,
279
+ c._calculate_codex_entry_cost(
280
+ str(getattr(entry, "model")),
281
+ int(getattr(entry, "input_tokens")),
282
+ int(getattr(entry, "cached_input_tokens")),
283
+ int(getattr(entry, "output_tokens")),
284
+ int(getattr(entry, "reasoning_output_tokens")),
285
+ speed=context.speed,
286
+ ),
287
+ ))
288
+ return tuple(events)
289
+
290
+
291
+ def _bucket_wire(bucket: Any) -> dict[str, object]:
292
+ return {
293
+ "label": bucket.bucket,
294
+ "cost_usd": bucket.cost_usd,
295
+ "input_tokens": bucket.input_tokens,
296
+ "cached_input_tokens": bucket.cached_input_tokens,
297
+ "output_tokens": bucket.output_tokens,
298
+ "reasoning_output_tokens": bucket.reasoning_output_tokens,
299
+ "total_tokens": bucket.total_tokens,
300
+ "models": tuple(bucket.models),
301
+ }
302
+
303
+
304
+ def _period_wire(view: Any) -> dict[str, object]:
305
+ return {
306
+ "rows": tuple(_bucket_wire(row) for row in view.rows),
307
+ "total_cost_usd": view.total_cost_usd,
308
+ "total_tokens": view.total_tokens,
309
+ "display_tz": view.display_tz_label,
310
+ }
311
+
312
+
313
+ def _session_wire(view: Any) -> dict[str, object]:
314
+ rows = []
315
+ for ordinal, row in enumerate(view.rows, start=1):
316
+ # The Codex session aggregator intentionally splits equal relative
317
+ # session paths from distinct $CODEX_HOME roots. The opaque detail
318
+ # key must use that same grouping identity or two visible rows route
319
+ # to one another's detail payload.
320
+ root_identity = row.codex_root or "single-root"
321
+ rows.append({
322
+ "key": dashboard_resource_key(
323
+ "session", "codex", root_identity, row.session_id_path,
324
+ ),
325
+ "source": "codex",
326
+ "label": f"Session {ordinal}",
327
+ "last_activity": row.last_activity.astimezone(UTC).isoformat(),
328
+ "cost_usd": row.cost_usd,
329
+ "input_tokens": row.input_tokens,
330
+ "cached_input_tokens": row.cached_input_tokens,
331
+ "output_tokens": row.output_tokens,
332
+ "reasoning_output_tokens": row.reasoning_output_tokens,
333
+ "total_tokens": row.total_tokens,
334
+ "models": tuple(row.models),
335
+ })
336
+ return {
337
+ "rows": tuple(rows),
338
+ "total_sessions": view.total_sessions,
339
+ "total_cost_usd": view.total_cost_usd,
340
+ "total_tokens": view.total_tokens,
341
+ }
342
+
343
+
344
+ def _quota_wire(stats_conn: sqlite3.Connection) -> tuple[dict[str, object], ...]:
345
+ try:
346
+ rows = stats_conn.execute(
347
+ "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' "
350
+ "ORDER BY resets_at_utc DESC, source_root_key, logical_limit_key, observed_slot "
351
+ "LIMIT ?",
352
+ (SOURCE_HISTORY_LIMIT,),
353
+ ).fetchall()
354
+ except sqlite3.Error:
355
+ 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 (
366
+ root_key, logical_limit_key, observed_slot, window_minutes,
367
+ limit_name, resets_at, current_percent, orphaned_at,
368
+ ) in rows)
369
+
370
+
371
+ def _budget_wire(stats_conn: sqlite3.Connection) -> tuple[dict[str, object], ...]:
372
+ try:
373
+ rows = stats_conn.execute(
374
+ "SELECT period_start_at, period, threshold, budget_usd, spent_usd, "
375
+ "consumption_pct FROM budget_milestones WHERE vendor='codex' "
376
+ "ORDER BY period_start_at DESC, threshold DESC LIMIT ?",
377
+ (SOURCE_HISTORY_LIMIT,),
378
+ ).fetchall()
379
+ except sqlite3.Error:
380
+ return ()
381
+ return tuple({
382
+ "period_start_at": period_start_at,
383
+ "period": period,
384
+ "threshold": threshold,
385
+ "budget_usd": budget_usd,
386
+ "spent_usd": spent_usd,
387
+ "consumption_pct": consumption_pct,
388
+ } for period_start_at, period, threshold, budget_usd, spent_usd, consumption_pct in rows)
389
+
390
+
391
+ def _projected_budget_wire(stats_conn: sqlite3.Connection) -> tuple[dict[str, object], ...]:
392
+ try:
393
+ rows = stats_conn.execute(
394
+ "SELECT period, threshold, projected_value, denominator, crossed_at_utc, alerted_at "
395
+ "FROM projected_milestones WHERE metric='codex_budget_usd' "
396
+ "ORDER BY crossed_at_utc DESC, threshold DESC LIMIT ?",
397
+ (SOURCE_HISTORY_LIMIT,),
398
+ ).fetchall()
399
+ except sqlite3.Error:
400
+ return ()
401
+ return tuple({
402
+ "period": period,
403
+ "threshold": threshold,
404
+ "projected_value": projected_value,
405
+ "denominator": denominator,
406
+ "crossed_at": crossed_at,
407
+ "alerted_at": alerted_at,
408
+ } for period, threshold, projected_value, denominator, crossed_at, alerted_at in rows)
409
+
410
+
411
+ def _configured_codex_budget_status(
412
+ context: DashboardReadContext,
413
+ entries: Iterable[object],
414
+ *,
415
+ cost_events: tuple[tuple[dt.datetime, float], ...] | None = None,
416
+ ) -> dict[str, object] | None:
417
+ """Compute the live configured Codex budget from the coordinated entries.
418
+
419
+ Durable milestone rows are alert history, not the current budget status.
420
+ This reuses the CLI's calendar-window and ``BudgetInputs``/status kernels
421
+ while deliberately keeping the accounting read on the caller-owned cache
422
+ snapshot.
423
+ """
424
+ config = context.codex_budget
425
+ if config is None:
426
+ return None
427
+ c = sys.modules["cctally"]
428
+ period, start_at, end_at = _configured_codex_budget_window(context)
429
+
430
+ resolved_events = cost_events if cost_events is not None else _codex_budget_cost_events(
431
+ context, entries,
432
+ )
433
+
434
+ def _sum_cost(start: dt.datetime, end: dt.datetime) -> float:
435
+ return sum(cost for timestamp, cost in resolved_events if start <= timestamp < end)
436
+
437
+ recent_start = max(start_at, context.now_utc - dt.timedelta(hours=24))
438
+ inputs = c.BudgetInputs(
439
+ target_usd=float(config["amount_usd"]),
440
+ spent_usd=_sum_cost(start_at, context.now_utc),
441
+ recent_24h_usd=_sum_cost(recent_start, context.now_utc),
442
+ week_start_at=start_at,
443
+ week_end_at=end_at,
444
+ now=context.now_utc,
445
+ alert_thresholds=tuple(config["alert_thresholds"]),
446
+ )
447
+ status = c.compute_budget_status(inputs)
448
+ return {
449
+ "period": period,
450
+ "budget_usd": inputs.target_usd,
451
+ "spent_usd": status.spent_usd,
452
+ "remaining_usd": status.remaining_usd,
453
+ "consumption_pct": status.consumption_pct,
454
+ "verdict": status.verdict,
455
+ "low_confidence": status.low_confidence,
456
+ "window_start_at": start_at.astimezone(UTC).isoformat(),
457
+ "window_end_at": end_at.astimezone(UTC).isoformat(),
458
+ "recent_24h_usd": inputs.recent_24h_usd,
459
+ "alert_thresholds": inputs.alert_thresholds,
460
+ "pace": {
461
+ "daily_usd": status.daily_pace_usd,
462
+ "projected_low_usd": status.projected_eow_low_usd,
463
+ "projected_high_usd": status.projected_eow_high_usd,
464
+ "week_avg_projection_usd": status.week_avg_projection_usd,
465
+ },
466
+ }
467
+
468
+
469
+ def _configured_codex_budget_window(
470
+ context: DashboardReadContext,
471
+ ) -> tuple[str, dt.datetime, dt.datetime]:
472
+ """Resolve the configured Codex accounting window through the CLI kernel."""
473
+ config = context.codex_budget
474
+ if config is None:
475
+ raise ValueError("Codex budget is not configured")
476
+ c = sys.modules["cctally"]
477
+ period = str(config["period"])
478
+ tz = ZoneInfo(context.display_tz_name) if context.display_tz_name else None
479
+ forecast = c._load_sibling("_cctally_forecast")
480
+ start_at, end_at = forecast._resolve_calendar_window(
481
+ period,
482
+ context.now_utc,
483
+ {"collector": {"week_start": context.week_start_name}},
484
+ tz,
485
+ )
486
+ return period, start_at.astimezone(UTC), end_at.astimezone(UTC)
487
+
488
+
489
+ def _quota_read_model(
490
+ context: DashboardReadContext,
491
+ observations: Iterable[object],
492
+ ) -> dict[str, object]:
493
+ """Use S2's pure history/block/forecast kernels over cache evidence."""
494
+ quota_observations = tuple(observations)
495
+ histories = build_history(quota_observations)
496
+ blocks = build_blocks(quota_observations)
497
+ history_rows: list[dict[str, object]] = []
498
+ milestone_rows: list[dict[str, object]] = []
499
+ active_rows: list[dict[str, object]] = []
500
+ for history in histories:
501
+ identity = history.identity
502
+ key_parts = (
503
+ identity.source_root_key,
504
+ identity.logical_limit_key,
505
+ identity.observed_slot,
506
+ identity.window_minutes,
507
+ )
508
+ baseline = select_baseline(history.observations, context.now_utc)
509
+ freshness = quota_freshness(history.physical_observations, context.now_utc)
510
+ forecast = forecast_quota(history.physical_observations, context.now_utc)
511
+ history_rows.append({
512
+ "key": dashboard_resource_key("quota", "codex", *key_parts),
513
+ "source": "codex",
514
+ "label": identity.limit_name or "Codex quota",
515
+ "observed_slot": identity.observed_slot,
516
+ "window_minutes": identity.window_minutes,
517
+ "current_percent": baseline.used_percent if baseline is not None else None,
518
+ "captured_at": (
519
+ freshness.captured_at.astimezone(UTC).isoformat()
520
+ if freshness.captured_at is not None else None
521
+ ),
522
+ "freshness": freshness.state,
523
+ "stale_after_seconds": freshness.stale_after_seconds,
524
+ "forecast": {
525
+ "status": forecast.status,
526
+ "current_percent": forecast.current_percent,
527
+ "rate_percent_per_hour": forecast.rate_percent_per_hour,
528
+ "projected_percent": forecast.projected_percent,
529
+ "resets_at": forecast.resets_at.astimezone(UTC).isoformat() if forecast.resets_at else None,
530
+ "remaining_seconds": forecast.remaining_seconds,
531
+ "sample_count": forecast.sample_count,
532
+ "sample_span_seconds": forecast.sample_span_seconds,
533
+ "confidence": forecast.confidence,
534
+ },
535
+ })
536
+ if baseline is not None and baseline.resets_at > context.now_utc:
537
+ active_rows.append({
538
+ "key": dashboard_resource_key("quota", "codex", *key_parts),
539
+ "current_percent": baseline.used_percent,
540
+ "captured_at": baseline.captured_at.astimezone(UTC).isoformat(),
541
+ "resets_at": baseline.resets_at.astimezone(UTC).isoformat(),
542
+ "freshness": freshness.state,
543
+ "stale_after_seconds": freshness.stale_after_seconds,
544
+ })
545
+ for block in blocks:
546
+ identity = block.identity
547
+ block_parts = (
548
+ identity.source_root_key,
549
+ identity.logical_limit_key,
550
+ identity.observed_slot,
551
+ identity.window_minutes,
552
+ block.resets_at.astimezone(UTC).isoformat(),
553
+ )
554
+ for milestone in percent_milestones(block):
555
+ milestone_rows.append({
556
+ "key": dashboard_resource_key(
557
+ "quota_milestone", "codex", *block_parts,
558
+ milestone.percent, milestone.captured_at.astimezone(UTC).isoformat(),
559
+ ),
560
+ "source": "codex",
561
+ "block_key": dashboard_resource_key("block", "codex", *block_parts),
562
+ "percent": milestone.percent,
563
+ "captured_at": milestone.captured_at.astimezone(UTC).isoformat(),
564
+ })
565
+ latest_percent = max(
566
+ (float(row["current_percent"]) for row in active_rows), default=None,
567
+ )
568
+ active_freshness = (
569
+ "fresh" if active_rows and all(row["freshness"] == "fresh" for row in active_rows)
570
+ else ("unavailable" if not active_rows else "stale")
571
+ )
572
+ # Active identities are presentation-critical. Keep them ahead of
573
+ # inactive retained history before enforcing the public cardinality cap.
574
+ active_keys = {str(row["key"]) for row in active_rows}
575
+ history_rows.sort(key=lambda row: (str(row["key"]) not in active_keys, str(row["key"])))
576
+ history_rows = history_rows[:SOURCE_HISTORY_LIMIT]
577
+ milestone_rows.sort(key=lambda row: str(row["captured_at"]), reverse=True)
578
+ milestone_rows = milestone_rows[:SOURCE_HISTORY_LIMIT]
579
+ active_rows = active_rows[:SOURCE_HISTORY_LIMIT]
580
+ return {
581
+ "summary": {
582
+ "window_count": len(blocks),
583
+ "active_window_count": len(active_rows),
584
+ "latest_percent": latest_percent,
585
+ "freshness": active_freshness,
586
+ "active": tuple(active_rows),
587
+ },
588
+ "histories": tuple(history_rows),
589
+ "milestones": tuple(milestone_rows),
590
+ }
591
+
592
+
593
+ def _clock_freshness(
594
+ captured_at: object,
595
+ stale_after: object,
596
+ now_utc: dt.datetime,
597
+ ) -> str:
598
+ if not isinstance(captured_at, str) or not isinstance(stale_after, int):
599
+ return "unavailable"
600
+ try:
601
+ captured = dt.datetime.fromisoformat(captured_at.replace("Z", "+00:00")).astimezone(UTC)
602
+ except (TypeError, ValueError):
603
+ return "unavailable"
604
+ age_seconds = int((now_utc - captured).total_seconds())
605
+ if age_seconds < -300:
606
+ return "future"
607
+ return "stale" if age_seconds > stale_after else "fresh"
608
+
609
+
610
+ def _refresh_budget_status_clock(
611
+ status: Mapping[str, object] | None,
612
+ now_utc: dt.datetime,
613
+ *,
614
+ cost_events: object = (),
615
+ ) -> dict[str, object] | None:
616
+ """Re-run only the pure pace kernel from already-published budget facts."""
617
+ if status is None:
618
+ return None
619
+ try:
620
+ c = sys.modules["cctally"]
621
+ start_at = dt.datetime.fromisoformat(
622
+ str(status["window_start_at"]).replace("Z", "+00:00")
623
+ ).astimezone(UTC)
624
+ end_at = dt.datetime.fromisoformat(
625
+ str(status["window_end_at"]).replace("Z", "+00:00")
626
+ ).astimezone(UTC)
627
+ recent_start = max(start_at, now_utc - dt.timedelta(hours=24))
628
+ recent_24h_usd = sum(
629
+ float(cost) for timestamp, cost in cost_events
630
+ if isinstance(timestamp, dt.datetime)
631
+ and start_at <= timestamp.astimezone(UTC) < now_utc
632
+ and timestamp.astimezone(UTC) >= recent_start
633
+ )
634
+ inputs = c.BudgetInputs(
635
+ target_usd=float(status["budget_usd"]),
636
+ spent_usd=float(status["spent_usd"]),
637
+ recent_24h_usd=recent_24h_usd,
638
+ week_start_at=start_at,
639
+ week_end_at=end_at,
640
+ now=now_utc,
641
+ alert_thresholds=tuple(int(value) for value in status["alert_thresholds"]),
642
+ )
643
+ refreshed = c.compute_budget_status(inputs)
644
+ except (KeyError, TypeError, ValueError, OverflowError):
645
+ return dict(status)
646
+ return {
647
+ **dict(status),
648
+ "recent_24h_usd": inputs.recent_24h_usd,
649
+ "remaining_usd": refreshed.remaining_usd,
650
+ "consumption_pct": refreshed.consumption_pct,
651
+ "verdict": refreshed.verdict,
652
+ "low_confidence": refreshed.low_confidence,
653
+ "pace": {
654
+ "daily_usd": refreshed.daily_pace_usd,
655
+ "projected_low_usd": refreshed.projected_eow_low_usd,
656
+ "projected_high_usd": refreshed.projected_eow_high_usd,
657
+ "week_avg_projection_usd": refreshed.week_avg_projection_usd,
658
+ },
659
+ }
660
+
661
+
662
+ def refresh_codex_source_clock(
663
+ state: SourceDashboardState,
664
+ *,
665
+ now_utc: dt.datetime,
666
+ ) -> SourceDashboardState:
667
+ """Refresh idle-only freshness/pace from frozen facts without provider I/O."""
668
+ if state.source != "codex" or not isinstance(state.data, Mapping):
669
+ return state
670
+ if now_utc.tzinfo is None or now_utc.utcoffset() is None:
671
+ raise ValueError("now_utc must be timezone-aware")
672
+ now_utc = now_utc.astimezone(UTC)
673
+ # Structural copy only: untouched period/session/project branches remain
674
+ # the exact frozen objects from the prior publication. Idle refresh must
675
+ # not walk or re-freeze the provider's heavy read model.
676
+ data = dict(state.data)
677
+ quota = data.get("quota")
678
+ quota_changed = False
679
+ if isinstance(quota, Mapping):
680
+ quota = dict(quota)
681
+ refreshed_histories: list[dict[str, object]] = []
682
+ active_rows: list[dict[str, object]] = []
683
+ for raw_history in quota.get("histories", ()):
684
+ if not isinstance(raw_history, Mapping):
685
+ continue
686
+ history = dict(raw_history)
687
+ freshness = _clock_freshness(
688
+ history.get("captured_at"), history.get("stale_after_seconds"), now_utc,
689
+ )
690
+ history["freshness"] = freshness
691
+ forecast = history.get("forecast")
692
+ if isinstance(forecast, Mapping):
693
+ forecast = dict(forecast)
694
+ resets_at = forecast.get("resets_at")
695
+ try:
696
+ reset = dt.datetime.fromisoformat(
697
+ str(resets_at).replace("Z", "+00:00")
698
+ ).astimezone(UTC)
699
+ except (TypeError, ValueError):
700
+ reset = None
701
+ remaining = max(0, int((reset - now_utc).total_seconds())) if reset else None
702
+ forecast["remaining_seconds"] = remaining
703
+ sample_count = int(forecast.get("sample_count") or 0)
704
+ if freshness == "future":
705
+ forecast["status"] = "future"
706
+ elif freshness == "stale":
707
+ forecast["status"] = "stale"
708
+ elif sample_count == 0:
709
+ forecast["status"] = "insufficient-history"
710
+ else:
711
+ forecast["status"] = "ok"
712
+ rate = forecast.get("rate_percent_per_hour")
713
+ current = forecast.get("current_percent")
714
+ if (
715
+ isinstance(rate, (int, float)) and not isinstance(rate, bool)
716
+ and isinstance(current, (int, float)) and not isinstance(current, bool)
717
+ and remaining is not None
718
+ ):
719
+ forecast["projected_percent"] = min(
720
+ 100.0, max(float(current), float(current) + float(rate) * remaining / 3600),
721
+ )
722
+ history["forecast"] = forecast
723
+ if reset is not None and reset > now_utc and current is not None:
724
+ active_rows.append({
725
+ "key": history.get("key"),
726
+ "current_percent": current,
727
+ "captured_at": history.get("captured_at"),
728
+ "resets_at": resets_at,
729
+ "freshness": freshness,
730
+ "stale_after_seconds": history.get("stale_after_seconds"),
731
+ })
732
+ refreshed_histories.append(history)
733
+ quota["histories"] = refreshed_histories
734
+ latest_percent = max(
735
+ (float(row["current_percent"]) for row in active_rows), default=None,
736
+ )
737
+ summary = dict(quota.get("summary") or {})
738
+ prior_active = summary.get("active")
739
+ if isinstance(prior_active, (tuple, list)):
740
+ active_order = {
741
+ str(row.get("key")): index
742
+ for index, row in enumerate(prior_active)
743
+ if isinstance(row, Mapping)
744
+ }
745
+ active_rows.sort(
746
+ key=lambda row: active_order.get(str(row.get("key")), len(active_order)),
747
+ )
748
+ summary.update({
749
+ "active_window_count": len(active_rows),
750
+ "latest_percent": latest_percent,
751
+ "freshness": (
752
+ "fresh" if active_rows and all(row["freshness"] == "fresh" for row in active_rows)
753
+ else ("unavailable" if not active_rows else "stale")
754
+ ),
755
+ "active": active_rows,
756
+ })
757
+ quota["summary"] = summary
758
+ data["quota"] = quota
759
+ quota_changed = bool(refreshed_histories)
760
+ budget_domain = data.get("budget")
761
+ budget_changed = False
762
+ if isinstance(budget_domain, Mapping):
763
+ budget_domain = dict(budget_domain)
764
+ refreshed_budget = _refresh_budget_status_clock(
765
+ budget_domain.get("status") if isinstance(budget_domain.get("status"), Mapping) else None,
766
+ now_utc,
767
+ cost_events=(
768
+ state.clock_data.get("codex_budget_cost_events", ())
769
+ if isinstance(state.clock_data, Mapping) else ()
770
+ ),
771
+ )
772
+ if refreshed_budget is not None:
773
+ budget_domain["status"] = refreshed_budget
774
+ data["budget"] = budget_domain
775
+ hero = data.get("hero")
776
+ if isinstance(hero, Mapping):
777
+ hero = dict(hero)
778
+ hero["budget"] = refreshed_budget
779
+ data["hero"] = hero
780
+ budget_changed = True
781
+ if not (quota_changed or budget_changed):
782
+ return state
783
+ refreshed_state = SourceDashboardState(
784
+ source=state.source,
785
+ availability=state.availability,
786
+ freshness=state.freshness,
787
+ warnings=state.warnings,
788
+ data_version=state.data_version,
789
+ last_success_at=state.last_success_at,
790
+ capabilities=state.capabilities,
791
+ data=data,
792
+ clock_data=state.clock_data,
793
+ )
794
+ return state if refreshed_state.data == state.data else refreshed_state
795
+
796
+
797
+ def _alerts_wire(stats_conn: sqlite3.Connection) -> tuple[dict[str, object], ...]:
798
+ """Return only safe, source-owned Codex alert context in newest-first order."""
799
+ rows: list[dict[str, object]] = []
800
+ try:
801
+ for period, threshold, consumption_pct, crossed_at in stats_conn.execute(
802
+ "SELECT period, threshold, consumption_pct, crossed_at_utc "
803
+ "FROM budget_milestones WHERE vendor='codex' AND alerted_at IS NOT NULL "
804
+ "ORDER BY crossed_at_utc DESC, threshold DESC LIMIT ?",
805
+ (SOURCE_HISTORY_LIMIT,),
806
+ ):
807
+ rows.append({
808
+ "key": dashboard_resource_key("alert", "codex", "codex_budget", period, threshold, crossed_at),
809
+ "source": "codex",
810
+ "axis": "codex_budget", "period": period, "threshold": threshold,
811
+ "value": consumption_pct, "created_at": crossed_at,
812
+ })
813
+ for period, threshold, projected_value, crossed_at in stats_conn.execute(
814
+ "SELECT period, threshold, projected_value, crossed_at_utc "
815
+ "FROM projected_milestones WHERE metric='codex_budget_usd' AND alerted_at IS NOT NULL "
816
+ "ORDER BY crossed_at_utc DESC, threshold DESC LIMIT ?",
817
+ (SOURCE_HISTORY_LIMIT,),
818
+ ):
819
+ rows.append({
820
+ "key": dashboard_resource_key("alert", "codex", "projected", period, threshold, crossed_at),
821
+ "source": "codex",
822
+ "axis": "projected", "period": period, "threshold": threshold,
823
+ "value": projected_value, "created_at": crossed_at,
824
+ })
825
+ for root_key, logical_key, observed_slot, window_minutes, resets_at, threshold, severity, created_at in stats_conn.execute(
826
+ "SELECT source_root_key, logical_limit_key, observed_slot, window_minutes, resets_at_utc, "
827
+ "threshold, severity, created_at_utc FROM quota_threshold_events "
828
+ "WHERE source='codex' AND disposition='alerted' AND orphaned_at IS NULL "
829
+ "ORDER BY created_at_utc DESC, source_root_key, logical_limit_key, observed_slot, threshold "
830
+ "LIMIT ?",
831
+ (SOURCE_HISTORY_LIMIT,),
832
+ ):
833
+ rows.append({
834
+ "key": dashboard_resource_key(
835
+ "alert", "codex", "quota", root_key, logical_key, observed_slot,
836
+ window_minutes, resets_at, threshold, created_at,
837
+ ),
838
+ "source": "codex",
839
+ "axis": "quota", "threshold": threshold, "severity": severity,
840
+ "created_at": created_at,
841
+ })
842
+ except sqlite3.Error:
843
+ return ()
844
+ return tuple(sorted(
845
+ rows,
846
+ key=lambda item: str(item.get("created_at") or ""),
847
+ reverse=True,
848
+ )[:SOURCE_HISTORY_LIMIT])
849
+
850
+
851
+ def _projects_wire(
852
+ context: DashboardReadContext,
853
+ quota_observations: Iterable[object],
854
+ entries: Iterable[object],
855
+ *,
856
+ accounting_end: dt.datetime,
857
+ ) -> dict[str, object]:
858
+ """Adapt S3's already-qualified attribution result without re-formulas."""
859
+ qualified_entries = tuple(entries)
860
+ result = build_codex_project_result(
861
+ qualified_entries,
862
+ range_start=context.range_start,
863
+ range_end=accounting_end,
864
+ blocks=build_blocks(quota_observations),
865
+ as_of=context.now_utc,
866
+ allocation_entries=qualified_entries,
867
+ )
868
+ data = result.data
869
+ if data is None:
870
+ return {"rows": (), "total_cost_usd": 0.0, "total_tokens": 0}
871
+ return {
872
+ "rows": tuple({
873
+ "key": dashboard_resource_key("project", "codex", row.project_key),
874
+ "source": "codex",
875
+ "label": row.display_label,
876
+ "session_count": row.session_count,
877
+ "first_seen": row.first_seen.astimezone(UTC).isoformat(),
878
+ "last_seen": row.last_seen.astimezone(UTC).isoformat(),
879
+ "cost_usd": row.totals.cost_usd,
880
+ "input_tokens": row.totals.input_tokens,
881
+ "cached_input_tokens": row.totals.cached_input_tokens,
882
+ "output_tokens": row.totals.output_tokens,
883
+ "reasoning_output_tokens": row.totals.reasoning_output_tokens,
884
+ "total_tokens": row.totals.total_tokens,
885
+ } for row in data.projects),
886
+ "total_cost_usd": data.totals.cost_usd,
887
+ "total_tokens": data.totals.total_tokens,
888
+ }
889
+
890
+
891
+ def _codex_entries_from_qualified(entries: Iterable[object]) -> list[CodexEntry]:
892
+ """Adapt the one root-qualified accounting read for shipped view kernels."""
893
+ converted: list[CodexEntry] = []
894
+ for entry in entries:
895
+ source_path = str(getattr(entry, "source_path", "") or "")
896
+ session_id = str(getattr(entry, "session_id", "") or "")
897
+ if not source_path or not session_id:
898
+ raise SourceCapabilityUnavailable("qualified accounting lacks session identity")
899
+ converted.append(CodexEntry(
900
+ timestamp=getattr(entry, "timestamp"),
901
+ session_id=session_id,
902
+ model=str(getattr(entry, "model")),
903
+ input_tokens=int(getattr(entry, "input_tokens")),
904
+ cached_input_tokens=int(getattr(entry, "cached_input_tokens")),
905
+ output_tokens=int(getattr(entry, "output_tokens")),
906
+ reasoning_output_tokens=int(getattr(entry, "reasoning_output_tokens")),
907
+ total_tokens=int(getattr(entry, "total_tokens")),
908
+ source_path=source_path,
909
+ ))
910
+ return converted
911
+
912
+
913
+ def build_codex_source_state(
914
+ context: DashboardReadContext,
915
+ *,
916
+ data_version: str,
917
+ ) -> SourceDashboardState:
918
+ """Build Codex data strictly from the coordinated cache/stats reads.
919
+
920
+ No sync, rollout scan, CLI parser, or fallback is reachable from this
921
+ adapter. Period and session arithmetic remains delegated to the shipped
922
+ S3 view kernels, preserving the CLI's inclusive-token vocabulary.
923
+ """
924
+ active_roots = tuple(sorted(
925
+ str(row[0]) for row in context.cache_conn.execute(
926
+ "SELECT source_root_key FROM codex_source_roots"
927
+ )
928
+ ))
929
+ quota_observations = load_codex_quota_observations(
930
+ source_root_keys=active_roots,
931
+ cache_conn=context.cache_conn,
932
+ captured_at_or_after=(
933
+ context.now_utc - dt.timedelta(days=DASHBOARD_QUOTA_RECENT_DAYS)
934
+ ),
935
+ active_at=context.now_utc,
936
+ max_rows=DASHBOARD_QUOTA_OBSERVATION_LIMIT,
937
+ )
938
+ coherence = codex_projection_coherence(
939
+ context,
940
+ )
941
+ if not coherence.coherent:
942
+ raise CodexProjectionIncoherent(coherence.reason)
943
+ # The cache reader's established report surface treats the ``now`` instant
944
+ # as inclusive. The qualified adapter is half-open, so extend only its
945
+ # query/result boundary by one microsecond and keep all live budget sums
946
+ # explicitly half-open at ``now`` below.
947
+ accounting_end = context.now_utc + dt.timedelta(microseconds=1)
948
+ accounting_start = context.range_start
949
+ if context.codex_budget is not None:
950
+ _period, budget_start, _budget_end = _configured_codex_budget_window(context)
951
+ 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,
957
+ cache_conn=context.cache_conn,
958
+ )
959
+ budget_entries = _codex_entries_from_qualified(qualified_entries)
960
+ visible_qualified_entries = tuple(
961
+ entry for entry in qualified_entries
962
+ if context.range_start <= getattr(entry, "timestamp").astimezone(UTC) < accounting_end
963
+ )
964
+ entries = _codex_entries_from_qualified(visible_qualified_entries)
965
+ daily = build_codex_daily_view(
966
+ entries, now_utc=context.now_utc, tz_name=context.display_tz_name, speed=context.speed,
967
+ )
968
+ monthly = build_codex_monthly_view(
969
+ entries, now_utc=context.now_utc, tz_name=context.display_tz_name, speed=context.speed,
970
+ )
971
+ weekly = build_codex_weekly_view(
972
+ entries,
973
+ now_utc=context.now_utc,
974
+ tz_name=context.display_tz_name,
975
+ week_start_idx=context.week_start_idx,
976
+ speed=context.speed,
977
+ )
978
+ sessions = build_codex_session_view(
979
+ entries, now_utc=context.now_utc, tz_name=context.display_tz_name, speed=context.speed,
980
+ )
981
+ quota = _quota_read_model(context, quota_observations)
982
+ quota_blocks = _quota_wire(context.stats_conn)
983
+ quota = {**quota, "blocks": quota_blocks}
984
+ budget_rows = _budget_wire(context.stats_conn)
985
+ projected_budget_rows = _projected_budget_wire(context.stats_conn)
986
+ budget_cost_events = _codex_budget_cost_events(context, budget_entries)
987
+ configured_budget = _configured_codex_budget_status(
988
+ context, budget_entries, cost_events=budget_cost_events,
989
+ )
990
+ projects = _projects_wire(
991
+ context,
992
+ quota_observations,
993
+ visible_qualified_entries,
994
+ accounting_end=accounting_end,
995
+ )
996
+ 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)
1002
+ return SourceDashboardState(
1003
+ source="codex",
1004
+ availability=availability,
1005
+ freshness="fresh",
1006
+ warnings=(),
1007
+ data_version=data_version,
1008
+ last_success_at=context.now_utc,
1009
+ capabilities={
1010
+ "hero": CapabilityRecord("supported", "calendar-accounting"),
1011
+ "daily": CapabilityRecord("supported", "calendar-day"),
1012
+ "monthly": CapabilityRecord("supported", "calendar-month"),
1013
+ "weekly": CapabilityRecord("supported", "calendar-week"),
1014
+ "sessions": CapabilityRecord("supported", "inclusive-input-tokens"),
1015
+ "forensics": CapabilityRecord("supported", "inclusive-input-token-reuse"),
1016
+ "quota": CapabilityRecord("derived", "native-windows"),
1017
+ "budget": CapabilityRecord("supported", "calendar-period"),
1018
+ "projects": CapabilityRecord("supported", "qualified-attribution"),
1019
+ "alerts": CapabilityRecord("supported", "provider-native"),
1020
+ },
1021
+ data={
1022
+ "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,
1029
+ "quota": quota["summary"],
1030
+ "budget": configured_budget,
1031
+ "alerts": {"count": len(alerts)},
1032
+ },
1033
+ "periods": {
1034
+ "daily": _period_wire(daily),
1035
+ "monthly": _period_wire(monthly),
1036
+ "weekly": _period_wire(weekly),
1037
+ },
1038
+ "sessions": _session_wire(sessions),
1039
+ "quota": quota,
1040
+ "budget": {
1041
+ "status": configured_budget,
1042
+ "milestones": budget_rows,
1043
+ "projected": projected_budget_rows,
1044
+ },
1045
+ "projects": projects,
1046
+ "alerts": {"rows": alerts},
1047
+ },
1048
+ clock_data={"codex_budget_cost_events": budget_cost_events},
1049
+ )