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.
- package/CHANGELOG.md +13 -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 +175 -20
- package/bin/_cctally_dashboard_conversation.py +1 -1
- package/bin/_cctally_dashboard_envelope.py +12 -0
- package/bin/_cctally_dashboard_sources.py +15 -1
- package/bin/_cctally_doctor.py +16 -1
- package/bin/_cctally_milestone_history.py +845 -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 +158 -0
- package/bin/cctally +31 -0
- package/dashboard/static/assets/index-BwvAcS_k.js +92 -0
- package/dashboard/static/assets/index-CrIlpAlQ.css +1 -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,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
|
+
}
|