cctally 1.82.1 → 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 +58 -0
- package/README.md +12 -5
- package/bin/_cctally_alerts.py +8 -1
- package/bin/_cctally_cache.py +912 -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 +116 -8
- package/bin/_cctally_dashboard_share.py +50 -19
- package/bin/_cctally_dashboard_sources.py +223 -48
- 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 +350 -44
- 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 +81 -33
- package/bin/_lib_dashboard_sources.py +75 -0
- 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_rederive.py +395 -0
- package/bin/_lib_share.py +58 -2
- package/bin/cctally +56 -8
- package/dashboard/static/assets/{index-BKM43pxK.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 +5 -1
- package/dashboard/static/assets/index-Dk1nplOz.css +0 -1
|
@@ -47,8 +47,9 @@ from zoneinfo import ZoneInfo
|
|
|
47
47
|
from _cctally_core import open_db, parse_iso_datetime
|
|
48
48
|
from _cctally_config import save_config, _load_config_unlocked
|
|
49
49
|
from _lib_fmt import stable_sum
|
|
50
|
-
from _lib_pricing import _calculate_entry_cost
|
|
50
|
+
from _lib_pricing import _calculate_entry_cost, claude_usage_dict
|
|
51
51
|
from _lib_five_hour import _canonical_5h_window_key
|
|
52
|
+
from _lib_dashboard_sources import source_domain_freshness
|
|
52
53
|
|
|
53
54
|
|
|
54
55
|
# Share-CLI helpers consumed by the dashboard's share-data builders.
|
|
@@ -395,12 +396,14 @@ def _share_top_projects_for_range(
|
|
|
395
396
|
# an empty top_projects.
|
|
396
397
|
return []
|
|
397
398
|
for entry in entries:
|
|
398
|
-
usage =
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
399
|
+
usage = claude_usage_dict( # #195 chokepoint
|
|
400
|
+
input_tokens=entry.input_tokens,
|
|
401
|
+
output_tokens=entry.output_tokens,
|
|
402
|
+
cache_creation_tokens=entry.cache_creation_tokens,
|
|
403
|
+
cache_read_tokens=entry.cache_read_tokens,
|
|
404
|
+
cache_1h_tokens=getattr(entry, "cache_1h_tokens", None),
|
|
405
|
+
speed=getattr(entry, "speed", None),
|
|
406
|
+
)
|
|
404
407
|
cost = _calculate_entry_cost(
|
|
405
408
|
entry.model, usage, mode="auto", cost_usd=entry.cost_usd,
|
|
406
409
|
)
|
|
@@ -434,12 +437,14 @@ def _share_all_projects_for_range(
|
|
|
434
437
|
except Exception:
|
|
435
438
|
return bucket
|
|
436
439
|
for entry in entries:
|
|
437
|
-
usage =
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
440
|
+
usage = claude_usage_dict( # #195 chokepoint
|
|
441
|
+
input_tokens=entry.input_tokens,
|
|
442
|
+
output_tokens=entry.output_tokens,
|
|
443
|
+
cache_creation_tokens=entry.cache_creation_tokens,
|
|
444
|
+
cache_read_tokens=entry.cache_read_tokens,
|
|
445
|
+
cache_1h_tokens=getattr(entry, "cache_1h_tokens", None),
|
|
446
|
+
speed=getattr(entry, "speed", None),
|
|
447
|
+
)
|
|
443
448
|
cost = _calculate_entry_cost(
|
|
444
449
|
entry.model, usage, mode="auto", cost_usd=entry.cost_usd,
|
|
445
450
|
)
|
|
@@ -477,12 +482,14 @@ def _share_per_day_per_project_for_range(
|
|
|
477
482
|
except Exception:
|
|
478
483
|
return out
|
|
479
484
|
for entry in entries:
|
|
480
|
-
usage =
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
485
|
+
usage = claude_usage_dict( # #195 chokepoint
|
|
486
|
+
input_tokens=entry.input_tokens,
|
|
487
|
+
output_tokens=entry.output_tokens,
|
|
488
|
+
cache_creation_tokens=entry.cache_creation_tokens,
|
|
489
|
+
cache_read_tokens=entry.cache_read_tokens,
|
|
490
|
+
cache_1h_tokens=getattr(entry, "cache_1h_tokens", None),
|
|
491
|
+
speed=getattr(entry, "speed", None),
|
|
492
|
+
)
|
|
486
493
|
cost = _calculate_entry_cost(
|
|
487
494
|
entry.model, usage, mode="auto", cost_usd=entry.cost_usd,
|
|
488
495
|
)
|
|
@@ -1645,6 +1652,20 @@ def _share_state_domain(state, panel: str):
|
|
|
1645
1652
|
return data.get(panel)
|
|
1646
1653
|
|
|
1647
1654
|
|
|
1655
|
+
def _share_apply_current_week_freshness(snapshot, state, panel: str):
|
|
1656
|
+
"""Qualify retained current-week actuals with provider-local evidence age."""
|
|
1657
|
+
if (
|
|
1658
|
+
panel != "current-week"
|
|
1659
|
+
or source_domain_freshness(state, "hero") != "stale"
|
|
1660
|
+
):
|
|
1661
|
+
return snapshot
|
|
1662
|
+
provider = "Claude" if state.source == "claude" else "Codex"
|
|
1663
|
+
note = (
|
|
1664
|
+
f"{provider} current-week spend is based on stale provider-cycle evidence."
|
|
1665
|
+
)
|
|
1666
|
+
return replace(snapshot, notes=tuple(snapshot.notes) + (note,))
|
|
1667
|
+
|
|
1668
|
+
|
|
1648
1669
|
def _share_digest_input(*, panel: str, template_id: str, source: str,
|
|
1649
1670
|
source_explicit: bool, states, snapshots,
|
|
1650
1671
|
panel_data, account: "str | None" = None):
|
|
@@ -1665,6 +1686,10 @@ def _share_digest_input(*, panel: str, template_id: str, source: str,
|
|
|
1665
1686
|
"source": state.source,
|
|
1666
1687
|
"data_version": state.data_version,
|
|
1667
1688
|
"availability": state.availability,
|
|
1689
|
+
**(
|
|
1690
|
+
{"hero_freshness": source_domain_freshness(state, "hero")}
|
|
1691
|
+
if panel == "current-week" else {}
|
|
1692
|
+
),
|
|
1668
1693
|
"period": {
|
|
1669
1694
|
"start": snapshot.period.start,
|
|
1670
1695
|
"end": snapshot.period.end,
|
|
@@ -1712,6 +1737,9 @@ def _share_build_source_snapshots(*, ls, template, template_id: str,
|
|
|
1712
1737
|
# does not carry the additive S4 source bundle.
|
|
1713
1738
|
if source_explicit or source == "all":
|
|
1714
1739
|
claude_state = _source_state_for_share(data_snap, "claude")
|
|
1740
|
+
claude_snapshot = _share_apply_current_week_freshness(
|
|
1741
|
+
claude_snapshot, claude_state, panel,
|
|
1742
|
+
)
|
|
1715
1743
|
|
|
1716
1744
|
codex_snapshot = None
|
|
1717
1745
|
codex_state = None
|
|
@@ -1726,6 +1754,9 @@ def _share_build_source_snapshots(*, ls, template, template_id: str,
|
|
|
1726
1754
|
template_id=template_id,
|
|
1727
1755
|
options=options,
|
|
1728
1756
|
)
|
|
1757
|
+
codex_snapshot = _share_apply_current_week_freshness(
|
|
1758
|
+
codex_snapshot, codex_state, panel,
|
|
1759
|
+
)
|
|
1729
1760
|
|
|
1730
1761
|
if source == "claude":
|
|
1731
1762
|
return (claude_snapshot,), (claude_state,), panel_data
|
|
@@ -49,7 +49,13 @@ from _lib_quota import (
|
|
|
49
49
|
select_baseline,
|
|
50
50
|
stale_after_seconds,
|
|
51
51
|
)
|
|
52
|
-
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
|
|
53
59
|
from _lib_fmt import stable_sum
|
|
54
60
|
from _lib_aggregators import _aggregate_codex_buckets
|
|
55
61
|
from _lib_five_hour import _FIVE_HOUR_JITTER_FLOOR_SECONDS
|
|
@@ -158,19 +164,22 @@ class CodexWeeklyPeriod:
|
|
|
158
164
|
used_percent: float | None = None
|
|
159
165
|
|
|
160
166
|
|
|
161
|
-
def
|
|
162
|
-
"""Whether
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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"))
|
|
174
183
|
|
|
175
184
|
|
|
176
185
|
def _resolve_codex_weekly_cycle(
|
|
@@ -212,11 +221,13 @@ def _resolve_codex_weekly_cycle(
|
|
|
212
221
|
for history in build_history(tuple(observations)):
|
|
213
222
|
if history.identity.window_minutes != 10_080:
|
|
214
223
|
continue
|
|
215
|
-
|
|
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):
|
|
216
228
|
continue
|
|
217
229
|
account = history.identity.account_key
|
|
218
230
|
accounts_seen.add(account)
|
|
219
|
-
baseline = select_baseline(history.observations, now_utc)
|
|
220
231
|
if baseline is None or baseline.resets_at <= now_utc:
|
|
221
232
|
continue
|
|
222
233
|
state = quota_freshness(history.physical_observations, now_utc).state
|
|
@@ -283,6 +294,66 @@ def _resolve_codex_weekly_cycle(
|
|
|
283
294
|
return cycles
|
|
284
295
|
|
|
285
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
|
+
|
|
286
357
|
def _codex_next_decision_at(
|
|
287
358
|
observations: Iterable[object],
|
|
288
359
|
cycles: Iterable[CodexCycleBoundary],
|
|
@@ -326,9 +397,9 @@ def _codex_next_decision_at(
|
|
|
326
397
|
for history in build_history(tuple(observations)):
|
|
327
398
|
if history.identity.window_minutes != 10_080:
|
|
328
399
|
continue
|
|
329
|
-
if _is_model_scoped_codex_quota(history.identity.logical_limit_key):
|
|
330
|
-
continue
|
|
331
400
|
baseline = select_baseline(history.observations, now_utc)
|
|
401
|
+
if codex_history_is_model_scoped(history, baseline=baseline):
|
|
402
|
+
continue
|
|
332
403
|
if baseline is not None and baseline.resets_at > now_utc:
|
|
333
404
|
latest = latest_physical_observation(history.physical_observations)
|
|
334
405
|
if latest is not None:
|
|
@@ -395,7 +466,7 @@ def _codex_weekly_periods(
|
|
|
395
466
|
placeholders = ",".join("?" for _ in roots)
|
|
396
467
|
try:
|
|
397
468
|
rows = stats_conn.execute(
|
|
398
|
-
"SELECT source_root_key, logical_limit_key, resets_at_utc, "
|
|
469
|
+
"SELECT source_root_key, logical_limit_key, limit_name, resets_at_utc, "
|
|
399
470
|
"nominal_start_at_utc, current_percent "
|
|
400
471
|
"FROM quota_window_blocks "
|
|
401
472
|
"WHERE source='codex' AND window_minutes=10080 "
|
|
@@ -407,10 +478,18 @@ def _codex_weekly_periods(
|
|
|
407
478
|
except sqlite3.Error:
|
|
408
479
|
rows = ()
|
|
409
480
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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):
|
|
414
493
|
continue
|
|
415
494
|
try:
|
|
416
495
|
start_at = dt.datetime.fromisoformat(str(start_at_raw).replace("Z", "+00:00"))
|
|
@@ -426,18 +505,23 @@ def _codex_weekly_periods(
|
|
|
426
505
|
used_values = []
|
|
427
506
|
if isinstance(current_percent, (int, float)) and not isinstance(current_percent, bool):
|
|
428
507
|
used_values.append(float(current_percent))
|
|
429
|
-
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))
|
|
430
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.
|
|
431
512
|
if active_cycle is not None:
|
|
432
513
|
raw_boundaries.append((
|
|
433
514
|
active_cycle.start_at.astimezone(UTC),
|
|
434
515
|
active_cycle.resets_at.astimezone(UTC),
|
|
435
516
|
set(active_cycle.source_root_keys),
|
|
436
517
|
[active_cycle.used_percent] if active_cycle.used_percent is not None else [],
|
|
518
|
+
True,
|
|
437
519
|
))
|
|
438
520
|
|
|
439
|
-
ordered: list[
|
|
440
|
-
|
|
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(
|
|
441
525
|
raw_boundaries, key=lambda item: (item[0], item[1]),
|
|
442
526
|
):
|
|
443
527
|
if (
|
|
@@ -445,17 +529,23 @@ def _codex_weekly_periods(
|
|
|
445
529
|
and (start_at - ordered[-1][0]).total_seconds()
|
|
446
530
|
< _FIVE_HOUR_JITTER_FLOOR_SECONDS
|
|
447
531
|
):
|
|
448
|
-
first_start, latest_reset, existing_roots, existing_used = ordered[-1]
|
|
532
|
+
first_start, latest_reset, existing_roots, existing_used, existing_live = ordered[-1]
|
|
449
533
|
existing_roots.update(period_roots)
|
|
450
534
|
existing_used.extend(used_values)
|
|
451
535
|
ordered[-1] = (
|
|
452
536
|
first_start, max(latest_reset, resets_at), existing_roots, existing_used,
|
|
537
|
+
existing_live or is_live,
|
|
453
538
|
)
|
|
454
539
|
else:
|
|
455
|
-
ordered.append((
|
|
540
|
+
ordered.append((
|
|
541
|
+
start_at, resets_at, set(period_roots), list(used_values), is_live,
|
|
542
|
+
))
|
|
456
543
|
periods: list[CodexWeeklyPeriod] = []
|
|
457
|
-
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):
|
|
458
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
|
|
459
549
|
end_at = min(resets_at, next_start) if next_start is not None else resets_at
|
|
460
550
|
if end_at <= start_at:
|
|
461
551
|
continue
|
|
@@ -1058,7 +1148,9 @@ def _codex_conversation_metadata(
|
|
|
1058
1148
|
f"SELECT id, title FROM threads WHERE id IN ({placeholders})",
|
|
1059
1149
|
batch,
|
|
1060
1150
|
):
|
|
1061
|
-
clean_title =
|
|
1151
|
+
clean_title = _codex_display_title(
|
|
1152
|
+
str(title) if title is not None else None
|
|
1153
|
+
)
|
|
1062
1154
|
if clean_title:
|
|
1063
1155
|
short_names[str(thread_id)] = clean_title
|
|
1064
1156
|
except (OSError, sqlite3.Error):
|
|
@@ -1123,6 +1215,7 @@ def _session_wire(
|
|
|
1123
1215
|
view: Any,
|
|
1124
1216
|
*,
|
|
1125
1217
|
metadata: Mapping[tuple[str, str], Mapping[str, object]] | None = None,
|
|
1218
|
+
private_labels: dict[str, str] | None = None,
|
|
1126
1219
|
) -> dict[str, object]:
|
|
1127
1220
|
rows = []
|
|
1128
1221
|
for row in view.rows:
|
|
@@ -1148,7 +1241,11 @@ def _session_wire(
|
|
|
1148
1241
|
)
|
|
1149
1242
|
)
|
|
1150
1243
|
), None)
|
|
1151
|
-
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
|
+
)
|
|
1152
1249
|
project = str(row_metadata.get("project_label") or "").strip() if row_metadata else ""
|
|
1153
1250
|
started_at = row_metadata.get("started_at") if row_metadata else None
|
|
1154
1251
|
duration_min = None
|
|
@@ -1158,12 +1255,14 @@ def _session_wire(
|
|
|
1158
1255
|
duration_min = max(0, round((row.last_activity.astimezone(UTC) - started_dt.astimezone(UTC)).total_seconds() / 60))
|
|
1159
1256
|
except (TypeError, ValueError):
|
|
1160
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
|
|
1161
1263
|
rows.append({
|
|
1162
|
-
"key":
|
|
1163
|
-
"session", "codex", root_identity, row.session_id_path,
|
|
1164
|
-
),
|
|
1264
|
+
"key": key,
|
|
1165
1265
|
"source": "codex",
|
|
1166
|
-
"label": title or None,
|
|
1167
1266
|
"project": project or None,
|
|
1168
1267
|
"project_key": row_metadata.get("project_key") if row_metadata else None,
|
|
1169
1268
|
"started_at": started_at,
|
|
@@ -1454,9 +1553,16 @@ def _quota_read_model(
|
|
|
1454
1553
|
baseline = select_baseline(history.observations, context.now_utc)
|
|
1455
1554
|
freshness = quota_freshness(history.physical_observations, context.now_utc)
|
|
1456
1555
|
forecast = forecast_quota(history.physical_observations, context.now_utc)
|
|
1457
|
-
|
|
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 = {
|
|
1458
1563
|
"key": dashboard_resource_key("quota", "codex", *key_parts),
|
|
1459
1564
|
"source": "codex",
|
|
1565
|
+
**({"model_scoped": True} if model_scoped else {}),
|
|
1460
1566
|
**({"account_key": identity.account_key} if _codex_decorated else {}),
|
|
1461
1567
|
"label": _native_limit_label(identity.limit_name, identity.window_minutes),
|
|
1462
1568
|
"observed_slot": identity.observed_slot,
|
|
@@ -1479,7 +1585,10 @@ def _quota_read_model(
|
|
|
1479
1585
|
"sample_span_seconds": forecast.sample_span_seconds,
|
|
1480
1586
|
"confidence": forecast.confidence,
|
|
1481
1587
|
},
|
|
1482
|
-
}
|
|
1588
|
+
}
|
|
1589
|
+
history_rows.append(row)
|
|
1590
|
+
if _codex_history_row_is_model_scoped(row):
|
|
1591
|
+
continue
|
|
1483
1592
|
if baseline is not None and baseline.resets_at > context.now_utc:
|
|
1484
1593
|
active_rows.append({
|
|
1485
1594
|
"key": dashboard_resource_key("quota", "codex", *key_parts),
|
|
@@ -1597,10 +1706,28 @@ def _quota_read_model(
|
|
|
1597
1706
|
"fresh" if active_rows and all(row["freshness"] == "fresh" for row in active_rows)
|
|
1598
1707
|
else ("unavailable" if not active_rows else "stale")
|
|
1599
1708
|
)
|
|
1600
|
-
# Active identities are presentation-critical.
|
|
1601
|
-
#
|
|
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.
|
|
1602
1713
|
active_keys = {str(row["key"]) for row in active_rows}
|
|
1603
|
-
|
|
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)
|
|
1604
1731
|
history_rows = history_rows[:SOURCE_HISTORY_LIMIT]
|
|
1605
1732
|
milestone_rows.sort(key=lambda row: str(row["captured_at"]), reverse=True)
|
|
1606
1733
|
milestone_rows = milestone_rows[:SOURCE_HISTORY_LIMIT]
|
|
@@ -1730,6 +1857,7 @@ def refresh_codex_source_clock(
|
|
|
1730
1857
|
warnings = state.warnings
|
|
1731
1858
|
availability = state.availability
|
|
1732
1859
|
freshness = state.freshness
|
|
1860
|
+
domain_freshness = dict(state.domain_freshness or {})
|
|
1733
1861
|
if isinstance(quota, Mapping):
|
|
1734
1862
|
quota = dict(quota)
|
|
1735
1863
|
refreshed_histories: list[dict[str, object]] = []
|
|
@@ -1780,7 +1908,12 @@ def refresh_codex_source_clock(
|
|
|
1780
1908
|
100.0, max(float(current), float(current) + float(rate) * remaining / 3600),
|
|
1781
1909
|
)
|
|
1782
1910
|
history["forecast"] = forecast
|
|
1783
|
-
|
|
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
|
+
):
|
|
1784
1917
|
active_rows.append({
|
|
1785
1918
|
"key": history.get("key"),
|
|
1786
1919
|
"current_percent": current,
|
|
@@ -1814,6 +1947,13 @@ def refresh_codex_source_clock(
|
|
|
1814
1947
|
),
|
|
1815
1948
|
"active": active_rows,
|
|
1816
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
|
+
)
|
|
1817
1957
|
quota["summary"] = summary
|
|
1818
1958
|
data["quota"] = quota
|
|
1819
1959
|
quota_changed = bool(refreshed_histories)
|
|
@@ -1891,9 +2031,11 @@ def refresh_codex_source_clock(
|
|
|
1891
2031
|
last_success_at=state.last_success_at,
|
|
1892
2032
|
capabilities=capabilities,
|
|
1893
2033
|
data=data,
|
|
2034
|
+
domain_freshness=domain_freshness,
|
|
1894
2035
|
clock_data=state.clock_data,
|
|
2036
|
+
private_session_labels=state.private_session_labels,
|
|
1895
2037
|
)
|
|
1896
|
-
return state if refreshed_state
|
|
2038
|
+
return state if refreshed_state == state else refreshed_state
|
|
1897
2039
|
|
|
1898
2040
|
|
|
1899
2041
|
def _alerts_wire(stats_conn: sqlite3.Connection) -> tuple[dict[str, object], ...]:
|
|
@@ -2043,7 +2185,10 @@ def _partial_projects_wire(
|
|
|
2043
2185
|
"reasoning_output_tokens": 0, "total_tokens": 0,
|
|
2044
2186
|
})
|
|
2045
2187
|
session_totals = group["session_rows"].setdefault(identity, {
|
|
2046
|
-
|
|
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",
|
|
2047
2192
|
"last_activity": timestamp.astimezone(UTC).isoformat(),
|
|
2048
2193
|
"cost_usd": 0.0, "input_tokens": 0, "cached_input_tokens": 0,
|
|
2049
2194
|
"output_tokens": 0, "reasoning_output_tokens": 0, "total_tokens": 0,
|
|
@@ -2211,7 +2356,12 @@ def _codex_account_five_hour_percent(
|
|
|
2211
2356
|
for history in build_history(tuple(observations)):
|
|
2212
2357
|
if history.identity.window_minutes != 300:
|
|
2213
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.
|
|
2214
2362
|
baseline = select_baseline(history.observations, now_utc)
|
|
2363
|
+
if codex_history_is_model_scoped(history, baseline=baseline):
|
|
2364
|
+
continue
|
|
2215
2365
|
if baseline is None or baseline.resets_at <= now_utc:
|
|
2216
2366
|
continue
|
|
2217
2367
|
acct = history.identity.account_key
|
|
@@ -2666,10 +2816,13 @@ def build_codex_source_state(
|
|
|
2666
2816
|
# degrade to the byte-stable undecorated shape.
|
|
2667
2817
|
accounts_wire = []
|
|
2668
2818
|
hero_cycles_wire = []
|
|
2819
|
+
private_session_labels: dict[str, str] = {}
|
|
2669
2820
|
return SourceDashboardState(
|
|
2670
2821
|
source="codex",
|
|
2671
2822
|
availability=availability,
|
|
2672
|
-
|
|
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",
|
|
2673
2826
|
warnings=tuple(warnings),
|
|
2674
2827
|
data_version=data_version,
|
|
2675
2828
|
last_success_at=context.now_utc,
|
|
@@ -2717,9 +2870,8 @@ def build_codex_source_state(
|
|
|
2717
2870
|
),
|
|
2718
2871
|
# #350 (spec §3.4): additive, hero-local staleness disclosure.
|
|
2719
2872
|
# OMITTED when the cycle is fresh — never emitted as "fresh" —
|
|
2720
|
-
#
|
|
2721
|
-
#
|
|
2722
|
-
# untouched because five separate gates read them as one meaning.
|
|
2873
|
+
# for the legacy client transition. Provider metadata remains
|
|
2874
|
+
# coherent; ``domain_freshness.hero`` owns the shared axis.
|
|
2723
2875
|
**(
|
|
2724
2876
|
{"cycle_freshness": "stale"}
|
|
2725
2877
|
if cycle is not None and not hero_failure and cycle.evidence_stale
|
|
@@ -2736,7 +2888,11 @@ def build_codex_source_state(
|
|
|
2736
2888
|
"monthly": _period_wire(monthly),
|
|
2737
2889
|
"weekly": _period_wire(weekly),
|
|
2738
2890
|
},
|
|
2739
|
-
"sessions": _session_wire(
|
|
2891
|
+
"sessions": _session_wire(
|
|
2892
|
+
sessions,
|
|
2893
|
+
metadata=conversation_metadata,
|
|
2894
|
+
private_labels=private_session_labels,
|
|
2895
|
+
),
|
|
2740
2896
|
"quota": quota,
|
|
2741
2897
|
"budget": {
|
|
2742
2898
|
"status": configured_budget,
|
|
@@ -2751,6 +2907,24 @@ def build_codex_source_state(
|
|
|
2751
2907
|
},
|
|
2752
2908
|
"cache_report": cache_report,
|
|
2753
2909
|
},
|
|
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
|
+
},
|
|
2754
2928
|
clock_data={
|
|
2755
2929
|
"codex_budget_cost_events": budget_cost_events,
|
|
2756
2930
|
# #350 spec §3.3: when the tick passes this instant it must rebuild
|
|
@@ -2760,4 +2934,5 @@ def build_codex_source_state(
|
|
|
2760
2934
|
quota_observations, cycles_all, context.now_utc,
|
|
2761
2935
|
),
|
|
2762
2936
|
},
|
|
2937
|
+
private_session_labels=private_session_labels,
|
|
2763
2938
|
)
|