cctally 1.79.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 +5 -0
- package/bin/_cctally_dashboard.py +5 -3
- package/bin/_cctally_dashboard_sources.py +24 -10
- package/bin/_cctally_milestone_history.py +275 -154
- package/bin/_lib_milestone_history.py +1 -78
- package/dashboard/static/assets/index-Dc2u8c52.js +92 -0
- package/dashboard/static/dashboard.html +1 -1
- package/package.json +1 -1
- package/dashboard/static/assets/index-BwvAcS_k.js +0 -92
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,11 @@ based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [1.79.1] - 2026-07-22
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- Dashboard hero history now steps through actual provider reset/re-anchor cycles with one milestone ledger per cycle, exposes every retained 5-hour block overlapping the selected cycle, and preserves the requested block direction across lazy detail loading for both Claude and Codex. (#338)
|
|
12
|
+
|
|
8
13
|
## [1.79.0] - 2026-07-22
|
|
9
14
|
|
|
10
15
|
### Added
|
|
@@ -6345,9 +6345,11 @@ class DashboardHTTPHandler(BaseHTTPRequestHandler):
|
|
|
6345
6345
|
c = sys.modules["cctally"]
|
|
6346
6346
|
try:
|
|
6347
6347
|
if source == "claude":
|
|
6348
|
-
# Claude
|
|
6349
|
-
|
|
6350
|
-
|
|
6348
|
+
# Reset-defined Claude cycles use the same opaque server-issued
|
|
6349
|
+
# resource-key contract as Codex; the storage date never rides
|
|
6350
|
+
# the route.
|
|
6351
|
+
if not _re.fullmatch(r"milestone_cycle:[A-Za-z0-9_-]{43}", key):
|
|
6352
|
+
self._send_milestones_json(400, {"error": "invalid cycle key"})
|
|
6351
6353
|
return
|
|
6352
6354
|
conn = open_db()
|
|
6353
6355
|
try:
|
|
@@ -38,6 +38,7 @@ from _lib_dashboard_sources import (
|
|
|
38
38
|
dashboard_resource_key,
|
|
39
39
|
)
|
|
40
40
|
from _lib_quota import (
|
|
41
|
+
QuotaWindowIdentity,
|
|
41
42
|
build_blocks,
|
|
42
43
|
build_history,
|
|
43
44
|
forecast_quota,
|
|
@@ -94,6 +95,9 @@ class CodexCycleBoundary:
|
|
|
94
95
|
# Root provenance is server-only accounting input, never public wire data.
|
|
95
96
|
source_root_keys: tuple[str, ...]
|
|
96
97
|
used_percent: float | None = None
|
|
98
|
+
# Exact server-side quota identity selected for the hero. It is never
|
|
99
|
+
# serialized; milestone-history keys hash it opaquely.
|
|
100
|
+
quota_identity: QuotaWindowIdentity | None = None
|
|
97
101
|
|
|
98
102
|
|
|
99
103
|
@dataclass(frozen=True)
|
|
@@ -126,7 +130,7 @@ def _resolve_codex_weekly_cycle(
|
|
|
126
130
|
now_utc: dt.datetime,
|
|
127
131
|
) -> CodexCycleBoundary:
|
|
128
132
|
"""Select exactly one active account-level 10,080-minute native cycle."""
|
|
129
|
-
boundaries: dict[tuple[int, dt.datetime],
|
|
133
|
+
boundaries: dict[tuple[int, dt.datetime], list[tuple[object, object]]] = {}
|
|
130
134
|
stale_weekly_evidence = False
|
|
131
135
|
for history in build_history(tuple(observations)):
|
|
132
136
|
if history.identity.window_minutes != 10_080:
|
|
@@ -140,23 +144,33 @@ def _resolve_codex_weekly_cycle(
|
|
|
140
144
|
stale_weekly_evidence = True
|
|
141
145
|
continue
|
|
142
146
|
boundary = (history.identity.window_minutes, baseline.resets_at)
|
|
143
|
-
|
|
144
|
-
item["roots"].add(history.identity.source_root_key)
|
|
145
|
-
if baseline.used_percent is not None:
|
|
146
|
-
item["used"].append(float(baseline.used_percent))
|
|
147
|
+
boundaries.setdefault(boundary, []).append((history, baseline))
|
|
147
148
|
if len(boundaries) != 1:
|
|
148
149
|
if not boundaries:
|
|
149
150
|
raise CodexCycleUnavailable("stale" if stale_weekly_evidence else "missing")
|
|
150
151
|
raise CodexCycleUnavailable("conflicting")
|
|
151
|
-
(window_minutes, resets_at),
|
|
152
|
-
|
|
153
|
-
|
|
152
|
+
(window_minutes, resets_at), candidates = next(iter(boundaries.items()))
|
|
153
|
+
# Preserve the existing hero's max-used-percent choice, then pin every
|
|
154
|
+
# remaining tie deterministically. The selected full identity—not the
|
|
155
|
+
# union of sibling roots/slots/limits—owns the hero cycle.
|
|
156
|
+
history, baseline = max(
|
|
157
|
+
candidates,
|
|
158
|
+
key=lambda item: (
|
|
159
|
+
float(item[1].used_percent),
|
|
160
|
+
item[1].captured_at.astimezone(UTC),
|
|
161
|
+
item[0].identity.source_root_key,
|
|
162
|
+
item[0].identity.logical_limit_key,
|
|
163
|
+
item[0].identity.observed_slot,
|
|
164
|
+
),
|
|
165
|
+
)
|
|
166
|
+
selected_identity = history.identity
|
|
154
167
|
return CodexCycleBoundary(
|
|
155
168
|
window_minutes=window_minutes,
|
|
156
169
|
start_at=resets_at - dt.timedelta(minutes=window_minutes),
|
|
157
170
|
resets_at=resets_at,
|
|
158
|
-
source_root_keys=
|
|
159
|
-
used_percent=
|
|
171
|
+
source_root_keys=(selected_identity.source_root_key,),
|
|
172
|
+
used_percent=float(baseline.used_percent),
|
|
173
|
+
quota_identity=selected_identity,
|
|
160
174
|
)
|
|
161
175
|
|
|
162
176
|
|