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.
- package/CHANGELOG.md +18 -0
- package/README.md +2 -0
- package/bin/_cctally_cache.py +140 -2
- package/bin/_cctally_config.py +50 -0
- package/bin/_cctally_dashboard.py +177 -20
- package/bin/_cctally_dashboard_conversation.py +1 -1
- package/bin/_cctally_dashboard_envelope.py +12 -0
- package/bin/_cctally_dashboard_sources.py +39 -11
- package/bin/_cctally_doctor.py +16 -1
- package/bin/_cctally_milestone_history.py +966 -0
- package/bin/_cctally_tui.py +23 -0
- package/bin/_cctally_update.py +338 -15
- package/bin/_cctally_weekrefs.py +6 -2
- package/bin/_lib_codex_conversation.py +1342 -18
- package/bin/_lib_codex_conversation_export.py +21 -1
- package/bin/_lib_codex_conversation_query.py +530 -67
- package/bin/_lib_conversation_dispatch.py +2 -2
- package/bin/_lib_doctor.py +38 -0
- package/bin/_lib_milestone_history.py +81 -0
- package/bin/cctally +31 -0
- package/dashboard/static/assets/index-CrIlpAlQ.css +1 -0
- package/dashboard/static/assets/index-Dc2u8c52.js +92 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +3 -1
- package/dashboard/static/assets/index-CkTe6VJt.js +0 -87
- package/dashboard/static/assets/index-DsEXuMpq.css +0 -1
|
@@ -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
|
+
}
|