cctally 1.82.0 → 1.83.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 +70 -0
- package/README.md +52 -74
- package/bin/_cctally_alerts.py +8 -1
- package/bin/_cctally_cache.py +963 -149
- package/bin/_cctally_config.py +43 -4
- package/bin/_cctally_core.py +933 -759
- package/bin/_cctally_dashboard.py +157 -47
- package/bin/_cctally_dashboard_cache_report.py +13 -6
- package/bin/_cctally_dashboard_conversation.py +1 -0
- package/bin/_cctally_dashboard_envelope.py +186 -8
- package/bin/_cctally_dashboard_share.py +60 -20
- package/bin/_cctally_dashboard_sources.py +427 -128
- package/bin/_cctally_db.py +605 -128
- package/bin/_cctally_doctor.py +413 -28
- package/bin/_cctally_five_hour.py +12 -5
- package/bin/_cctally_journal.py +2050 -156
- package/bin/_cctally_journal_repair.py +519 -0
- package/bin/_cctally_milestone_history.py +142 -56
- package/bin/_cctally_milestones.py +179 -111
- package/bin/_cctally_parser.py +42 -0
- package/bin/_cctally_project.py +24 -18
- package/bin/_cctally_quota.py +139 -25
- package/bin/_cctally_record.py +279 -108
- package/bin/_cctally_rederive.py +1052 -0
- package/bin/_cctally_reporting.py +58 -53
- package/bin/_cctally_setup.py +1 -0
- package/bin/_cctally_source_analytics.py +4 -1
- package/bin/_cctally_statusline.py +11 -11
- package/bin/_cctally_store.py +1039 -31
- package/bin/_cctally_sync_week.py +17 -8
- package/bin/_cctally_tui.py +421 -54
- package/bin/_cctally_update.py +133 -8
- package/bin/_cctally_weekrefs.py +14 -0
- package/bin/_lib_aggregators.py +10 -6
- package/bin/_lib_cache_report.py +101 -9
- package/bin/_lib_codex_pools.py +82 -0
- package/bin/_lib_conversation_query.py +126 -33
- package/bin/_lib_dashboard_sources.py +126 -1
- package/bin/_lib_diff_kernel.py +28 -15
- package/bin/_lib_doctor.py +342 -4
- package/bin/_lib_journal.py +924 -2
- package/bin/_lib_jsonl.py +43 -14
- package/bin/_lib_pricing.py +140 -21
- package/bin/_lib_readme_refresh.py +401 -0
- package/bin/_lib_rederive.py +395 -0
- package/bin/_lib_share.py +58 -2
- package/bin/cctally +56 -8
- package/dashboard/static/assets/{index-DJP4gEB7.js → index-3bgCMVHb.js} +52 -52
- package/dashboard/static/assets/index-D27EIHEI.css +1 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +6 -1
- package/dashboard/static/assets/index-Dk1nplOz.css +0 -1
|
@@ -43,11 +43,19 @@ from _lib_quota import (
|
|
|
43
43
|
build_blocks,
|
|
44
44
|
build_history,
|
|
45
45
|
forecast_quota,
|
|
46
|
+
latest_physical_observation,
|
|
46
47
|
percent_milestones,
|
|
47
48
|
quota_freshness,
|
|
48
49
|
select_baseline,
|
|
50
|
+
stale_after_seconds,
|
|
49
51
|
)
|
|
50
|
-
from _lib_jsonl import CodexEntry
|
|
52
|
+
from _lib_jsonl import CodexEntry
|
|
53
|
+
from _lib_codex_pools import (
|
|
54
|
+
codex_history_is_model_scoped,
|
|
55
|
+
codex_model_scoped_quota_pool,
|
|
56
|
+
is_model_scoped_codex_quota,
|
|
57
|
+
)
|
|
58
|
+
from _lib_codex_conversation import _display_title as _codex_display_title
|
|
51
59
|
from _lib_fmt import stable_sum
|
|
52
60
|
from _lib_aggregators import _aggregate_codex_buckets
|
|
53
61
|
from _lib_five_hour import _FIVE_HOUR_JITTER_FLOOR_SECONDS
|
|
@@ -140,6 +148,10 @@ class CodexCycleBoundary:
|
|
|
140
148
|
# Exact server-side quota identity selected for the hero. It is never
|
|
141
149
|
# serialized; milestone-history keys hash it opaquely.
|
|
142
150
|
quota_identity: QuotaWindowIdentity | None = None
|
|
151
|
+
# #350: whether this boundary won the §3.2 ranking on STALE evidence (no
|
|
152
|
+
# fresh boundary existed for the account). Backward-looking actuals stay
|
|
153
|
+
# bounded, but the hero discloses it through ``hero.cycle_freshness``.
|
|
154
|
+
evidence_stale: bool = False
|
|
143
155
|
|
|
144
156
|
|
|
145
157
|
@dataclass(frozen=True)
|
|
@@ -152,19 +164,22 @@ class CodexWeeklyPeriod:
|
|
|
152
164
|
used_percent: float | None = None
|
|
153
165
|
|
|
154
166
|
|
|
155
|
-
def
|
|
156
|
-
"""Whether
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
167
|
+
def _codex_history_row_is_model_scoped(row: object) -> bool:
|
|
168
|
+
"""Whether a SERIALIZED Codex quota history row sits outside account quota.
|
|
169
|
+
|
|
170
|
+
The single predicate both the initial build (``_quota_read_model``) and the
|
|
171
|
+
idle refresh (``refresh_codex_source_clock``) consult, so the two paths
|
|
172
|
+
cannot drift apart: the build stamps ``model_scoped`` from
|
|
173
|
+
``codex_history_is_model_scoped`` and then asks THIS function about the row
|
|
174
|
+
it just built, and the refresh — which only ever sees the serialized row —
|
|
175
|
+
asks the same function. Fixing one path and not the other is exactly how
|
|
176
|
+
the quota summary and its idle refresh would disagree (#373 spec §7.2).
|
|
177
|
+
|
|
178
|
+
The key is additive and OMITTED when false (spec §7.3), so a row without it
|
|
179
|
+
is standard account quota and every fixture that has no model-scoped window
|
|
180
|
+
serializes byte-identically.
|
|
181
|
+
"""
|
|
182
|
+
return bool(isinstance(row, Mapping) and row.get("model_scoped"))
|
|
168
183
|
|
|
169
184
|
|
|
170
185
|
def _resolve_codex_weekly_cycle(
|
|
@@ -180,34 +195,68 @@ def _resolve_codex_weekly_cycle(
|
|
|
180
195
|
Raises ``CodexCycleUnavailable`` only when NO account yields a live cycle. A
|
|
181
196
|
single-account install returns a 1-element list = today's boundary
|
|
182
197
|
byte-for-byte (the hero path is unchanged, spec R8).
|
|
198
|
+
|
|
199
|
+
#350 — FRESH-FIRST ranking (spec §3.2). Codex has no background quota poll,
|
|
200
|
+
so ``stale_after_seconds(10_080) == 3600`` makes an idle weekly observation
|
|
201
|
+
stale after exactly one hour. Discarding a stale-but-FUTURE boundary blanked
|
|
202
|
+
the hero's backward-looking actuals even though the spend was never lost, so
|
|
203
|
+
each account's future weekly boundaries are now collected into a fresh set
|
|
204
|
+
and a stale set and ranked:
|
|
205
|
+
|
|
206
|
+
1. exactly one FRESH boundary -> valid, cycle fresh;
|
|
207
|
+
2. else, no fresh boundaries and exactly one STALE boundary -> valid, cycle
|
|
208
|
+
stale (``CodexCycleBoundary.evidence_stale``, surfaced as the additive
|
|
209
|
+
``hero.cycle_freshness``);
|
|
210
|
+
3. else -> invalid, with today's ``conflicting``/``stale``/``missing`` reason.
|
|
211
|
+
|
|
212
|
+
Fresh-first ordering is load-bearing: a flat count over the union would
|
|
213
|
+
regress one-fresh-plus-one-stale, which resolves valid today. Only the EXACT
|
|
214
|
+
``"stale"`` freshness state is eligible — ``"future"`` (a capture ahead of
|
|
215
|
+
``now``) and ``"unavailable"`` stay invalid and keep today's reason.
|
|
183
216
|
"""
|
|
184
|
-
|
|
217
|
+
fresh_by_account: dict[str, dict[tuple[int, dt.datetime], list[tuple[object, object]]]] = {}
|
|
218
|
+
stale_by_account: dict[str, dict[tuple[int, dt.datetime], list[tuple[object, object]]]] = {}
|
|
185
219
|
accounts_seen: set[str] = set()
|
|
186
|
-
|
|
220
|
+
ineligible_by_account: dict[str, bool] = {}
|
|
187
221
|
for history in build_history(tuple(observations)):
|
|
188
222
|
if history.identity.window_minutes != 10_080:
|
|
189
223
|
continue
|
|
190
|
-
|
|
224
|
+
# The baseline observation is the §7.1 label authority, so it is
|
|
225
|
+
# resolved BEFORE classification wherever the call site has one.
|
|
226
|
+
baseline = select_baseline(history.observations, now_utc)
|
|
227
|
+
if codex_history_is_model_scoped(history, baseline=baseline):
|
|
191
228
|
continue
|
|
192
229
|
account = history.identity.account_key
|
|
193
230
|
accounts_seen.add(account)
|
|
194
|
-
baseline = select_baseline(history.observations, now_utc)
|
|
195
231
|
if baseline is None or baseline.resets_at <= now_utc:
|
|
196
232
|
continue
|
|
197
|
-
|
|
198
|
-
stale_by_account[account] = True
|
|
199
|
-
continue
|
|
233
|
+
state = quota_freshness(history.physical_observations, now_utc).state
|
|
200
234
|
boundary = (history.identity.window_minutes, baseline.resets_at)
|
|
201
|
-
|
|
235
|
+
if state == "fresh":
|
|
236
|
+
bucket = fresh_by_account
|
|
237
|
+
elif state == "stale":
|
|
238
|
+
bucket = stale_by_account
|
|
239
|
+
else:
|
|
240
|
+
# "future"/"unavailable" evidence is never a stale fallback; it keeps
|
|
241
|
+
# today's non-fresh reason so the envelope degrades exactly as before.
|
|
242
|
+
ineligible_by_account[account] = True
|
|
243
|
+
continue
|
|
244
|
+
bucket.setdefault(account, {}).setdefault(boundary, []).append((history, baseline))
|
|
202
245
|
cycles: list[CodexCycleBoundary] = []
|
|
203
246
|
reasons: list[str] = []
|
|
204
|
-
for account in sorted(accounts_seen | set(
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
247
|
+
for account in sorted(accounts_seen | set(fresh_by_account) | set(stale_by_account)):
|
|
248
|
+
fresh_boundaries = fresh_by_account.get(account, {})
|
|
249
|
+
stale_boundaries = stale_by_account.get(account, {})
|
|
250
|
+
if len(fresh_boundaries) == 1:
|
|
251
|
+
boundaries, evidence_stale = fresh_boundaries, False
|
|
252
|
+
elif not fresh_boundaries and len(stale_boundaries) == 1:
|
|
253
|
+
boundaries, evidence_stale = stale_boundaries, True
|
|
254
|
+
else:
|
|
255
|
+
# Within one account: >=2 fresh, or no fresh and >=2 stale ->
|
|
256
|
+
# conflicting; nothing eligible -> today's stale/missing reason.
|
|
208
257
|
reasons.append(
|
|
209
|
-
"conflicting" if
|
|
210
|
-
else ("stale" if
|
|
258
|
+
"conflicting" if (fresh_boundaries or stale_boundaries)
|
|
259
|
+
else ("stale" if ineligible_by_account.get(account) else "missing")
|
|
211
260
|
)
|
|
212
261
|
continue
|
|
213
262
|
(window_minutes, resets_at), candidates = next(iter(boundaries.items()))
|
|
@@ -232,6 +281,7 @@ def _resolve_codex_weekly_cycle(
|
|
|
232
281
|
source_root_keys=(selected_identity.source_root_key,),
|
|
233
282
|
used_percent=float(baseline.used_percent),
|
|
234
283
|
quota_identity=selected_identity,
|
|
284
|
+
evidence_stale=evidence_stale,
|
|
235
285
|
))
|
|
236
286
|
if not cycles:
|
|
237
287
|
# Aggregate reason: for a single account this is exactly the old reason
|
|
@@ -244,6 +294,157 @@ def _resolve_codex_weekly_cycle(
|
|
|
244
294
|
return cycles
|
|
245
295
|
|
|
246
296
|
|
|
297
|
+
def resolve_codex_cycle_detail_identity(
|
|
298
|
+
cache_conn,
|
|
299
|
+
*,
|
|
300
|
+
source_root_keys: Iterable[str],
|
|
301
|
+
now_utc: dt.datetime,
|
|
302
|
+
):
|
|
303
|
+
"""The live-cycle identity for a per-request Codex cycle-DETAIL read (#373).
|
|
304
|
+
|
|
305
|
+
The cycle INDEX is built with the hero's live ``CodexCycleBoundary``; the
|
|
306
|
+
detail route runs outside the snapshot build and has no envelope, so it used
|
|
307
|
+
to pass a stub carrying no ``resets_at``. The former future-reset proxy then
|
|
308
|
+
reported ``is_current: true`` for every future-ending cycle — including a
|
|
309
|
+
historic one that an early re-anchor had already closed. Resolving the same
|
|
310
|
+
boundary here is what makes one cycle key describe one cycle on both routes.
|
|
311
|
+
|
|
312
|
+
``source_root_keys`` scopes the returned identity's cycle LOOKUP (the caller
|
|
313
|
+
passes every retained Codex root, so a just-closed cycle stays fetchable);
|
|
314
|
+
the live boundary itself is resolved from the active roots' observations,
|
|
315
|
+
exactly as the source build does.
|
|
316
|
+
|
|
317
|
+
Degrades to a bare-roots identity — today's behaviour — whenever no live
|
|
318
|
+
cycle resolves. The clip guard stays unarmed on that identity by design
|
|
319
|
+
(``_boundary_has_live_reset``), so the detail keeps clipping as it did
|
|
320
|
+
before #373 rather than trusting the proxy.
|
|
321
|
+
"""
|
|
322
|
+
identity = SimpleNamespace(
|
|
323
|
+
source_root_keys=tuple(source_root_keys),
|
|
324
|
+
resets_at=None,
|
|
325
|
+
quota_identity=None,
|
|
326
|
+
)
|
|
327
|
+
if cache_conn is None:
|
|
328
|
+
return identity
|
|
329
|
+
try:
|
|
330
|
+
active_roots = tuple(sorted(
|
|
331
|
+
str(row[0]) for row in cache_conn.execute(
|
|
332
|
+
"SELECT source_root_key FROM codex_source_roots"
|
|
333
|
+
)
|
|
334
|
+
))
|
|
335
|
+
if not active_roots:
|
|
336
|
+
return identity
|
|
337
|
+
observations = load_codex_quota_observations(
|
|
338
|
+
source_root_keys=active_roots,
|
|
339
|
+
cache_conn=cache_conn,
|
|
340
|
+
captured_at_or_after=(
|
|
341
|
+
now_utc - dt.timedelta(days=DASHBOARD_QUOTA_RECENT_DAYS)
|
|
342
|
+
),
|
|
343
|
+
active_at=now_utc,
|
|
344
|
+
max_rows=DASHBOARD_QUOTA_OBSERVATION_LIMIT,
|
|
345
|
+
)
|
|
346
|
+
cycles = _resolve_codex_weekly_cycle(observations, now_utc)
|
|
347
|
+
except (sqlite3.Error, CodexCycleUnavailable, ValueError):
|
|
348
|
+
return identity
|
|
349
|
+
if not cycles:
|
|
350
|
+
return identity
|
|
351
|
+
boundary = cycles[0]
|
|
352
|
+
identity.resets_at = boundary.resets_at
|
|
353
|
+
identity.quota_identity = boundary.quota_identity
|
|
354
|
+
return identity
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _codex_next_decision_at(
|
|
358
|
+
observations: Iterable[object],
|
|
359
|
+
cycles: Iterable[CodexCycleBoundary],
|
|
360
|
+
now_utc: dt.datetime,
|
|
361
|
+
) -> dt.datetime | None:
|
|
362
|
+
"""The earliest future instant at which weekly-cycle resolution can change.
|
|
363
|
+
|
|
364
|
+
#350 spec §3.3. Cycle validity is time-dependent even on FROZEN evidence:
|
|
365
|
+
``_resolve_codex_weekly_cycle`` passes ``now_utc`` to both ``select_baseline``
|
|
366
|
+
(a future-dated capture becomes baseline-eligible purely because time passed,
|
|
367
|
+
which can switch the selected reset) and ``quota_freshness`` (fresh flips to
|
|
368
|
+
stale as age crosses ``stale_after_seconds``). One fresh plus one stale
|
|
369
|
+
boundary resolves FRESH today and ``conflicting`` an hour later on the very
|
|
370
|
+
same rows. The idle clock cannot re-resolve that itself — the public
|
|
371
|
+
histories it sees are capped at ``SOURCE_HISTORY_LIMIT`` and omit
|
|
372
|
+
``logical_limit_key`` (§2.3) — so build time instead records WHEN the clock
|
|
373
|
+
must stop trusting its verdict, and the tick rebuilds authoritatively at the
|
|
374
|
+
crossing. That is one rebuild per deadline (a handful per weekly cycle), not
|
|
375
|
+
one per tick.
|
|
376
|
+
|
|
377
|
+
The deadline is the ``min`` of three candidate kinds, dropping any candidate
|
|
378
|
+
at or before ``now_utc``:
|
|
379
|
+
|
|
380
|
+
1. every selected cycle's ``resets_at`` (expiry — including the
|
|
381
|
+
#341 multi-account case where account A expires while B stays live);
|
|
382
|
+
2. ``latest_physical_capture + stale_after_seconds(window)`` for every weekly
|
|
383
|
+
history with a live baseline (fresh -> stale). This is a deliberate
|
|
384
|
+
SUPERSET of the §3.2 ranking participants — it also covers histories whose
|
|
385
|
+
freshness is ``"future"``/``"unavailable"`` and so never enter the ranking
|
|
386
|
+
— because an extra candidate can only pull the deadline EARLIER, and an
|
|
387
|
+
earlier deadline is always the conservative direction;
|
|
388
|
+
3. the ``captured_at`` of any future-dated weekly observation
|
|
389
|
+
(future -> fresh / baseline eligibility).
|
|
390
|
+
|
|
391
|
+
Returns ``None`` when nothing can flip. Server-only: it rides ``clock_data``
|
|
392
|
+
and never reaches the public source envelope.
|
|
393
|
+
"""
|
|
394
|
+
candidates: list[dt.datetime] = []
|
|
395
|
+
for cycle in cycles:
|
|
396
|
+
candidates.append(cycle.resets_at.astimezone(UTC))
|
|
397
|
+
for history in build_history(tuple(observations)):
|
|
398
|
+
if history.identity.window_minutes != 10_080:
|
|
399
|
+
continue
|
|
400
|
+
baseline = select_baseline(history.observations, now_utc)
|
|
401
|
+
if codex_history_is_model_scoped(history, baseline=baseline):
|
|
402
|
+
continue
|
|
403
|
+
if baseline is not None and baseline.resets_at > now_utc:
|
|
404
|
+
latest = latest_physical_observation(history.physical_observations)
|
|
405
|
+
if latest is not None:
|
|
406
|
+
candidates.append(
|
|
407
|
+
latest.captured_at.astimezone(UTC)
|
|
408
|
+
+ dt.timedelta(
|
|
409
|
+
seconds=stale_after_seconds(history.identity.window_minutes),
|
|
410
|
+
)
|
|
411
|
+
)
|
|
412
|
+
# A capture ahead of ``now`` is not baseline-eligible yet, so this runs
|
|
413
|
+
# even for a history with no live baseline at all.
|
|
414
|
+
for observation in (
|
|
415
|
+
*history.observations, *history.physical_observations,
|
|
416
|
+
):
|
|
417
|
+
captured_at = observation.captured_at.astimezone(UTC)
|
|
418
|
+
if captured_at > now_utc:
|
|
419
|
+
candidates.append(captured_at)
|
|
420
|
+
live = [candidate for candidate in candidates if candidate > now_utc]
|
|
421
|
+
return min(live) if live else None
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def codex_decision_deadline_passed(
|
|
425
|
+
state: object,
|
|
426
|
+
now_utc: dt.datetime,
|
|
427
|
+
) -> bool:
|
|
428
|
+
"""Whether a published Codex state's cycle decision deadline has elapsed.
|
|
429
|
+
|
|
430
|
+
#350 spec §3.3. When this holds the tick MUST rebuild Codex authoritatively
|
|
431
|
+
via ``build_codex_source_state`` — bypassing both the idle clock and
|
|
432
|
+
``reuse_coherent_source_state`` — because the frozen evidence would now
|
|
433
|
+
resolve to a different cycle (or to none). A state with no recorded deadline
|
|
434
|
+
(``None``, or an older generation that predates the field) never forces a
|
|
435
|
+
rebuild; the clock's expiry guard remains its safety net.
|
|
436
|
+
"""
|
|
437
|
+
clock_data = getattr(state, "clock_data", None)
|
|
438
|
+
if not isinstance(clock_data, Mapping):
|
|
439
|
+
return False
|
|
440
|
+
deadline = clock_data.get("codex_next_decision_at")
|
|
441
|
+
if not isinstance(deadline, dt.datetime):
|
|
442
|
+
return False
|
|
443
|
+
if deadline.tzinfo is None or deadline.utcoffset() is None:
|
|
444
|
+
return False
|
|
445
|
+
return now_utc.astimezone(UTC) >= deadline.astimezone(UTC)
|
|
446
|
+
|
|
447
|
+
|
|
247
448
|
def _codex_weekly_periods(
|
|
248
449
|
stats_conn: sqlite3.Connection,
|
|
249
450
|
*,
|
|
@@ -265,7 +466,7 @@ def _codex_weekly_periods(
|
|
|
265
466
|
placeholders = ",".join("?" for _ in roots)
|
|
266
467
|
try:
|
|
267
468
|
rows = stats_conn.execute(
|
|
268
|
-
"SELECT source_root_key, logical_limit_key, resets_at_utc, "
|
|
469
|
+
"SELECT source_root_key, logical_limit_key, limit_name, resets_at_utc, "
|
|
269
470
|
"nominal_start_at_utc, current_percent "
|
|
270
471
|
"FROM quota_window_blocks "
|
|
271
472
|
"WHERE source='codex' AND window_minutes=10080 "
|
|
@@ -277,10 +478,18 @@ def _codex_weekly_periods(
|
|
|
277
478
|
except sqlite3.Error:
|
|
278
479
|
rows = ()
|
|
279
480
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
481
|
+
# #373 §7.4: the fifth element marks the LIVE boundary, which is never
|
|
482
|
+
# clipped by a successor. Durable rows are never live on their own — only
|
|
483
|
+
# the caller's `active_cycle` is — but the jitter-merge below folds the
|
|
484
|
+
# live boundary together with its own durable row, so the flag is OR-ed on
|
|
485
|
+
# merge rather than taken from either side.
|
|
486
|
+
raw_boundaries: list[
|
|
487
|
+
tuple[dt.datetime, dt.datetime, set[str], list[float], bool]
|
|
488
|
+
] = []
|
|
489
|
+
|
|
490
|
+
for (root_key, logical_limit_key, limit_name, resets_at_raw,
|
|
491
|
+
start_at_raw, current_percent) in rows:
|
|
492
|
+
if is_model_scoped_codex_quota(logical_limit_key, limit_name):
|
|
284
493
|
continue
|
|
285
494
|
try:
|
|
286
495
|
start_at = dt.datetime.fromisoformat(str(start_at_raw).replace("Z", "+00:00"))
|
|
@@ -296,18 +505,23 @@ def _codex_weekly_periods(
|
|
|
296
505
|
used_values = []
|
|
297
506
|
if isinstance(current_percent, (int, float)) and not isinstance(current_percent, bool):
|
|
298
507
|
used_values.append(float(current_percent))
|
|
299
|
-
raw_boundaries.append((start_at, resets_at, {str(root_key)}, used_values))
|
|
508
|
+
raw_boundaries.append((start_at, resets_at, {str(root_key)}, used_values, False))
|
|
300
509
|
|
|
510
|
+
# `active_cycle is None` is the case §7.4 calls out explicitly: no boundary
|
|
511
|
+
# is live, so nothing is exempt and every period clips exactly as before.
|
|
301
512
|
if active_cycle is not None:
|
|
302
513
|
raw_boundaries.append((
|
|
303
514
|
active_cycle.start_at.astimezone(UTC),
|
|
304
515
|
active_cycle.resets_at.astimezone(UTC),
|
|
305
516
|
set(active_cycle.source_root_keys),
|
|
306
517
|
[active_cycle.used_percent] if active_cycle.used_percent is not None else [],
|
|
518
|
+
True,
|
|
307
519
|
))
|
|
308
520
|
|
|
309
|
-
ordered: list[
|
|
310
|
-
|
|
521
|
+
ordered: list[
|
|
522
|
+
tuple[dt.datetime, dt.datetime, set[str], list[float], bool]
|
|
523
|
+
] = []
|
|
524
|
+
for start_at, resets_at, period_roots, used_values, is_live in sorted(
|
|
311
525
|
raw_boundaries, key=lambda item: (item[0], item[1]),
|
|
312
526
|
):
|
|
313
527
|
if (
|
|
@@ -315,17 +529,23 @@ def _codex_weekly_periods(
|
|
|
315
529
|
and (start_at - ordered[-1][0]).total_seconds()
|
|
316
530
|
< _FIVE_HOUR_JITTER_FLOOR_SECONDS
|
|
317
531
|
):
|
|
318
|
-
first_start, latest_reset, existing_roots, existing_used = ordered[-1]
|
|
532
|
+
first_start, latest_reset, existing_roots, existing_used, existing_live = ordered[-1]
|
|
319
533
|
existing_roots.update(period_roots)
|
|
320
534
|
existing_used.extend(used_values)
|
|
321
535
|
ordered[-1] = (
|
|
322
536
|
first_start, max(latest_reset, resets_at), existing_roots, existing_used,
|
|
537
|
+
existing_live or is_live,
|
|
323
538
|
)
|
|
324
539
|
else:
|
|
325
|
-
ordered.append((
|
|
540
|
+
ordered.append((
|
|
541
|
+
start_at, resets_at, set(period_roots), list(used_values), is_live,
|
|
542
|
+
))
|
|
326
543
|
periods: list[CodexWeeklyPeriod] = []
|
|
327
|
-
for index, (start_at, resets_at, period_roots, used_values) in enumerate(ordered):
|
|
544
|
+
for index, (start_at, resets_at, period_roots, used_values, is_live) in enumerate(ordered):
|
|
328
545
|
next_start = ordered[index + 1][0] if index + 1 < len(ordered) else None
|
|
546
|
+
# The live cycle always ends at its own reset (#373 §7.4).
|
|
547
|
+
if is_live:
|
|
548
|
+
next_start = None
|
|
329
549
|
end_at = min(resets_at, next_start) if next_start is not None else resets_at
|
|
330
550
|
if end_at <= start_at:
|
|
331
551
|
continue
|
|
@@ -928,7 +1148,9 @@ def _codex_conversation_metadata(
|
|
|
928
1148
|
f"SELECT id, title FROM threads WHERE id IN ({placeholders})",
|
|
929
1149
|
batch,
|
|
930
1150
|
):
|
|
931
|
-
clean_title =
|
|
1151
|
+
clean_title = _codex_display_title(
|
|
1152
|
+
str(title) if title is not None else None
|
|
1153
|
+
)
|
|
932
1154
|
if clean_title:
|
|
933
1155
|
short_names[str(thread_id)] = clean_title
|
|
934
1156
|
except (OSError, sqlite3.Error):
|
|
@@ -993,6 +1215,7 @@ def _session_wire(
|
|
|
993
1215
|
view: Any,
|
|
994
1216
|
*,
|
|
995
1217
|
metadata: Mapping[tuple[str, str], Mapping[str, object]] | None = None,
|
|
1218
|
+
private_labels: dict[str, str] | None = None,
|
|
996
1219
|
) -> dict[str, object]:
|
|
997
1220
|
rows = []
|
|
998
1221
|
for row in view.rows:
|
|
@@ -1018,7 +1241,11 @@ def _session_wire(
|
|
|
1018
1241
|
)
|
|
1019
1242
|
)
|
|
1020
1243
|
), None)
|
|
1021
|
-
title =
|
|
1244
|
+
title = _codex_display_title(
|
|
1245
|
+
str(row_metadata.get("title"))
|
|
1246
|
+
if row_metadata and row_metadata.get("title") is not None
|
|
1247
|
+
else None
|
|
1248
|
+
)
|
|
1022
1249
|
project = str(row_metadata.get("project_label") or "").strip() if row_metadata else ""
|
|
1023
1250
|
started_at = row_metadata.get("started_at") if row_metadata else None
|
|
1024
1251
|
duration_min = None
|
|
@@ -1028,12 +1255,14 @@ def _session_wire(
|
|
|
1028
1255
|
duration_min = max(0, round((row.last_activity.astimezone(UTC) - started_dt.astimezone(UTC)).total_seconds() / 60))
|
|
1029
1256
|
except (TypeError, ValueError):
|
|
1030
1257
|
started_at = None
|
|
1258
|
+
key = dashboard_resource_key(
|
|
1259
|
+
"session", "codex", root_identity, row.session_id_path,
|
|
1260
|
+
)
|
|
1261
|
+
if title and private_labels is not None:
|
|
1262
|
+
private_labels[key] = title
|
|
1031
1263
|
rows.append({
|
|
1032
|
-
"key":
|
|
1033
|
-
"session", "codex", root_identity, row.session_id_path,
|
|
1034
|
-
),
|
|
1264
|
+
"key": key,
|
|
1035
1265
|
"source": "codex",
|
|
1036
|
-
"label": title or None,
|
|
1037
1266
|
"project": project or None,
|
|
1038
1267
|
"project_key": row_metadata.get("project_key") if row_metadata else None,
|
|
1039
1268
|
"started_at": started_at,
|
|
@@ -1298,10 +1527,14 @@ def _quota_read_model(
|
|
|
1298
1527
|
milestone_rows: list[dict[str, object]] = []
|
|
1299
1528
|
active_rows: list[dict[str, object]] = []
|
|
1300
1529
|
# R8 (#341 Task 4): the per-account `account_key` is serialized onto each
|
|
1301
|
-
# history row ONLY when the Codex provider has >1 REAL account, so the
|
|
1302
|
-
#
|
|
1303
|
-
#
|
|
1304
|
-
#
|
|
1530
|
+
# history row ONLY when the Codex provider has >1 REAL account, so the
|
|
1531
|
+
# dashboard client can scope per-account quota rows instead of merging them.
|
|
1532
|
+
# A <=1-real-account install (all fixtures) stays byte-identical (no key
|
|
1533
|
+
# added). #350 removed the original consumer, `_clock_cycle_validity`: the
|
|
1534
|
+
# idle clock no longer re-derives weekly-cycle validity at all, because this
|
|
1535
|
+
# public history view is LOSSY — capped at `SOURCE_HISTORY_LIMIT` and without
|
|
1536
|
+
# `logical_limit_key` — so it cannot resolve the cycle authoritatively. Build
|
|
1537
|
+
# time owns resolution; a `clock_data` decision deadline forces the rebuild.
|
|
1305
1538
|
_codex_decorated = False
|
|
1306
1539
|
try:
|
|
1307
1540
|
import _cctally_account
|
|
@@ -1320,9 +1553,16 @@ def _quota_read_model(
|
|
|
1320
1553
|
baseline = select_baseline(history.observations, context.now_utc)
|
|
1321
1554
|
freshness = quota_freshness(history.physical_observations, context.now_utc)
|
|
1322
1555
|
forecast = forecast_quota(history.physical_observations, context.now_utc)
|
|
1323
|
-
|
|
1556
|
+
# #373: a window outside account-level standard quota (a separate model
|
|
1557
|
+
# pool such as GPT-5.3-Codex-Spark) stays LISTED — a legitimate
|
|
1558
|
+
# independent pool must remain visible — but is excluded from every
|
|
1559
|
+
# account-level aggregate below. `baseline` is the label authority when
|
|
1560
|
+
# one exists (spec §7.1).
|
|
1561
|
+
model_scoped = codex_history_is_model_scoped(history, baseline=baseline)
|
|
1562
|
+
row = {
|
|
1324
1563
|
"key": dashboard_resource_key("quota", "codex", *key_parts),
|
|
1325
1564
|
"source": "codex",
|
|
1565
|
+
**({"model_scoped": True} if model_scoped else {}),
|
|
1326
1566
|
**({"account_key": identity.account_key} if _codex_decorated else {}),
|
|
1327
1567
|
"label": _native_limit_label(identity.limit_name, identity.window_minutes),
|
|
1328
1568
|
"observed_slot": identity.observed_slot,
|
|
@@ -1345,7 +1585,10 @@ def _quota_read_model(
|
|
|
1345
1585
|
"sample_span_seconds": forecast.sample_span_seconds,
|
|
1346
1586
|
"confidence": forecast.confidence,
|
|
1347
1587
|
},
|
|
1348
|
-
}
|
|
1588
|
+
}
|
|
1589
|
+
history_rows.append(row)
|
|
1590
|
+
if _codex_history_row_is_model_scoped(row):
|
|
1591
|
+
continue
|
|
1349
1592
|
if baseline is not None and baseline.resets_at > context.now_utc:
|
|
1350
1593
|
active_rows.append({
|
|
1351
1594
|
"key": dashboard_resource_key("quota", "codex", *key_parts),
|
|
@@ -1463,10 +1706,28 @@ def _quota_read_model(
|
|
|
1463
1706
|
"fresh" if active_rows and all(row["freshness"] == "fresh" for row in active_rows)
|
|
1464
1707
|
else ("unavailable" if not active_rows else "stale")
|
|
1465
1708
|
)
|
|
1466
|
-
# Active identities are presentation-critical.
|
|
1467
|
-
#
|
|
1709
|
+
# Active account identities are presentation-critical. Independent
|
|
1710
|
+
# model-scoped pools are also legitimate provider facts, so reserve the
|
|
1711
|
+
# remaining cap space for their newest captures before inactive account
|
|
1712
|
+
# history. Opaque resource-key order is only a stable tie-breaker.
|
|
1468
1713
|
active_keys = {str(row["key"]) for row in active_rows}
|
|
1469
|
-
|
|
1714
|
+
|
|
1715
|
+
def _history_retention_key(row):
|
|
1716
|
+
key = str(row["key"])
|
|
1717
|
+
if key in active_keys:
|
|
1718
|
+
return (0, 0.0, key)
|
|
1719
|
+
if _codex_history_row_is_model_scoped(row):
|
|
1720
|
+
captured_at = row.get("captured_at")
|
|
1721
|
+
try:
|
|
1722
|
+
captured_epoch = dt.datetime.fromisoformat(
|
|
1723
|
+
str(captured_at).replace("Z", "+00:00")
|
|
1724
|
+
).timestamp()
|
|
1725
|
+
except (TypeError, ValueError):
|
|
1726
|
+
captured_epoch = float("-inf")
|
|
1727
|
+
return (1, -captured_epoch, key)
|
|
1728
|
+
return (2, 0.0, key)
|
|
1729
|
+
|
|
1730
|
+
history_rows.sort(key=_history_retention_key)
|
|
1470
1731
|
history_rows = history_rows[:SOURCE_HISTORY_LIMIT]
|
|
1471
1732
|
milestone_rows.sort(key=lambda row: str(row["captured_at"]), reverse=True)
|
|
1472
1733
|
milestone_rows = milestone_rows[:SOURCE_HISTORY_LIMIT]
|
|
@@ -1501,67 +1762,25 @@ def _clock_freshness(
|
|
|
1501
1762
|
return "stale" if age_seconds > stale_after else "fresh"
|
|
1502
1763
|
|
|
1503
1764
|
|
|
1504
|
-
def
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
``account_key`` (serialized only when the Codex provider is DECORATED, i.e.
|
|
1512
|
-
>1 real account — R8). The idle clock mirrors the build-time
|
|
1513
|
-
``_resolve_codex_weekly_cycle`` resolution: an account yields a live cycle iff
|
|
1514
|
-
it has EXACTLY ONE fresh future boundary, and the hero is valid iff AT LEAST
|
|
1515
|
-
ONE account does (raising the aggregate ``conflicting > stale > missing``
|
|
1516
|
-
reason only when NO account yields one). When ``account_key`` is absent
|
|
1517
|
-
(<=1-real-account install — no decoration) every row falls into one global
|
|
1518
|
-
bucket, so this reduces EXACTLY to the prior single-boundary logic
|
|
1519
|
-
(byte-stable). This removes the documented placeholder degrade where two real
|
|
1520
|
-
accounts with distinct weekly cycles collapsed the whole hero to
|
|
1521
|
-
``conflicting`` on the idle clock.
|
|
1765
|
+
def _clock_cycle_expired(cycle: object, now_utc: dt.datetime) -> bool:
|
|
1766
|
+
"""Whether a retained hero cycle has already reset (#350 spec §3.3).
|
|
1767
|
+
|
|
1768
|
+
The one invariant the idle clock still enforces on frozen evidence: a cycle
|
|
1769
|
+
whose ``resets_at`` is at or before ``now_utc`` cannot bound current
|
|
1770
|
+
accounting. Fails CLOSED on an unparseable or absent boundary, matching the
|
|
1771
|
+
prior behavior where malformed evidence yielded no valid boundary.
|
|
1522
1772
|
"""
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
if raw_history.get("window_minutes") != 10_080:
|
|
1529
|
-
continue
|
|
1530
|
-
current = raw_history.get("current_percent")
|
|
1531
|
-
forecast = raw_history.get("forecast")
|
|
1532
|
-
if current is None or not isinstance(forecast, Mapping):
|
|
1533
|
-
continue
|
|
1534
|
-
try:
|
|
1535
|
-
resets_at = dt.datetime.fromisoformat(
|
|
1536
|
-
str(forecast.get("resets_at")).replace("Z", "+00:00")
|
|
1537
|
-
).astimezone(UTC)
|
|
1538
|
-
except (TypeError, ValueError):
|
|
1539
|
-
continue
|
|
1540
|
-
if resets_at <= now_utc:
|
|
1541
|
-
continue
|
|
1542
|
-
# None account_key (undecorated / <=1 real account) -> one global bucket
|
|
1543
|
-
# == today's behavior. A real key buckets per account.
|
|
1544
|
-
acct = raw_history.get("account_key")
|
|
1545
|
-
bucket = acct if acct is not None else "__all__"
|
|
1546
|
-
if raw_history.get("freshness") != "fresh":
|
|
1547
|
-
stale_by_account[bucket] = True
|
|
1548
|
-
continue
|
|
1549
|
-
per_account.setdefault(bucket, set()).add(resets_at)
|
|
1550
|
-
accounts = set(per_account) | set(stale_by_account)
|
|
1551
|
-
reasons: list[str] = []
|
|
1552
|
-
for acct in accounts:
|
|
1553
|
-
boundaries = per_account.get(acct, set())
|
|
1554
|
-
if len(boundaries) == 1:
|
|
1555
|
-
return True, "ok" # at least one account yields a live cycle
|
|
1556
|
-
reasons.append(
|
|
1557
|
-
"conflicting" if boundaries
|
|
1558
|
-
else ("stale" if stale_by_account.get(acct) else "missing")
|
|
1773
|
+
if not isinstance(cycle, Mapping):
|
|
1774
|
+
return True
|
|
1775
|
+
try:
|
|
1776
|
+
resets_at = dt.datetime.fromisoformat(
|
|
1777
|
+
str(cycle.get("resets_at")).replace("Z", "+00:00")
|
|
1559
1778
|
)
|
|
1560
|
-
|
|
1561
|
-
return
|
|
1562
|
-
if
|
|
1563
|
-
return
|
|
1564
|
-
return
|
|
1779
|
+
except (TypeError, ValueError):
|
|
1780
|
+
return True
|
|
1781
|
+
if resets_at.tzinfo is None or resets_at.utcoffset() is None:
|
|
1782
|
+
return True
|
|
1783
|
+
return resets_at.astimezone(UTC) <= now_utc
|
|
1565
1784
|
|
|
1566
1785
|
|
|
1567
1786
|
def _refresh_budget_status_clock(
|
|
@@ -1638,6 +1857,7 @@ def refresh_codex_source_clock(
|
|
|
1638
1857
|
warnings = state.warnings
|
|
1639
1858
|
availability = state.availability
|
|
1640
1859
|
freshness = state.freshness
|
|
1860
|
+
domain_freshness = dict(state.domain_freshness or {})
|
|
1641
1861
|
if isinstance(quota, Mapping):
|
|
1642
1862
|
quota = dict(quota)
|
|
1643
1863
|
refreshed_histories: list[dict[str, object]] = []
|
|
@@ -1646,10 +1866,16 @@ def refresh_codex_source_clock(
|
|
|
1646
1866
|
if not isinstance(raw_history, Mapping):
|
|
1647
1867
|
continue
|
|
1648
1868
|
history = dict(raw_history)
|
|
1649
|
-
|
|
1869
|
+
# #350 spec §3.9: this is a PER-ROW value and must never shadow the
|
|
1870
|
+
# envelope-level `freshness`. It used to, so after the loop the
|
|
1871
|
+
# envelope held the LAST retained history row's freshness — often an
|
|
1872
|
+
# inactive row, and with a single weekly history the active weekly
|
|
1873
|
+
# one, which silently marked the whole provider stale on an idle
|
|
1874
|
+
# stale crossing and tripped idle eligibility on its own.
|
|
1875
|
+
row_freshness = _clock_freshness(
|
|
1650
1876
|
history.get("captured_at"), history.get("stale_after_seconds"), now_utc,
|
|
1651
1877
|
)
|
|
1652
|
-
history["freshness"] =
|
|
1878
|
+
history["freshness"] = row_freshness
|
|
1653
1879
|
forecast = history.get("forecast")
|
|
1654
1880
|
if isinstance(forecast, Mapping):
|
|
1655
1881
|
forecast = dict(forecast)
|
|
@@ -1663,9 +1889,9 @@ def refresh_codex_source_clock(
|
|
|
1663
1889
|
remaining = max(0, int((reset - now_utc).total_seconds())) if reset else None
|
|
1664
1890
|
forecast["remaining_seconds"] = remaining
|
|
1665
1891
|
sample_count = int(forecast.get("sample_count") or 0)
|
|
1666
|
-
if
|
|
1892
|
+
if row_freshness == "future":
|
|
1667
1893
|
forecast["status"] = "future"
|
|
1668
|
-
elif
|
|
1894
|
+
elif row_freshness == "stale":
|
|
1669
1895
|
forecast["status"] = "stale"
|
|
1670
1896
|
elif sample_count == 0:
|
|
1671
1897
|
forecast["status"] = "insufficient-history"
|
|
@@ -1682,13 +1908,18 @@ def refresh_codex_source_clock(
|
|
|
1682
1908
|
100.0, max(float(current), float(current) + float(rate) * remaining / 3600),
|
|
1683
1909
|
)
|
|
1684
1910
|
history["forecast"] = forecast
|
|
1685
|
-
|
|
1911
|
+
# #373: same rule as the initial build, through the same
|
|
1912
|
+
# predicate, so the two paths cannot drift.
|
|
1913
|
+
if (
|
|
1914
|
+
not _codex_history_row_is_model_scoped(history)
|
|
1915
|
+
and reset is not None and reset > now_utc and current is not None
|
|
1916
|
+
):
|
|
1686
1917
|
active_rows.append({
|
|
1687
1918
|
"key": history.get("key"),
|
|
1688
1919
|
"current_percent": current,
|
|
1689
1920
|
"captured_at": history.get("captured_at"),
|
|
1690
1921
|
"resets_at": resets_at,
|
|
1691
|
-
"freshness":
|
|
1922
|
+
"freshness": row_freshness,
|
|
1692
1923
|
"stale_after_seconds": history.get("stale_after_seconds"),
|
|
1693
1924
|
})
|
|
1694
1925
|
refreshed_histories.append(history)
|
|
@@ -1716,6 +1947,13 @@ def refresh_codex_source_clock(
|
|
|
1716
1947
|
),
|
|
1717
1948
|
"active": active_rows,
|
|
1718
1949
|
})
|
|
1950
|
+
# Only account-level active histories reach ``active_rows``; the shared
|
|
1951
|
+
# model-scoped predicate above excludes foreign pools. An unavailable
|
|
1952
|
+
# active set is a capability/data-availability fact, not invented
|
|
1953
|
+
# staleness, so only the exact stale verdict moves this axis.
|
|
1954
|
+
domain_freshness["quota"] = (
|
|
1955
|
+
"stale" if summary["freshness"] == "stale" else "fresh"
|
|
1956
|
+
)
|
|
1719
1957
|
quota["summary"] = summary
|
|
1720
1958
|
data["quota"] = quota
|
|
1721
1959
|
quota_changed = bool(refreshed_histories)
|
|
@@ -1727,8 +1965,18 @@ def refresh_codex_source_clock(
|
|
|
1727
1965
|
and hero_capability is not None
|
|
1728
1966
|
and hero_capability.status == "supported"
|
|
1729
1967
|
):
|
|
1730
|
-
|
|
1731
|
-
|
|
1968
|
+
# #350 spec §3.3: the clock no longer RE-DERIVES cycle validity.
|
|
1969
|
+
# Its public-history view is lossy (capped, no `logical_limit_key`,
|
|
1970
|
+
# no `quota_identity`), so it cannot resolve the cycle correctly —
|
|
1971
|
+
# and per §2.2 it cannot simply trust the old verdict forever either,
|
|
1972
|
+
# because resolution is time-dependent on frozen evidence. Build time
|
|
1973
|
+
# owns resolution and records a decision deadline in `clock_data`; the
|
|
1974
|
+
# tick rebuilds authoritatively at the crossing. All the clock keeps
|
|
1975
|
+
# is this cheap invariant guard: a cycle that has already RESET cannot
|
|
1976
|
+
# bound current accounting, so it degrades exactly as before.
|
|
1977
|
+
# Expiry is also deadline candidate #1, so the two paths are disjoint
|
|
1978
|
+
# belt-and-suspenders rather than a single mechanism.
|
|
1979
|
+
if _clock_cycle_expired(hero.get("cycle"), now_utc):
|
|
1732
1980
|
hero = dict(hero)
|
|
1733
1981
|
for field in (
|
|
1734
1982
|
"cost_usd", "input_tokens", "cached_input_tokens", "output_tokens",
|
|
@@ -1750,8 +1998,6 @@ def refresh_codex_source_clock(
|
|
|
1750
1998
|
"hero",
|
|
1751
1999
|
),)
|
|
1752
2000
|
availability = "partial"
|
|
1753
|
-
if cycle_reason == "stale":
|
|
1754
|
-
freshness = "stale"
|
|
1755
2001
|
cycle_changed = True
|
|
1756
2002
|
budget_domain = data.get("budget")
|
|
1757
2003
|
budget_changed = False
|
|
@@ -1785,9 +2031,11 @@ def refresh_codex_source_clock(
|
|
|
1785
2031
|
last_success_at=state.last_success_at,
|
|
1786
2032
|
capabilities=capabilities,
|
|
1787
2033
|
data=data,
|
|
2034
|
+
domain_freshness=domain_freshness,
|
|
1788
2035
|
clock_data=state.clock_data,
|
|
2036
|
+
private_session_labels=state.private_session_labels,
|
|
1789
2037
|
)
|
|
1790
|
-
return state if refreshed_state
|
|
2038
|
+
return state if refreshed_state == state else refreshed_state
|
|
1791
2039
|
|
|
1792
2040
|
|
|
1793
2041
|
def _alerts_wire(stats_conn: sqlite3.Connection) -> tuple[dict[str, object], ...]:
|
|
@@ -1937,7 +2185,10 @@ def _partial_projects_wire(
|
|
|
1937
2185
|
"reasoning_output_tokens": 0, "total_tokens": 0,
|
|
1938
2186
|
})
|
|
1939
2187
|
session_totals = group["session_rows"].setdefault(identity, {
|
|
1940
|
-
|
|
2188
|
+
# A persisted Codex task title is transcript-derived content. The
|
|
2189
|
+
# partial project projection is shared across every dashboard
|
|
2190
|
+
# client, so retain only a non-sensitive generic label here.
|
|
2191
|
+
"label": "Session",
|
|
1941
2192
|
"last_activity": timestamp.astimezone(UTC).isoformat(),
|
|
1942
2193
|
"cost_usd": 0.0, "input_tokens": 0, "cached_input_tokens": 0,
|
|
1943
2194
|
"output_tokens": 0, "reasoning_output_tokens": 0, "total_tokens": 0,
|
|
@@ -2105,7 +2356,12 @@ def _codex_account_five_hour_percent(
|
|
|
2105
2356
|
for history in build_history(tuple(observations)):
|
|
2106
2357
|
if history.identity.window_minutes != 300:
|
|
2107
2358
|
continue
|
|
2359
|
+
# #373: the retained `codex_bengalfox` 5h rows are on the PRIMARY slot —
|
|
2360
|
+
# the same slot this account aggregate reads — so a foreign pool at 95%
|
|
2361
|
+
# would win the max outright over the real account window.
|
|
2108
2362
|
baseline = select_baseline(history.observations, now_utc)
|
|
2363
|
+
if codex_history_is_model_scoped(history, baseline=baseline):
|
|
2364
|
+
continue
|
|
2109
2365
|
if baseline is None or baseline.resets_at <= now_utc:
|
|
2110
2366
|
continue
|
|
2111
2367
|
acct = history.identity.account_key
|
|
@@ -2560,10 +2816,13 @@ def build_codex_source_state(
|
|
|
2560
2816
|
# degrade to the byte-stable undecorated shape.
|
|
2561
2817
|
accounts_wire = []
|
|
2562
2818
|
hero_cycles_wire = []
|
|
2819
|
+
private_session_labels: dict[str, str] = {}
|
|
2563
2820
|
return SourceDashboardState(
|
|
2564
2821
|
source="codex",
|
|
2565
2822
|
availability=availability,
|
|
2566
|
-
|
|
2823
|
+
# A successful source build is one coherent provider generation. Quota
|
|
2824
|
+
# observation age and weekly-cycle evidence live on their own axes.
|
|
2825
|
+
freshness="fresh",
|
|
2567
2826
|
warnings=tuple(warnings),
|
|
2568
2827
|
data_version=data_version,
|
|
2569
2828
|
last_success_at=context.now_utc,
|
|
@@ -2609,6 +2868,15 @@ def build_codex_source_state(
|
|
|
2609
2868
|
}
|
|
2610
2869
|
if cycle is not None and not hero_failure else None
|
|
2611
2870
|
),
|
|
2871
|
+
# #350 (spec §3.4): additive, hero-local staleness disclosure.
|
|
2872
|
+
# OMITTED when the cycle is fresh — never emitted as "fresh" —
|
|
2873
|
+
# for the legacy client transition. Provider metadata remains
|
|
2874
|
+
# coherent; ``domain_freshness.hero`` owns the shared axis.
|
|
2875
|
+
**(
|
|
2876
|
+
{"cycle_freshness": "stale"}
|
|
2877
|
+
if cycle is not None and not hero_failure and cycle.evidence_stale
|
|
2878
|
+
else {}
|
|
2879
|
+
),
|
|
2612
2880
|
"quota": quota["summary"],
|
|
2613
2881
|
"budget": configured_budget,
|
|
2614
2882
|
"alerts": {"count": len(alerts)},
|
|
@@ -2620,7 +2888,11 @@ def build_codex_source_state(
|
|
|
2620
2888
|
"monthly": _period_wire(monthly),
|
|
2621
2889
|
"weekly": _period_wire(weekly),
|
|
2622
2890
|
},
|
|
2623
|
-
"sessions": _session_wire(
|
|
2891
|
+
"sessions": _session_wire(
|
|
2892
|
+
sessions,
|
|
2893
|
+
metadata=conversation_metadata,
|
|
2894
|
+
private_labels=private_session_labels,
|
|
2895
|
+
),
|
|
2624
2896
|
"quota": quota,
|
|
2625
2897
|
"budget": {
|
|
2626
2898
|
"status": configured_budget,
|
|
@@ -2635,5 +2907,32 @@ def build_codex_source_state(
|
|
|
2635
2907
|
},
|
|
2636
2908
|
"cache_report": cache_report,
|
|
2637
2909
|
},
|
|
2638
|
-
|
|
2910
|
+
domain_freshness={
|
|
2911
|
+
"hero": (
|
|
2912
|
+
"stale"
|
|
2913
|
+
if cycle_reason == "stale"
|
|
2914
|
+
or (
|
|
2915
|
+
cycle is not None
|
|
2916
|
+
and not hero_failure
|
|
2917
|
+
and cycle.evidence_stale
|
|
2918
|
+
)
|
|
2919
|
+
else "fresh"
|
|
2920
|
+
),
|
|
2921
|
+
"quota": (
|
|
2922
|
+
"stale"
|
|
2923
|
+
if quota["summary"]["freshness"] == "stale"
|
|
2924
|
+
else "fresh"
|
|
2925
|
+
),
|
|
2926
|
+
"sessions": "fresh",
|
|
2927
|
+
},
|
|
2928
|
+
clock_data={
|
|
2929
|
+
"codex_budget_cost_events": budget_cost_events,
|
|
2930
|
+
# #350 spec §3.3: when the tick passes this instant it must rebuild
|
|
2931
|
+
# Codex authoritatively instead of idle-clocking or reusing, because
|
|
2932
|
+
# weekly-cycle resolution can change on identical frozen evidence.
|
|
2933
|
+
"codex_next_decision_at": _codex_next_decision_at(
|
|
2934
|
+
quota_observations, cycles_all, context.now_utc,
|
|
2935
|
+
),
|
|
2936
|
+
},
|
|
2937
|
+
private_session_labels=private_session_labels,
|
|
2639
2938
|
)
|