cctally 1.78.0 → 1.79.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.
@@ -0,0 +1,845 @@
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`` / ``get_milestones_for_week`` /
15
+ ``_tui_build_five_hour_milestones`` / ``load_config``) 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 sqlite3
28
+ import sys
29
+
30
+ from _cctally_core import make_week_ref, parse_iso_datetime
31
+ from _cctally_quota import codex_quota_breakdown
32
+ from _lib_dashboard_sources import dashboard_resource_key
33
+ from _lib_display_tz import _resolve_display_tz_obj, format_display_dt
34
+ from _lib_json_envelope import _iso_z
35
+ from _lib_quota import QuotaWindowIdentity
36
+
37
+ import datetime as dt
38
+ import json
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
+ segments are coalesced to one outer-boundary ref; 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
+ refs = _mh.coalesce_week_refs(refs)
141
+
142
+ present = {r.key for r in refs}
143
+ for k in sorted(milestone_keys - present, reverse=True):
144
+ ref = _synthesize_milestone_only_ref(conn, k)
145
+ if ref is not None:
146
+ refs.append(ref)
147
+ return refs
148
+
149
+
150
+ def _synthesize_milestone_only_ref(conn: sqlite3.Connection, key: str):
151
+ """Defensive ref for a milestone-only week (no usage snapshot).
152
+
153
+ Boundaries fall back to the milestone rows' stored
154
+ ``week_start_at``/``week_end_at``/``week_end_date`` (spec §1a). Returns
155
+ ``None`` if a valid ref can't be built.
156
+ """
157
+ row = conn.execute(
158
+ "SELECT week_end_date, week_start_at, week_end_at FROM percent_milestones "
159
+ "WHERE week_start_date = ? "
160
+ "ORDER BY (week_start_at IS NULL), captured_at_utc ASC LIMIT 1",
161
+ (key,),
162
+ ).fetchone()
163
+ if row is None:
164
+ return None
165
+ try:
166
+ return make_week_ref(
167
+ week_start_date=key,
168
+ week_end_date=row["week_end_date"],
169
+ week_start_at=row["week_start_at"],
170
+ week_end_at=row["week_end_at"],
171
+ )
172
+ except (ValueError, TypeError):
173
+ return None
174
+
175
+
176
+ def _current_claude_week_key(conn: sqlite3.Connection) -> "str | None":
177
+ row = conn.execute(
178
+ "SELECT week_start_date FROM weekly_usage_snapshots "
179
+ "ORDER BY captured_at_utc DESC, id DESC LIMIT 1"
180
+ ).fetchone()
181
+ return row[0] if row is not None else None
182
+
183
+
184
+ def _index_entry(conn, ref, current_key, tz) -> dict:
185
+ counts = conn.execute(
186
+ "SELECT COUNT(*), COUNT(DISTINCT reset_event_id), MAX(captured_at_utc) "
187
+ "FROM percent_milestones WHERE week_start_date = ?",
188
+ (ref.key,),
189
+ ).fetchone()
190
+ milestone_count = int(counts[0] or 0)
191
+ segment_count = int(counts[1] or 0)
192
+ max_captured = counts[2]
193
+
194
+ start_z = _to_iso_z(ref.week_start_at)
195
+ end_z = _to_iso_z(ref.week_end_at)
196
+ block_count = _count_blocks(conn, start_z, end_z)
197
+
198
+ return {
199
+ "key": ref.key,
200
+ "start_at_utc": start_z,
201
+ "end_at_utc": end_z,
202
+ "label": _week_label(ref, tz),
203
+ "is_current": ref.key == current_key,
204
+ "milestone_count": milestone_count,
205
+ "block_count": block_count,
206
+ "segment_count": segment_count,
207
+ "detail_stamp": _mh.compute_detail_stamp(
208
+ ref.key, milestone_count, block_count, segment_count, max_captured
209
+ ),
210
+ }
211
+
212
+
213
+ def build_claude_week_index(conn: sqlite3.Connection) -> list:
214
+ """Newest-first navigable Claude week index (spec §1a, §3).
215
+
216
+ One entry per navigable week; credit-split weeks collapse to a single
217
+ entry (segment structure lives in the detail payload). ``detail_stamp``
218
+ is a content digest that moves when the week's underlying rows change so
219
+ the client cache revalidates.
220
+ """
221
+ refs = _navigable_claude_refs(conn)
222
+ current_key = _current_claude_week_key(conn)
223
+ tz = _resolve_display_tz()
224
+ entries = [_index_entry(conn, ref, current_key, tz) for ref in refs]
225
+ entries.sort(key=lambda e: e["key"], reverse=True)
226
+ return entries
227
+
228
+
229
+ # ── Claude week detail (spec §1b) ──────────────────────────────────────
230
+
231
+
232
+ def _shape_weekly_milestone(row) -> dict:
233
+ """Reshape a ``percent_milestones`` row to the envelope
234
+ ``current_week.milestones`` wire shape (byte-parallel with
235
+ ``_cctally_dashboard_envelope.snapshot_to_envelope``)."""
236
+ marginal = row["marginal_cost_usd"]
237
+ fh = row["five_hour_percent_at_crossing"]
238
+ return {
239
+ "percent": int(row["percent_threshold"]),
240
+ "crossed_at_utc": _to_iso_z(row["captured_at_utc"]),
241
+ "cumulative_usd": round(float(row["cumulative_cost_usd"]), 4),
242
+ "marginal_usd": None if marginal is None else round(float(marginal), 4),
243
+ "five_hour_pct_at_cross": None if fh is None else float(fh),
244
+ }
245
+
246
+
247
+ def _build_segments(rows) -> list:
248
+ """Group milestone rows into segments (by ``reset_event_id``), ordered
249
+ chronologically by each segment's first ``captured_at_utc``; rows within a
250
+ segment ascend by capture time then threshold (spec §1b)."""
251
+ groups: dict = {}
252
+ for r in rows:
253
+ seg = int(r["reset_event_id"] or 0)
254
+ groups.setdefault(seg, []).append(r)
255
+ ordered: list = []
256
+ for seg, seg_rows in groups.items():
257
+ seg_rows.sort(key=lambda r: (r["captured_at_utc"], int(r["percent_threshold"])))
258
+ first_cap = seg_rows[0]["captured_at_utc"]
259
+ ordered.append(
260
+ (first_cap, seg, [_shape_weekly_milestone(r) for r in seg_rows])
261
+ )
262
+ ordered.sort(key=lambda t: (t[0], t[1]))
263
+ return [{"reset_event_id": seg, "milestones": ms} for (_c, seg, ms) in ordered]
264
+
265
+
266
+ def _build_dividers(conn: sqlite3.Connection, segments: list) -> list:
267
+ """Credit dividers between consecutive segments (spec §1b, Q4).
268
+
269
+ A divider is emitted for each later segment whose ``reset_event_id``
270
+ resolves to an in-place-credit ``week_reset_events`` row
271
+ (``old_week_end_at == effective_reset_at_utc``). It carries the effective
272
+ time + prior percent (``observed_pre_credit_pct``); no post/delta value is
273
+ recorded for weekly credits.
274
+ """
275
+ dividers: list = []
276
+ for later in segments[1:]:
277
+ seg_id = later["reset_event_id"]
278
+ if not seg_id:
279
+ continue
280
+ ev = conn.execute(
281
+ "SELECT effective_reset_at_utc, observed_pre_credit_pct, old_week_end_at "
282
+ "FROM week_reset_events WHERE id = ?",
283
+ (seg_id,),
284
+ ).fetchone()
285
+ if ev is None:
286
+ continue
287
+ if ev["old_week_end_at"] != ev["effective_reset_at_utc"]:
288
+ continue # not an in-place credit → no divider
289
+ prior = ev["observed_pre_credit_pct"]
290
+ dividers.append(
291
+ {
292
+ "effective_at_utc": _to_iso_z(ev["effective_reset_at_utc"]),
293
+ "prior_percent": None if prior is None else float(prior),
294
+ }
295
+ )
296
+ return dividers
297
+
298
+
299
+ def _load_block_credits(conn: sqlite3.Connection, window_key: int) -> list:
300
+ """5h in-place credit rows for a block, ascending by effective time —
301
+ same wire shape as the envelope's ``five_hour_block.credits``."""
302
+ rows = conn.execute(
303
+ "SELECT effective_reset_at_utc, prior_percent, post_percent "
304
+ "FROM five_hour_reset_events WHERE five_hour_window_key = ? "
305
+ "ORDER BY effective_reset_at_utc ASC",
306
+ (int(window_key),),
307
+ ).fetchall()
308
+ return [
309
+ {
310
+ "effective_reset_at_utc": r["effective_reset_at_utc"],
311
+ "prior_percent": float(r["prior_percent"]),
312
+ "post_percent": float(r["post_percent"]),
313
+ "delta_pp": round(float(r["post_percent"]) - float(r["prior_percent"]), 1),
314
+ }
315
+ for r in rows
316
+ ]
317
+
318
+
319
+ def _build_blocks(conn: sqlite3.Connection, start_iso, end_iso) -> list:
320
+ """``five_hour_blocks`` intersecting ``[start, end)``, ascending by start;
321
+ each carries its 5h milestone stream + credit rows (spec §1b). A block
322
+ straddling a week boundary appears in every week it intersects."""
323
+ if not start_iso or not end_iso:
324
+ return []
325
+ c = _cctally()
326
+ rows = conn.execute(
327
+ "SELECT five_hour_window_key, block_start_at, five_hour_resets_at, "
328
+ " final_five_hour_percent, total_cost_usd, "
329
+ " crossed_seven_day_reset, is_closed "
330
+ "FROM five_hour_blocks "
331
+ "WHERE unixepoch(block_start_at) < unixepoch(?) "
332
+ " AND unixepoch(five_hour_resets_at) > unixepoch(?) "
333
+ "ORDER BY unixepoch(block_start_at) ASC, five_hour_window_key ASC",
334
+ (end_iso, start_iso),
335
+ ).fetchall()
336
+ out: list = []
337
+ for b in rows:
338
+ wk = int(b["five_hour_window_key"])
339
+ final_pct = b["final_five_hour_percent"]
340
+ cost = b["total_cost_usd"]
341
+ out.append(
342
+ {
343
+ "five_hour_window_key": wk,
344
+ "block_start_at": b["block_start_at"],
345
+ "five_hour_resets_at": b["five_hour_resets_at"],
346
+ "final_five_hour_percent": None if final_pct is None else float(final_pct),
347
+ "total_cost_usd": None if cost is None else float(cost),
348
+ "crossed_seven_day_reset": bool(b["crossed_seven_day_reset"]),
349
+ "is_closed": bool(b["is_closed"]),
350
+ "milestones": c._tui_build_five_hour_milestones(conn, wk),
351
+ "credits": _load_block_credits(conn, wk),
352
+ }
353
+ )
354
+ return out
355
+
356
+
357
+ def build_claude_week_detail(
358
+ conn: sqlite3.Connection, week_start_date: str
359
+ ) -> "dict | None":
360
+ """Complete payload for one Claude week (spec §1b). ``None`` for an
361
+ unknown/non-navigable key.
362
+
363
+ Segments carry ALL ``reset_event_id`` cohorts (both pre- and post-credit)
364
+ ordered chronologically; ``dividers`` sit between consecutive segments;
365
+ ``blocks`` are the intersecting 5h blocks (dual-membership straddlers
366
+ included). Boundary/label/``is_current``/``detail_stamp`` are taken from
367
+ the index entry so the detail and index agree exactly.
368
+ """
369
+ c = _cctally()
370
+ entry = None
371
+ for e in build_claude_week_index(conn):
372
+ if e["key"] == week_start_date:
373
+ entry = e
374
+ break
375
+ if entry is None:
376
+ return None
377
+
378
+ rows = c.get_milestones_for_week(conn, week_start_date)
379
+ segments = _build_segments(rows)
380
+ dividers = _build_dividers(conn, segments)
381
+ blocks = _build_blocks(conn, entry["start_at_utc"], entry["end_at_utc"])
382
+
383
+ return {
384
+ "source": "claude",
385
+ "key": entry["key"],
386
+ "start_at_utc": entry["start_at_utc"],
387
+ "end_at_utc": entry["end_at_utc"],
388
+ "label": entry["label"],
389
+ "is_current": entry["is_current"],
390
+ "detail_stamp": entry["detail_stamp"],
391
+ "segments": segments,
392
+ "dividers": dividers,
393
+ "blocks": blocks,
394
+ }
395
+
396
+
397
+ # ── Codex durable-projection cycle index + detail (spec §1c) ───────────
398
+ #
399
+ # The index is a DEDICATED UNBOUNDED query over the durable projection
400
+ # (quota_window_blocks, stats.db) — explicitly NOT the bounded public quota
401
+ # read model (35-day / 1000-observation / 250-row caps). Boundaries are the
402
+ # effective clipped, non-overlapping periods: a provider early re-anchor
403
+ # ends the prior cycle at min(raw reset, next nominal start). Cycle keys
404
+ # embed the full native identity tuple so two identities sharing a
405
+ # resets_at get distinct keys and resolve exactly.
406
+ #
407
+ # `_codex_weekly_periods` (bin/_cctally_dashboard_sources.py) computes the
408
+ # same clip formula but collapses across roots (losing the per-identity key)
409
+ # and caps at 250 rows, so it cannot back the per-identity unbounded index;
410
+ # the clip FORMULA (`end = min(reset, next nominal start)`) is applied here
411
+ # per-identity — the spec-faithful realization for the disambiguated index.
412
+
413
+
414
+ def _is_model_scoped_codex_quota(logical_limit_key) -> bool:
415
+ """Whether an interpreted native identity is a per-model pool (outside the
416
+ account-level standard quota). Mirrors the source builder's check."""
417
+ if not isinstance(logical_limit_key, str):
418
+ return False
419
+ try:
420
+ payload = json.loads(logical_limit_key)
421
+ except (json.JSONDecodeError, TypeError, ValueError):
422
+ return False
423
+ return (
424
+ isinstance(payload, dict)
425
+ and isinstance(payload.get("modelPool"), str)
426
+ and bool(payload["modelPool"].strip())
427
+ )
428
+
429
+
430
+ def _parse_utc(value) -> "dt.datetime | None":
431
+ if not value:
432
+ return None
433
+ try:
434
+ parsed = dt.datetime.fromisoformat(str(value).replace("Z", "+00:00"))
435
+ except (TypeError, ValueError):
436
+ return None
437
+ if parsed.tzinfo is None:
438
+ parsed = parsed.replace(tzinfo=UTC)
439
+ return parsed.astimezone(UTC)
440
+
441
+
442
+ class _CodexCycle:
443
+ """Parsed durable 7-day cycle with its effective clipped boundary.
444
+
445
+ After jitter-canonicalization (``_canonicalize_codex_cluster``) a cycle is
446
+ a *cluster representative*: its ``reset`` is the cluster's max (latest)
447
+ observation, ``start`` the cluster's min start, and ``members`` the raw
448
+ per-observation ``_CodexCycle`` rows the cluster collapsed (used to union
449
+ milestones/counts across every jittered reset). ``members`` is ``None`` for
450
+ a not-yet-clustered per-row cycle (equivalent to a single-member cluster).
451
+ """
452
+
453
+ __slots__ = (
454
+ "root", "limit", "slot", "window", "limit_id", "limit_name",
455
+ "start", "reset", "end", "current_percent", "members",
456
+ )
457
+
458
+ def __init__(self, row):
459
+ self.root = row["source_root_key"]
460
+ self.limit = row["logical_limit_key"]
461
+ self.slot = row["observed_slot"]
462
+ self.window = int(row["window_minutes"])
463
+ self.limit_id = row["limit_id"]
464
+ self.limit_name = row["limit_name"]
465
+ self.start = _parse_utc(row["nominal_start_at_utc"])
466
+ self.reset = _parse_utc(row["resets_at_utc"])
467
+ self.end = self.reset
468
+ cp = row["current_percent"]
469
+ self.current_percent = (
470
+ None if cp is None or isinstance(cp, bool) else float(cp)
471
+ )
472
+ self.members = None
473
+
474
+ @property
475
+ def cluster_members(self) -> list:
476
+ """The raw member cycles this representative collapsed (``[self]`` when
477
+ unclustered) — the resets to union milestones/counts over."""
478
+ return self.members if self.members else [self]
479
+
480
+ @property
481
+ def member_reset_isos(self) -> list:
482
+ """Canonical ISO resets of every cluster member (jitter siblings)."""
483
+ return [m.reset_iso for m in self.cluster_members]
484
+
485
+ @property
486
+ def reset_iso(self) -> str:
487
+ return self.reset.astimezone(UTC).isoformat()
488
+
489
+ @property
490
+ def key_parts(self) -> tuple:
491
+ return (self.root, self.limit, self.slot, self.window, self.reset_iso)
492
+
493
+ @property
494
+ def key(self) -> str:
495
+ return dashboard_resource_key("milestone_cycle", "codex", *self.key_parts)
496
+
497
+ @property
498
+ def block_key(self) -> str:
499
+ return dashboard_resource_key("block", "codex", *self.key_parts)
500
+
501
+
502
+ def _load_codex_cycles(stats_conn, root_keys, *, include_orphaned=False) -> list:
503
+ """Non-model-scoped 7-day (window_minutes=10080) cycles for the identity
504
+ roots, effective-clipped per identity. Unbounded (no 35-day / row cap)."""
505
+ roots = tuple(sorted({r for r in root_keys if isinstance(r, str) and r}))
506
+ if not roots:
507
+ return []
508
+ placeholders = ",".join("?" for _ in roots)
509
+ orphan_clause = "" if include_orphaned else "AND orphaned_at IS NULL "
510
+ rows = stats_conn.execute(
511
+ "SELECT source_root_key, logical_limit_key, observed_slot, "
512
+ " window_minutes, limit_id, limit_name, resets_at_utc, "
513
+ " nominal_start_at_utc, current_percent "
514
+ "FROM quota_window_blocks "
515
+ "WHERE source='codex' AND window_minutes=10080 "
516
+ f"{orphan_clause}"
517
+ f"AND source_root_key IN ({placeholders}) "
518
+ "ORDER BY unixepoch(resets_at_utc) DESC",
519
+ (*roots,),
520
+ ).fetchall()
521
+
522
+ cycles: list = []
523
+ for row in rows:
524
+ if _is_model_scoped_codex_quota(row["logical_limit_key"]):
525
+ continue
526
+ cyc = _CodexCycle(row)
527
+ if cyc.start is None or cyc.reset is None or cyc.reset <= cyc.start:
528
+ continue
529
+ cycles.append(cyc)
530
+
531
+ # Canonicalize per identity (root, limit, slot): jitter-cluster the raw
532
+ # observations so one physical weekly reset — surfacing as several rows
533
+ # whose ``resets_at`` differ by seconds — becomes ONE cycle instead of a
534
+ # fan of degenerate 1-second spans (ui-qa P2). Genuine early re-anchors
535
+ # (hours apart) stay distinct (see ``cluster_by_reset_jitter``).
536
+ groups: dict = {}
537
+ for cyc in cycles:
538
+ groups.setdefault((cyc.root, cyc.limit, cyc.slot), []).append(cyc)
539
+ clustered: list = []
540
+ for members in groups.values():
541
+ for cluster in _mh.cluster_by_reset_jitter(
542
+ members, reset_key=lambda c: c.reset.timestamp()
543
+ ):
544
+ clustered.append(_canonicalize_codex_cluster(cluster))
545
+
546
+ # Effective clip per identity: end each canonical cycle at the NEXT
547
+ # canonical cycle's start (min with its own reset), non-overlapping.
548
+ cgroups: dict = {}
549
+ for cyc in clustered:
550
+ cgroups.setdefault((cyc.root, cyc.limit, cyc.slot), []).append(cyc)
551
+ for members in cgroups.values():
552
+ members.sort(key=lambda c: c.start)
553
+ for i, cyc in enumerate(members):
554
+ nxt = members[i + 1].start if i + 1 < len(members) else None
555
+ cyc.end = min(cyc.reset, nxt) if nxt is not None else cyc.reset
556
+ clustered = [c for c in clustered if c.end > c.start]
557
+ clustered.sort(key=lambda c: c.reset, reverse=True)
558
+ return clustered
559
+
560
+
561
+ def _canonicalize_codex_cluster(members: list) -> "_CodexCycle":
562
+ """Collapse jitter-sibling cycles into one canonical representative.
563
+
564
+ Canonical ``reset`` = the cluster's max (latest observation wins);
565
+ ``start`` = the cluster's min start; scalar identity fields (limit_id,
566
+ limit_name, current_percent, window) come from the latest member. The raw
567
+ members are retained on ``.members`` so milestone rows/counts union across
568
+ every member reset (each via the existing per-reset breakdown path).
569
+ """
570
+ rep = max(members, key=lambda c: c.reset)
571
+ rep.start = min(m.start for m in members)
572
+ rep.reset = max(m.reset for m in members)
573
+ rep.end = rep.reset
574
+ rep.members = list(members)
575
+ return rep
576
+
577
+
578
+ def _codex_is_current(cyc, identity, now_utc) -> bool:
579
+ identity_reset = getattr(identity, "resets_at", None)
580
+ if identity_reset is not None:
581
+ try:
582
+ target = identity_reset.astimezone(UTC)
583
+ except (AttributeError, ValueError):
584
+ target = None
585
+ if target is not None:
586
+ # The live boundary's reset (``select_baseline``) need not be the
587
+ # cluster's max jittered reset, so match against ANY member within
588
+ # the jitter floor — distinct clusters are > floor apart, so only
589
+ # the live cluster can match.
590
+ return any(
591
+ abs((m.reset - target).total_seconds())
592
+ <= _mh.CODEX_CYCLE_JITTER_FLOOR_SECONDS
593
+ for m in cyc.cluster_members
594
+ )
595
+ return cyc.reset > now_utc
596
+
597
+
598
+ def _codex_milestone_count(stats_conn, cyc) -> tuple[int, str | None]:
599
+ """Milestone count + max captured-at over the cluster's union — every
600
+ jittered member reset contributes (rows split across sibling resets)."""
601
+ resets = cyc.member_reset_isos
602
+ placeholders = ",".join("unixepoch(?)" for _ in resets)
603
+ row = stats_conn.execute(
604
+ "SELECT COUNT(*), MAX(captured_at_utc) FROM quota_percent_milestones "
605
+ "WHERE source='codex' AND source_root_key=? AND logical_limit_key=? "
606
+ " AND observed_slot=? AND window_minutes=? "
607
+ f" AND unixepoch(resets_at_utc) IN ({placeholders}) AND orphaned_at IS NULL",
608
+ (cyc.root, cyc.limit, cyc.slot, cyc.window, *resets),
609
+ ).fetchone()
610
+ return int(row[0] or 0), row[1]
611
+
612
+
613
+ def _codex_five_hour_rows(stats_conn, cyc, *, include_orphaned=False) -> list:
614
+ """Retained 5h (window_minutes=300) blocks for this cycle's identity
615
+ (root + observed_slot + limit_id) intersecting [start, end)."""
616
+ orphan_clause = "" if include_orphaned else "AND orphaned_at IS NULL "
617
+ return stats_conn.execute(
618
+ "SELECT source_root_key, logical_limit_key, observed_slot, "
619
+ " window_minutes, limit_id, limit_name, resets_at_utc, "
620
+ " nominal_start_at_utc, current_percent "
621
+ "FROM quota_window_blocks "
622
+ "WHERE source='codex' AND window_minutes=300 "
623
+ f"{orphan_clause}"
624
+ "AND source_root_key=? AND observed_slot=? AND limit_id IS ? "
625
+ "AND unixepoch(nominal_start_at_utc) < unixepoch(?) "
626
+ "AND unixepoch(resets_at_utc) > unixepoch(?) "
627
+ "ORDER BY unixepoch(nominal_start_at_utc) ASC",
628
+ (cyc.root, cyc.slot, cyc.limit_id,
629
+ cyc.end.astimezone(UTC).isoformat(), cyc.start.astimezone(UTC).isoformat()),
630
+ ).fetchall()
631
+
632
+
633
+ def _codex_five_hour_clusters(stats_conn, cyc, *, include_orphaned=False) -> list:
634
+ """Retained 5h blocks intersecting the cycle, jitter-canonicalized.
635
+
636
+ The 300-minute rows carry the same second-level ``resets_at`` jitter as the
637
+ weekly rows (one physical 5h block surfaces as many rows), so they are
638
+ clustered the same way — one canonical block per physical reset — before
639
+ counting/rendering. Returns cluster-representative ``_CodexCycle`` blocks
640
+ (``.members`` set), ordered by ascending start.
641
+ """
642
+ blocks: list = []
643
+ for row in _codex_five_hour_rows(stats_conn, cyc, include_orphaned=include_orphaned):
644
+ block = _CodexCycle(row)
645
+ if block.start is None or block.reset is None:
646
+ continue
647
+ blocks.append(block)
648
+ clusters = [
649
+ _canonicalize_codex_cluster(members)
650
+ for members in _mh.cluster_by_reset_jitter(
651
+ blocks, reset_key=lambda b: b.reset.timestamp()
652
+ )
653
+ ]
654
+ clusters.sort(key=lambda b: b.start)
655
+ return clusters
656
+
657
+
658
+ def _codex_cycle_entry(stats_conn, cyc, identity, now_utc, tz) -> dict:
659
+ milestone_count, max_captured = _codex_milestone_count(stats_conn, cyc)
660
+ block_count = len(_codex_five_hour_clusters(stats_conn, cyc))
661
+ key = cyc.key
662
+ return {
663
+ "key": key,
664
+ "start_at_utc": _iso_z(cyc.start),
665
+ "end_at_utc": _iso_z(cyc.end),
666
+ "resets_at_utc": _iso_z(cyc.reset),
667
+ "label": _codex_cycle_label(cyc, tz),
668
+ "is_current": _codex_is_current(cyc, identity, now_utc),
669
+ "milestone_count": milestone_count,
670
+ "block_count": block_count,
671
+ "detail_stamp": _mh.compute_detail_stamp(
672
+ key, milestone_count, block_count, max_captured
673
+ ),
674
+ }
675
+
676
+
677
+ def _codex_cycle_label(cyc, tz) -> str:
678
+ start_iso = cyc.start.astimezone(UTC).isoformat()
679
+ end_iso = cyc.end.astimezone(UTC).isoformat()
680
+ return (
681
+ f"{format_display_dt(start_iso, tz, fmt='%b %d', suffix=False)}"
682
+ f"–"
683
+ f"{format_display_dt(end_iso, tz, fmt='%b %d', suffix=False)}"
684
+ )
685
+
686
+
687
+ def build_codex_cycle_index(stats_conn, *, identity, now_utc) -> list:
688
+ """Newest-first Codex cycle index over the durable projection (spec §1c,
689
+ §3). Enumerates cycles of the hero-selected identity's roots with no depth
690
+ cap; one entry per (root, limit, slot, reset) cycle."""
691
+ now = now_utc.astimezone(UTC) if now_utc.tzinfo else now_utc.replace(tzinfo=UTC)
692
+ tz = _resolve_display_tz()
693
+ cycles = _load_codex_cycles(stats_conn, getattr(identity, "source_root_keys", ()))
694
+ return [_codex_cycle_entry(stats_conn, c, identity, now, tz) for c in cycles]
695
+
696
+
697
+ def _shape_codex_milestone(row, *, key_parts, block_key) -> dict:
698
+ captured_iso = row.captured_at.astimezone(UTC).isoformat()
699
+ root, limit, slot, window, reset_iso = key_parts
700
+ return {
701
+ "key": dashboard_resource_key(
702
+ "quota_milestone", "codex", *key_parts, row.percent, captured_iso
703
+ ),
704
+ "source": "codex",
705
+ "block_key": block_key,
706
+ "window_minutes": window,
707
+ "resets_at": _iso_z(_parse_utc(reset_iso)),
708
+ "percent": int(row.percent),
709
+ "captured_at": _iso_z(row.captured_at),
710
+ "cumulative_usd": row.cost_usd,
711
+ "marginal_usd": row.marginal_cost_usd,
712
+ "input_tokens": row.input_tokens,
713
+ "cached_input_tokens": row.cached_input_tokens,
714
+ "output_tokens": row.output_tokens,
715
+ "reasoning_output_tokens": row.reasoning_output_tokens,
716
+ "total_tokens": row.total_tokens,
717
+ "five_hour_percent": None,
718
+ }
719
+
720
+
721
+ def _codex_breakdown_rows(ident, reset, speed, cache_conn, stats_conn):
722
+ try:
723
+ return codex_quota_breakdown(
724
+ ident, reset, speed=speed, cache_conn=cache_conn, stats_conn=stats_conn,
725
+ )
726
+ except sqlite3.Error:
727
+ return None
728
+
729
+
730
+ def _union_cluster_milestones(cluster, block_key, speed, cache_conn, stats_conn):
731
+ """Union ``codex_quota_breakdown`` milestone rows across every cluster
732
+ member (each jittered sibling reset via the existing per-reset path).
733
+
734
+ One physical reset can split its crossings across sibling resets, so the
735
+ full ledger is the union — shaped and ordered by ``(captured_at, percent)``.
736
+ Returns ``None`` if any member's projection is incoherent (mirrors the
737
+ single-reset signal so the caller can surface ``projection_incoherent``).
738
+ """
739
+ shaped: list = []
740
+ for member in cluster.cluster_members:
741
+ ident = QuotaWindowIdentity(
742
+ source="codex", source_root_key=member.root,
743
+ logical_limit_key=member.limit, observed_slot=member.slot,
744
+ window_minutes=member.window, limit_id=member.limit_id,
745
+ limit_name=member.limit_name,
746
+ )
747
+ breakdown = _codex_breakdown_rows(
748
+ ident, member.reset, speed, cache_conn, stats_conn
749
+ )
750
+ if breakdown is None:
751
+ return None # projection-incoherent signal to the caller
752
+ shaped.extend(
753
+ _shape_codex_milestone(r, key_parts=member.key_parts, block_key=block_key)
754
+ for r in breakdown
755
+ )
756
+ shaped.sort(key=lambda m: (m["captured_at"] or "", m["percent"]))
757
+ return shaped
758
+
759
+
760
+ def _codex_cycle_blocks(stats_conn, cache_conn, cyc, speed) -> "list | None":
761
+ out: list = []
762
+ for block in _codex_five_hour_clusters(stats_conn, cyc):
763
+ block_key = block.block_key
764
+ milestones = _union_cluster_milestones(
765
+ block, block_key, speed, cache_conn, stats_conn
766
+ )
767
+ if milestones is None:
768
+ return None # projection-incoherent signal to the caller
769
+ out.append(
770
+ {
771
+ "key": block_key,
772
+ "block_start_at": _iso_z(block.start),
773
+ "five_hour_resets_at": _iso_z(block.reset),
774
+ "final_five_hour_percent": block.current_percent,
775
+ "total_cost_usd": None,
776
+ "crossed_seven_day_reset": False,
777
+ "is_closed": block.reset <= cyc.reset and block.reset <= _now_guard(cyc),
778
+ "milestones": milestones,
779
+ "credits": [],
780
+ }
781
+ )
782
+ return out
783
+
784
+
785
+ def _now_guard(cyc):
786
+ # A 5h block is closed once its reset has passed the cycle's effective end
787
+ # (historic cycle) — a conservative closed flag for retained blocks.
788
+ return cyc.end
789
+
790
+
791
+ def build_codex_cycle_detail(
792
+ stats_conn, cache_conn, *, identity, key, speed, now_utc
793
+ ):
794
+ """Complete payload for one Codex cycle (spec §1c). On success returns a
795
+ dict mirroring the Claude detail shape (segments = a single Codex segment,
796
+ dividers = []). On failure returns ``(None, reason)`` where reason ∈
797
+ {pruned, rebuild_pending, projection_incoherent, unknown} — Task 4 maps it
798
+ into the 404 body.
799
+ """
800
+ now = now_utc.astimezone(UTC) if now_utc.tzinfo else now_utc.replace(tzinfo=UTC)
801
+ tz = _resolve_display_tz()
802
+ try:
803
+ cycles = _load_codex_cycles(
804
+ stats_conn, getattr(identity, "source_root_keys", ())
805
+ )
806
+ except sqlite3.Error:
807
+ return (None, "projection_incoherent")
808
+
809
+ match = next((c for c in cycles if c.key == key), None)
810
+ if match is None:
811
+ # Distinguish a pruned/orphaned cycle from a genuinely unknown key.
812
+ try:
813
+ orphaned = _load_codex_cycles(
814
+ stats_conn, getattr(identity, "source_root_keys", ()),
815
+ include_orphaned=True,
816
+ )
817
+ except sqlite3.Error:
818
+ return (None, "projection_incoherent")
819
+ if any(c.key == key for c in orphaned):
820
+ return (None, "pruned")
821
+ return (None, "unknown")
822
+
823
+ milestones = _union_cluster_milestones(
824
+ match, match.block_key, speed, cache_conn, stats_conn
825
+ )
826
+ if milestones is None:
827
+ return (None, "projection_incoherent")
828
+ blocks = _codex_cycle_blocks(stats_conn, cache_conn, match, speed)
829
+ if blocks is None:
830
+ return (None, "projection_incoherent")
831
+
832
+ entry = _codex_cycle_entry(stats_conn, match, identity, now, tz)
833
+ return {
834
+ "source": "codex",
835
+ "key": entry["key"],
836
+ "start_at_utc": entry["start_at_utc"],
837
+ "end_at_utc": entry["end_at_utc"],
838
+ "resets_at_utc": entry["resets_at_utc"],
839
+ "label": entry["label"],
840
+ "is_current": entry["is_current"],
841
+ "detail_stamp": entry["detail_stamp"],
842
+ "segments": [{"reset_event_id": 0, "milestones": milestones}],
843
+ "dividers": [],
844
+ "blocks": blocks,
845
+ }