cctally 1.98.0 → 1.99.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -31,6 +31,7 @@ from _cctally_source_analytics import (
31
31
  )
32
32
  import _lib_log
33
33
  import _lib_accounts
34
+ import _lib_snapshot_cache
34
35
  from _lib_dashboard_sources import (
35
36
  CapabilityRecord,
36
37
  ProjectionCoherence,
@@ -53,6 +54,7 @@ from _lib_quota import (
53
54
  stale_after_seconds,
54
55
  )
55
56
  from _lib_jsonl import CodexEntry
57
+ from _lib_codex_account_adoption import ACCOUNT_WEEKLY_WINDOW_MINUTES
56
58
  from _lib_codex_pools import (
57
59
  codex_history_is_model_scoped,
58
60
  codex_model_scoped_quota_pool,
@@ -60,9 +62,10 @@ from _lib_codex_pools import (
60
62
  )
61
63
  from _lib_codex_conversation import _display_title as _codex_display_title
62
64
  from _lib_fmt import stable_sum
63
- from _lib_aggregators import _aggregate_codex_buckets
65
+ from _lib_aggregators import _aggregate_codex_buckets, codex_path_scope
64
66
  from _lib_five_hour import _FIVE_HOUR_JITTER_FLOOR_SECONDS
65
67
  from _lib_source_analytics import (
68
+ assign_collision_safe_project_labels,
66
69
  build_codex_project_result,
67
70
  collision_safe_project_label_map,
68
71
  )
@@ -76,6 +79,70 @@ from _lib_view_models import (
76
79
 
77
80
 
78
81
  UTC = dt.timezone.utc
82
+
83
+
84
+ _CODEX_QUOTA_OBSERVATION_CACHE: dict[object, tuple[object, ...]] = {}
85
+
86
+
87
+ def reset_codex_quota_observation_cache() -> None:
88
+ """Clear #582's value-only quota-read memo."""
89
+ _CODEX_QUOTA_OBSERVATION_CACHE.clear()
90
+
91
+
92
+ def _cached_codex_quota_observations(**kwargs) -> tuple[object, ...]:
93
+ """Reuse bounded quota reads while their dedicated mutation stream is idle."""
94
+ conn = kwargs.get("cache_conn")
95
+ if not isinstance(conn, sqlite3.Connection):
96
+ return load_codex_quota_observations(**kwargs)
97
+ try:
98
+ seq_row = conn.execute(
99
+ "SELECT seq FROM sqlite_sequence "
100
+ "WHERE name='quota_window_change_log'"
101
+ ).fetchone()
102
+ table = conn.execute(
103
+ "SELECT 1 FROM sqlite_master WHERE type='table' "
104
+ "AND name='quota_window_change_log'"
105
+ ).fetchone()
106
+ revision_row = conn.execute(
107
+ "SELECT value FROM cache_meta "
108
+ "WHERE key='codex_window_attribution_revision'"
109
+ ).fetchone()
110
+ db_path = next(
111
+ str(row[2]) for row in conn.execute("PRAGMA database_list")
112
+ if str(row[1]) == "main"
113
+ )
114
+ except (sqlite3.Error, StopIteration):
115
+ return load_codex_quota_observations(**kwargs)
116
+ if table is None:
117
+ return load_codex_quota_observations(**kwargs)
118
+ roots = kwargs.get("source_root_keys")
119
+ physical_signatures = kwargs.get("physical_signatures")
120
+ physical_groups = kwargs.get("physical_groups")
121
+ key = (
122
+ id(load_codex_quota_observations),
123
+ db_path,
124
+ 0 if seq_row is None else int(seq_row[0]),
125
+ "" if revision_row is None else str(revision_row[0]),
126
+ None if roots is None else tuple(sorted(str(root) for root in roots)),
127
+ kwargs.get("captured_at_or_after"),
128
+ kwargs.get("active_at"),
129
+ kwargs.get("max_rows"),
130
+ None if physical_signatures is None else tuple(sorted(
131
+ (str(name), str(value))
132
+ for name, value in physical_signatures.items()
133
+ )),
134
+ kwargs.get("canonical_resets_between"),
135
+ None if physical_groups is None else tuple(sorted(physical_groups)),
136
+ bool(kwargs.get("latest_per_identity", False)),
137
+ )
138
+ cached = _CODEX_QUOTA_OBSERVATION_CACHE.get(key)
139
+ if cached is not None:
140
+ return cached
141
+ loaded = tuple(load_codex_quota_observations(**kwargs))
142
+ if len(_CODEX_QUOTA_OBSERVATION_CACHE) >= 32:
143
+ _CODEX_QUOTA_OBSERVATION_CACHE.clear()
144
+ _CODEX_QUOTA_OBSERVATION_CACHE[key] = loaded
145
+ return loaded
79
146
  SOURCE_HISTORY_LIMIT = 250
80
147
  DASHBOARD_QUOTA_OBSERVATION_LIMIT = 1000
81
148
  DASHBOARD_QUOTA_RECENT_DAYS = 35
@@ -418,7 +485,7 @@ def resolve_codex_cycle_detail_identity(
418
485
  ))
419
486
  if not active_roots:
420
487
  return identity
421
- observations = load_codex_quota_observations(
488
+ observations = _cached_codex_quota_observations(
422
489
  source_root_keys=active_roots,
423
490
  cache_conn=cache_conn,
424
491
  captured_at_or_after=(
@@ -744,6 +811,12 @@ class DashboardSourceSemantics:
744
811
  cache_report_anomaly_threshold_pp: int
745
812
  claude_identity: str
746
813
  codex_identity: str
814
+ # #556 S5 §3.4 — the Claude half of the same budget block, resolved through
815
+ # the same `_get_budget_config` call so the validator's warn-and-ignore
816
+ # stderr line is emitted ONCE per tick rather than once per consumer.
817
+ # Always a mapping (the validator fills defaults); `weekly_usd` is `None`
818
+ # when nothing is configured.
819
+ claude_budget: Mapping[str, object] | None = None
747
820
 
748
821
 
749
822
  def resolve_dashboard_source_semantics(
@@ -822,6 +895,10 @@ def resolve_dashboard_source_semantics(
822
895
  cache_report_anomaly_threshold_pp=cache_threshold,
823
896
  claude_identity=claude_identity,
824
897
  codex_identity=codex_identity,
898
+ claude_budget=MappingProxyType({
899
+ name: value for name, value in budget_config.items()
900
+ if name != "codex"
901
+ }),
825
902
  )
826
903
 
827
904
 
@@ -1013,14 +1090,55 @@ def codex_projection_coherence(
1013
1090
  )
1014
1091
 
1015
1092
 
1093
+ # #556 S5 Unit 2 review F13 — one-shot per process, following the repo's
1094
+ # established warn-once pattern (`_CONFIG_CORRUPT_WARNED` in
1095
+ # `bin/_cctally_config.py`, `_DISPLAY_TZ_BAD_CONFIG_WARNED` in
1096
+ # `bin/_lib_display_tz.py`). Both call sites below run on EVERY dashboard
1097
+ # rebuild tick, so an unresolvable window — a bad `display.tz` or `period` is
1098
+ # not transient — wrote a full traceback per tick indefinitely. The condition
1099
+ # is already visible to the user: the same failure nulls the budget status and
1100
+ # publishes `budget_compute_failed`.
1101
+ _CODEX_BUDGET_WINDOW_WARNED: set[str] = set()
1102
+
1103
+
1104
+ def _warn_codex_budget_window_once(site: str) -> None:
1105
+ """Log an unresolvable Codex budget window once PER CALL SITE.
1106
+
1107
+ Keyed by site, not by module. A single flag shared by both callers would let
1108
+ whichever failed first permanently silence the other, and the two are not
1109
+ interchangeable: one degrades the accounting range, the other is the site
1110
+ that used to destroy the whole Codex provider. The site also reaches the
1111
+ message, so a reader of the log knows which one they are looking at.
1112
+ """
1113
+ if site in _CODEX_BUDGET_WINDOW_WARNED:
1114
+ return
1115
+ _CODEX_BUDGET_WINDOW_WARNED.add(site)
1116
+ _lib_log.get_logger("dashboard").error(
1117
+ "codex budget window could not be resolved at %s "
1118
+ "(logged once per process per site)", site, exc_info=True,
1119
+ )
1120
+
1121
+
1016
1122
  def _codex_budget_cost_events(
1017
1123
  context: DashboardReadContext,
1018
1124
  entries: Iterable[object],
1019
1125
  ) -> tuple[tuple[dt.datetime, float], ...]:
1020
- """Freeze every configured-window cost event for exact idle pace updates."""
1126
+ """Freeze every configured-window cost event for exact idle pace updates.
1127
+
1128
+ #556 S5 §3.5: window resolution is guarded here as well as in
1129
+ ``_codex_budget_status_domain``. This runs FIRST at the build site, so an
1130
+ unresolvable period raising out of it would take the provider down before
1131
+ the status helper's own boundary could name the reason. Degrading to no
1132
+ events cannot publish a false ``$0``, because the same failure reaches the
1133
+ status helper and nulls the status instead.
1134
+ """
1021
1135
  if context.codex_budget is None:
1022
1136
  return ()
1023
- _period, start_at, end_at = _configured_codex_budget_window(context)
1137
+ try:
1138
+ _period, start_at, end_at = _configured_codex_budget_window(context)
1139
+ except Exception:
1140
+ _warn_codex_budget_window_once("cost_events")
1141
+ return ()
1024
1142
  c = sys.modules["cctally"]
1025
1143
  events: list[tuple[dt.datetime, float]] = []
1026
1144
  for entry in entries:
@@ -1030,8 +1148,10 @@ def _codex_budget_cost_events(
1030
1148
  timestamp = timestamp.astimezone(UTC)
1031
1149
  if not start_at <= timestamp < end_at:
1032
1150
  continue
1151
+ loaded_cost = getattr(entry, "cost_usd", None)
1033
1152
  events.append((
1034
1153
  timestamp,
1154
+ float(loaded_cost) if loaded_cost is not None else
1035
1155
  c._calculate_codex_entry_cost(
1036
1156
  str(getattr(entry, "model")),
1037
1157
  int(getattr(entry, "input_tokens")),
@@ -1079,6 +1199,251 @@ def _period_wire(view: Any) -> dict[str, object]:
1079
1199
  }
1080
1200
 
1081
1201
 
1202
+ _CODEX_PERIOD_VIEW_CACHE: dict[object, tuple] = {}
1203
+ _CODEX_CACHE_REPORT_ROWS: dict[object, tuple] = {}
1204
+ _CODEX_SESSION_VIEW_CACHE: dict[object, tuple[object, Any]] = {}
1205
+
1206
+
1207
+ def _codex_session_row_key(row: object) -> tuple[str, str]:
1208
+ return (
1209
+ str(getattr(row, "codex_root", "") or ""),
1210
+ str(getattr(row, "session_id_path", "") or ""),
1211
+ )
1212
+
1213
+
1214
+ def _codex_session_path_key(source_path: str) -> tuple[str, str]:
1215
+ import _lib_aggregators
1216
+ id_path, _file_name, _directory = _lib_aggregators._session_path_parts(
1217
+ source_path,
1218
+ )
1219
+ suffix = id_path + ".jsonl"
1220
+ root_prefix = (
1221
+ source_path[: -len(suffix)]
1222
+ if source_path.endswith(suffix) else source_path
1223
+ )
1224
+ return (
1225
+ _lib_aggregators._codex_home_root_from_prefix(root_prefix), id_path,
1226
+ )
1227
+
1228
+
1229
+ def _codex_incremental_entry_order(entry: object) -> tuple[object, str, str, int]:
1230
+ """Canonical accounting encounter key retained by incremental groups."""
1231
+ return (
1232
+ getattr(entry, "timestamp"),
1233
+ str(getattr(entry, "source_root_key", "") or ""),
1234
+ str(getattr(entry, "conversation_key", "") or ""),
1235
+ int(getattr(entry, "cache_entry_id", 0) or 0),
1236
+ )
1237
+
1238
+
1239
+ def _cached_codex_session_view(
1240
+ entries: Iterable[CodexEntry],
1241
+ *,
1242
+ changed_old: Iterable[object],
1243
+ changed_new: Iterable[object],
1244
+ cache_key: object,
1245
+ semantic_signature: object,
1246
+ now_utc: dt.datetime,
1247
+ tz_name: str | None,
1248
+ speed: str,
1249
+ ) -> Any:
1250
+ """Rebuild only session-file groups touched by accounting changes."""
1251
+ values = tuple(entries)
1252
+ signature = (semantic_signature, tz_name, speed)
1253
+ state = _CODEX_SESSION_VIEW_CACHE.get(cache_key)
1254
+ if state is None or state[0] != signature:
1255
+ view = build_codex_session_view(
1256
+ values, now_utc=now_utc, tz_name=tz_name, speed=speed,
1257
+ )
1258
+ groups: dict[tuple[str, str], list[CodexEntry]] = {}
1259
+ for entry in values:
1260
+ groups.setdefault(
1261
+ _codex_session_path_key(entry.source_path), [],
1262
+ ).append(entry)
1263
+ _CODEX_SESSION_VIEW_CACHE[cache_key] = (
1264
+ signature, view,
1265
+ {key: tuple(group) for key, group in groups.items()},
1266
+ )
1267
+ return view
1268
+ changed_paths = {
1269
+ str(getattr(entry, "source_path", "") or "")
1270
+ for entry in (*tuple(changed_old), *tuple(changed_new))
1271
+ if getattr(entry, "source_path", None)
1272
+ }
1273
+ if not changed_paths:
1274
+ return state[1]
1275
+ affected_keys = {
1276
+ _codex_session_path_key(source_path) for source_path in changed_paths
1277
+ }
1278
+ old_ids = {
1279
+ int(getattr(entry, "cache_entry_id", 0) or 0)
1280
+ for entry in changed_old
1281
+ }
1282
+ groups = dict(state[2])
1283
+ for key in affected_keys:
1284
+ groups[key] = tuple(
1285
+ entry for entry in groups.get(key, ())
1286
+ if int(getattr(entry, "cache_entry_id", 0) or 0) not in old_ids
1287
+ )
1288
+ for entry in changed_new:
1289
+ key = _codex_session_path_key(entry.source_path)
1290
+ groups[key] = (*groups.get(key, ()), entry)
1291
+ for key in affected_keys:
1292
+ if groups.get(key):
1293
+ groups[key] = tuple(sorted(
1294
+ groups[key], key=_codex_incremental_entry_order,
1295
+ ))
1296
+ else:
1297
+ groups.pop(key, None)
1298
+ partial_entries = tuple(sorted(
1299
+ (
1300
+ entry for key in affected_keys for entry in groups.get(key, ())
1301
+ ),
1302
+ key=_codex_incremental_entry_order,
1303
+ ))
1304
+ partial = build_codex_session_view(
1305
+ partial_entries,
1306
+ now_utc=now_utc, tz_name=tz_name, speed=speed,
1307
+ )
1308
+ encounter_order = {
1309
+ key: _codex_incremental_entry_order(group[0])
1310
+ for key, group in groups.items() if group
1311
+ }
1312
+ rows_by_encounter = sorted(
1313
+ (
1314
+ *(row for row in state[1].rows
1315
+ if _codex_session_row_key(row) not in affected_keys),
1316
+ *partial.rows,
1317
+ ),
1318
+ key=lambda row: encounter_order.get(
1319
+ _codex_session_row_key(row),
1320
+ (dt.datetime.max.replace(tzinfo=UTC), "", "", 0),
1321
+ ),
1322
+ )
1323
+ # The canonical aggregator performs a stable descending timestamp sort, so
1324
+ # establish first-encounter order before applying that same stable sort.
1325
+ rows = tuple(sorted(
1326
+ rows_by_encounter, key=lambda row: row.last_activity, reverse=True,
1327
+ ))
1328
+ total_cost = 0.0
1329
+ total_tokens = 0
1330
+ for row in rows:
1331
+ total_cost += row.cost_usd
1332
+ total_tokens += row.total_tokens
1333
+ view = replace(
1334
+ state[1], rows=rows, total_sessions=len(rows),
1335
+ total_cost_usd=total_cost,
1336
+ total_tokens=total_tokens,
1337
+ period_start=(min((row.last_activity for row in rows), default=None)),
1338
+ period_end=now_utc,
1339
+ )
1340
+ _CODEX_SESSION_VIEW_CACHE[cache_key] = (signature, view, groups)
1341
+ return view
1342
+
1343
+
1344
+ def _codex_period_bucket(
1345
+ entry: CodexEntry, *, kind: str, tz_name: str | None,
1346
+ ) -> str:
1347
+ zone = ZoneInfo(tz_name) if tz_name else None
1348
+ local = entry.timestamp.astimezone(zone) if zone else entry.timestamp.astimezone()
1349
+ return local.strftime("%Y-%m-%d" if kind == "daily" else "%Y-%m")
1350
+
1351
+
1352
+ def _cached_codex_period_view(
1353
+ entries: Iterable[CodexEntry],
1354
+ *,
1355
+ changed_old: Iterable[CodexEntry],
1356
+ changed_new: Iterable[CodexEntry],
1357
+ kind: str,
1358
+ cache_key: object,
1359
+ semantic_signature: object,
1360
+ now_utc: dt.datetime,
1361
+ tz_name: str | None,
1362
+ speed: str,
1363
+ ) -> Any:
1364
+ """Rebuild only date/month buckets touched by changed accounting rows."""
1365
+ values = tuple(entries)
1366
+ signature = (semantic_signature, kind, tz_name, speed)
1367
+ state = _CODEX_PERIOD_VIEW_CACHE.get((cache_key, kind))
1368
+ builder = build_codex_daily_view if kind == "daily" else build_codex_monthly_view
1369
+ if state is None or state[0] != signature:
1370
+ view = builder(values, now_utc=now_utc, tz_name=tz_name, speed=speed)
1371
+ groups: dict[str, list[CodexEntry]] = {}
1372
+ for entry in values:
1373
+ groups.setdefault(
1374
+ _codex_period_bucket(entry, kind=kind, tz_name=tz_name), [],
1375
+ ).append(entry)
1376
+ _CODEX_PERIOD_VIEW_CACHE[(cache_key, kind)] = (signature, view, groups)
1377
+ return view
1378
+ old_values = tuple(changed_old)
1379
+ new_values = tuple(changed_new)
1380
+ affected = {
1381
+ _codex_period_bucket(entry, kind=kind, tz_name=tz_name)
1382
+ for entry in (*old_values, *new_values)
1383
+ }
1384
+ if not affected:
1385
+ return state[1]
1386
+ prior = state[1]
1387
+ # Copy the mapping only. Unaffected bucket lists are immutable-by-contract
1388
+ # and remain shared; affected buckets below receive fresh lists.
1389
+ groups = dict(state[2])
1390
+ old_ids = {int(entry.cache_entry_id) for entry in old_values}
1391
+ for label in affected:
1392
+ groups[label] = [
1393
+ entry for entry in groups.get(label, ())
1394
+ if int(entry.cache_entry_id) not in old_ids
1395
+ ]
1396
+ for entry in new_values:
1397
+ label = _codex_period_bucket(entry, kind=kind, tz_name=tz_name)
1398
+ groups.setdefault(label, []).append(entry)
1399
+ for label in affected:
1400
+ groups[label].sort(key=lambda entry: (
1401
+ entry.timestamp, entry.source_root_key,
1402
+ entry.conversation_key, int(entry.cache_entry_id),
1403
+ ))
1404
+ replacements: dict[str, object] = {}
1405
+ for label in affected:
1406
+ partial = builder(
1407
+ tuple(groups[label]),
1408
+ now_utc=now_utc, tz_name=tz_name, speed=speed,
1409
+ )
1410
+ if partial.rows:
1411
+ replacements[label] = partial.rows[0]
1412
+ else:
1413
+ groups.pop(label, None)
1414
+ rows = tuple(sorted(
1415
+ (
1416
+ *(row for row in prior.rows if row.bucket not in affected),
1417
+ *replacements.values(),
1418
+ ),
1419
+ key=lambda row: row.bucket,
1420
+ ))
1421
+ total_cost = 0.0
1422
+ total_tokens = 0
1423
+ for row in rows:
1424
+ total_cost += row.cost_usd
1425
+ total_tokens += row.total_tokens
1426
+ period_start = None
1427
+ if rows:
1428
+ if kind == "daily":
1429
+ first = dt.date.fromisoformat(rows[0].bucket)
1430
+ period_start = dt.datetime.combine(first, dt.time.min, tzinfo=UTC)
1431
+ else:
1432
+ year, month = rows[0].bucket.split("-")
1433
+ period_start = dt.datetime(int(year), int(month), 1, tzinfo=UTC)
1434
+ view = replace(
1435
+ prior,
1436
+ rows=rows,
1437
+ total_cost_usd=total_cost,
1438
+ total_tokens=total_tokens,
1439
+ period_start=period_start,
1440
+ period_end=now_utc,
1441
+ period_civil_bucket=bool(rows),
1442
+ )
1443
+ _CODEX_PERIOD_VIEW_CACHE[(cache_key, kind)] = (signature, view, groups)
1444
+ return view
1445
+
1446
+
1082
1447
  def _codex_cache_report_wire(
1083
1448
  entries: Iterable[object],
1084
1449
  *,
@@ -1088,6 +1453,10 @@ def _codex_cache_report_wire(
1088
1453
  speed: str,
1089
1454
  anomaly_threshold_pp: int = 15,
1090
1455
  window_days: int = 14,
1456
+ cache_key: object | None = None,
1457
+ changed_old: Iterable[object] = (),
1458
+ changed_new: Iterable[object] = (),
1459
+ semantic_signature: object | None = None,
1091
1460
  ) -> dict[str, object]:
1092
1461
  """Compute the canonical cache report from Codex's inclusive counters.
1093
1462
 
@@ -1102,6 +1471,7 @@ def _codex_cache_report_wire(
1102
1471
  wire = c._load_sibling("_lib_cache_report_wire")
1103
1472
  display_tz = ZoneInfo(display_tz_name) if display_tz_name else None
1104
1473
  cutoff = now_utc - dt.timedelta(days=window_days)
1474
+ bucket_tz = crk._resolve_bucket_tz(display_tz)
1105
1475
 
1106
1476
  def _tiered_cost(tokens: int, pricing: Mapping[str, object], base: str, above: str) -> float:
1107
1477
  if tokens <= 0:
@@ -1113,11 +1483,10 @@ def _codex_cache_report_wire(
1113
1483
  return threshold * base_rate + (tokens - threshold) * float(above_rate)
1114
1484
  return tokens * base_rate
1115
1485
 
1116
- wrapped = []
1117
- for entry in entries:
1486
+ def _wrap_entry(entry: object) -> object | None:
1118
1487
  timestamp = getattr(entry, "timestamp", None)
1119
1488
  if not isinstance(timestamp, dt.datetime) or timestamp < cutoff:
1120
- continue
1489
+ return None
1121
1490
  model = str(getattr(entry, "model", "") or "unknown")
1122
1491
  input_tokens = int(getattr(entry, "input_tokens", 0))
1123
1492
  cached_tokens = min(input_tokens, int(getattr(entry, "cached_input_tokens", 0)))
@@ -1144,7 +1513,7 @@ def _codex_cache_report_wire(
1144
1513
  or str(item_metadata.get("project_label") or "").strip()
1145
1514
  or "(unknown)"
1146
1515
  )
1147
- wrapped.append(SimpleNamespace(
1516
+ return SimpleNamespace(
1148
1517
  timestamp=timestamp,
1149
1518
  model=model,
1150
1519
  cost_usd=float(getattr(entry, "cost_usd", 0.0)),
@@ -1156,22 +1525,189 @@ def _codex_cache_report_wire(
1156
1525
  cache_saved_usd=saved,
1157
1526
  cache_wasted_usd=0.0,
1158
1527
  cache_net_usd=saved,
1528
+ _cache_entry_id=int(getattr(entry, "cache_entry_id", 0) or 0),
1529
+ _cache_day=timestamp.astimezone(bucket_tz).strftime("%Y-%m-%d"),
1530
+ _cache_order=_codex_incremental_entry_order(entry),
1159
1531
  usage={
1160
1532
  "input_tokens": uncached_tokens,
1161
1533
  "output_tokens": int(getattr(entry, "output_tokens", 0)),
1162
1534
  "cache_creation_input_tokens": 0,
1163
1535
  "cache_read_input_tokens": cached_tokens,
1164
1536
  },
1537
+ )
1538
+
1539
+ def _freeze_day(day_entries: Iterable[object]):
1540
+ """Freeze one changed day without re-folding the retained window."""
1541
+ day_values = tuple(day_entries)
1542
+ rows = crk._aggregate_cache_by_day(
1543
+ day_values,
1544
+ display_tz=display_tz,
1545
+ pricing=c.CODEX_MODEL_PRICING,
1546
+ cost_calculator=(
1547
+ lambda _model, _usage, _mode, cost: float(cost or 0.0)
1548
+ ),
1549
+ )
1550
+ if not rows:
1551
+ return None
1552
+ if len(rows) != 1:
1553
+ raise AssertionError("one cache-report day produced multiple rows")
1554
+ row = rows[0]
1555
+ project_nets: dict[str, list[float]] = {}
1556
+ project_tokens: dict[str, list[int]] = {}
1557
+ for entry in day_values:
1558
+ if entry.model == "<synthetic>":
1559
+ continue
1560
+ project = entry.project_path or "(unknown)"
1561
+ project_nets.setdefault(project, []).append(entry.cache_net_usd)
1562
+ tokens = project_tokens.setdefault(project, [0, 0, 0])
1563
+ tokens[0] += entry.input_tokens
1564
+ tokens[1] += entry.cache_creation_tokens
1565
+ tokens[2] += entry.cache_read_tokens
1566
+ project_partials = tuple(sorted(
1567
+ (
1568
+ project,
1569
+ crk._ProjectPartial(
1570
+ net_usd=stable_sum(project_nets[project]),
1571
+ input_tokens=tokens[0],
1572
+ cache_creation_tokens=tokens[1],
1573
+ cache_read_tokens=tokens[2],
1574
+ ),
1575
+ )
1576
+ for project, tokens in project_tokens.items()
1165
1577
  ))
1578
+ return crk.CachedCacheReportDay(
1579
+ date=row.date,
1580
+ cache_hit_percent=row.cache_hit_percent,
1581
+ input_tokens=row.input_tokens,
1582
+ output_tokens=row.output_tokens,
1583
+ cache_creation_tokens=row.cache_creation_tokens,
1584
+ cache_read_tokens=row.cache_read_tokens,
1585
+ cost=row.cost,
1586
+ saved_usd=row.saved_usd,
1587
+ wasted_usd=row.wasted_usd,
1588
+ net_usd=row.net_usd,
1589
+ model_breakdowns=tuple(
1590
+ crk._FrozenModelBreakdown(
1591
+ model_name=model.model_name,
1592
+ input_tokens=model.input_tokens,
1593
+ output_tokens=model.output_tokens,
1594
+ cache_creation_tokens=model.cache_creation_tokens,
1595
+ cache_read_tokens=model.cache_read_tokens,
1596
+ cache_hit_percent=model.cache_hit_percent,
1597
+ cost=model.cost,
1598
+ saved_usd=model.saved_usd,
1599
+ wasted_usd=model.wasted_usd,
1600
+ net_usd=model.net_usd,
1601
+ )
1602
+ for model in row.model_breakdowns
1603
+ ),
1604
+ project_partials=project_partials,
1605
+ )
1606
+
1607
+ values = tuple(entries)
1608
+ cacheable = cache_key is not None and all(
1609
+ int(getattr(entry, "cache_entry_id", 0) or 0) for entry in values
1610
+ )
1611
+ signature = (semantic_signature, speed, window_days, display_tz_name)
1612
+ cached_days = None
1613
+ if not cacheable:
1614
+ wrapped = [
1615
+ wrapped_entry for entry in values
1616
+ if (wrapped_entry := _wrap_entry(entry)) is not None
1617
+ ]
1618
+ else:
1619
+ state = _CODEX_CACHE_REPORT_ROWS.get(cache_key)
1620
+ if state is None or len(state) != 4 or state[0] != signature:
1621
+ cached_rows = {}
1622
+ for entry in values:
1623
+ wrapped_entry = _wrap_entry(entry)
1624
+ if wrapped_entry is not None:
1625
+ cached_rows[int(entry.cache_entry_id)] = wrapped_entry
1626
+ groups: dict[str, tuple[int, ...]] = {}
1627
+ mutable_groups: dict[str, list[int]] = {}
1628
+ for entry in cached_rows.values():
1629
+ mutable_groups.setdefault(entry._cache_day, []).append(
1630
+ entry._cache_entry_id)
1631
+ for day, ids in mutable_groups.items():
1632
+ groups[day] = tuple(sorted(
1633
+ ids, key=lambda cache_id: cached_rows[cache_id]._cache_order,
1634
+ ))
1635
+ cached_days = {
1636
+ day: _freeze_day(cached_rows[cache_id] for cache_id in ids)
1637
+ for day, ids in groups.items()
1638
+ }
1639
+ else:
1640
+ cached_rows = dict(state[1])
1641
+ groups = dict(state[2])
1642
+ cached_days = dict(state[3])
1643
+ affected_days: set[str] = set()
1644
+ old_ids: set[int] = set()
1645
+ # The accounting population is wider than the cache-report window.
1646
+ # Rows can therefore age out without any ledger mutation. Preserve
1647
+ # the old full-fold contract by evicting them whenever a later
1648
+ # dirty source build advances `now_utc`.
1649
+ for cache_entry_id, prior in tuple(cached_rows.items()):
1650
+ if prior.timestamp < cutoff:
1651
+ old_ids.add(cache_entry_id)
1652
+ affected_days.add(prior._cache_day)
1653
+ cached_rows.pop(cache_entry_id, None)
1654
+ for entry in changed_old:
1655
+ cache_entry_id = int(getattr(entry, "cache_entry_id", 0) or 0)
1656
+ if cache_entry_id:
1657
+ old_ids.add(cache_entry_id)
1658
+ prior = cached_rows.get(cache_entry_id)
1659
+ if prior is not None:
1660
+ affected_days.add(prior._cache_day)
1661
+ cached_rows.pop(cache_entry_id, None)
1662
+ for entry in changed_new:
1663
+ cache_entry_id = int(getattr(entry, "cache_entry_id", 0) or 0)
1664
+ if not cache_entry_id:
1665
+ continue
1666
+ wrapped_entry = _wrap_entry(entry)
1667
+ if wrapped_entry is None:
1668
+ cached_rows.pop(cache_entry_id, None)
1669
+ else:
1670
+ cached_rows[cache_entry_id] = wrapped_entry
1671
+ affected_days.add(wrapped_entry._cache_day)
1672
+ for day in affected_days:
1673
+ groups[day] = tuple(
1674
+ cache_id for cache_id in groups.get(day, ())
1675
+ if cache_id not in old_ids
1676
+ )
1677
+ for entry in changed_new:
1678
+ cache_entry_id = int(getattr(entry, "cache_entry_id", 0) or 0)
1679
+ wrapped_entry = cached_rows.get(cache_entry_id)
1680
+ if wrapped_entry is not None:
1681
+ groups[wrapped_entry._cache_day] = (
1682
+ *groups.get(wrapped_entry._cache_day, ()), cache_entry_id,
1683
+ )
1684
+ for day in affected_days:
1685
+ ids = tuple(sorted(
1686
+ dict.fromkeys(groups.get(day, ())),
1687
+ key=lambda cache_id: cached_rows[cache_id]._cache_order,
1688
+ ))
1689
+ if ids:
1690
+ groups[day] = ids
1691
+ cached_days[day] = _freeze_day(
1692
+ cached_rows[cache_id] for cache_id in ids)
1693
+ else:
1694
+ groups.pop(day, None)
1695
+ cached_days.pop(day, None)
1696
+ cached_days = {
1697
+ day: unit for day, unit in cached_days.items() if unit is not None
1698
+ }
1699
+ _CODEX_CACHE_REPORT_ROWS[cache_key] = (
1700
+ signature, cached_rows, groups, cached_days,
1701
+ )
1702
+ wrapped = []
1166
1703
 
1167
1704
  # One current day per invocation (#443 S3 F23): the focal day and the
1168
1705
  # entry filter below both resolve through the SAME zone the kernel
1169
1706
  # buckets by. ``display_tz or UTC`` diverged from host-local bucketing
1170
1707
  # on every non-UTC host, which published a fabricated spotlight and let
1171
1708
  # the breakdowns draw from a different entry population than the days.
1172
- bucket_tz = crk._resolve_bucket_tz(display_tz)
1173
1709
  today_iso = now_utc.astimezone(bucket_tz).strftime("%Y-%m-%d")
1174
- if not wrapped:
1710
+ if not wrapped and not cached_days:
1175
1711
  # An empty store measured nothing, so ``observed`` is False and
1176
1712
  # every applicable predicate is unevaluated. The client
1177
1713
  # short-circuits on ``is_empty`` before reading either, so this
@@ -1194,16 +1730,29 @@ def _codex_cache_report_wire(
1194
1730
  fourteen_day_efficiency_ratio=0.0, is_empty=True,
1195
1731
  )
1196
1732
 
1197
- result = crk._build_cache_report(
1198
- wrapped,
1199
- now_utc=now_utc,
1200
- window_days=window_days,
1201
- anomaly_threshold_pp=anomaly_threshold_pp,
1202
- anomaly_window_days=window_days,
1203
- display_tz=display_tz,
1204
- pricing=c.CODEX_MODEL_PRICING,
1205
- cost_calculator=lambda _model, _usage, _mode, cost: float(cost or 0.0),
1206
- )
1733
+ if cached_days is None:
1734
+ result = crk._build_cache_report(
1735
+ wrapped,
1736
+ now_utc=now_utc,
1737
+ window_days=window_days,
1738
+ anomaly_threshold_pp=anomaly_threshold_pp,
1739
+ anomaly_window_days=window_days,
1740
+ display_tz=display_tz,
1741
+ pricing=c.CODEX_MODEL_PRICING,
1742
+ cost_calculator=lambda _model, _usage, _mode, cost: float(cost or 0.0),
1743
+ )
1744
+ else:
1745
+ result = crk.classify_and_summarize(
1746
+ [
1747
+ crk.reconstruct_cache_row(cached_days[day])
1748
+ for day in sorted(cached_days)
1749
+ ],
1750
+ now_utc=now_utc,
1751
+ window_days=window_days,
1752
+ anomaly_threshold_pp=anomaly_threshold_pp,
1753
+ anomaly_window_days=window_days,
1754
+ display_tz=display_tz,
1755
+ )
1207
1756
  raw_rows = sorted(result.rows, key=lambda row: row.date or "", reverse=True)
1208
1757
  today_row = next((row for row in raw_rows if row.date == today_iso), None)
1209
1758
  # #443 F13/F14 — both charts label their rightmost element "Today"
@@ -1243,18 +1792,65 @@ def _codex_cache_report_wire(
1243
1792
  baseline = result.today_baseline_median
1244
1793
  today_hit = today_row.cache_hit_percent if today_row else 0.0
1245
1794
  kept_dates = {row["date"] for row in days}
1246
- kept_entries = [
1247
- entry for entry in wrapped
1248
- if entry.timestamp.astimezone(bucket_tz).strftime("%Y-%m-%d") in kept_dates
1249
- ]
1250
- by_project = crk._aggregate_cache_breakdown(
1251
- kept_entries, key_fn=lambda entry: entry.project_path,
1252
- pricing=c.CODEX_MODEL_PRICING,
1253
- )
1254
- by_model = crk._aggregate_cache_breakdown(
1255
- kept_entries, key_fn=lambda entry: entry.model,
1256
- pricing=c.CODEX_MODEL_PRICING,
1257
- )
1795
+ if cached_days is None:
1796
+ kept_entries = [
1797
+ entry for entry in wrapped
1798
+ if entry.timestamp.astimezone(bucket_tz).strftime("%Y-%m-%d")
1799
+ in kept_dates
1800
+ ]
1801
+ by_project = crk._aggregate_cache_breakdown(
1802
+ kept_entries, key_fn=lambda entry: entry.project_path,
1803
+ pricing=c.CODEX_MODEL_PRICING,
1804
+ )
1805
+ by_model = crk._aggregate_cache_breakdown(
1806
+ kept_entries, key_fn=lambda entry: entry.model,
1807
+ pricing=c.CODEX_MODEL_PRICING,
1808
+ )
1809
+ else:
1810
+ # Preserve the prior exact float fold: one stable_sum over every
1811
+ # individual entry net, not a stable_sum of per-day stable_sums (the
1812
+ # latter can differ by one ULP). Walk the already-wrapped immutable
1813
+ # rows once and accumulate both axes together, avoiding the old two
1814
+ # full pricing/adapter folds while retaining first-seen tie order.
1815
+ breakdowns: dict[str, dict[str, list[object]]] = {
1816
+ "project": {}, "model": {},
1817
+ }
1818
+ for raw in values:
1819
+ cache_entry_id = int(getattr(raw, "cache_entry_id", 0) or 0)
1820
+ entry = cached_rows.get(cache_entry_id)
1821
+ if (
1822
+ entry is None or entry._cache_day not in kept_dates
1823
+ or entry.model == "<synthetic>"
1824
+ ):
1825
+ continue
1826
+ for axis, key in (
1827
+ ("project", entry.project_path), ("model", entry.model),
1828
+ ):
1829
+ parts = breakdowns[axis].setdefault(
1830
+ key, [[], 0, 0, 0],
1831
+ )
1832
+ parts[0].append(entry.cache_net_usd)
1833
+ parts[1] += entry.input_tokens
1834
+ parts[2] += entry.cache_creation_tokens
1835
+ parts[3] += entry.cache_read_tokens
1836
+
1837
+ def _finish_breakdown(axis: str):
1838
+ rows = []
1839
+ for key, parts in breakdowns[axis].items():
1840
+ rows.append(crk.CacheBreakdownRow(
1841
+ key=key,
1842
+ cache_hit_percent=crk._compute_cache_hit_percent(
1843
+ parts[1], parts[2], parts[3],
1844
+ ),
1845
+ net_usd=stable_sum(parts[0]),
1846
+ input_tokens=parts[1],
1847
+ cache_creation_tokens=parts[2],
1848
+ cache_read_tokens=parts[3],
1849
+ ))
1850
+ return crk._finalize_breakdown_rows(rows)
1851
+
1852
+ by_project = _finish_breakdown("project")
1853
+ by_model = _finish_breakdown("model")
1258
1854
  seven = days[:7]
1259
1855
  saved_total = stable_sum(float(row["saved_usd"]) for row in days)
1260
1856
  wasted_total = stable_sum(float(row["wasted_usd"]) for row in days)
@@ -1759,6 +2355,15 @@ def _configured_codex_budget_status(
1759
2355
  with no configured budget therefore has no budget status at all (``None``),
1760
2356
  exactly as an unconfigured vendor does; the merged vendor status stays on the
1761
2357
  parent. ``None`` keeps the merged behaviour and is byte-stable.
2358
+
2359
+ #556 S5 §3.5: a VENDOR-WIDE read of a PER-ACCOUNT-ONLY configuration is the
2360
+ same "nothing to compute against" state. ``_validate_codex_budget_block``
2361
+ accepts a Codex block with no ``amount_usd`` when a non-empty ``accounts``
2362
+ map is present (``bin/_cctally_core.py:1350``), and this function used to
2363
+ call ``float(amount_usd)`` on that ``None`` with no exception boundary
2364
+ anywhere between here and ``_tui_build_source_bundle``'s
2365
+ ``source_build_failed`` handler — so one valid configuration destroyed the
2366
+ entire Codex provider's data.
1762
2367
  """
1763
2368
  config = context.codex_budget
1764
2369
  if config is None:
@@ -1769,6 +2374,8 @@ def _configured_codex_budget_status(
1769
2374
  if not isinstance(per_account, Mapping) or account_key not in per_account:
1770
2375
  return None
1771
2376
  amount_usd = per_account[account_key]
2377
+ if amount_usd is None:
2378
+ return None
1772
2379
  c = sys.modules["cctally"]
1773
2380
  period, start_at, end_at = _configured_codex_budget_window(context)
1774
2381
 
@@ -1777,38 +2384,83 @@ def _configured_codex_budget_status(
1777
2384
  )
1778
2385
 
1779
2386
  def _sum_cost(start: dt.datetime, end: dt.datetime) -> float:
1780
- return sum(cost for timestamp, cost in resolved_events if start <= timestamp < end)
2387
+ # stable_sum, not sum: this feeds the byte-compared budget wire, and
2388
+ # the built-in sum() switched to Neumaier compensated summation for
2389
+ # floats in CPython 3.12, so 3.11 renders a different figure.
2390
+ return stable_sum(
2391
+ cost for timestamp, cost in resolved_events if start <= timestamp < end
2392
+ )
1781
2393
 
1782
2394
  recent_start = max(start_at, context.now_utc - dt.timedelta(hours=24))
1783
- inputs = c.BudgetInputs(
1784
- target_usd=float(amount_usd),
2395
+ # #556 S5 §3.1/§3.3: ONE producer of the wire status, shared with Claude.
2396
+ return c.budget_status_payload(
2397
+ period=period,
2398
+ window_start_at=start_at,
2399
+ window_end_at=end_at,
2400
+ target_usd=amount_usd,
1785
2401
  spent_usd=_sum_cost(start_at, context.now_utc),
1786
2402
  recent_24h_usd=_sum_cost(recent_start, context.now_utc),
1787
- week_start_at=start_at,
1788
- week_end_at=end_at,
1789
2403
  now=context.now_utc,
1790
- alert_thresholds=tuple(config["alert_thresholds"]),
2404
+ alert_thresholds=config["alert_thresholds"],
1791
2405
  )
1792
- status = c.compute_budget_status(inputs)
1793
- return {
1794
- "period": period,
1795
- "budget_usd": inputs.target_usd,
1796
- "spent_usd": status.spent_usd,
1797
- "remaining_usd": status.remaining_usd,
1798
- "consumption_pct": status.consumption_pct,
1799
- "verdict": status.verdict,
1800
- "low_confidence": status.low_confidence,
1801
- "window_start_at": start_at.astimezone(UTC).isoformat(),
1802
- "window_end_at": end_at.astimezone(UTC).isoformat(),
1803
- "recent_24h_usd": inputs.recent_24h_usd,
1804
- "alert_thresholds": inputs.alert_thresholds,
1805
- "pace": {
1806
- "daily_usd": status.daily_pace_usd,
1807
- "projected_low_usd": status.projected_eow_low_usd,
1808
- "projected_high_usd": status.projected_eow_high_usd,
1809
- "week_avg_projection_usd": status.week_avg_projection_usd,
1810
- },
1811
- }
2406
+
2407
+
2408
+ def _codex_budget_status_domain(
2409
+ context: DashboardReadContext,
2410
+ entries: Iterable[object],
2411
+ *,
2412
+ cost_events: tuple[tuple[dt.datetime, float], ...] | None = None,
2413
+ account_key: str | None = None,
2414
+ ) -> dict[str, object]:
2415
+ """The Codex budget domain's status half, with a LOCAL failure contract.
2416
+
2417
+ #556 S5 §3.5. ``status`` stays required-and-nullable, which is the shape
2418
+ Codex's enclosing domain has always emitted and which this session does not
2419
+ normalise. What is new is the boundary: a computation that raises now
2420
+ degrades this one domain and names its reason, following the same
2421
+ ``{code, message, provider}`` shape the Claude half uses, instead of
2422
+ escaping into the provider-level handler and turning the whole Codex source
2423
+ into ``source_build_failed``.
2424
+
2425
+ ``status_unavailable`` is additive and omitted when inapplicable, so the
2426
+ ordinary payload is byte-identical.
2427
+
2428
+ #556 S5 Unit 2 review F1. A VENDOR-WIDE read of a per-account-only Codex
2429
+ configuration is a configured state, not an unset one, and returning a bare
2430
+ ``{"status": None}`` for it made the client render "No budget set." beside
2431
+ the command that sets one — to a user who has budgets set. It publishes the
2432
+ same ``not_configured.disposition`` the Claude half publishes, which the
2433
+ client already handles. An ACCOUNT-SCOPED read is deliberately excluded:
2434
+ ``account_budgets_only`` describes the vendor-wide axis, and that account's
2435
+ own missing budget is genuinely unset.
2436
+ """
2437
+ config = context.codex_budget
2438
+ if (
2439
+ account_key is None
2440
+ and isinstance(config, Mapping)
2441
+ and config.get("amount_usd") is None
2442
+ and config.get("accounts")
2443
+ ):
2444
+ return {
2445
+ "status": None,
2446
+ "not_configured": {"disposition": "account_budgets_only"},
2447
+ }
2448
+ try:
2449
+ return {"status": _configured_codex_budget_status(
2450
+ context, entries, cost_events=cost_events, account_key=account_key,
2451
+ )}
2452
+ except Exception:
2453
+ _lib_log.get_logger("dashboard").error(
2454
+ "codex budget status could not be computed", exc_info=True,
2455
+ )
2456
+ return {
2457
+ "status": None,
2458
+ "status_unavailable": {
2459
+ "code": "budget_compute_failed",
2460
+ "message": "Codex's budget status could not be computed.",
2461
+ "provider": "codex",
2462
+ },
2463
+ }
1812
2464
 
1813
2465
 
1814
2466
  def _configured_codex_budget_window(
@@ -1995,7 +2647,7 @@ def _quota_read_model(
1995
2647
  # with whichever ACCOUNT's 5h observation happened to sort last.
1996
2648
  correlated_five_hour = tuple(
1997
2649
  observation
1998
- for observation in load_codex_quota_observations(
2650
+ for observation in _cached_codex_quota_observations(
1999
2651
  source_root_keys={identity.source_root_key},
2000
2652
  cache_conn=context.cache_conn,
2001
2653
  captured_at_or_after=block.nominal_start_at,
@@ -2172,7 +2824,9 @@ def _refresh_budget_status_clock(
2172
2824
  str(status["window_end_at"]).replace("Z", "+00:00")
2173
2825
  ).astimezone(UTC)
2174
2826
  recent_start = max(start_at, now_utc - dt.timedelta(hours=24))
2175
- recent_24h_usd = sum(
2827
+ # stable_sum, not sum: same byte-compared budget wire as the producer
2828
+ # above, and the same CPython 3.12 sum() change applies.
2829
+ recent_24h_usd = stable_sum(
2176
2830
  float(cost) for timestamp, cost in cost_events
2177
2831
  if isinstance(timestamp, dt.datetime)
2178
2832
  and start_at <= timestamp.astimezone(UTC) < now_utc
@@ -2363,19 +3017,50 @@ def refresh_codex_source_clock(
2363
3017
  scopes = data.get("account_scopes")
2364
3018
  scopes_changed = False
2365
3019
  if isinstance(scopes, Mapping):
3020
+ # #556 S5 §3.8: each child's budget reclocks from ITS OWN retained event
3021
+ # tuple, never the vendor-wide one — the accounts hold different spend,
3022
+ # so borrowing the parent's events would publish another account's
3023
+ # trailing-24h rate under this account's card.
3024
+ child_budget_events = (
3025
+ state.clock_data.get("codex_budget_cost_events_by_account", {})
3026
+ if isinstance(state.clock_data, Mapping) else {}
3027
+ )
2366
3028
  rebuilt_scopes = dict(scopes)
2367
3029
  for scope_key, scope in scopes.items():
2368
3030
  if not isinstance(scope, Mapping):
2369
3031
  continue
3032
+ rebuilt_scope: dict[str, object] | None = None
2370
3033
  scope_quota = scope.get("quota")
2371
- if not isinstance(scope_quota, Mapping):
2372
- continue
2373
- reclocked_scope_quota = _reclock_quota_domain(
2374
- scope_quota, now_utc=now_utc)
2375
- if reclocked_scope_quota == scope_quota:
3034
+ if isinstance(scope_quota, Mapping):
3035
+ reclocked_scope_quota = _reclock_quota_domain(
3036
+ scope_quota, now_utc=now_utc)
3037
+ if reclocked_scope_quota != scope_quota:
3038
+ rebuilt_scope = dict(scope)
3039
+ rebuilt_scope["quota"] = reclocked_scope_quota
3040
+ # #556 S5 §3.8: the child budget status was computed at build and
3041
+ # then never advanced, while the quota beside it was. Focused cards
3042
+ # read this object, so an unclocked one is a knowingly stale number
3043
+ # on the surface a user is looking straight at.
3044
+ scope_budget = scope.get("budget")
3045
+ if isinstance(scope_budget, Mapping) and isinstance(
3046
+ scope_budget.get("status"), Mapping,
3047
+ ):
3048
+ reclocked_child_budget = _refresh_budget_status_clock(
3049
+ scope_budget["status"],
3050
+ now_utc,
3051
+ cost_events=child_budget_events.get(scope_key, ()),
3052
+ )
3053
+ if (
3054
+ reclocked_child_budget is not None
3055
+ and reclocked_child_budget != scope_budget["status"]
3056
+ ):
3057
+ if rebuilt_scope is None:
3058
+ rebuilt_scope = dict(scope)
3059
+ rebuilt_scope["budget"] = {
3060
+ **dict(scope_budget), "status": reclocked_child_budget,
3061
+ }
3062
+ if rebuilt_scope is None:
2376
3063
  continue
2377
- rebuilt_scope = dict(scope)
2378
- rebuilt_scope["quota"] = reclocked_scope_quota
2379
3064
  rebuilt_scopes[scope_key] = rebuilt_scope
2380
3065
  scopes_changed = True
2381
3066
  if scopes_changed:
@@ -2596,22 +3281,66 @@ def _alerts_wire(
2596
3281
  )[:SOURCE_HISTORY_LIMIT])
2597
3282
 
2598
3283
 
3284
+ _CODEX_PROJECT_LABEL_CACHE: dict[object, dict[str, object]] = {}
3285
+
3286
+
3287
+ def _cached_project_labeled_entries(
3288
+ entries: tuple[object, ...], cache_key: object | None,
3289
+ ) -> tuple[object, ...]:
3290
+ """Reuse per-scope display-label annotation for unchanged accounting rows."""
3291
+ if cache_key is None or any(
3292
+ not int(getattr(entry, "cache_entry_id", 0) or 0) for entry in entries
3293
+ ):
3294
+ return tuple(assign_collision_safe_project_labels(entries))
3295
+ pairs = frozenset(
3296
+ (str(entry.project_key), str(entry.project_label)) for entry in entries
3297
+ )
3298
+ state = _CODEX_PROJECT_LABEL_CACHE.get(cache_key)
3299
+ if state is None or state.get("pairs") != pairs:
3300
+ labeled = tuple(assign_collision_safe_project_labels(entries))
3301
+ else:
3302
+ labels = state["labels"]
3303
+ prior = state["entries"]
3304
+ labeled = tuple(
3305
+ prior[int(entry.cache_entry_id)][1]
3306
+ if (
3307
+ int(entry.cache_entry_id) in prior
3308
+ and prior[int(entry.cache_entry_id)][0] == entry
3309
+ ) else replace(
3310
+ entry, display_label=labels[str(entry.project_key)],
3311
+ )
3312
+ for entry in entries
3313
+ )
3314
+ _CODEX_PROJECT_LABEL_CACHE[cache_key] = {
3315
+ "pairs": pairs,
3316
+ "labels": {
3317
+ str(entry.project_key): entry.display_label for entry in labeled
3318
+ },
3319
+ "entries": {
3320
+ int(entry.cache_entry_id): (raw, entry)
3321
+ for raw, entry in zip(entries, labeled)
3322
+ },
3323
+ }
3324
+ return labeled
3325
+
3326
+
2599
3327
  def _projects_wire(
2600
3328
  context: DashboardReadContext,
2601
- quota_observations: Iterable[object],
3329
+ _quota_observations: Iterable[object],
2602
3330
  entries: Iterable[object],
2603
3331
  *,
2604
3332
  accounting_end: dt.datetime,
3333
+ cache_key: object | None = None,
2605
3334
  ) -> dict[str, object]:
2606
3335
  """Adapt S3's already-qualified attribution result without re-formulas."""
2607
- qualified_entries = tuple(entries)
3336
+ qualified_entries = _cached_project_labeled_entries(
3337
+ tuple(entries), cache_key,
3338
+ )
2608
3339
  result = build_codex_project_result(
2609
3340
  qualified_entries,
2610
3341
  range_start=context.range_start,
2611
3342
  range_end=accounting_end,
2612
- blocks=build_blocks(quota_observations),
2613
3343
  as_of=context.now_utc,
2614
- allocation_entries=qualified_entries,
2615
3344
  )
2616
3345
  data = result.data
2617
3346
  if data is None:
@@ -2636,6 +3365,125 @@ def _projects_wire(
2636
3365
  }
2637
3366
 
2638
3367
 
3368
+ _CODEX_PROJECT_WIRE_CACHE: dict[object, tuple] = {}
3369
+
3370
+
3371
+ def _cached_projects_wire(
3372
+ context: DashboardReadContext,
3373
+ quota_observations: Iterable[object],
3374
+ entries: Iterable[object],
3375
+ *,
3376
+ changed_old: Iterable[object],
3377
+ changed_new: Iterable[object],
3378
+ accounting_end: dt.datetime,
3379
+ cache_key: object,
3380
+ semantic_signature: object,
3381
+ ) -> dict[str, object]:
3382
+ """Rebuild only project groups touched by accounting changes."""
3383
+ values = tuple(entries)
3384
+ pairs = frozenset(
3385
+ (str(entry.project_key), str(entry.project_label)) for entry in values
3386
+ )
3387
+ # `entries` is already the complete half-open population for the advancing
3388
+ # upper bound. A moving wall clock is therefore not an aggregation
3389
+ # semantic: `build_cached_codex_accounting` emits newly-visible rows in the
3390
+ # delta when its upper bound advances. Keeping `accounting_end` here made
3391
+ # every live dirty tick discard every project group before that delta could
3392
+ # be spliced.
3393
+ signature = (semantic_signature, pairs, context.range_start)
3394
+ state = _CODEX_PROJECT_WIRE_CACHE.get(cache_key)
3395
+ if state is None or state[0] != signature:
3396
+ value = _projects_wire(
3397
+ context, quota_observations, values,
3398
+ accounting_end=accounting_end, cache_key=cache_key,
3399
+ )
3400
+ groups: dict[tuple[str, str], list[object]] = {}
3401
+ for entry in values:
3402
+ groups.setdefault(
3403
+ (str(entry.source_root_key), str(entry.project_key)), [],
3404
+ ).append(entry)
3405
+ _CODEX_PROJECT_WIRE_CACHE[cache_key] = (
3406
+ signature, value,
3407
+ {key: tuple(group) for key, group in groups.items()},
3408
+ )
3409
+ return value
3410
+
3411
+ affected = {
3412
+ (str(entry.source_root_key), str(entry.project_key))
3413
+ for entry in (*tuple(changed_old), *tuple(changed_new))
3414
+ }
3415
+ if not affected:
3416
+ return state[1]
3417
+ label_state = _CODEX_PROJECT_LABEL_CACHE.get(cache_key) or {}
3418
+ labels = label_state.get("labels") or {}
3419
+ old_ids = {
3420
+ int(getattr(entry, "cache_entry_id", 0) or 0)
3421
+ for entry in changed_old
3422
+ }
3423
+ groups = dict(state[2])
3424
+ for key in affected:
3425
+ groups[key] = tuple(
3426
+ entry for entry in groups.get(key, ())
3427
+ if int(getattr(entry, "cache_entry_id", 0) or 0) not in old_ids
3428
+ )
3429
+ for entry in changed_new:
3430
+ key = (str(entry.source_root_key), str(entry.project_key))
3431
+ groups[key] = (*groups.get(key, ()), entry)
3432
+ for key in affected:
3433
+ if groups.get(key):
3434
+ groups[key] = tuple(sorted(
3435
+ groups[key], key=_codex_incremental_entry_order,
3436
+ ))
3437
+ else:
3438
+ groups.pop(key, None)
3439
+ partial_entries = tuple(
3440
+ replace(entry, display_label=labels[str(entry.project_key)])
3441
+ for key in sorted(affected) for entry in groups.get(key, ())
3442
+ )
3443
+ result = build_codex_project_result(
3444
+ partial_entries,
3445
+ range_start=context.range_start,
3446
+ range_end=accounting_end,
3447
+ as_of=context.now_utc,
3448
+ )
3449
+ partial_rows = () if result.data is None else tuple({
3450
+ "key": dashboard_resource_key("project", "codex", row.project_key),
3451
+ "source": "codex",
3452
+ "label": row.display_label,
3453
+ "session_count": row.session_count,
3454
+ "first_seen": row.first_seen.astimezone(UTC).isoformat(),
3455
+ "last_seen": row.last_seen.astimezone(UTC).isoformat(),
3456
+ "cost_usd": row.totals.cost_usd,
3457
+ "input_tokens": row.totals.input_tokens,
3458
+ "cached_input_tokens": row.totals.cached_input_tokens,
3459
+ "output_tokens": row.totals.output_tokens,
3460
+ "reasoning_output_tokens": row.totals.reasoning_output_tokens,
3461
+ "total_tokens": row.totals.total_tokens,
3462
+ } for row in result.data.projects)
3463
+ affected_keys = {
3464
+ dashboard_resource_key("project", "codex", project_key)
3465
+ for _root_key, project_key in affected
3466
+ }
3467
+ rows = tuple(sorted(
3468
+ (
3469
+ *(row for row in state[1]["rows"] if row["key"] not in affected_keys),
3470
+ *partial_rows,
3471
+ ),
3472
+ key=lambda row: (
3473
+ float(row["cost_usd"]), str(row["label"]), str(row["key"]),
3474
+ ),
3475
+ reverse=True,
3476
+ ))
3477
+ value = {
3478
+ "rows": rows,
3479
+ "total_cost_usd": stable_sum(
3480
+ float(row["cost_usd"]) for row in rows),
3481
+ "total_tokens": sum(int(row["total_tokens"]) for row in rows),
3482
+ }
3483
+ _CODEX_PROJECT_WIRE_CACHE[cache_key] = (signature, value, groups)
3484
+ return value
3485
+
3486
+
2639
3487
  def _partial_projects_wire(
2640
3488
  entries: Iterable[object],
2641
3489
  metadata: Mapping[tuple[str, str], Mapping[str, object]],
@@ -2744,15 +3592,23 @@ def _partial_projects_wire(
2744
3592
  }
2745
3593
 
2746
3594
 
3595
+ _CODEX_ENTRY_ADAPTER_CACHE: dict[int, tuple[object, CodexEntry]] = {}
3596
+
3597
+
2747
3598
  def _codex_entries_from_accounting(entries: Iterable[object]) -> list[CodexEntry]:
2748
3599
  """Adapt coordinated accounting rows for the shipped non-project kernels."""
2749
3600
  converted: list[CodexEntry] = []
2750
3601
  for entry in entries:
3602
+ cache_entry_id = int(getattr(entry, "cache_entry_id", 0) or 0)
3603
+ cached = _CODEX_ENTRY_ADAPTER_CACHE.get(cache_entry_id)
3604
+ if cache_entry_id and cached is not None and cached[0] == entry:
3605
+ converted.append(cached[1])
3606
+ continue
2751
3607
  source_path = str(getattr(entry, "source_path", "") or "")
2752
3608
  session_id = str(getattr(entry, "session_id", "") or "")
2753
3609
  if not source_path or not session_id:
2754
3610
  raise SourceCapabilityUnavailable("Codex accounting lacks session identity")
2755
- converted.append(CodexEntry(
3611
+ value = CodexEntry(
2756
3612
  timestamp=getattr(entry, "timestamp"),
2757
3613
  session_id=session_id,
2758
3614
  model=str(getattr(entry, "model")),
@@ -2762,7 +3618,17 @@ def _codex_entries_from_accounting(entries: Iterable[object]) -> list[CodexEntry
2762
3618
  reasoning_output_tokens=int(getattr(entry, "reasoning_output_tokens")),
2763
3619
  total_tokens=int(getattr(entry, "total_tokens")),
2764
3620
  source_path=source_path,
2765
- ))
3621
+ cost_usd=(
3622
+ float(entry.cost_usd)
3623
+ if getattr(entry, "cost_usd", None) is not None else None
3624
+ ),
3625
+ cache_entry_id=cache_entry_id,
3626
+ source_root_key=str(getattr(entry, "source_root_key", "") or ""),
3627
+ conversation_key=str(getattr(entry, "conversation_key", "") or ""),
3628
+ )
3629
+ converted.append(value)
3630
+ if cache_entry_id:
3631
+ _CODEX_ENTRY_ADAPTER_CACHE[cache_entry_id] = (entry, value)
2766
3632
  return converted
2767
3633
 
2768
3634
 
@@ -2867,6 +3733,136 @@ def _build_codex_native_weekly_view(
2867
3733
  )
2868
3734
 
2869
3735
 
3736
+ _CODEX_WEEKLY_VIEW_CACHE: dict[object, tuple] = {}
3737
+
3738
+
3739
+ def _codex_weekly_period_for_entry(
3740
+ entry: object, periods: Iterable[CodexWeeklyPeriod],
3741
+ ) -> CodexWeeklyPeriod | None:
3742
+ if codex_model_scoped_quota_pool(getattr(entry, "model", None)) is not None:
3743
+ return None
3744
+ timestamp = getattr(entry, "timestamp").astimezone(UTC)
3745
+ root_key = str(getattr(entry, "source_root_key", "") or "")
3746
+ return next((
3747
+ period for period in periods
3748
+ if root_key in period.source_root_keys
3749
+ and period.start_at <= timestamp < period.end_at
3750
+ ), None)
3751
+
3752
+
3753
+ def _cached_codex_native_weekly_view(
3754
+ stats_conn: sqlite3.Connection,
3755
+ entries: Iterable[object],
3756
+ *,
3757
+ changed_old: Iterable[object],
3758
+ changed_new: Iterable[object],
3759
+ cache_key: object,
3760
+ semantic_signature: object,
3761
+ source_root_keys: Iterable[str],
3762
+ active_cycle: CodexCycleBoundary | None,
3763
+ now_utc: dt.datetime,
3764
+ display_tz_name: str | None,
3765
+ speed: str,
3766
+ account_key: str | None = None,
3767
+ include_account_keys: bool = False,
3768
+ ) -> CodexWeeklyView:
3769
+ """Rebuild only native quota periods touched by accounting changes."""
3770
+ values = tuple(entries)
3771
+ roots = tuple(source_root_keys)
3772
+ periods = _codex_weekly_periods(
3773
+ stats_conn,
3774
+ source_root_keys=roots,
3775
+ active_cycle=active_cycle,
3776
+ account_key=account_key,
3777
+ )
3778
+ signature = (
3779
+ semantic_signature, periods, roots, display_tz_name, speed,
3780
+ account_key, include_account_keys,
3781
+ )
3782
+ state = _CODEX_WEEKLY_VIEW_CACHE.get(cache_key)
3783
+ if state is None or state[0] != signature:
3784
+ view = _build_codex_native_weekly_view(
3785
+ stats_conn, values, source_root_keys=roots,
3786
+ active_cycle=active_cycle, now_utc=now_utc,
3787
+ display_tz_name=display_tz_name, speed=speed,
3788
+ account_key=account_key, include_account_keys=include_account_keys,
3789
+ )
3790
+ groups: dict[dt.datetime, list[object]] = {}
3791
+ for entry in values:
3792
+ period = _codex_weekly_period_for_entry(entry, periods)
3793
+ if period is not None:
3794
+ groups.setdefault(period.start_at, []).append(entry)
3795
+ _CODEX_WEEKLY_VIEW_CACHE[cache_key] = (
3796
+ signature, view,
3797
+ {key: tuple(group) for key, group in groups.items()},
3798
+ )
3799
+ return view
3800
+
3801
+ affected = {
3802
+ period.start_at
3803
+ for entry in (*tuple(changed_old), *tuple(changed_new))
3804
+ if (period := _codex_weekly_period_for_entry(entry, periods)) is not None
3805
+ }
3806
+ if not affected:
3807
+ return state[1]
3808
+
3809
+ prior = state[1]
3810
+ old_ids = {
3811
+ int(getattr(entry, "cache_entry_id", 0) or 0)
3812
+ for entry in changed_old
3813
+ }
3814
+ groups = dict(state[2])
3815
+ for start_at in affected:
3816
+ groups[start_at] = tuple(
3817
+ entry for entry in groups.get(start_at, ())
3818
+ if int(getattr(entry, "cache_entry_id", 0) or 0) not in old_ids
3819
+ )
3820
+ for entry in changed_new:
3821
+ period = _codex_weekly_period_for_entry(entry, periods)
3822
+ if period is not None:
3823
+ groups[period.start_at] = (*groups.get(period.start_at, ()), entry)
3824
+ for start_at in affected:
3825
+ if groups.get(start_at):
3826
+ groups[start_at] = tuple(sorted(
3827
+ groups[start_at], key=_codex_incremental_entry_order,
3828
+ ))
3829
+ else:
3830
+ groups.pop(start_at, None)
3831
+ replacements: dict[dt.datetime, object] = {}
3832
+ for start_at in affected:
3833
+ partial = _build_codex_native_weekly_view(
3834
+ stats_conn,
3835
+ groups.get(start_at, ()),
3836
+ source_root_keys=roots, active_cycle=active_cycle,
3837
+ now_utc=now_utc, display_tz_name=display_tz_name, speed=speed,
3838
+ account_key=account_key, include_account_keys=include_account_keys,
3839
+ )
3840
+ row = next((
3841
+ row for row in partial.rows
3842
+ if getattr(row, "period_start_at", None) == start_at
3843
+ ), None)
3844
+ if row is not None:
3845
+ replacements[start_at] = row
3846
+ rows = tuple(sorted(
3847
+ (
3848
+ *(row for row in prior.rows
3849
+ if getattr(row, "period_start_at", None) not in affected),
3850
+ *replacements.values(),
3851
+ ),
3852
+ key=lambda row: row.period_start_at,
3853
+ ))
3854
+ view = replace(
3855
+ prior,
3856
+ rows=rows,
3857
+ total_cost_usd=stable_sum(row.cost_usd for row in rows),
3858
+ total_tokens=sum(row.total_tokens for row in rows),
3859
+ period_start=(periods[0].start_at if periods else None),
3860
+ period_end=now_utc,
3861
+ )
3862
+ _CODEX_WEEKLY_VIEW_CACHE[cache_key] = (signature, view, groups)
3863
+ return view
3864
+
3865
+
2870
3866
  def _codex_account_five_hour_percent(
2871
3867
  observations: Iterable[object],
2872
3868
  now_utc: dt.datetime,
@@ -2913,8 +3909,9 @@ def _codex_accounts_wire(
2913
3909
  so the envelope stays byte-identical, spec R8). Each account carries
2914
3910
  ``{accountKey, label, plan, active, weeklyPercent, fiveHourPercent, resetsAt,
2915
3911
  spendUsd, inputTokens, cachedInputTokens, outputTokens,
2916
- reasoningOutputTokens, totalTokens, unattributed?}``; ``hero_cycles_wire`` is
2917
- the thin per-account cycle-boundary list the hero renders (``cycles[]``).
3912
+ reasoningOutputTokens, totalTokens, unattributed?, spendWindow?}``;
3913
+ ``hero_cycles_wire`` is the thin per-account cycle-boundary list the hero
3914
+ renders (``cycles[]``).
2918
3915
  """
2919
3916
  import _cctally_account
2920
3917
  active_keys = _cctally_account.resolve_active_account_keys()
@@ -2932,11 +3929,39 @@ def _codex_accounts_wire(
2932
3929
  reg = _cctally_account.load_accounts(context.stats_conn, "codex")
2933
3930
  plan_by_key = {r["account_key"]: r.get("plan_type") for r in reg}
2934
3931
  ordered_keys = [r["account_key"] for r in reg]
3932
+ # #564: a card with no live cycle is read over ONE native cycle width
3933
+ # ending at `now`, never the whole accounting range. The decorated hero is
3934
+ # the sum of these cards under a week label, so an addend spanning the full
3935
+ # ~30-day range put spend that label does not cover into the headline.
3936
+ #
3937
+ # The start comes from `now_utc`, NOT `accounting_end`: the latter is
3938
+ # `now + 1us`, an adapter that lets an inclusive-now surface call a
3939
+ # half-open reader, so subtracting the width from it would drop a row
3940
+ # landing exactly on the boundary while keeping one landing at `now`.
3941
+ fallback_start = max(
3942
+ accounting_start,
3943
+ context.now_utc - dt.timedelta(minutes=ACCOUNT_WEEKLY_WINDOW_MINUTES),
3944
+ )
3945
+ fallback_window = {
3946
+ "kind": "trailing-cycle",
3947
+ "startAt": fallback_start.astimezone(UTC).isoformat(),
3948
+ "endAt": context.now_utc.astimezone(UTC).isoformat(),
3949
+ }
2935
3950
  # Include unattributed last iff it has cycle/5h/spend evidence.
2936
3951
  unattributed_rows = load_cached_rooted_codex_accounting_entries(
2937
3952
  accounting_start, accounting_end, speed=context.speed,
2938
3953
  cache_conn=context.cache_conn, account_key=_lib_accounts.UNATTRIBUTED,
2939
3954
  )
3955
+ # Existence is decided over the accounting range so a sentinel holding only
3956
+ # older spend keeps its card; the totals below cover the bounded window, so
3957
+ # a resolved $0.00 is an honest empty state rather than an absence (#564).
3958
+ # The bounded set is a strict subset of the rows already loaded above, so it
3959
+ # is derived in memory rather than re-queried on every publish. `timestamp`
3960
+ # is normalized to UTC by the reader and the upper bound is already applied,
3961
+ # so the two predicates coincide.
3962
+ unattributed_window_rows = tuple(
3963
+ row for row in unattributed_rows if row.timestamp >= fallback_start
3964
+ )
2940
3965
  if (
2941
3966
  unattributed_rows
2942
3967
  or _lib_accounts.UNATTRIBUTED in cycle_by_account
@@ -2978,12 +4003,14 @@ def _codex_accounts_wire(
2978
4003
  )
2979
4004
  totals = _totals(rows)
2980
4005
  elif is_unattributed:
2981
- totals = _totals(unattributed_rows)
4006
+ totals = _totals(unattributed_window_rows)
2982
4007
  else:
2983
- # A real account without a live weekly cycle: totals over the
2984
- # accounting range so the card still shows spend (no bars/reset).
4008
+ # A real account without a live weekly cycle: totals over ONE
4009
+ # native cycle width ending now, so this card can be summed into a
4010
+ # week-labelled headline without overstating it (#564). No bars or
4011
+ # reset, because there is no live cycle to describe.
2985
4012
  rows = load_cached_rooted_codex_accounting_entries(
2986
- accounting_start, accounting_end, speed=context.speed,
4013
+ fallback_start, accounting_end, speed=context.speed,
2987
4014
  cache_conn=context.cache_conn, account_key=key,
2988
4015
  )
2989
4016
  totals = _totals(rows)
@@ -3010,6 +4037,12 @@ def _codex_accounts_wire(
3010
4037
  # cannot speak for a fresh sibling, and staleness is disclosure
3011
4038
  # only — the retained percentage, reset and spend remain useful.
3012
4039
  card["cycleFreshness"] = "stale"
4040
+ if is_unattributed or cyc is None:
4041
+ # The card's totals came from the bounded fallback rather than a
4042
+ # live cycle, so it publishes the exact window it covers. The client
4043
+ # reads this key and never infers the case from a null `resetsAt`,
4044
+ # which is true of several unrelated states (#564 D3).
4045
+ card["spendWindow"] = fallback_window
3013
4046
  accounts_wire.append(card)
3014
4047
  if cyc is not None and not is_unattributed:
3015
4048
  hero_cycles_wire.append({
@@ -3054,6 +4087,66 @@ def _codex_partition_by_account(
3054
4087
  return {key: tuple(values) for key, values in buckets.items()}
3055
4088
 
3056
4089
 
4090
+ def _codex_fold_visible_rows(
4091
+ entries: Iterable[object],
4092
+ ) -> "tuple[list[CodexEntry], dict[str, tuple[object, ...]], dict[str, tuple[CodexEntry, ...]]]":
4093
+ """One encounter-ordered pass producing the parent's and each account's rows.
4094
+
4095
+ #566 §5.1 item 2. Each visible row is adapted to a ``CodexEntry`` exactly
4096
+ once and then routed into the merged "All" list and into its owning
4097
+ account's list, instead of the parent converting the whole population and
4098
+ every child re-converting its own slice.
4099
+
4100
+ This removes exactly the four whole-population re-adaptations the children
4101
+ performed, worth about 0.4s of a profiled tick on the maintainer's store.
4102
+ It does NOT reduce the 191,225 total calls to
4103
+ ``_codex_entries_from_accounting`` that a build makes: 191,220 of them come
4104
+ from ``_build_codex_native_weekly_view``, which adapts one entry at a time
4105
+ per scope, and this fold does not touch that site.
4106
+
4107
+ Encounter order is preserved in every output, and the ordering matters:
4108
+ ``_aggregate_codex_buckets`` accumulates in encounter order and preserves
4109
+ first-seen model order, so routing through a set, or sorting, would move a
4110
+ bucket's ``models`` order for free. Adaptation is
4111
+ 1:1 and order-preserving, so each account's list is byte-identical to
4112
+ adapting that account's rows on their own — which is what makes the fold a
4113
+ reuse of work rather than a change to any builder's arithmetic. The
4114
+ shipped builders still run per scope, so the merged parent stays
4115
+ byte-identical BY CONSTRUCTION (#416 §5.2 review F9/F10).
4116
+ """
4117
+ rows = tuple(entries)
4118
+ all_entries = _codex_entries_from_accounting(rows)
4119
+ rows_by_account: dict[str, list[object]] = {}
4120
+ entries_by_account: dict[str, list[CodexEntry]] = {}
4121
+ for row, converted in zip(rows, all_entries):
4122
+ key = str(
4123
+ getattr(row, "account_key", "") or _lib_accounts.UNATTRIBUTED)
4124
+ rows_by_account.setdefault(key, []).append(row)
4125
+ entries_by_account.setdefault(key, []).append(converted)
4126
+ return (
4127
+ all_entries,
4128
+ {key: tuple(values) for key, values in rows_by_account.items()},
4129
+ {key: tuple(values) for key, values in entries_by_account.items()},
4130
+ )
4131
+
4132
+
4133
+ _CODEX_ACCOUNT_SCOPE_CACHE: dict[
4134
+ str, tuple[object, dict[str, object]]
4135
+ ] = {}
4136
+
4137
+
4138
+ def reset_codex_account_scope_cache() -> None:
4139
+ """Test/process reset for #582's immutable finalized account scopes."""
4140
+ _CODEX_ACCOUNT_SCOPE_CACHE.clear()
4141
+ _CODEX_ENTRY_ADAPTER_CACHE.clear()
4142
+ _CODEX_PROJECT_LABEL_CACHE.clear()
4143
+ _CODEX_PERIOD_VIEW_CACHE.clear()
4144
+ _CODEX_WEEKLY_VIEW_CACHE.clear()
4145
+ _CODEX_CACHE_REPORT_ROWS.clear()
4146
+ _CODEX_SESSION_VIEW_CACHE.clear()
4147
+ _CODEX_PROJECT_WIRE_CACHE.clear()
4148
+
4149
+
3057
4150
  def _codex_account_scopes_wire(
3058
4151
  context: DashboardReadContext,
3059
4152
  *,
@@ -3061,6 +4154,8 @@ def _codex_account_scopes_wire(
3061
4154
  quota_observations: Iterable[object],
3062
4155
  cycle_by_account: Mapping[str, "CodexCycleBoundary"],
3063
4156
  visible_accounting_entries: Iterable[object],
4157
+ visible_rows_by_account: "Mapping[str, tuple[object, ...]] | None" = None,
4158
+ visible_entries_by_account: "Mapping[str, tuple[CodexEntry, ...]] | None" = None,
3064
4159
  active_roots: Iterable[str],
3065
4160
  accounting_end: dt.datetime,
3066
4161
  metadata_incomplete: bool,
@@ -3071,6 +4166,12 @@ def _codex_account_scopes_wire(
3071
4166
  budget_cost_events_by_account: Mapping[str, tuple[tuple[dt.datetime, float], ...]],
3072
4167
  private_session_labels: dict[str, str],
3073
4168
  hero_failure: bool = False,
4169
+ dirty_accounts: Iterable[str] = (),
4170
+ scope_signature: object | None = None,
4171
+ changed_old_by_account: Mapping[str, tuple[CodexEntry, ...]] | None = None,
4172
+ changed_new_by_account: Mapping[str, tuple[CodexEntry, ...]] | None = None,
4173
+ changed_old_rows_by_account: Mapping[str, tuple[object, ...]] | None = None,
4174
+ changed_new_rows_by_account: Mapping[str, tuple[object, ...]] | None = None,
3074
4175
  ) -> dict[str, dict[str, object]]:
3075
4176
  """The per-account CHILDREN of the merged Codex read model (spec §5.3).
3076
4177
 
@@ -3101,7 +4202,15 @@ def _codex_account_scopes_wire(
3101
4202
  """
3102
4203
  visible = tuple(visible_accounting_entries)
3103
4204
  observations = tuple(quota_observations)
3104
- partition = _codex_partition_by_account(visible)
4205
+ # #566 §5.1 item 2: the caller folded the visible rows once and hands both
4206
+ # partitions down. Re-deriving them here is retained only for direct
4207
+ # callers (tests, the source-detail reader) that have no fold to share.
4208
+ if visible_rows_by_account is None or visible_entries_by_account is None:
4209
+ _all, visible_rows_by_account, visible_entries_by_account = (
4210
+ _codex_fold_visible_rows(visible)
4211
+ )
4212
+ partition = visible_rows_by_account
4213
+ entries_partition = visible_entries_by_account
3105
4214
  obs_partition: dict[str, list[object]] = {}
3106
4215
  for observation in observations:
3107
4216
  obs_partition.setdefault(
@@ -3110,19 +4219,29 @@ def _codex_account_scopes_wire(
3110
4219
  budget_rows = tuple(budget_milestones)
3111
4220
  projected_rows = tuple(projected_budget_milestones)
3112
4221
  roots = tuple(active_roots)
4222
+ dirty_account_keys = {str(key) for key in dirty_accounts}
4223
+ changed_old_by_account = changed_old_by_account or {}
4224
+ changed_new_by_account = changed_new_by_account or {}
4225
+ changed_old_rows_by_account = changed_old_rows_by_account or {}
4226
+ changed_new_rows_by_account = changed_new_rows_by_account or {}
3113
4227
 
3114
4228
  def _for_account(key: str) -> dict[str, object]:
3115
4229
  rows = partition.get(key, ())
3116
4230
  account_observations = tuple(obs_partition.get(key, ()))
3117
- entries = _codex_entries_from_accounting(rows)
4231
+ entries = list(entries_partition.get(key, ()))
3118
4232
  cycle = cycle_by_account.get(key)
3119
4233
  sessions_view = (
3120
4234
  build_rooted_codex_session_view(
3121
4235
  rows, now_utc=context.now_utc,
3122
4236
  tz_name=context.display_tz_name, speed=context.speed,
3123
4237
  )
3124
- if metadata_incomplete else build_codex_session_view(
3125
- entries, now_utc=context.now_utc,
4238
+ if metadata_incomplete else _cached_codex_session_view(
4239
+ entries,
4240
+ changed_old=changed_old_rows_by_account.get(key, ()),
4241
+ changed_new=changed_new_rows_by_account.get(key, ()),
4242
+ cache_key=("account", key),
4243
+ semantic_signature=scope_signature,
4244
+ now_utc=context.now_utc,
3126
4245
  tz_name=context.display_tz_name, speed=context.speed,
3127
4246
  )
3128
4247
  )
@@ -3165,16 +4284,31 @@ def _codex_account_scopes_wire(
3165
4284
  # quota window with no spend yet, and a retired one the reverse.
3166
4285
  "is_empty": not rows and not account_observations,
3167
4286
  "periods": {
3168
- "daily": _period_wire(build_codex_daily_view(
3169
- entries, now_utc=context.now_utc,
3170
- tz_name=context.display_tz_name, speed=context.speed,
4287
+ "daily": _period_wire(_cached_codex_period_view(
4288
+ entries,
4289
+ changed_old=changed_old_by_account.get(key, ()),
4290
+ changed_new=changed_new_by_account.get(key, ()),
4291
+ kind="daily", cache_key=("account", key),
4292
+ semantic_signature=scope_signature,
4293
+ now_utc=context.now_utc, tz_name=context.display_tz_name,
4294
+ speed=context.speed,
3171
4295
  )),
3172
- "monthly": _period_wire(build_codex_monthly_view(
3173
- entries, now_utc=context.now_utc,
3174
- tz_name=context.display_tz_name, speed=context.speed,
4296
+ "monthly": _period_wire(_cached_codex_period_view(
4297
+ entries,
4298
+ changed_old=changed_old_by_account.get(key, ()),
4299
+ changed_new=changed_new_by_account.get(key, ()),
4300
+ kind="monthly", cache_key=("account", key),
4301
+ semantic_signature=scope_signature,
4302
+ now_utc=context.now_utc, tz_name=context.display_tz_name,
4303
+ speed=context.speed,
3175
4304
  )),
3176
- "weekly": _period_wire(_build_codex_native_weekly_view(
3177
- context.stats_conn, rows, source_root_keys=roots,
4305
+ "weekly": _period_wire(_cached_codex_native_weekly_view(
4306
+ context.stats_conn, rows,
4307
+ changed_old=changed_old_rows_by_account.get(key, ()),
4308
+ changed_new=changed_new_rows_by_account.get(key, ()),
4309
+ cache_key=("account", key),
4310
+ semantic_signature=scope_signature,
4311
+ source_root_keys=roots,
3178
4312
  active_cycle=cycle, now_utc=context.now_utc,
3179
4313
  display_tz_name=context.display_tz_name, speed=context.speed,
3180
4314
  account_key=key,
@@ -3186,18 +4320,26 @@ def _codex_account_scopes_wire(
3186
4320
  ),
3187
4321
  "projects": (
3188
4322
  _partial_projects_wire(rows, conversation_metadata)
3189
- if metadata_incomplete else _projects_wire(
4323
+ if metadata_incomplete else _cached_projects_wire(
3190
4324
  context, account_observations, rows,
4325
+ changed_old=changed_old_rows_by_account.get(key, ()),
4326
+ changed_new=changed_new_rows_by_account.get(key, ()),
3191
4327
  accounting_end=accounting_end,
4328
+ cache_key=("account", key),
4329
+ semantic_signature=scope_signature,
3192
4330
  )
3193
4331
  ),
3194
4332
  "cache_report": _codex_cache_report_wire(
3195
4333
  rows, metadata=conversation_metadata, now_utc=context.now_utc,
3196
4334
  display_tz_name=context.display_tz_name, speed=context.speed,
3197
4335
  anomaly_threshold_pp=context.cache_report_anomaly_threshold_pp,
4336
+ cache_key=("account", key),
4337
+ changed_old=changed_old_rows_by_account.get(key, ()),
4338
+ changed_new=changed_new_rows_by_account.get(key, ()),
4339
+ semantic_signature=scope_signature,
3198
4340
  ),
3199
4341
  "budget": {
3200
- "status": _configured_codex_budget_status(
4342
+ **_codex_budget_status_domain(
3201
4343
  context, rows,
3202
4344
  cost_events=budget_cost_events_by_account.get(key, ()),
3203
4345
  account_key=key,
@@ -3237,7 +4379,52 @@ def _codex_account_scopes_wire(
3237
4379
  (set(partition) | set(obs_partition) | _codex_block_account_keys(
3238
4380
  context.stats_conn, roots)) - set(ordered_keys)
3239
4381
  )
3240
- return {key: _for_account(key) for key in ordered_keys + residual_keys}
4382
+ result: dict[str, dict[str, object]] = {}
4383
+ live_keys = ordered_keys + residual_keys
4384
+ for key in live_keys:
4385
+ account_observations = tuple(obs_partition.get(key, ()))
4386
+ account_metadata = tuple(
4387
+ (identity, conversation_metadata.get(identity))
4388
+ for identity in sorted({
4389
+ (
4390
+ str(getattr(row, "source_root_key", "")),
4391
+ str(getattr(row, "source_path", "")),
4392
+ )
4393
+ for row in partition.get(key, ())
4394
+ })
4395
+ )
4396
+ signature = (
4397
+ scope_signature,
4398
+ account_observations,
4399
+ cycle_by_account.get(key),
4400
+ account_metadata,
4401
+ tuple(_codex_account_scoped_rows(alert_rows, key)),
4402
+ tuple(_codex_account_scoped_rows(budget_rows, key)),
4403
+ tuple(_codex_account_scoped_rows(projected_rows, key)),
4404
+ budget_cost_events_by_account.get(key, ()),
4405
+ context.codex_budget,
4406
+ context.codex_quota_actual_thresholds,
4407
+ context.codex_quota_projected_thresholds,
4408
+ context.cache_report_anomaly_threshold_pp,
4409
+ metadata_incomplete,
4410
+ hero_failure,
4411
+ )
4412
+ cached = _CODEX_ACCOUNT_SCOPE_CACHE.get(key)
4413
+ if (
4414
+ scope_signature is not None
4415
+ and key not in dirty_account_keys
4416
+ and cached is not None
4417
+ and cached[0] == signature
4418
+ ):
4419
+ result[key] = cached[1]
4420
+ continue
4421
+ value = _for_account(key)
4422
+ result[key] = value
4423
+ if scope_signature is not None:
4424
+ _CODEX_ACCOUNT_SCOPE_CACHE[key] = (signature, value)
4425
+ for stale_key in set(_CODEX_ACCOUNT_SCOPE_CACHE) - set(live_keys):
4426
+ _CODEX_ACCOUNT_SCOPE_CACHE.pop(stale_key, None)
4427
+ return result
3241
4428
 
3242
4429
 
3243
4430
  def _codex_block_account_keys(
@@ -3418,13 +4605,61 @@ def build_codex_source_state(
3418
4605
  No sync, rollout scan, CLI parser, or fallback is reachable from this
3419
4606
  adapter. Period and session arithmetic remains delegated to the shipped
3420
4607
  S3 view kernels, preserving the CLI's inclusive-token vocabulary.
4608
+
4609
+ The whole read runs under ONE ``codex_path_scope`` (#566 §5.1 item 1), so
4610
+ the merged parent view and every per-account child share a single session
4611
+ root resolution and a single parse per distinct session file. The scope is
4612
+ opened here rather than further out because this is the boundary that owns
4613
+ every Codex session view in the build, and it is discarded when the read
4614
+ returns.
3421
4615
  """
4616
+ # This memo deduplicates the several account/parent consumers inside ONE
4617
+ # coordinated source build. It may not cross that boundary: a caller can
4618
+ # deliberately request a fresh build after stats/account decoration changes
4619
+ # without advancing cache.db's quota ledger, and the established contract
4620
+ # requires one bounded physical load for that new build.
4621
+ reset_codex_quota_observation_cache()
4622
+ caches = (
4623
+ _CODEX_QUOTA_OBSERVATION_CACHE,
4624
+ _CODEX_PERIOD_VIEW_CACHE,
4625
+ _CODEX_CACHE_REPORT_ROWS,
4626
+ _CODEX_SESSION_VIEW_CACHE,
4627
+ _CODEX_PROJECT_LABEL_CACHE,
4628
+ _CODEX_PROJECT_WIRE_CACHE,
4629
+ _CODEX_ENTRY_ADAPTER_CACHE,
4630
+ _CODEX_WEEKLY_VIEW_CACHE,
4631
+ _CODEX_ACCOUNT_SCOPE_CACHE,
4632
+ )
4633
+ cache_checkpoint = tuple(dict(cache) for cache in caches)
4634
+ accounting_checkpoint = (
4635
+ _lib_snapshot_cache.checkpoint_codex_accounting_cache_state()
4636
+ )
4637
+ try:
4638
+ with codex_path_scope() as path_scope:
4639
+ return _build_codex_source_state(
4640
+ context, data_version=data_version, path_scope=path_scope,
4641
+ )
4642
+ except Exception:
4643
+ for cache, prior in zip(caches, cache_checkpoint):
4644
+ cache.clear()
4645
+ cache.update(prior)
4646
+ _lib_snapshot_cache.restore_codex_accounting_cache_state(
4647
+ accounting_checkpoint)
4648
+ raise
4649
+
4650
+
4651
+ def _build_codex_source_state(
4652
+ context: DashboardReadContext,
4653
+ *,
4654
+ data_version: str,
4655
+ path_scope: object,
4656
+ ) -> SourceDashboardState:
3422
4657
  active_roots = tuple(sorted(
3423
4658
  str(row[0]) for row in context.cache_conn.execute(
3424
4659
  "SELECT source_root_key FROM codex_source_roots"
3425
4660
  )
3426
4661
  ))
3427
- quota_observations = load_codex_quota_observations(
4662
+ quota_observations = _cached_codex_quota_observations(
3428
4663
  source_root_keys=active_roots,
3429
4664
  cache_conn=context.cache_conn,
3430
4665
  captured_at_or_after=(
@@ -3445,8 +4680,23 @@ def build_codex_source_state(
3445
4680
  accounting_end = context.now_utc + dt.timedelta(microseconds=1)
3446
4681
  accounting_start = context.range_start
3447
4682
  if context.codex_budget is not None:
3448
- _period, budget_start, _budget_end = _configured_codex_budget_window(context)
3449
- accounting_start = min(accounting_start, budget_start)
4683
+ # #556 S5 Unit 2 (Unit 1 review R6, widened) — this is the SECOND
4684
+ # unguarded call to the window resolver, and unlike
4685
+ # `_codex_budget_cost_events` it had no boundary of its own. It sits
4686
+ # outside every other `try` in this function, so an unresolvable window
4687
+ # escaped into `_tui_build_source_bundle`'s `source_build_failed`
4688
+ # handler and destroyed the entire Codex provider's data — the exact
4689
+ # failure §3.5 exists to prevent, reached from a different line.
4690
+ #
4691
+ # Degrading to the un-widened accounting range cannot publish a false
4692
+ # figure: the same failure reaches `_codex_budget_status_domain`, which
4693
+ # nulls the status and names `budget_compute_failed`.
4694
+ try:
4695
+ _period, budget_start, _budget_end = _configured_codex_budget_window(context)
4696
+ except Exception:
4697
+ _warn_codex_budget_window_once("accounting_range")
4698
+ else:
4699
+ accounting_start = min(accounting_start, budget_start)
3450
4700
  health = load_codex_project_metadata_health(
3451
4701
  cache_conn=context.cache_conn,
3452
4702
  start=accounting_start,
@@ -3461,15 +4711,51 @@ def build_codex_source_state(
3461
4711
  "run `cctally cache-sync --source codex --rebuild`."
3462
4712
  )
3463
4713
  qualified_entries: tuple[object, ...] = ()
4714
+ accounting_dirty_accounts: tuple[str, ...] = ()
4715
+ accounting_changed_old: tuple[object, ...] = ()
4716
+ accounting_changed_new: tuple[object, ...] = ()
3464
4717
  if not metadata_incomplete:
3465
4718
  try:
3466
- qualified_entries = load_qualified_codex_entries(
3467
- accounting_start,
3468
- accounting_end,
3469
- speed=context.speed,
3470
- sync=False,
4719
+ cached_accounting = _lib_snapshot_cache.build_cached_codex_accounting(
3471
4720
  cache_conn=context.cache_conn,
4721
+ range_start=accounting_start,
4722
+ range_end=accounting_end,
4723
+ extra_signature=(
4724
+ context.speed,
4725
+ tuple(str(root) for root in path_scope.roots),
4726
+ active_roots,
4727
+ ),
4728
+ load_all=lambda: load_qualified_codex_entries(
4729
+ accounting_start,
4730
+ accounting_end,
4731
+ speed=context.speed,
4732
+ sync=False,
4733
+ cache_conn=context.cache_conn,
4734
+ ),
4735
+ load_paths=lambda identities: load_qualified_codex_entries(
4736
+ accounting_start,
4737
+ accounting_end,
4738
+ speed=context.speed,
4739
+ sync=False,
4740
+ cache_conn=context.cache_conn,
4741
+ source_identities=identities,
4742
+ ),
4743
+ path_of=lambda entry: (
4744
+ str(entry.source_root_key), str(entry.source_path),
4745
+ ),
4746
+ account_of=lambda entry: str(entry.account_key),
4747
+ order_key=lambda entry: (
4748
+ entry.timestamp,
4749
+ str(entry.source_root_key),
4750
+ str(entry.conversation_key),
4751
+ int(entry.cache_entry_id),
4752
+ ),
4753
+ identity_of=lambda entry: int(entry.cache_entry_id),
3472
4754
  )
4755
+ qualified_entries = cached_accounting.entries
4756
+ accounting_dirty_accounts = cached_accounting.dirty_accounts
4757
+ accounting_changed_old = cached_accounting.changed_old
4758
+ accounting_changed_new = cached_accounting.changed_new
3473
4759
  accounting_entries: tuple[object, ...] = qualified_entries
3474
4760
  except QualifiedMetadataUnavailable:
3475
4761
  # A cached read must be internally coherent, but retain accounting
@@ -3478,19 +4764,31 @@ def build_codex_source_state(
3478
4764
  "Codex qualified metadata read became unavailable; using cache-only accounting fallback"
3479
4765
  )
3480
4766
  metadata_incomplete = True
4767
+ _lib_snapshot_cache.reset_codex_accounting_cache_state()
3481
4768
  accounting_entries = load_cached_rooted_codex_accounting_entries(
3482
4769
  accounting_start,
3483
4770
  accounting_end,
3484
4771
  speed=context.speed,
3485
4772
  cache_conn=context.cache_conn,
3486
4773
  )
4774
+ accounting_dirty_accounts = tuple(sorted({
4775
+ str(getattr(entry, "account_key", "") or
4776
+ _lib_accounts.UNATTRIBUTED)
4777
+ for entry in accounting_entries
4778
+ }))
3487
4779
  else:
4780
+ _lib_snapshot_cache.reset_codex_accounting_cache_state()
3488
4781
  accounting_entries = load_cached_rooted_codex_accounting_entries(
3489
4782
  accounting_start,
3490
4783
  accounting_end,
3491
4784
  speed=context.speed,
3492
4785
  cache_conn=context.cache_conn,
3493
4786
  )
4787
+ accounting_dirty_accounts = tuple(sorted({
4788
+ str(getattr(entry, "account_key", "") or
4789
+ _lib_accounts.UNATTRIBUTED)
4790
+ for entry in accounting_entries
4791
+ }))
3494
4792
  budget_entries = _codex_entries_from_accounting(accounting_entries)
3495
4793
  cycles_all: list[CodexCycleBoundary] = []
3496
4794
  try:
@@ -3535,12 +4833,66 @@ def build_codex_source_state(
3535
4833
  entry for entry in accounting_entries
3536
4834
  if context.range_start <= getattr(entry, "timestamp").astimezone(UTC) < accounting_end
3537
4835
  )
3538
- entries = _codex_entries_from_accounting(visible_accounting_entries)
3539
- daily = build_codex_daily_view(
3540
- entries, now_utc=context.now_utc, tz_name=context.display_tz_name, speed=context.speed,
4836
+ # #566 §5.1 item 2: one pass over the visible rows produces the merged
4837
+ # population and both per-account partitions. The children below reuse
4838
+ # these instead of re-partitioning and re-adapting the same rows.
4839
+ entries, visible_rows_by_account, visible_entries_by_account = (
4840
+ _codex_fold_visible_rows(visible_accounting_entries)
4841
+ )
4842
+ changed_old_visible = tuple(
4843
+ entry for entry in accounting_changed_old
4844
+ if context.range_start <= entry.timestamp.astimezone(UTC) < accounting_end
4845
+ )
4846
+ changed_new_visible = tuple(
4847
+ entry for entry in accounting_changed_new
4848
+ if context.range_start <= entry.timestamp.astimezone(UTC) < accounting_end
4849
+ )
4850
+ changed_old_entries = tuple(_codex_entries_from_accounting(changed_old_visible))
4851
+ changed_new_entries = tuple(_codex_entries_from_accounting(changed_new_visible))
4852
+
4853
+ def _changed_by_account(rows, converted):
4854
+ grouped: dict[str, list[CodexEntry]] = {}
4855
+ for row, entry in zip(rows, converted):
4856
+ key = str(getattr(row, "account_key", "") or
4857
+ _lib_accounts.UNATTRIBUTED)
4858
+ grouped.setdefault(key, []).append(entry)
4859
+ return {key: tuple(values) for key, values in grouped.items()}
4860
+
4861
+ changed_old_by_account = _changed_by_account(
4862
+ changed_old_visible, changed_old_entries)
4863
+ changed_new_by_account = _changed_by_account(
4864
+ changed_new_visible, changed_new_entries)
4865
+
4866
+ def _changed_rows_by_account(rows):
4867
+ grouped: dict[str, list[object]] = {}
4868
+ for row in rows:
4869
+ key = str(getattr(row, "account_key", "") or
4870
+ _lib_accounts.UNATTRIBUTED)
4871
+ grouped.setdefault(key, []).append(row)
4872
+ return {key: tuple(values) for key, values in grouped.items()}
4873
+
4874
+ changed_old_rows_by_account = _changed_rows_by_account(changed_old_visible)
4875
+ changed_new_rows_by_account = _changed_rows_by_account(changed_new_visible)
4876
+ # The published provider version also carries quota/stat generations.
4877
+ # Those generations must rebuild quota domains, but they are not accounting
4878
+ # semantics: folding them into these cache keys made one fresh quota sample
4879
+ # discard every clean period/session/project/account group. The accounting
4880
+ # population cache above owns upper-bound, root and speed invalidation; the
4881
+ # individual builders add their own tz/speed/period/cycle dimensions.
4882
+ period_signature = (
4883
+ "codex-accounting-v1", context.range_start, metadata_incomplete,
4884
+ )
4885
+ daily = _cached_codex_period_view(
4886
+ entries, changed_old=changed_old_entries,
4887
+ changed_new=changed_new_entries, kind="daily", cache_key=("parent",),
4888
+ semantic_signature=period_signature, now_utc=context.now_utc,
4889
+ tz_name=context.display_tz_name, speed=context.speed,
3541
4890
  )
3542
- monthly = build_codex_monthly_view(
3543
- entries, now_utc=context.now_utc, tz_name=context.display_tz_name, speed=context.speed,
4891
+ monthly = _cached_codex_period_view(
4892
+ entries, changed_old=changed_old_entries,
4893
+ changed_new=changed_new_entries, kind="monthly", cache_key=("parent",),
4894
+ semantic_signature=period_signature, now_utc=context.now_utc,
4895
+ tz_name=context.display_tz_name, speed=context.speed,
3544
4896
  )
3545
4897
  # R8 gate, resolved once before the parent weekly projection so that only
3546
4898
  # a decorated merged row gains the additive account axis. Focused children
@@ -3551,9 +4903,13 @@ def build_codex_source_state(
3551
4903
  context.stats_conn, "codex")
3552
4904
  except Exception:
3553
4905
  _codex_decorated = False
3554
- weekly = _build_codex_native_weekly_view(
4906
+ weekly = _cached_codex_native_weekly_view(
3555
4907
  context.stats_conn,
3556
4908
  visible_accounting_entries,
4909
+ changed_old=changed_old_visible,
4910
+ changed_new=changed_new_visible,
4911
+ cache_key=("parent",),
4912
+ semantic_signature=period_signature,
3557
4913
  source_root_keys=active_roots,
3558
4914
  active_cycle=cycle,
3559
4915
  now_utc=context.now_utc,
@@ -3568,8 +4924,11 @@ def build_codex_source_state(
3568
4924
  tz_name=context.display_tz_name,
3569
4925
  speed=context.speed,
3570
4926
  )
3571
- if metadata_incomplete else build_codex_session_view(
3572
- entries, now_utc=context.now_utc, tz_name=context.display_tz_name, speed=context.speed,
4927
+ if metadata_incomplete else _cached_codex_session_view(
4928
+ entries, changed_old=changed_old_visible,
4929
+ changed_new=changed_new_visible, cache_key=("parent",),
4930
+ semantic_signature=period_signature, now_utc=context.now_utc,
4931
+ tz_name=context.display_tz_name, speed=context.speed,
3573
4932
  )
3574
4933
  )
3575
4934
  quota = _quota_read_model(
@@ -3611,9 +4970,10 @@ def build_codex_source_state(
3611
4970
  projected_budget_rows = _projected_budget_wire(
3612
4971
  context.stats_conn, decorated=_codex_decorated)
3613
4972
  budget_cost_events = _codex_budget_cost_events(context, budget_entries)
3614
- configured_budget = _configured_codex_budget_status(
4973
+ configured_budget_domain = _codex_budget_status_domain(
3615
4974
  context, budget_entries, cost_events=budget_cost_events,
3616
4975
  )
4976
+ configured_budget = configured_budget_domain["status"]
3617
4977
  conversation_metadata = _codex_conversation_metadata(context.cache_conn)
3618
4978
  cache_report = _codex_cache_report_wire(
3619
4979
  visible_accounting_entries,
@@ -3622,14 +4982,22 @@ def build_codex_source_state(
3622
4982
  display_tz_name=context.display_tz_name,
3623
4983
  speed=context.speed,
3624
4984
  anomaly_threshold_pp=context.cache_report_anomaly_threshold_pp,
4985
+ cache_key=("parent",),
4986
+ changed_old=changed_old_visible,
4987
+ changed_new=changed_new_visible,
4988
+ semantic_signature=period_signature,
3625
4989
  )
3626
4990
  projects = (
3627
4991
  _partial_projects_wire(visible_accounting_entries, conversation_metadata)
3628
- if metadata_incomplete else _projects_wire(
4992
+ if metadata_incomplete else _cached_projects_wire(
3629
4993
  context,
3630
4994
  quota_observations,
3631
4995
  visible_accounting_entries,
4996
+ changed_old=changed_old_visible,
4997
+ changed_new=changed_new_visible,
3632
4998
  accounting_end=accounting_end,
4999
+ cache_key=("parent",),
5000
+ semantic_signature=period_signature,
3633
5001
  )
3634
5002
  )
3635
5003
  alerts = _alerts_wire(context.stats_conn, decorated=_codex_decorated)
@@ -3677,6 +5045,9 @@ def build_codex_source_state(
3677
5045
  accounts_wire: list[dict[str, object]] = []
3678
5046
  hero_cycles_wire: list[dict[str, object]] = []
3679
5047
  account_scopes: dict[str, dict[str, object]] = {}
5048
+ # #556 S5 §3.8: bound OUTSIDE the try, because the degrade path below has to
5049
+ # be able to clear it, and the retained `clock_data` reads it either way.
5050
+ budget_events_by_account: dict[str, tuple[tuple[dt.datetime, float], ...]] = {}
3680
5051
  if _codex_decorated:
3681
5052
  try:
3682
5053
  accounts_wire, hero_cycles_wire = _codex_accounts_wire(
@@ -3704,17 +5075,19 @@ def build_codex_source_state(
3704
5075
  # Budget cost events are frozen per account over the CONFIGURED
3705
5076
  # budget window, which can start before `range_start` — so they come
3706
5077
  # from the full `accounting_entries`, not the visible slice.
3707
- budget_events_by_account = {
5078
+ budget_events_by_account = ({
3708
5079
  key: _codex_budget_cost_events(context, rows)
3709
5080
  for key, rows in _codex_partition_by_account(
3710
5081
  accounting_entries).items()
3711
- } if context.codex_budget is not None else {}
5082
+ } if context.codex_budget is not None else {})
3712
5083
  account_scopes = _codex_account_scopes_wire(
3713
5084
  context,
3714
5085
  account_keys=[str(card["accountKey"]) for card in accounts_wire],
3715
5086
  quota_observations=quota_observations,
3716
5087
  cycle_by_account=cycle_by_account,
3717
5088
  visible_accounting_entries=visible_accounting_entries,
5089
+ visible_rows_by_account=visible_rows_by_account,
5090
+ visible_entries_by_account=visible_entries_by_account,
3718
5091
  active_roots=active_roots,
3719
5092
  accounting_end=accounting_end,
3720
5093
  metadata_incomplete=metadata_incomplete,
@@ -3725,6 +5098,17 @@ def build_codex_source_state(
3725
5098
  budget_cost_events_by_account=budget_events_by_account,
3726
5099
  private_session_labels=private_session_labels,
3727
5100
  hero_failure=hero_failure,
5101
+ dirty_accounts=accounting_dirty_accounts,
5102
+ changed_old_by_account=changed_old_by_account,
5103
+ changed_new_by_account=changed_new_by_account,
5104
+ changed_old_rows_by_account=changed_old_rows_by_account,
5105
+ changed_new_rows_by_account=changed_new_rows_by_account,
5106
+ # Quota/stat generations are already represented by each
5107
+ # child's quota observations, cycle and alert/budget rows in
5108
+ # `_codex_account_scopes_wire`'s outer signature. Reuse the
5109
+ # accounting-only semantic key here so an unrelated account's
5110
+ # fresh quota sample cannot evict every clean child.
5111
+ scope_signature=period_signature,
3728
5112
  )
3729
5113
  # #416 QA P1-A — the "All accounts" Blocks panel is the UNION of
3730
5114
  # every account's 5-hour blocks. `_quota_wire` filters
@@ -3787,6 +5171,7 @@ def build_codex_source_state(
3787
5171
  accounts_wire = []
3788
5172
  hero_cycles_wire = []
3789
5173
  account_scopes = {}
5174
+ budget_events_by_account = {}
3790
5175
  # #416 QA P0-A — the "All accounts" headline is the MERGED spend and tokens
3791
5176
  # (spec §6, decision D6). Everything above resolves the hero from ONE
3792
5177
  # representative cycle (`cycles_all[0]` plus that cycle's own
@@ -3800,11 +5185,11 @@ def build_codex_source_state(
3800
5185
  # blanks them with a pointer to the cards. The merge is a SUM OF THE CARDS
3801
5186
  # rather than a fresh query, so the headline can never disagree with the
3802
5187
  # strip it sits above (an account without a live cycle contributes exactly
3803
- # what its own card shows, over the accounting range — the card's documented
3804
- # fallback). Gated on `_codex_decorated`, so a <=1-real-account install
3805
- # keeps the single-cycle hero byte-for-byte (R8); gated on `hero_failure`,
3806
- # so an unavailable hero stays unavailable rather than gaining totals the
3807
- # rest of the envelope says are absent.
5188
+ # what its own card shows, over the bounded fallback window that card
5189
+ # publishes — #564). Gated on `_codex_decorated`, so a <=1-real-account
5190
+ # install keeps the single-cycle hero byte-for-byte (R8); gated on
5191
+ # `hero_failure`, so an unavailable hero stays unavailable rather than
5192
+ # gaining totals the rest of the envelope says are absent.
3808
5193
  if _codex_decorated and accounts_wire and not hero_failure:
3809
5194
  cycle_cost_usd = stable_sum(
3810
5195
  float(card["spendUsd"]) for card in accounts_wire)
@@ -3893,7 +5278,7 @@ def build_codex_source_state(
3893
5278
  "sessions": sessions_wire,
3894
5279
  "quota": quota,
3895
5280
  "budget": {
3896
- "status": configured_budget,
5281
+ **configured_budget_domain,
3897
5282
  "milestones": budget_rows,
3898
5283
  "projected": projected_budget_rows,
3899
5284
  },
@@ -3932,6 +5317,11 @@ def build_codex_source_state(
3932
5317
  },
3933
5318
  clock_data={
3934
5319
  "codex_budget_cost_events": budget_cost_events,
5320
+ # #556 S5 §3.8: the per-account tuples were computed at build and
5321
+ # discarded, so idle refresh had nothing to reclock a child budget
5322
+ # from. Empty for every undecorated install, which keeps the
5323
+ # retained carrier byte-neutral there.
5324
+ "codex_budget_cost_events_by_account": budget_events_by_account,
3935
5325
  # #350 spec §3.3: when the tick passes this instant it must rebuild
3936
5326
  # Codex authoritatively instead of idle-clocking or reusing, because
3937
5327
  # weekly-cycle resolution can change on identical frozen evidence.