cctally 1.79.0 → 1.79.2

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 CHANGED
@@ -5,6 +5,16 @@ based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [1.79.2] - 2026-07-23
9
+
10
+ ### Fixed
11
+ - The dashboard's current-period five-hour milestone section now shows the complete block position and both navigation controls immediately, matching historic periods while keeping older block details lazy-loaded. Claude receives the fix now, and Codex uses the same behavior whenever native 300-minute blocks are retained.
12
+
13
+ ## [1.79.1] - 2026-07-22
14
+
15
+ ### Fixed
16
+ - 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)
17
+
8
18
  ## [1.79.0] - 2026-07-22
9
19
 
10
20
  ### Added
@@ -6345,9 +6345,11 @@ class DashboardHTTPHandler(BaseHTTPRequestHandler):
6345
6345
  c = sys.modules["cctally"]
6346
6346
  try:
6347
6347
  if source == "claude":
6348
- # Claude key is a week_start_date (YYYY-MM-DD).
6349
- if not _re.fullmatch(r"\d{4}-\d{2}-\d{2}", key):
6350
- self._send_milestones_json(400, {"error": "invalid week key"})
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], dict[str, object]] = {}
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
- item = boundaries.setdefault(boundary, {"roots": set(), "used": []})
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), boundary_data = next(iter(boundaries.items()))
152
- root_keys = boundary_data["roots"]
153
- used_values = boundary_data["used"]
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=tuple(sorted(root_keys)),
159
- used_percent=max(used_values) if used_values else None,
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