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.
Files changed (51) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +12 -5
  3. package/bin/_cctally_alerts.py +8 -1
  4. package/bin/_cctally_cache.py +912 -149
  5. package/bin/_cctally_config.py +43 -4
  6. package/bin/_cctally_core.py +933 -759
  7. package/bin/_cctally_dashboard.py +157 -47
  8. package/bin/_cctally_dashboard_cache_report.py +13 -6
  9. package/bin/_cctally_dashboard_conversation.py +1 -0
  10. package/bin/_cctally_dashboard_envelope.py +116 -8
  11. package/bin/_cctally_dashboard_share.py +50 -19
  12. package/bin/_cctally_dashboard_sources.py +223 -48
  13. package/bin/_cctally_db.py +605 -128
  14. package/bin/_cctally_doctor.py +413 -28
  15. package/bin/_cctally_five_hour.py +12 -5
  16. package/bin/_cctally_journal.py +2050 -156
  17. package/bin/_cctally_journal_repair.py +519 -0
  18. package/bin/_cctally_milestone_history.py +142 -56
  19. package/bin/_cctally_milestones.py +179 -111
  20. package/bin/_cctally_parser.py +42 -0
  21. package/bin/_cctally_project.py +24 -18
  22. package/bin/_cctally_quota.py +139 -25
  23. package/bin/_cctally_record.py +279 -108
  24. package/bin/_cctally_rederive.py +1052 -0
  25. package/bin/_cctally_reporting.py +58 -53
  26. package/bin/_cctally_setup.py +1 -0
  27. package/bin/_cctally_source_analytics.py +4 -1
  28. package/bin/_cctally_statusline.py +11 -11
  29. package/bin/_cctally_store.py +1039 -31
  30. package/bin/_cctally_sync_week.py +17 -8
  31. package/bin/_cctally_tui.py +350 -44
  32. package/bin/_cctally_update.py +133 -8
  33. package/bin/_cctally_weekrefs.py +14 -0
  34. package/bin/_lib_aggregators.py +10 -6
  35. package/bin/_lib_cache_report.py +101 -9
  36. package/bin/_lib_codex_pools.py +82 -0
  37. package/bin/_lib_conversation_query.py +81 -33
  38. package/bin/_lib_dashboard_sources.py +75 -0
  39. package/bin/_lib_diff_kernel.py +28 -15
  40. package/bin/_lib_doctor.py +342 -4
  41. package/bin/_lib_journal.py +924 -2
  42. package/bin/_lib_jsonl.py +43 -14
  43. package/bin/_lib_pricing.py +140 -21
  44. package/bin/_lib_rederive.py +395 -0
  45. package/bin/_lib_share.py +58 -2
  46. package/bin/cctally +56 -8
  47. package/dashboard/static/assets/{index-BKM43pxK.js → index-3bgCMVHb.js} +52 -52
  48. package/dashboard/static/assets/index-D27EIHEI.css +1 -0
  49. package/dashboard/static/dashboard.html +2 -2
  50. package/package.json +5 -1
  51. 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
- "input_tokens": entry.input_tokens,
400
- "output_tokens": entry.output_tokens,
401
- "cache_creation_input_tokens": entry.cache_creation_tokens,
402
- "cache_read_input_tokens": entry.cache_read_tokens,
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
- "input_tokens": entry.input_tokens,
439
- "output_tokens": entry.output_tokens,
440
- "cache_creation_input_tokens": entry.cache_creation_tokens,
441
- "cache_read_input_tokens": entry.cache_read_tokens,
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
- "input_tokens": entry.input_tokens,
482
- "output_tokens": entry.output_tokens,
483
- "cache_creation_input_tokens": entry.cache_creation_tokens,
484
- "cache_read_input_tokens": entry.cache_read_tokens,
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, codex_model_scoped_quota_pool
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 _is_model_scoped_codex_quota(logical_limit_key: object) -> bool:
162
- """Whether an interpreted native identity belongs outside standard quota."""
163
- if not isinstance(logical_limit_key, str):
164
- return False
165
- try:
166
- payload = json.loads(logical_limit_key)
167
- except (json.JSONDecodeError, TypeError):
168
- return False
169
- return (
170
- isinstance(payload, dict)
171
- and isinstance(payload.get("modelPool"), str)
172
- and bool(payload["modelPool"].strip())
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
- if _is_model_scoped_codex_quota(history.identity.logical_limit_key):
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
- raw_boundaries: list[tuple[dt.datetime, dt.datetime, set[str], list[float]]] = []
411
-
412
- for root_key, logical_limit_key, resets_at_raw, start_at_raw, current_percent in rows:
413
- if _is_model_scoped_codex_quota(logical_limit_key):
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[tuple[dt.datetime, dt.datetime, set[str], list[float]]] = []
440
- for start_at, resets_at, period_roots, used_values in sorted(
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((start_at, resets_at, set(period_roots), list(used_values)))
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 = " ".join(str(title or "").split())
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 = str(row_metadata.get("title") or "").strip() if row_metadata else ""
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": dashboard_resource_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
- history_rows.append({
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. Keep them ahead of
1601
- # inactive retained history before enforcing the public cardinality cap.
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
- history_rows.sort(key=lambda row: (str(row["key"]) not in active_keys, str(row["key"])))
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
- if reset is not None and reset > now_utc and current is not None:
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.data == state.data else 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
- "label": str(row_metadata.get("title") or "Session"),
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
- freshness=("stale" if cycle_reason == "stale" else "fresh"),
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
- # which is what keeps every existing envelope golden byte-
2721
- # identical. availability/freshness/warnings/capabilities stay
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(sessions, metadata=conversation_metadata),
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
  )