cctally 1.78.0 → 1.79.1

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,966 @@
1
+ """I/O glue for the hero-modal milestone-history feature.
2
+
3
+ Assembles the compact per-provider navigation index (rides the SSE
4
+ envelope, built only on non-idle snapshot rebuilds) and the on-demand
5
+ per-week/-cycle detail payload served by
6
+ ``GET /api/milestones/<source>/week/<key>``. See
7
+ ``docs/superpowers/specs/2026-07-22-hero-milestone-history-design.md``
8
+ (spec §1a/§1b/§1c) and the implementation plan (Tasks 1–4).
9
+
10
+ This module is the IMPURE counterpart to the pure kernel
11
+ ``_lib_milestone_history.py``. Honest imports are restricted to pure
12
+ siblings (``_cctally_core`` kernel + ``_lib_*`` pure kernels, none of which
13
+ back-import ``cctally``); every impure cctally-namespace symbol
14
+ (``get_recent_weeks`` / ``_tui_build_five_hour_milestones`` / ``load_config``)
15
+ is reached via the
16
+ call-time ``_cctally()`` accessor so test monkeypatches through the
17
+ ``cctally`` namespace are preserved.
18
+
19
+ All Claude 5h keying uses stored ``five_hour_window_key`` values (already
20
+ canonical via ``_canonical_5h_window_key`` at write time) — never a new
21
+ key shape. Block/week interval comparisons run through SQL ``unixepoch()``
22
+ because stored offsets are not uniform (``block_start_at`` may carry a
23
+ host-local offset while boundaries are canonical UTC).
24
+ """
25
+ from __future__ import annotations
26
+
27
+ import datetime as dt
28
+ import json
29
+ import sqlite3
30
+ import sys
31
+ from dataclasses import replace
32
+
33
+ from _cctally_core import make_week_ref, parse_iso_datetime
34
+ from _cctally_quota import codex_quota_breakdown
35
+ from _lib_dashboard_sources import dashboard_resource_key
36
+ from _lib_display_tz import _resolve_display_tz_obj, format_display_dt
37
+ from _lib_json_envelope import _iso_z
38
+ from _lib_quota import QuotaWindowIdentity
39
+
40
+ import _lib_milestone_history as _mh
41
+
42
+ UTC = dt.timezone.utc
43
+
44
+
45
+ def _cctally():
46
+ """Resolve the current ``cctally`` module at call-time (ns-patchable)."""
47
+ return sys.modules["cctally"]
48
+
49
+
50
+ # ── shared formatting helpers ──────────────────────────────────────────
51
+
52
+
53
+ def _to_iso_z(iso: "str | None") -> "str | None":
54
+ """Canonical UTC-Z form of a stored ISO boundary, or ``None``."""
55
+ if not iso:
56
+ return None
57
+ try:
58
+ return _iso_z(parse_iso_datetime(iso, "milestone_history.boundary"))
59
+ except ValueError:
60
+ return None
61
+
62
+
63
+ def _resolve_display_tz():
64
+ """Resolve the configured display tz (never raises)."""
65
+ c = _cctally()
66
+ try:
67
+ config = c.load_config()
68
+ except Exception: # noqa: BLE001 — label formatting must never crash the build
69
+ config = {}
70
+ try:
71
+ return _resolve_display_tz_obj(config)
72
+ except Exception: # noqa: BLE001
73
+ return None
74
+
75
+
76
+ def _week_label(ref, tz) -> str:
77
+ """Week pill label — matches the dashboard header's full-window form
78
+ (``"Apr 13–20"``, en-dash U+2013), reusing ``format_display_dt`` so
79
+ historic pills render identically to the live header."""
80
+ start = ref.week_start_at
81
+ end = ref.week_end_at
82
+ if start and end:
83
+ return (
84
+ f"{format_display_dt(start, tz, fmt='%b %d', suffix=False)}"
85
+ f"–"
86
+ f"{format_display_dt(end, tz, fmt='%b %d', suffix=False)}"
87
+ )
88
+ if start:
89
+ return format_display_dt(start, tz, fmt="%b %d", suffix=False)
90
+ return ref.week_start.strftime("%b %d")
91
+
92
+
93
+ def _count_blocks(conn: sqlite3.Connection, start_iso, end_iso) -> int:
94
+ """Count ``five_hour_blocks`` whose interval intersects ``[start, end)``.
95
+
96
+ Half-open intersection on epoch seconds via ``unixepoch()`` (mixed stored
97
+ offsets). Returns 0 when the week lacks canonical boundaries.
98
+ """
99
+ if not start_iso or not end_iso:
100
+ return 0
101
+ row = conn.execute(
102
+ "SELECT COUNT(*) FROM five_hour_blocks "
103
+ "WHERE unixepoch(block_start_at) < unixepoch(?) "
104
+ " AND unixepoch(five_hour_resets_at) > unixepoch(?)",
105
+ (end_iso, start_iso),
106
+ ).fetchone()
107
+ return int(row[0] or 0)
108
+
109
+
110
+ # ── Claude week index (spec §1a) ───────────────────────────────────────
111
+
112
+
113
+ def _navigable_claude_refs(conn: sqlite3.Connection) -> list:
114
+ """The navigable Claude week refs, newest-first, per spec §1a.
115
+
116
+ Navigable set = weeks with ≥1 ``weekly_usage_snapshots`` row ∪ weeks with
117
+ ≥1 ``percent_milestones`` row (cost-only weeks excluded). Same-key credit
118
+ reset-defined segment remains independently navigable; a defensive
119
+ milestone-only week absent from ``get_recent_weeks`` is synthesized from
120
+ its milestone rows' stored boundaries.
121
+ """
122
+ c = _cctally()
123
+ refs = c.get_recent_weeks(conn, None) # unbounded, newest-first
124
+
125
+ usage_keys = {
126
+ r[0]
127
+ for r in conn.execute(
128
+ "SELECT DISTINCT week_start_date FROM weekly_usage_snapshots"
129
+ ).fetchall()
130
+ }
131
+ milestone_keys = {
132
+ r[0]
133
+ for r in conn.execute(
134
+ "SELECT DISTINCT week_start_date FROM percent_milestones"
135
+ ).fetchall()
136
+ }
137
+ navigable = usage_keys | milestone_keys
138
+
139
+ refs = [r for r in refs if r.key in navigable]
140
+ present = {r.key for r in refs}
141
+ for k in sorted(milestone_keys - present, reverse=True):
142
+ ref = _synthesize_milestone_only_ref(conn, k)
143
+ if ref is not None:
144
+ refs.append(ref)
145
+ return _derive_claude_reset_cycles(conn, refs)
146
+
147
+
148
+ def _parse_optional_iso(value):
149
+ if not value:
150
+ return None
151
+ try:
152
+ return parse_iso_datetime(value, "milestone_cycle.boundary")
153
+ except (TypeError, ValueError):
154
+ return None
155
+
156
+
157
+ def _claude_storage_boundaries(conn: sqlite3.Connection, key: str):
158
+ """Return the outer retained boundary for one storage date bucket."""
159
+ rows = conn.execute(
160
+ "SELECT week_start_at, week_end_at FROM weekly_usage_snapshots "
161
+ "WHERE week_start_date=? AND week_start_at IS NOT NULL "
162
+ "AND week_end_at IS NOT NULL "
163
+ "UNION ALL "
164
+ "SELECT week_start_at, week_end_at FROM percent_milestones "
165
+ "WHERE week_start_date=? AND week_start_at IS NOT NULL "
166
+ "AND week_end_at IS NOT NULL",
167
+ (key, key),
168
+ ).fetchall()
169
+ pairs = [
170
+ (start, end)
171
+ for row in rows
172
+ if (start := _parse_optional_iso(row[0])) is not None
173
+ and (end := _parse_optional_iso(row[1])) is not None
174
+ and end > start
175
+ ]
176
+ if not pairs:
177
+ return None
178
+ return min(start for start, _end in pairs), max(end for _start, end in pairs)
179
+
180
+
181
+ def _linked_reset_events(conn: sqlite3.Connection, key: str) -> list:
182
+ """Reset rows linked to any retained boundary in a storage bucket."""
183
+ return conn.execute(
184
+ "SELECT DISTINCT e.id, e.effective_reset_at_utc "
185
+ "FROM week_reset_events e "
186
+ "WHERE EXISTS ("
187
+ " SELECT 1 FROM weekly_usage_snapshots s "
188
+ " WHERE s.week_start_date=? AND ("
189
+ " unixepoch(s.week_end_at)=unixepoch(e.old_week_end_at) OR "
190
+ " unixepoch(s.week_end_at)=unixepoch(e.new_week_end_at)"
191
+ " )"
192
+ ") OR EXISTS ("
193
+ " SELECT 1 FROM percent_milestones m "
194
+ " WHERE m.week_start_date=? AND ("
195
+ " unixepoch(m.week_end_at)=unixepoch(e.old_week_end_at) OR "
196
+ " unixepoch(m.week_end_at)=unixepoch(e.new_week_end_at)"
197
+ " )"
198
+ ") "
199
+ "ORDER BY unixepoch(e.effective_reset_at_utc), e.id",
200
+ (key, key),
201
+ ).fetchall()
202
+
203
+
204
+ def _derive_claude_reset_cycles(conn: sqlite3.Connection, refs: list) -> list:
205
+ """Expand storage buckets into provider reset-defined cycles.
206
+
207
+ ``get_recent_weeks`` can split an in-place credit, but an early re-anchor
208
+ whose old and new boundaries both remain under one ``week_start_date``
209
+ only exposes the pre-reset half. The retained snapshot boundary set plus
210
+ ``week_reset_events`` is the authoritative reset ledger for both shapes.
211
+ """
212
+ out: list = []
213
+ by_key: dict = {}
214
+ for ref in refs:
215
+ by_key.setdefault(ref.key, ref)
216
+ for key, template in by_key.items():
217
+ outer = _claude_storage_boundaries(conn, key)
218
+ if outer is None:
219
+ out.append(template)
220
+ continue
221
+ start, end = outer
222
+ cuts = []
223
+ for event in _linked_reset_events(conn, key):
224
+ effective = _parse_optional_iso(event["effective_reset_at_utc"])
225
+ if effective is not None and start < effective < end:
226
+ cuts.append(effective)
227
+ boundaries = [start, *sorted(set(cuts)), end]
228
+ cycles = []
229
+ for cycle_start, cycle_end in zip(boundaries, boundaries[1:]):
230
+ cycles.append(replace(
231
+ template,
232
+ week_start_at=cycle_start.isoformat(timespec="seconds"),
233
+ week_end_at=cycle_end.isoformat(timespec="seconds"),
234
+ week_end=(cycle_end - dt.timedelta(seconds=1)).date(),
235
+ ))
236
+ out.extend(reversed(cycles))
237
+ return _cctally()._apply_overlap_clamp_to_weekrefs(out)
238
+
239
+
240
+ def _synthesize_milestone_only_ref(conn: sqlite3.Connection, key: str):
241
+ """Defensive ref for a milestone-only week (no usage snapshot).
242
+
243
+ Boundaries fall back to the milestone rows' stored
244
+ ``week_start_at``/``week_end_at``/``week_end_date`` (spec §1a). Returns
245
+ ``None`` if a valid ref can't be built.
246
+ """
247
+ row = conn.execute(
248
+ "SELECT week_end_date, week_start_at, week_end_at FROM percent_milestones "
249
+ "WHERE week_start_date = ? "
250
+ "ORDER BY (week_start_at IS NULL), captured_at_utc ASC LIMIT 1",
251
+ (key,),
252
+ ).fetchone()
253
+ if row is None:
254
+ return None
255
+ try:
256
+ return make_week_ref(
257
+ week_start_date=key,
258
+ week_end_date=row["week_end_date"],
259
+ week_start_at=row["week_start_at"],
260
+ week_end_at=row["week_end_at"],
261
+ )
262
+ except (ValueError, TypeError):
263
+ return None
264
+
265
+
266
+ def _current_claude_week_key(conn: sqlite3.Connection) -> "str | None":
267
+ row = conn.execute(
268
+ "SELECT week_start_date FROM weekly_usage_snapshots "
269
+ "ORDER BY captured_at_utc DESC, id DESC LIMIT 1"
270
+ ).fetchone()
271
+ return row[0] if row is not None else None
272
+
273
+
274
+ def _claude_cycle_key(ref) -> str:
275
+ return dashboard_resource_key(
276
+ "milestone_cycle", "claude", ref.key,
277
+ ref.week_start_at or "", ref.week_end_at or "",
278
+ )
279
+
280
+
281
+ def _claude_cycle_rows(conn: sqlite3.Connection, ref) -> list:
282
+ cohort_id = 0
283
+ if ref.week_start_at:
284
+ row = conn.execute(
285
+ "SELECT e.id FROM week_reset_events e "
286
+ "WHERE unixepoch(e.effective_reset_at_utc)=unixepoch(?) "
287
+ "AND (EXISTS ("
288
+ " SELECT 1 FROM weekly_usage_snapshots s "
289
+ " WHERE s.week_start_date=? AND ("
290
+ " unixepoch(s.week_end_at)=unixepoch(e.old_week_end_at) OR "
291
+ " unixepoch(s.week_end_at)=unixepoch(e.new_week_end_at)"
292
+ " )"
293
+ ") OR EXISTS ("
294
+ " SELECT 1 FROM percent_milestones m "
295
+ " WHERE m.week_start_date=? AND ("
296
+ " unixepoch(m.week_end_at)=unixepoch(e.old_week_end_at) OR "
297
+ " unixepoch(m.week_end_at)=unixepoch(e.new_week_end_at)"
298
+ " )"
299
+ ")) ORDER BY e.id DESC LIMIT 1",
300
+ (ref.week_start_at, ref.key, ref.key),
301
+ ).fetchone()
302
+ if row is not None:
303
+ cohort_id = int(row[0])
304
+ return conn.execute(
305
+ "SELECT * FROM percent_milestones WHERE week_start_date=? "
306
+ "AND reset_event_id=? "
307
+ "ORDER BY unixepoch(captured_at_utc) ASC, percent_threshold ASC",
308
+ (ref.key, cohort_id),
309
+ ).fetchall()
310
+
311
+
312
+ def _index_entry(conn, ref, current_cycle_key, tz) -> dict:
313
+ rows = _claude_cycle_rows(conn, ref)
314
+ milestone_count = len(rows)
315
+ segment_count = 1 if rows else 0
316
+ max_captured = max((r["captured_at_utc"] for r in rows), default=None)
317
+ start_z = _to_iso_z(ref.week_start_at)
318
+ end_z = _to_iso_z(ref.week_end_at)
319
+ block_count = _count_blocks(conn, start_z, end_z)
320
+ key = _claude_cycle_key(ref)
321
+
322
+ return {
323
+ "key": key,
324
+ "start_at_utc": start_z,
325
+ "end_at_utc": end_z,
326
+ "label": _week_label(ref, tz),
327
+ "is_current": key == current_cycle_key,
328
+ "milestone_count": milestone_count,
329
+ "block_count": block_count,
330
+ "segment_count": segment_count,
331
+ "detail_stamp": _mh.compute_detail_stamp(
332
+ key, milestone_count, block_count, segment_count, max_captured
333
+ ),
334
+ }
335
+
336
+
337
+ def build_claude_week_index(conn: sqlite3.Connection) -> list:
338
+ """Newest-first navigable Claude week index (spec §1a, §3).
339
+
340
+ One entry per effective reset-defined cycle. Multiple cycles may share one
341
+ storage ``week_start_date`` but always have distinct opaque keys.
342
+ """
343
+ refs = _navigable_claude_refs(conn)
344
+ current_key = _current_claude_week_key(conn)
345
+ current_refs = [r for r in refs if r.key == current_key]
346
+ current_ref = max(
347
+ current_refs,
348
+ key=lambda r: r.week_start_at or "",
349
+ default=None,
350
+ )
351
+ current_cycle_key = _claude_cycle_key(current_ref) if current_ref else None
352
+ tz = _resolve_display_tz()
353
+ entries = [_index_entry(conn, ref, current_cycle_key, tz) for ref in refs]
354
+ entries.sort(key=lambda e: e["start_at_utc"] or "", reverse=True)
355
+ return entries
356
+
357
+
358
+ # ── Claude week detail (spec §1b) ──────────────────────────────────────
359
+
360
+
361
+ def _shape_weekly_milestone(row) -> dict:
362
+ """Reshape a ``percent_milestones`` row to the envelope
363
+ ``current_week.milestones`` wire shape (byte-parallel with
364
+ ``_cctally_dashboard_envelope.snapshot_to_envelope``)."""
365
+ marginal = row["marginal_cost_usd"]
366
+ fh = row["five_hour_percent_at_crossing"]
367
+ return {
368
+ "percent": int(row["percent_threshold"]),
369
+ "crossed_at_utc": _to_iso_z(row["captured_at_utc"]),
370
+ "cumulative_usd": round(float(row["cumulative_cost_usd"]), 4),
371
+ "marginal_usd": None if marginal is None else round(float(marginal), 4),
372
+ "five_hour_pct_at_cross": None if fh is None else float(fh),
373
+ }
374
+
375
+
376
+ def _load_block_credits(conn: sqlite3.Connection, window_key: int) -> list:
377
+ """5h in-place credit rows for a block, ascending by effective time —
378
+ same wire shape as the envelope's ``five_hour_block.credits``."""
379
+ rows = conn.execute(
380
+ "SELECT effective_reset_at_utc, prior_percent, post_percent "
381
+ "FROM five_hour_reset_events WHERE five_hour_window_key = ? "
382
+ "ORDER BY effective_reset_at_utc ASC",
383
+ (int(window_key),),
384
+ ).fetchall()
385
+ return [
386
+ {
387
+ "effective_reset_at_utc": r["effective_reset_at_utc"],
388
+ "prior_percent": float(r["prior_percent"]),
389
+ "post_percent": float(r["post_percent"]),
390
+ "delta_pp": round(float(r["post_percent"]) - float(r["prior_percent"]), 1),
391
+ }
392
+ for r in rows
393
+ ]
394
+
395
+
396
+ def _build_blocks(conn: sqlite3.Connection, start_iso, end_iso) -> list:
397
+ """``five_hour_blocks`` intersecting ``[start, end)``, ascending by start;
398
+ each carries its 5h milestone stream + credit rows (spec §1b). A block
399
+ straddling a week boundary appears in every week it intersects."""
400
+ if not start_iso or not end_iso:
401
+ return []
402
+ c = _cctally()
403
+ rows = conn.execute(
404
+ "SELECT five_hour_window_key, block_start_at, five_hour_resets_at, "
405
+ " final_five_hour_percent, total_cost_usd, "
406
+ " crossed_seven_day_reset, is_closed "
407
+ "FROM five_hour_blocks "
408
+ "WHERE unixepoch(block_start_at) < unixepoch(?) "
409
+ " AND unixepoch(five_hour_resets_at) > unixepoch(?) "
410
+ "ORDER BY unixepoch(block_start_at) ASC, five_hour_window_key ASC",
411
+ (end_iso, start_iso),
412
+ ).fetchall()
413
+ out: list = []
414
+ for b in rows:
415
+ wk = int(b["five_hour_window_key"])
416
+ final_pct = b["final_five_hour_percent"]
417
+ cost = b["total_cost_usd"]
418
+ out.append(
419
+ {
420
+ "five_hour_window_key": wk,
421
+ "block_start_at": b["block_start_at"],
422
+ "five_hour_resets_at": b["five_hour_resets_at"],
423
+ "final_five_hour_percent": None if final_pct is None else float(final_pct),
424
+ "total_cost_usd": None if cost is None else float(cost),
425
+ "crossed_seven_day_reset": bool(b["crossed_seven_day_reset"]),
426
+ "is_closed": bool(b["is_closed"]),
427
+ "milestones": c._tui_build_five_hour_milestones(conn, wk),
428
+ "credits": _load_block_credits(conn, wk),
429
+ }
430
+ )
431
+ return out
432
+
433
+
434
+ def build_claude_week_detail(conn: sqlite3.Connection, key: str) -> "dict | None":
435
+ """Complete payload for one reset-defined Claude cycle."""
436
+ refs = _navigable_claude_refs(conn)
437
+ ref = next((candidate for candidate in refs if _claude_cycle_key(candidate) == key), None)
438
+ if ref is None:
439
+ return None
440
+ entry = next(e for e in build_claude_week_index(conn) if e["key"] == key)
441
+ rows = _claude_cycle_rows(conn, ref)
442
+ segment_key = dashboard_resource_key(
443
+ "milestone_segment", "claude", ref.key,
444
+ ref.week_start_at or "", ref.week_end_at or "",
445
+ )
446
+ segments = ([{"key": segment_key,
447
+ "milestones": [_shape_weekly_milestone(r) for r in rows]}]
448
+ if rows else [])
449
+ blocks = _build_blocks(conn, entry["start_at_utc"], entry["end_at_utc"])
450
+
451
+ return {
452
+ "source": "claude",
453
+ "key": entry["key"],
454
+ "start_at_utc": entry["start_at_utc"],
455
+ "end_at_utc": entry["end_at_utc"],
456
+ "label": entry["label"],
457
+ "is_current": entry["is_current"],
458
+ "detail_stamp": entry["detail_stamp"],
459
+ "segments": segments,
460
+ "dividers": [],
461
+ "blocks": blocks,
462
+ }
463
+
464
+
465
+ # ── Codex durable-projection cycle index + detail (spec §1c) ───────────
466
+ #
467
+ # The index is a DEDICATED UNBOUNDED query over the durable projection
468
+ # (quota_window_blocks, stats.db) — explicitly NOT the bounded public quota
469
+ # read model (35-day / 1000-observation / 250-row caps). Boundaries are the
470
+ # effective clipped, non-overlapping periods: a provider early re-anchor
471
+ # ends the prior cycle at min(raw reset, next nominal start). One full native
472
+ # identity is selected per physical reset; the opaque cycle key retains that
473
+ # identity so detail resolves the exact selected ledger.
474
+ #
475
+ # `_codex_weekly_periods` (bin/_cctally_dashboard_sources.py) computes the
476
+ # same clip formula but collapses across roots (losing the per-identity key)
477
+ # and caps at 250 rows, so it cannot back the per-identity unbounded index;
478
+ # the clip FORMULA (`end = min(reset, next nominal start)`) is applied here
479
+ # per-identity — the spec-faithful realization for the disambiguated index.
480
+
481
+
482
+ def _is_model_scoped_codex_quota(logical_limit_key) -> bool:
483
+ """Whether an interpreted native identity is a per-model pool (outside the
484
+ account-level standard quota). Mirrors the source builder's check."""
485
+ if not isinstance(logical_limit_key, str):
486
+ return False
487
+ try:
488
+ payload = json.loads(logical_limit_key)
489
+ except (json.JSONDecodeError, TypeError, ValueError):
490
+ return False
491
+ return (
492
+ isinstance(payload, dict)
493
+ and isinstance(payload.get("modelPool"), str)
494
+ and bool(payload["modelPool"].strip())
495
+ )
496
+
497
+
498
+ def _parse_utc(value) -> "dt.datetime | None":
499
+ if not value:
500
+ return None
501
+ try:
502
+ parsed = dt.datetime.fromisoformat(str(value).replace("Z", "+00:00"))
503
+ except (TypeError, ValueError):
504
+ return None
505
+ if parsed.tzinfo is None:
506
+ parsed = parsed.replace(tzinfo=UTC)
507
+ return parsed.astimezone(UTC)
508
+
509
+
510
+ class _CodexCycle:
511
+ """Parsed durable 7-day cycle with its effective clipped boundary.
512
+
513
+ After jitter-canonicalization (``_canonicalize_codex_cluster``) a cycle is
514
+ a *cluster representative*: its ``reset`` is the cluster's max (latest)
515
+ observation, ``start`` the cluster's min start, and ``members`` the raw
516
+ per-observation ``_CodexCycle`` rows the cluster collapsed (used to union
517
+ milestones/counts across every jittered reset). ``members`` is ``None`` for
518
+ a not-yet-clustered per-row cycle (equivalent to a single-member cluster).
519
+ """
520
+
521
+ __slots__ = (
522
+ "root", "limit", "slot", "window", "limit_id", "limit_name",
523
+ "start", "reset", "end", "current_percent", "members",
524
+ )
525
+
526
+ def __init__(self, row):
527
+ self.root = row["source_root_key"]
528
+ self.limit = row["logical_limit_key"]
529
+ self.slot = row["observed_slot"]
530
+ self.window = int(row["window_minutes"])
531
+ self.limit_id = row["limit_id"]
532
+ self.limit_name = row["limit_name"]
533
+ self.start = _parse_utc(row["nominal_start_at_utc"])
534
+ self.reset = _parse_utc(row["resets_at_utc"])
535
+ self.end = self.reset
536
+ cp = row["current_percent"]
537
+ self.current_percent = (
538
+ None if cp is None or isinstance(cp, bool) else float(cp)
539
+ )
540
+ self.members = None
541
+
542
+ @property
543
+ def cluster_members(self) -> list:
544
+ """The raw member cycles this representative collapsed (``[self]`` when
545
+ unclustered) — the resets to union milestones/counts over."""
546
+ return self.members if self.members else [self]
547
+
548
+ @property
549
+ def member_reset_isos(self) -> list:
550
+ """Canonical ISO resets of every cluster member (jitter siblings)."""
551
+ return [m.reset_iso for m in self.cluster_members]
552
+
553
+ @property
554
+ def reset_iso(self) -> str:
555
+ return self.reset.astimezone(UTC).isoformat()
556
+
557
+ @property
558
+ def key_parts(self) -> tuple:
559
+ return (self.root, self.limit, self.slot, self.window, self.reset_iso)
560
+
561
+ @property
562
+ def identity_parts(self) -> tuple:
563
+ return (self.root, self.limit, self.slot, self.window)
564
+
565
+ @property
566
+ def key(self) -> str:
567
+ return dashboard_resource_key("milestone_cycle", "codex", *self.key_parts)
568
+
569
+ @property
570
+ def block_key(self) -> str:
571
+ return dashboard_resource_key("block", "codex", *self.key_parts)
572
+
573
+
574
+ def _identity_matches_cycle(identity, cyc) -> bool:
575
+ return (
576
+ identity is not None
577
+ and getattr(identity, "source_root_key", None) == cyc.root
578
+ and getattr(identity, "logical_limit_key", None) == cyc.limit
579
+ and getattr(identity, "observed_slot", None) == cyc.slot
580
+ and getattr(identity, "window_minutes", None) == cyc.window
581
+ )
582
+
583
+
584
+ def _choose_physical_cycle(candidates: list, *, preferred_identity=None) -> _CodexCycle:
585
+ preferred = [c for c in candidates if _identity_matches_cycle(preferred_identity, c)]
586
+ pool = preferred or candidates
587
+ return max(
588
+ pool,
589
+ key=lambda c: (
590
+ -1.0 if c.current_percent is None else c.current_percent,
591
+ c.reset,
592
+ c.identity_parts,
593
+ ),
594
+ )
595
+
596
+
597
+ def _canonical_identity_cycles(cycles: list) -> list:
598
+ """Jitter-collapse rows within each full native identity."""
599
+ groups: dict = {}
600
+ for cyc in cycles:
601
+ groups.setdefault(cyc.identity_parts, []).append(cyc)
602
+ return [
603
+ _canonicalize_codex_cluster(cluster)
604
+ for members in groups.values()
605
+ for cluster in _mh.cluster_by_reset_jitter(
606
+ members, reset_key=lambda c: c.reset.timestamp()
607
+ )
608
+ ]
609
+
610
+
611
+ def _select_physical_cycles(cycles: list, *, preferred_identity=None) -> list:
612
+ """Select one full identity per physical reset boundary, then clip globally."""
613
+ identity_cycles = _canonical_identity_cycles(cycles)
614
+ selected = [
615
+ _choose_physical_cycle(cluster, preferred_identity=preferred_identity)
616
+ for cluster in _mh.cluster_by_reset_jitter(
617
+ identity_cycles, reset_key=lambda c: c.reset.timestamp()
618
+ )
619
+ ]
620
+ selected.sort(key=lambda c: (c.start, c.reset, c.identity_parts))
621
+ for index, cyc in enumerate(selected):
622
+ next_start = selected[index + 1].start if index + 1 < len(selected) else None
623
+ cyc.end = min(cyc.reset, next_start) if next_start is not None else cyc.reset
624
+ return [c for c in selected if c.end > c.start]
625
+
626
+
627
+ def _load_codex_cycles(stats_conn, root_keys, *, include_orphaned=False,
628
+ preferred_identity=None) -> list:
629
+ """Unbounded reset-defined 7-day cycles, one selected identity per boundary."""
630
+ roots = tuple(sorted({r for r in root_keys if isinstance(r, str) and r}))
631
+ if not roots:
632
+ return []
633
+ placeholders = ",".join("?" for _ in roots)
634
+ orphan_clause = "" if include_orphaned else "AND orphaned_at IS NULL "
635
+ rows = stats_conn.execute(
636
+ "SELECT source_root_key, logical_limit_key, observed_slot, "
637
+ " window_minutes, limit_id, limit_name, resets_at_utc, "
638
+ " nominal_start_at_utc, current_percent "
639
+ "FROM quota_window_blocks "
640
+ "WHERE source='codex' AND window_minutes=10080 "
641
+ f"{orphan_clause}"
642
+ f"AND source_root_key IN ({placeholders}) "
643
+ "ORDER BY unixepoch(resets_at_utc) DESC",
644
+ (*roots,),
645
+ ).fetchall()
646
+
647
+ cycles: list = []
648
+ for row in rows:
649
+ if _is_model_scoped_codex_quota(row["logical_limit_key"]):
650
+ continue
651
+ cyc = _CodexCycle(row)
652
+ if cyc.start is None or cyc.reset is None or cyc.reset <= cyc.start:
653
+ continue
654
+ cycles.append(cyc)
655
+
656
+ selected = _select_physical_cycles(cycles, preferred_identity=preferred_identity)
657
+ selected.sort(key=lambda c: c.start, reverse=True)
658
+ return selected
659
+
660
+
661
+ def _canonicalize_codex_cluster(members: list) -> "_CodexCycle":
662
+ """Collapse jitter-sibling cycles into one canonical representative.
663
+
664
+ Canonical ``reset`` = the cluster's max (latest observation wins);
665
+ ``start`` = the cluster's min start; scalar identity fields (limit_id,
666
+ limit_name, current_percent, window) come from the latest member. The raw
667
+ members are retained on ``.members`` so milestone rows/counts union across
668
+ every member reset (each via the existing per-reset breakdown path).
669
+ """
670
+ rep = max(members, key=lambda c: c.reset)
671
+ rep.start = min(m.start for m in members)
672
+ rep.reset = max(m.reset for m in members)
673
+ rep.end = rep.reset
674
+ rep.members = list(members)
675
+ return rep
676
+
677
+
678
+ def _codex_is_current(cyc, identity, now_utc) -> bool:
679
+ identity_reset = getattr(identity, "resets_at", None)
680
+ if identity_reset is not None:
681
+ try:
682
+ target = identity_reset.astimezone(UTC)
683
+ except (AttributeError, ValueError):
684
+ target = None
685
+ if target is not None:
686
+ # The live boundary's reset (``select_baseline``) need not be the
687
+ # cluster's max jittered reset, so match against ANY member within
688
+ # the jitter floor — distinct clusters are > floor apart, so only
689
+ # the live cluster can match.
690
+ return any(
691
+ abs((m.reset - target).total_seconds())
692
+ <= _mh.CODEX_CYCLE_JITTER_FLOOR_SECONDS
693
+ for m in cyc.cluster_members
694
+ )
695
+ return cyc.reset > now_utc
696
+
697
+
698
+ def _codex_milestone_count(stats_conn, cyc) -> tuple[int, str | None]:
699
+ """Unique threshold count + max captured-at for one selected identity."""
700
+ resets = cyc.member_reset_isos
701
+ placeholders = ",".join("unixepoch(?)" for _ in resets)
702
+ row = stats_conn.execute(
703
+ "SELECT COUNT(DISTINCT percent_threshold), MAX(captured_at_utc) "
704
+ "FROM quota_percent_milestones "
705
+ "WHERE source='codex' AND source_root_key=? AND logical_limit_key=? "
706
+ " AND observed_slot=? AND window_minutes=? "
707
+ f" AND unixepoch(resets_at_utc) IN ({placeholders}) AND orphaned_at IS NULL",
708
+ (cyc.root, cyc.limit, cyc.slot, cyc.window, *resets),
709
+ ).fetchone()
710
+ return int(row[0] or 0), row[1]
711
+
712
+
713
+ def _codex_five_hour_rows(stats_conn, cyc, *, include_orphaned=False) -> list:
714
+ """Every retained 5h block on the selected root intersecting [start, end)."""
715
+ orphan_clause = "" if include_orphaned else "AND orphaned_at IS NULL "
716
+ return stats_conn.execute(
717
+ "SELECT source_root_key, logical_limit_key, observed_slot, "
718
+ " window_minutes, limit_id, limit_name, resets_at_utc, "
719
+ " nominal_start_at_utc, current_percent "
720
+ "FROM quota_window_blocks "
721
+ "WHERE source='codex' AND window_minutes=300 "
722
+ f"{orphan_clause}"
723
+ "AND source_root_key=? "
724
+ "AND unixepoch(nominal_start_at_utc) < unixepoch(?) "
725
+ "AND unixepoch(resets_at_utc) > unixepoch(?) "
726
+ "ORDER BY unixepoch(nominal_start_at_utc) ASC",
727
+ (cyc.root, cyc.end.astimezone(UTC).isoformat(),
728
+ cyc.start.astimezone(UTC).isoformat()),
729
+ ).fetchall()
730
+
731
+
732
+ def _codex_five_hour_clusters(stats_conn, cyc, *, include_orphaned=False) -> list:
733
+ """Retained 5h blocks intersecting the cycle, jitter-canonicalized.
734
+
735
+ The 300-minute rows carry the same second-level ``resets_at`` jitter as the
736
+ weekly rows (one physical 5h block surfaces as many rows), so they are
737
+ clustered the same way — one canonical block per physical reset — before
738
+ counting/rendering. Returns cluster-representative ``_CodexCycle`` blocks
739
+ (``.members`` set), ordered by ascending start.
740
+ """
741
+ blocks: list = []
742
+ for row in _codex_five_hour_rows(stats_conn, cyc, include_orphaned=include_orphaned):
743
+ block = _CodexCycle(row)
744
+ if block.start is None or block.reset is None:
745
+ continue
746
+ blocks.append(block)
747
+ identity_clusters = _canonical_identity_cycles(blocks)
748
+ clusters = [
749
+ _choose_physical_cycle(members)
750
+ for members in _mh.cluster_by_reset_jitter(
751
+ identity_clusters, reset_key=lambda b: b.reset.timestamp()
752
+ )
753
+ ]
754
+ clusters.sort(key=lambda b: b.start)
755
+ return clusters
756
+
757
+
758
+ def _codex_cycle_entry(stats_conn, cyc, identity, now_utc, tz) -> dict:
759
+ milestone_count, max_captured = _codex_milestone_count(stats_conn, cyc)
760
+ block_count = len(_codex_five_hour_clusters(stats_conn, cyc))
761
+ key = cyc.key
762
+ return {
763
+ "key": key,
764
+ "start_at_utc": _iso_z(cyc.start),
765
+ "end_at_utc": _iso_z(cyc.end),
766
+ "resets_at_utc": _iso_z(cyc.reset),
767
+ "label": _codex_cycle_label(cyc, tz),
768
+ "is_current": _codex_is_current(cyc, identity, now_utc),
769
+ "milestone_count": milestone_count,
770
+ "block_count": block_count,
771
+ "detail_stamp": _mh.compute_detail_stamp(
772
+ key, milestone_count, block_count, max_captured
773
+ ),
774
+ }
775
+
776
+
777
+ def _codex_cycle_label(cyc, tz) -> str:
778
+ start_iso = cyc.start.astimezone(UTC).isoformat()
779
+ end_iso = cyc.end.astimezone(UTC).isoformat()
780
+ return (
781
+ f"{format_display_dt(start_iso, tz, fmt='%b %d', suffix=False)}"
782
+ f"–"
783
+ f"{format_display_dt(end_iso, tz, fmt='%b %d', suffix=False)}"
784
+ )
785
+
786
+
787
+ def build_codex_cycle_index(stats_conn, *, identity, now_utc) -> list:
788
+ """Newest-first Codex cycle index over the durable projection (spec §1c,
789
+ §3). Enumerates the hero root's reset-defined ledger with no depth cap;
790
+ one selected full native identity per physical reset."""
791
+ now = now_utc.astimezone(UTC) if now_utc.tzinfo else now_utc.replace(tzinfo=UTC)
792
+ tz = _resolve_display_tz()
793
+ cycles = _load_codex_cycles(
794
+ stats_conn, getattr(identity, "source_root_keys", ()),
795
+ preferred_identity=getattr(identity, "quota_identity", None),
796
+ )
797
+ return [_codex_cycle_entry(stats_conn, c, identity, now, tz) for c in cycles]
798
+
799
+
800
+ def _shape_codex_milestone(row, *, key_parts, block_key) -> dict:
801
+ captured_iso = row.captured_at.astimezone(UTC).isoformat()
802
+ root, limit, slot, window, reset_iso = key_parts
803
+ return {
804
+ "key": dashboard_resource_key(
805
+ "quota_milestone", "codex", *key_parts, row.percent, captured_iso
806
+ ),
807
+ "source": "codex",
808
+ "block_key": block_key,
809
+ "window_minutes": window,
810
+ "resets_at": _iso_z(_parse_utc(reset_iso)),
811
+ "percent": int(row.percent),
812
+ "captured_at": _iso_z(row.captured_at),
813
+ "cumulative_usd": row.cost_usd,
814
+ "marginal_usd": row.marginal_cost_usd,
815
+ "input_tokens": row.input_tokens,
816
+ "cached_input_tokens": row.cached_input_tokens,
817
+ "output_tokens": row.output_tokens,
818
+ "reasoning_output_tokens": row.reasoning_output_tokens,
819
+ "total_tokens": row.total_tokens,
820
+ "five_hour_percent": None,
821
+ }
822
+
823
+
824
+ def _codex_breakdown_rows(ident, reset, speed, cache_conn, stats_conn):
825
+ try:
826
+ return codex_quota_breakdown(
827
+ ident, reset, speed=speed, cache_conn=cache_conn, stats_conn=stats_conn,
828
+ )
829
+ except sqlite3.Error:
830
+ return None
831
+
832
+
833
+ def _union_cluster_milestones(cluster, block_key, speed, cache_conn, stats_conn):
834
+ """Union ``codex_quota_breakdown`` milestone rows across every cluster
835
+ member (each jittered sibling reset via the existing per-reset path).
836
+
837
+ One physical reset can split its crossings across sibling resets, so the
838
+ full ledger is the union — shaped and ordered by ``(captured_at, percent)``.
839
+ Returns ``None`` if any member's projection is incoherent (mirrors the
840
+ single-reset signal so the caller can surface ``projection_incoherent``).
841
+ """
842
+ shaped: list = []
843
+ for member in cluster.cluster_members:
844
+ ident = QuotaWindowIdentity(
845
+ source="codex", source_root_key=member.root,
846
+ logical_limit_key=member.limit, observed_slot=member.slot,
847
+ window_minutes=member.window, limit_id=member.limit_id,
848
+ limit_name=member.limit_name,
849
+ )
850
+ breakdown = _codex_breakdown_rows(
851
+ ident, member.reset, speed, cache_conn, stats_conn
852
+ )
853
+ if breakdown is None:
854
+ return None # projection-incoherent signal to the caller
855
+ shaped.extend(
856
+ _shape_codex_milestone(r, key_parts=member.key_parts, block_key=block_key)
857
+ for r in breakdown
858
+ )
859
+ # One physical quota ledger has one crossing per integer threshold. Jitter
860
+ # siblings can contribute missing evidence, but never duplicate a row.
861
+ by_percent: dict[int, dict] = {}
862
+ for row in shaped:
863
+ percent = int(row["percent"])
864
+ prior = by_percent.get(percent)
865
+ if prior is None or (row["captured_at"] or "", row["key"]) < (
866
+ prior["captured_at"] or "", prior["key"]
867
+ ):
868
+ by_percent[percent] = row
869
+ return sorted(
870
+ by_percent.values(), key=lambda m: (m["captured_at"] or "", m["percent"])
871
+ )
872
+
873
+
874
+ def _codex_cycle_blocks(stats_conn, cache_conn, cyc, speed) -> "list | None":
875
+ out: list = []
876
+ for block in _codex_five_hour_clusters(stats_conn, cyc):
877
+ block_key = block.block_key
878
+ milestones = _union_cluster_milestones(
879
+ block, block_key, speed, cache_conn, stats_conn
880
+ )
881
+ if milestones is None:
882
+ return None # projection-incoherent signal to the caller
883
+ out.append(
884
+ {
885
+ "key": block_key,
886
+ "block_start_at": _iso_z(block.start),
887
+ "five_hour_resets_at": _iso_z(block.reset),
888
+ "final_five_hour_percent": block.current_percent,
889
+ "total_cost_usd": None,
890
+ "crossed_seven_day_reset": False,
891
+ "is_closed": block.reset <= cyc.reset and block.reset <= _now_guard(cyc),
892
+ "milestones": milestones,
893
+ "credits": [],
894
+ }
895
+ )
896
+ return out
897
+
898
+
899
+ def _now_guard(cyc):
900
+ # A 5h block is closed once its reset has passed the cycle's effective end
901
+ # (historic cycle) — a conservative closed flag for retained blocks.
902
+ return cyc.end
903
+
904
+
905
+ def build_codex_cycle_detail(
906
+ stats_conn, cache_conn, *, identity, key, speed, now_utc
907
+ ):
908
+ """Complete payload for one Codex cycle (spec §1c). On success returns a
909
+ dict mirroring the Claude detail shape (segments = a single Codex segment,
910
+ dividers = []). On failure returns ``(None, reason)`` where reason ∈
911
+ {pruned, rebuild_pending, projection_incoherent, unknown} — Task 4 maps it
912
+ into the 404 body.
913
+ """
914
+ now = now_utc.astimezone(UTC) if now_utc.tzinfo else now_utc.replace(tzinfo=UTC)
915
+ tz = _resolve_display_tz()
916
+ try:
917
+ cycles = _load_codex_cycles(
918
+ stats_conn, getattr(identity, "source_root_keys", ()),
919
+ preferred_identity=getattr(identity, "quota_identity", None),
920
+ )
921
+ except sqlite3.Error:
922
+ return (None, "projection_incoherent")
923
+
924
+ match = next((c for c in cycles if c.key == key), None)
925
+ if match is None:
926
+ # Distinguish a pruned/orphaned cycle from a genuinely unknown key.
927
+ try:
928
+ orphaned = _load_codex_cycles(
929
+ stats_conn, getattr(identity, "source_root_keys", ()),
930
+ include_orphaned=True,
931
+ preferred_identity=getattr(identity, "quota_identity", None),
932
+ )
933
+ except sqlite3.Error:
934
+ return (None, "projection_incoherent")
935
+ if any(c.key == key for c in orphaned):
936
+ return (None, "pruned")
937
+ return (None, "unknown")
938
+
939
+ milestones = _union_cluster_milestones(
940
+ match, match.block_key, speed, cache_conn, stats_conn
941
+ )
942
+ if milestones is None:
943
+ return (None, "projection_incoherent")
944
+ blocks = _codex_cycle_blocks(stats_conn, cache_conn, match, speed)
945
+ if blocks is None:
946
+ return (None, "projection_incoherent")
947
+
948
+ entry = _codex_cycle_entry(stats_conn, match, identity, now, tz)
949
+ return {
950
+ "source": "codex",
951
+ "key": entry["key"],
952
+ "start_at_utc": entry["start_at_utc"],
953
+ "end_at_utc": entry["end_at_utc"],
954
+ "resets_at_utc": entry["resets_at_utc"],
955
+ "label": entry["label"],
956
+ "is_current": entry["is_current"],
957
+ "detail_stamp": entry["detail_stamp"],
958
+ "segments": [{
959
+ "key": dashboard_resource_key(
960
+ "milestone_segment", "codex", *match.key_parts
961
+ ),
962
+ "milestones": milestones,
963
+ }],
964
+ "dividers": [],
965
+ "blocks": blocks,
966
+ }