cctally 1.98.0 → 1.99.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
 
@@ -1780,35 +2387,75 @@ def _configured_codex_budget_status(
1780
2387
  return sum(cost for timestamp, cost in resolved_events if start <= timestamp < end)
1781
2388
 
1782
2389
  recent_start = max(start_at, context.now_utc - dt.timedelta(hours=24))
1783
- inputs = c.BudgetInputs(
1784
- target_usd=float(amount_usd),
2390
+ # #556 S5 §3.1/§3.3: ONE producer of the wire status, shared with Claude.
2391
+ return c.budget_status_payload(
2392
+ period=period,
2393
+ window_start_at=start_at,
2394
+ window_end_at=end_at,
2395
+ target_usd=amount_usd,
1785
2396
  spent_usd=_sum_cost(start_at, context.now_utc),
1786
2397
  recent_24h_usd=_sum_cost(recent_start, context.now_utc),
1787
- week_start_at=start_at,
1788
- week_end_at=end_at,
1789
2398
  now=context.now_utc,
1790
- alert_thresholds=tuple(config["alert_thresholds"]),
2399
+ alert_thresholds=config["alert_thresholds"],
1791
2400
  )
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
- }
2401
+
2402
+
2403
+ def _codex_budget_status_domain(
2404
+ context: DashboardReadContext,
2405
+ entries: Iterable[object],
2406
+ *,
2407
+ cost_events: tuple[tuple[dt.datetime, float], ...] | None = None,
2408
+ account_key: str | None = None,
2409
+ ) -> dict[str, object]:
2410
+ """The Codex budget domain's status half, with a LOCAL failure contract.
2411
+
2412
+ #556 S5 §3.5. ``status`` stays required-and-nullable, which is the shape
2413
+ Codex's enclosing domain has always emitted and which this session does not
2414
+ normalise. What is new is the boundary: a computation that raises now
2415
+ degrades this one domain and names its reason, following the same
2416
+ ``{code, message, provider}`` shape the Claude half uses, instead of
2417
+ escaping into the provider-level handler and turning the whole Codex source
2418
+ into ``source_build_failed``.
2419
+
2420
+ ``status_unavailable`` is additive and omitted when inapplicable, so the
2421
+ ordinary payload is byte-identical.
2422
+
2423
+ #556 S5 Unit 2 review F1. A VENDOR-WIDE read of a per-account-only Codex
2424
+ configuration is a configured state, not an unset one, and returning a bare
2425
+ ``{"status": None}`` for it made the client render "No budget set." beside
2426
+ the command that sets one — to a user who has budgets set. It publishes the
2427
+ same ``not_configured.disposition`` the Claude half publishes, which the
2428
+ client already handles. An ACCOUNT-SCOPED read is deliberately excluded:
2429
+ ``account_budgets_only`` describes the vendor-wide axis, and that account's
2430
+ own missing budget is genuinely unset.
2431
+ """
2432
+ config = context.codex_budget
2433
+ if (
2434
+ account_key is None
2435
+ and isinstance(config, Mapping)
2436
+ and config.get("amount_usd") is None
2437
+ and config.get("accounts")
2438
+ ):
2439
+ return {
2440
+ "status": None,
2441
+ "not_configured": {"disposition": "account_budgets_only"},
2442
+ }
2443
+ try:
2444
+ return {"status": _configured_codex_budget_status(
2445
+ context, entries, cost_events=cost_events, account_key=account_key,
2446
+ )}
2447
+ except Exception:
2448
+ _lib_log.get_logger("dashboard").error(
2449
+ "codex budget status could not be computed", exc_info=True,
2450
+ )
2451
+ return {
2452
+ "status": None,
2453
+ "status_unavailable": {
2454
+ "code": "budget_compute_failed",
2455
+ "message": "Codex's budget status could not be computed.",
2456
+ "provider": "codex",
2457
+ },
2458
+ }
1812
2459
 
1813
2460
 
1814
2461
  def _configured_codex_budget_window(
@@ -1995,7 +2642,7 @@ def _quota_read_model(
1995
2642
  # with whichever ACCOUNT's 5h observation happened to sort last.
1996
2643
  correlated_five_hour = tuple(
1997
2644
  observation
1998
- for observation in load_codex_quota_observations(
2645
+ for observation in _cached_codex_quota_observations(
1999
2646
  source_root_keys={identity.source_root_key},
2000
2647
  cache_conn=context.cache_conn,
2001
2648
  captured_at_or_after=block.nominal_start_at,
@@ -2363,19 +3010,50 @@ def refresh_codex_source_clock(
2363
3010
  scopes = data.get("account_scopes")
2364
3011
  scopes_changed = False
2365
3012
  if isinstance(scopes, Mapping):
3013
+ # #556 S5 §3.8: each child's budget reclocks from ITS OWN retained event
3014
+ # tuple, never the vendor-wide one — the accounts hold different spend,
3015
+ # so borrowing the parent's events would publish another account's
3016
+ # trailing-24h rate under this account's card.
3017
+ child_budget_events = (
3018
+ state.clock_data.get("codex_budget_cost_events_by_account", {})
3019
+ if isinstance(state.clock_data, Mapping) else {}
3020
+ )
2366
3021
  rebuilt_scopes = dict(scopes)
2367
3022
  for scope_key, scope in scopes.items():
2368
3023
  if not isinstance(scope, Mapping):
2369
3024
  continue
3025
+ rebuilt_scope: dict[str, object] | None = None
2370
3026
  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:
3027
+ if isinstance(scope_quota, Mapping):
3028
+ reclocked_scope_quota = _reclock_quota_domain(
3029
+ scope_quota, now_utc=now_utc)
3030
+ if reclocked_scope_quota != scope_quota:
3031
+ rebuilt_scope = dict(scope)
3032
+ rebuilt_scope["quota"] = reclocked_scope_quota
3033
+ # #556 S5 §3.8: the child budget status was computed at build and
3034
+ # then never advanced, while the quota beside it was. Focused cards
3035
+ # read this object, so an unclocked one is a knowingly stale number
3036
+ # on the surface a user is looking straight at.
3037
+ scope_budget = scope.get("budget")
3038
+ if isinstance(scope_budget, Mapping) and isinstance(
3039
+ scope_budget.get("status"), Mapping,
3040
+ ):
3041
+ reclocked_child_budget = _refresh_budget_status_clock(
3042
+ scope_budget["status"],
3043
+ now_utc,
3044
+ cost_events=child_budget_events.get(scope_key, ()),
3045
+ )
3046
+ if (
3047
+ reclocked_child_budget is not None
3048
+ and reclocked_child_budget != scope_budget["status"]
3049
+ ):
3050
+ if rebuilt_scope is None:
3051
+ rebuilt_scope = dict(scope)
3052
+ rebuilt_scope["budget"] = {
3053
+ **dict(scope_budget), "status": reclocked_child_budget,
3054
+ }
3055
+ if rebuilt_scope is None:
2376
3056
  continue
2377
- rebuilt_scope = dict(scope)
2378
- rebuilt_scope["quota"] = reclocked_scope_quota
2379
3057
  rebuilt_scopes[scope_key] = rebuilt_scope
2380
3058
  scopes_changed = True
2381
3059
  if scopes_changed:
@@ -2596,22 +3274,66 @@ def _alerts_wire(
2596
3274
  )[:SOURCE_HISTORY_LIMIT])
2597
3275
 
2598
3276
 
3277
+ _CODEX_PROJECT_LABEL_CACHE: dict[object, dict[str, object]] = {}
3278
+
3279
+
3280
+ def _cached_project_labeled_entries(
3281
+ entries: tuple[object, ...], cache_key: object | None,
3282
+ ) -> tuple[object, ...]:
3283
+ """Reuse per-scope display-label annotation for unchanged accounting rows."""
3284
+ if cache_key is None or any(
3285
+ not int(getattr(entry, "cache_entry_id", 0) or 0) for entry in entries
3286
+ ):
3287
+ return tuple(assign_collision_safe_project_labels(entries))
3288
+ pairs = frozenset(
3289
+ (str(entry.project_key), str(entry.project_label)) for entry in entries
3290
+ )
3291
+ state = _CODEX_PROJECT_LABEL_CACHE.get(cache_key)
3292
+ if state is None or state.get("pairs") != pairs:
3293
+ labeled = tuple(assign_collision_safe_project_labels(entries))
3294
+ else:
3295
+ labels = state["labels"]
3296
+ prior = state["entries"]
3297
+ labeled = tuple(
3298
+ prior[int(entry.cache_entry_id)][1]
3299
+ if (
3300
+ int(entry.cache_entry_id) in prior
3301
+ and prior[int(entry.cache_entry_id)][0] == entry
3302
+ ) else replace(
3303
+ entry, display_label=labels[str(entry.project_key)],
3304
+ )
3305
+ for entry in entries
3306
+ )
3307
+ _CODEX_PROJECT_LABEL_CACHE[cache_key] = {
3308
+ "pairs": pairs,
3309
+ "labels": {
3310
+ str(entry.project_key): entry.display_label for entry in labeled
3311
+ },
3312
+ "entries": {
3313
+ int(entry.cache_entry_id): (raw, entry)
3314
+ for raw, entry in zip(entries, labeled)
3315
+ },
3316
+ }
3317
+ return labeled
3318
+
3319
+
2599
3320
  def _projects_wire(
2600
3321
  context: DashboardReadContext,
2601
- quota_observations: Iterable[object],
3322
+ _quota_observations: Iterable[object],
2602
3323
  entries: Iterable[object],
2603
3324
  *,
2604
3325
  accounting_end: dt.datetime,
3326
+ cache_key: object | None = None,
2605
3327
  ) -> dict[str, object]:
2606
3328
  """Adapt S3's already-qualified attribution result without re-formulas."""
2607
- qualified_entries = tuple(entries)
3329
+ qualified_entries = _cached_project_labeled_entries(
3330
+ tuple(entries), cache_key,
3331
+ )
2608
3332
  result = build_codex_project_result(
2609
3333
  qualified_entries,
2610
3334
  range_start=context.range_start,
2611
3335
  range_end=accounting_end,
2612
- blocks=build_blocks(quota_observations),
2613
3336
  as_of=context.now_utc,
2614
- allocation_entries=qualified_entries,
2615
3337
  )
2616
3338
  data = result.data
2617
3339
  if data is None:
@@ -2636,6 +3358,125 @@ def _projects_wire(
2636
3358
  }
2637
3359
 
2638
3360
 
3361
+ _CODEX_PROJECT_WIRE_CACHE: dict[object, tuple] = {}
3362
+
3363
+
3364
+ def _cached_projects_wire(
3365
+ context: DashboardReadContext,
3366
+ quota_observations: Iterable[object],
3367
+ entries: Iterable[object],
3368
+ *,
3369
+ changed_old: Iterable[object],
3370
+ changed_new: Iterable[object],
3371
+ accounting_end: dt.datetime,
3372
+ cache_key: object,
3373
+ semantic_signature: object,
3374
+ ) -> dict[str, object]:
3375
+ """Rebuild only project groups touched by accounting changes."""
3376
+ values = tuple(entries)
3377
+ pairs = frozenset(
3378
+ (str(entry.project_key), str(entry.project_label)) for entry in values
3379
+ )
3380
+ # `entries` is already the complete half-open population for the advancing
3381
+ # upper bound. A moving wall clock is therefore not an aggregation
3382
+ # semantic: `build_cached_codex_accounting` emits newly-visible rows in the
3383
+ # delta when its upper bound advances. Keeping `accounting_end` here made
3384
+ # every live dirty tick discard every project group before that delta could
3385
+ # be spliced.
3386
+ signature = (semantic_signature, pairs, context.range_start)
3387
+ state = _CODEX_PROJECT_WIRE_CACHE.get(cache_key)
3388
+ if state is None or state[0] != signature:
3389
+ value = _projects_wire(
3390
+ context, quota_observations, values,
3391
+ accounting_end=accounting_end, cache_key=cache_key,
3392
+ )
3393
+ groups: dict[tuple[str, str], list[object]] = {}
3394
+ for entry in values:
3395
+ groups.setdefault(
3396
+ (str(entry.source_root_key), str(entry.project_key)), [],
3397
+ ).append(entry)
3398
+ _CODEX_PROJECT_WIRE_CACHE[cache_key] = (
3399
+ signature, value,
3400
+ {key: tuple(group) for key, group in groups.items()},
3401
+ )
3402
+ return value
3403
+
3404
+ affected = {
3405
+ (str(entry.source_root_key), str(entry.project_key))
3406
+ for entry in (*tuple(changed_old), *tuple(changed_new))
3407
+ }
3408
+ if not affected:
3409
+ return state[1]
3410
+ label_state = _CODEX_PROJECT_LABEL_CACHE.get(cache_key) or {}
3411
+ labels = label_state.get("labels") or {}
3412
+ old_ids = {
3413
+ int(getattr(entry, "cache_entry_id", 0) or 0)
3414
+ for entry in changed_old
3415
+ }
3416
+ groups = dict(state[2])
3417
+ for key in affected:
3418
+ groups[key] = tuple(
3419
+ entry for entry in groups.get(key, ())
3420
+ if int(getattr(entry, "cache_entry_id", 0) or 0) not in old_ids
3421
+ )
3422
+ for entry in changed_new:
3423
+ key = (str(entry.source_root_key), str(entry.project_key))
3424
+ groups[key] = (*groups.get(key, ()), entry)
3425
+ for key in affected:
3426
+ if groups.get(key):
3427
+ groups[key] = tuple(sorted(
3428
+ groups[key], key=_codex_incremental_entry_order,
3429
+ ))
3430
+ else:
3431
+ groups.pop(key, None)
3432
+ partial_entries = tuple(
3433
+ replace(entry, display_label=labels[str(entry.project_key)])
3434
+ for key in sorted(affected) for entry in groups.get(key, ())
3435
+ )
3436
+ result = build_codex_project_result(
3437
+ partial_entries,
3438
+ range_start=context.range_start,
3439
+ range_end=accounting_end,
3440
+ as_of=context.now_utc,
3441
+ )
3442
+ partial_rows = () if result.data is None else tuple({
3443
+ "key": dashboard_resource_key("project", "codex", row.project_key),
3444
+ "source": "codex",
3445
+ "label": row.display_label,
3446
+ "session_count": row.session_count,
3447
+ "first_seen": row.first_seen.astimezone(UTC).isoformat(),
3448
+ "last_seen": row.last_seen.astimezone(UTC).isoformat(),
3449
+ "cost_usd": row.totals.cost_usd,
3450
+ "input_tokens": row.totals.input_tokens,
3451
+ "cached_input_tokens": row.totals.cached_input_tokens,
3452
+ "output_tokens": row.totals.output_tokens,
3453
+ "reasoning_output_tokens": row.totals.reasoning_output_tokens,
3454
+ "total_tokens": row.totals.total_tokens,
3455
+ } for row in result.data.projects)
3456
+ affected_keys = {
3457
+ dashboard_resource_key("project", "codex", project_key)
3458
+ for _root_key, project_key in affected
3459
+ }
3460
+ rows = tuple(sorted(
3461
+ (
3462
+ *(row for row in state[1]["rows"] if row["key"] not in affected_keys),
3463
+ *partial_rows,
3464
+ ),
3465
+ key=lambda row: (
3466
+ float(row["cost_usd"]), str(row["label"]), str(row["key"]),
3467
+ ),
3468
+ reverse=True,
3469
+ ))
3470
+ value = {
3471
+ "rows": rows,
3472
+ "total_cost_usd": stable_sum(
3473
+ float(row["cost_usd"]) for row in rows),
3474
+ "total_tokens": sum(int(row["total_tokens"]) for row in rows),
3475
+ }
3476
+ _CODEX_PROJECT_WIRE_CACHE[cache_key] = (signature, value, groups)
3477
+ return value
3478
+
3479
+
2639
3480
  def _partial_projects_wire(
2640
3481
  entries: Iterable[object],
2641
3482
  metadata: Mapping[tuple[str, str], Mapping[str, object]],
@@ -2744,15 +3585,23 @@ def _partial_projects_wire(
2744
3585
  }
2745
3586
 
2746
3587
 
3588
+ _CODEX_ENTRY_ADAPTER_CACHE: dict[int, tuple[object, CodexEntry]] = {}
3589
+
3590
+
2747
3591
  def _codex_entries_from_accounting(entries: Iterable[object]) -> list[CodexEntry]:
2748
3592
  """Adapt coordinated accounting rows for the shipped non-project kernels."""
2749
3593
  converted: list[CodexEntry] = []
2750
3594
  for entry in entries:
3595
+ cache_entry_id = int(getattr(entry, "cache_entry_id", 0) or 0)
3596
+ cached = _CODEX_ENTRY_ADAPTER_CACHE.get(cache_entry_id)
3597
+ if cache_entry_id and cached is not None and cached[0] == entry:
3598
+ converted.append(cached[1])
3599
+ continue
2751
3600
  source_path = str(getattr(entry, "source_path", "") or "")
2752
3601
  session_id = str(getattr(entry, "session_id", "") or "")
2753
3602
  if not source_path or not session_id:
2754
3603
  raise SourceCapabilityUnavailable("Codex accounting lacks session identity")
2755
- converted.append(CodexEntry(
3604
+ value = CodexEntry(
2756
3605
  timestamp=getattr(entry, "timestamp"),
2757
3606
  session_id=session_id,
2758
3607
  model=str(getattr(entry, "model")),
@@ -2762,7 +3611,17 @@ def _codex_entries_from_accounting(entries: Iterable[object]) -> list[CodexEntry
2762
3611
  reasoning_output_tokens=int(getattr(entry, "reasoning_output_tokens")),
2763
3612
  total_tokens=int(getattr(entry, "total_tokens")),
2764
3613
  source_path=source_path,
2765
- ))
3614
+ cost_usd=(
3615
+ float(entry.cost_usd)
3616
+ if getattr(entry, "cost_usd", None) is not None else None
3617
+ ),
3618
+ cache_entry_id=cache_entry_id,
3619
+ source_root_key=str(getattr(entry, "source_root_key", "") or ""),
3620
+ conversation_key=str(getattr(entry, "conversation_key", "") or ""),
3621
+ )
3622
+ converted.append(value)
3623
+ if cache_entry_id:
3624
+ _CODEX_ENTRY_ADAPTER_CACHE[cache_entry_id] = (entry, value)
2766
3625
  return converted
2767
3626
 
2768
3627
 
@@ -2867,6 +3726,136 @@ def _build_codex_native_weekly_view(
2867
3726
  )
2868
3727
 
2869
3728
 
3729
+ _CODEX_WEEKLY_VIEW_CACHE: dict[object, tuple] = {}
3730
+
3731
+
3732
+ def _codex_weekly_period_for_entry(
3733
+ entry: object, periods: Iterable[CodexWeeklyPeriod],
3734
+ ) -> CodexWeeklyPeriod | None:
3735
+ if codex_model_scoped_quota_pool(getattr(entry, "model", None)) is not None:
3736
+ return None
3737
+ timestamp = getattr(entry, "timestamp").astimezone(UTC)
3738
+ root_key = str(getattr(entry, "source_root_key", "") or "")
3739
+ return next((
3740
+ period for period in periods
3741
+ if root_key in period.source_root_keys
3742
+ and period.start_at <= timestamp < period.end_at
3743
+ ), None)
3744
+
3745
+
3746
+ def _cached_codex_native_weekly_view(
3747
+ stats_conn: sqlite3.Connection,
3748
+ entries: Iterable[object],
3749
+ *,
3750
+ changed_old: Iterable[object],
3751
+ changed_new: Iterable[object],
3752
+ cache_key: object,
3753
+ semantic_signature: object,
3754
+ source_root_keys: Iterable[str],
3755
+ active_cycle: CodexCycleBoundary | None,
3756
+ now_utc: dt.datetime,
3757
+ display_tz_name: str | None,
3758
+ speed: str,
3759
+ account_key: str | None = None,
3760
+ include_account_keys: bool = False,
3761
+ ) -> CodexWeeklyView:
3762
+ """Rebuild only native quota periods touched by accounting changes."""
3763
+ values = tuple(entries)
3764
+ roots = tuple(source_root_keys)
3765
+ periods = _codex_weekly_periods(
3766
+ stats_conn,
3767
+ source_root_keys=roots,
3768
+ active_cycle=active_cycle,
3769
+ account_key=account_key,
3770
+ )
3771
+ signature = (
3772
+ semantic_signature, periods, roots, display_tz_name, speed,
3773
+ account_key, include_account_keys,
3774
+ )
3775
+ state = _CODEX_WEEKLY_VIEW_CACHE.get(cache_key)
3776
+ if state is None or state[0] != signature:
3777
+ view = _build_codex_native_weekly_view(
3778
+ stats_conn, values, source_root_keys=roots,
3779
+ active_cycle=active_cycle, now_utc=now_utc,
3780
+ display_tz_name=display_tz_name, speed=speed,
3781
+ account_key=account_key, include_account_keys=include_account_keys,
3782
+ )
3783
+ groups: dict[dt.datetime, list[object]] = {}
3784
+ for entry in values:
3785
+ period = _codex_weekly_period_for_entry(entry, periods)
3786
+ if period is not None:
3787
+ groups.setdefault(period.start_at, []).append(entry)
3788
+ _CODEX_WEEKLY_VIEW_CACHE[cache_key] = (
3789
+ signature, view,
3790
+ {key: tuple(group) for key, group in groups.items()},
3791
+ )
3792
+ return view
3793
+
3794
+ affected = {
3795
+ period.start_at
3796
+ for entry in (*tuple(changed_old), *tuple(changed_new))
3797
+ if (period := _codex_weekly_period_for_entry(entry, periods)) is not None
3798
+ }
3799
+ if not affected:
3800
+ return state[1]
3801
+
3802
+ prior = state[1]
3803
+ old_ids = {
3804
+ int(getattr(entry, "cache_entry_id", 0) or 0)
3805
+ for entry in changed_old
3806
+ }
3807
+ groups = dict(state[2])
3808
+ for start_at in affected:
3809
+ groups[start_at] = tuple(
3810
+ entry for entry in groups.get(start_at, ())
3811
+ if int(getattr(entry, "cache_entry_id", 0) or 0) not in old_ids
3812
+ )
3813
+ for entry in changed_new:
3814
+ period = _codex_weekly_period_for_entry(entry, periods)
3815
+ if period is not None:
3816
+ groups[period.start_at] = (*groups.get(period.start_at, ()), entry)
3817
+ for start_at in affected:
3818
+ if groups.get(start_at):
3819
+ groups[start_at] = tuple(sorted(
3820
+ groups[start_at], key=_codex_incremental_entry_order,
3821
+ ))
3822
+ else:
3823
+ groups.pop(start_at, None)
3824
+ replacements: dict[dt.datetime, object] = {}
3825
+ for start_at in affected:
3826
+ partial = _build_codex_native_weekly_view(
3827
+ stats_conn,
3828
+ groups.get(start_at, ()),
3829
+ source_root_keys=roots, active_cycle=active_cycle,
3830
+ now_utc=now_utc, display_tz_name=display_tz_name, speed=speed,
3831
+ account_key=account_key, include_account_keys=include_account_keys,
3832
+ )
3833
+ row = next((
3834
+ row for row in partial.rows
3835
+ if getattr(row, "period_start_at", None) == start_at
3836
+ ), None)
3837
+ if row is not None:
3838
+ replacements[start_at] = row
3839
+ rows = tuple(sorted(
3840
+ (
3841
+ *(row for row in prior.rows
3842
+ if getattr(row, "period_start_at", None) not in affected),
3843
+ *replacements.values(),
3844
+ ),
3845
+ key=lambda row: row.period_start_at,
3846
+ ))
3847
+ view = replace(
3848
+ prior,
3849
+ rows=rows,
3850
+ total_cost_usd=stable_sum(row.cost_usd for row in rows),
3851
+ total_tokens=sum(row.total_tokens for row in rows),
3852
+ period_start=(periods[0].start_at if periods else None),
3853
+ period_end=now_utc,
3854
+ )
3855
+ _CODEX_WEEKLY_VIEW_CACHE[cache_key] = (signature, view, groups)
3856
+ return view
3857
+
3858
+
2870
3859
  def _codex_account_five_hour_percent(
2871
3860
  observations: Iterable[object],
2872
3861
  now_utc: dt.datetime,
@@ -2913,8 +3902,9 @@ def _codex_accounts_wire(
2913
3902
  so the envelope stays byte-identical, spec R8). Each account carries
2914
3903
  ``{accountKey, label, plan, active, weeklyPercent, fiveHourPercent, resetsAt,
2915
3904
  spendUsd, inputTokens, cachedInputTokens, outputTokens,
2916
- reasoningOutputTokens, totalTokens, unattributed?}``; ``hero_cycles_wire`` is
2917
- the thin per-account cycle-boundary list the hero renders (``cycles[]``).
3905
+ reasoningOutputTokens, totalTokens, unattributed?, spendWindow?}``;
3906
+ ``hero_cycles_wire`` is the thin per-account cycle-boundary list the hero
3907
+ renders (``cycles[]``).
2918
3908
  """
2919
3909
  import _cctally_account
2920
3910
  active_keys = _cctally_account.resolve_active_account_keys()
@@ -2932,11 +3922,39 @@ def _codex_accounts_wire(
2932
3922
  reg = _cctally_account.load_accounts(context.stats_conn, "codex")
2933
3923
  plan_by_key = {r["account_key"]: r.get("plan_type") for r in reg}
2934
3924
  ordered_keys = [r["account_key"] for r in reg]
3925
+ # #564: a card with no live cycle is read over ONE native cycle width
3926
+ # ending at `now`, never the whole accounting range. The decorated hero is
3927
+ # the sum of these cards under a week label, so an addend spanning the full
3928
+ # ~30-day range put spend that label does not cover into the headline.
3929
+ #
3930
+ # The start comes from `now_utc`, NOT `accounting_end`: the latter is
3931
+ # `now + 1us`, an adapter that lets an inclusive-now surface call a
3932
+ # half-open reader, so subtracting the width from it would drop a row
3933
+ # landing exactly on the boundary while keeping one landing at `now`.
3934
+ fallback_start = max(
3935
+ accounting_start,
3936
+ context.now_utc - dt.timedelta(minutes=ACCOUNT_WEEKLY_WINDOW_MINUTES),
3937
+ )
3938
+ fallback_window = {
3939
+ "kind": "trailing-cycle",
3940
+ "startAt": fallback_start.astimezone(UTC).isoformat(),
3941
+ "endAt": context.now_utc.astimezone(UTC).isoformat(),
3942
+ }
2935
3943
  # Include unattributed last iff it has cycle/5h/spend evidence.
2936
3944
  unattributed_rows = load_cached_rooted_codex_accounting_entries(
2937
3945
  accounting_start, accounting_end, speed=context.speed,
2938
3946
  cache_conn=context.cache_conn, account_key=_lib_accounts.UNATTRIBUTED,
2939
3947
  )
3948
+ # Existence is decided over the accounting range so a sentinel holding only
3949
+ # older spend keeps its card; the totals below cover the bounded window, so
3950
+ # a resolved $0.00 is an honest empty state rather than an absence (#564).
3951
+ # The bounded set is a strict subset of the rows already loaded above, so it
3952
+ # is derived in memory rather than re-queried on every publish. `timestamp`
3953
+ # is normalized to UTC by the reader and the upper bound is already applied,
3954
+ # so the two predicates coincide.
3955
+ unattributed_window_rows = tuple(
3956
+ row for row in unattributed_rows if row.timestamp >= fallback_start
3957
+ )
2940
3958
  if (
2941
3959
  unattributed_rows
2942
3960
  or _lib_accounts.UNATTRIBUTED in cycle_by_account
@@ -2978,12 +3996,14 @@ def _codex_accounts_wire(
2978
3996
  )
2979
3997
  totals = _totals(rows)
2980
3998
  elif is_unattributed:
2981
- totals = _totals(unattributed_rows)
3999
+ totals = _totals(unattributed_window_rows)
2982
4000
  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).
4001
+ # A real account without a live weekly cycle: totals over ONE
4002
+ # native cycle width ending now, so this card can be summed into a
4003
+ # week-labelled headline without overstating it (#564). No bars or
4004
+ # reset, because there is no live cycle to describe.
2985
4005
  rows = load_cached_rooted_codex_accounting_entries(
2986
- accounting_start, accounting_end, speed=context.speed,
4006
+ fallback_start, accounting_end, speed=context.speed,
2987
4007
  cache_conn=context.cache_conn, account_key=key,
2988
4008
  )
2989
4009
  totals = _totals(rows)
@@ -3010,6 +4030,12 @@ def _codex_accounts_wire(
3010
4030
  # cannot speak for a fresh sibling, and staleness is disclosure
3011
4031
  # only — the retained percentage, reset and spend remain useful.
3012
4032
  card["cycleFreshness"] = "stale"
4033
+ if is_unattributed or cyc is None:
4034
+ # The card's totals came from the bounded fallback rather than a
4035
+ # live cycle, so it publishes the exact window it covers. The client
4036
+ # reads this key and never infers the case from a null `resetsAt`,
4037
+ # which is true of several unrelated states (#564 D3).
4038
+ card["spendWindow"] = fallback_window
3013
4039
  accounts_wire.append(card)
3014
4040
  if cyc is not None and not is_unattributed:
3015
4041
  hero_cycles_wire.append({
@@ -3054,6 +4080,66 @@ def _codex_partition_by_account(
3054
4080
  return {key: tuple(values) for key, values in buckets.items()}
3055
4081
 
3056
4082
 
4083
+ def _codex_fold_visible_rows(
4084
+ entries: Iterable[object],
4085
+ ) -> "tuple[list[CodexEntry], dict[str, tuple[object, ...]], dict[str, tuple[CodexEntry, ...]]]":
4086
+ """One encounter-ordered pass producing the parent's and each account's rows.
4087
+
4088
+ #566 §5.1 item 2. Each visible row is adapted to a ``CodexEntry`` exactly
4089
+ once and then routed into the merged "All" list and into its owning
4090
+ account's list, instead of the parent converting the whole population and
4091
+ every child re-converting its own slice.
4092
+
4093
+ This removes exactly the four whole-population re-adaptations the children
4094
+ performed, worth about 0.4s of a profiled tick on the maintainer's store.
4095
+ It does NOT reduce the 191,225 total calls to
4096
+ ``_codex_entries_from_accounting`` that a build makes: 191,220 of them come
4097
+ from ``_build_codex_native_weekly_view``, which adapts one entry at a time
4098
+ per scope, and this fold does not touch that site.
4099
+
4100
+ Encounter order is preserved in every output, and the ordering matters:
4101
+ ``_aggregate_codex_buckets`` accumulates in encounter order and preserves
4102
+ first-seen model order, so routing through a set, or sorting, would move a
4103
+ bucket's ``models`` order for free. Adaptation is
4104
+ 1:1 and order-preserving, so each account's list is byte-identical to
4105
+ adapting that account's rows on their own — which is what makes the fold a
4106
+ reuse of work rather than a change to any builder's arithmetic. The
4107
+ shipped builders still run per scope, so the merged parent stays
4108
+ byte-identical BY CONSTRUCTION (#416 §5.2 review F9/F10).
4109
+ """
4110
+ rows = tuple(entries)
4111
+ all_entries = _codex_entries_from_accounting(rows)
4112
+ rows_by_account: dict[str, list[object]] = {}
4113
+ entries_by_account: dict[str, list[CodexEntry]] = {}
4114
+ for row, converted in zip(rows, all_entries):
4115
+ key = str(
4116
+ getattr(row, "account_key", "") or _lib_accounts.UNATTRIBUTED)
4117
+ rows_by_account.setdefault(key, []).append(row)
4118
+ entries_by_account.setdefault(key, []).append(converted)
4119
+ return (
4120
+ all_entries,
4121
+ {key: tuple(values) for key, values in rows_by_account.items()},
4122
+ {key: tuple(values) for key, values in entries_by_account.items()},
4123
+ )
4124
+
4125
+
4126
+ _CODEX_ACCOUNT_SCOPE_CACHE: dict[
4127
+ str, tuple[object, dict[str, object]]
4128
+ ] = {}
4129
+
4130
+
4131
+ def reset_codex_account_scope_cache() -> None:
4132
+ """Test/process reset for #582's immutable finalized account scopes."""
4133
+ _CODEX_ACCOUNT_SCOPE_CACHE.clear()
4134
+ _CODEX_ENTRY_ADAPTER_CACHE.clear()
4135
+ _CODEX_PROJECT_LABEL_CACHE.clear()
4136
+ _CODEX_PERIOD_VIEW_CACHE.clear()
4137
+ _CODEX_WEEKLY_VIEW_CACHE.clear()
4138
+ _CODEX_CACHE_REPORT_ROWS.clear()
4139
+ _CODEX_SESSION_VIEW_CACHE.clear()
4140
+ _CODEX_PROJECT_WIRE_CACHE.clear()
4141
+
4142
+
3057
4143
  def _codex_account_scopes_wire(
3058
4144
  context: DashboardReadContext,
3059
4145
  *,
@@ -3061,6 +4147,8 @@ def _codex_account_scopes_wire(
3061
4147
  quota_observations: Iterable[object],
3062
4148
  cycle_by_account: Mapping[str, "CodexCycleBoundary"],
3063
4149
  visible_accounting_entries: Iterable[object],
4150
+ visible_rows_by_account: "Mapping[str, tuple[object, ...]] | None" = None,
4151
+ visible_entries_by_account: "Mapping[str, tuple[CodexEntry, ...]] | None" = None,
3064
4152
  active_roots: Iterable[str],
3065
4153
  accounting_end: dt.datetime,
3066
4154
  metadata_incomplete: bool,
@@ -3071,6 +4159,12 @@ def _codex_account_scopes_wire(
3071
4159
  budget_cost_events_by_account: Mapping[str, tuple[tuple[dt.datetime, float], ...]],
3072
4160
  private_session_labels: dict[str, str],
3073
4161
  hero_failure: bool = False,
4162
+ dirty_accounts: Iterable[str] = (),
4163
+ scope_signature: object | None = None,
4164
+ changed_old_by_account: Mapping[str, tuple[CodexEntry, ...]] | None = None,
4165
+ changed_new_by_account: Mapping[str, tuple[CodexEntry, ...]] | None = None,
4166
+ changed_old_rows_by_account: Mapping[str, tuple[object, ...]] | None = None,
4167
+ changed_new_rows_by_account: Mapping[str, tuple[object, ...]] | None = None,
3074
4168
  ) -> dict[str, dict[str, object]]:
3075
4169
  """The per-account CHILDREN of the merged Codex read model (spec §5.3).
3076
4170
 
@@ -3101,7 +4195,15 @@ def _codex_account_scopes_wire(
3101
4195
  """
3102
4196
  visible = tuple(visible_accounting_entries)
3103
4197
  observations = tuple(quota_observations)
3104
- partition = _codex_partition_by_account(visible)
4198
+ # #566 §5.1 item 2: the caller folded the visible rows once and hands both
4199
+ # partitions down. Re-deriving them here is retained only for direct
4200
+ # callers (tests, the source-detail reader) that have no fold to share.
4201
+ if visible_rows_by_account is None or visible_entries_by_account is None:
4202
+ _all, visible_rows_by_account, visible_entries_by_account = (
4203
+ _codex_fold_visible_rows(visible)
4204
+ )
4205
+ partition = visible_rows_by_account
4206
+ entries_partition = visible_entries_by_account
3105
4207
  obs_partition: dict[str, list[object]] = {}
3106
4208
  for observation in observations:
3107
4209
  obs_partition.setdefault(
@@ -3110,19 +4212,29 @@ def _codex_account_scopes_wire(
3110
4212
  budget_rows = tuple(budget_milestones)
3111
4213
  projected_rows = tuple(projected_budget_milestones)
3112
4214
  roots = tuple(active_roots)
4215
+ dirty_account_keys = {str(key) for key in dirty_accounts}
4216
+ changed_old_by_account = changed_old_by_account or {}
4217
+ changed_new_by_account = changed_new_by_account or {}
4218
+ changed_old_rows_by_account = changed_old_rows_by_account or {}
4219
+ changed_new_rows_by_account = changed_new_rows_by_account or {}
3113
4220
 
3114
4221
  def _for_account(key: str) -> dict[str, object]:
3115
4222
  rows = partition.get(key, ())
3116
4223
  account_observations = tuple(obs_partition.get(key, ()))
3117
- entries = _codex_entries_from_accounting(rows)
4224
+ entries = list(entries_partition.get(key, ()))
3118
4225
  cycle = cycle_by_account.get(key)
3119
4226
  sessions_view = (
3120
4227
  build_rooted_codex_session_view(
3121
4228
  rows, now_utc=context.now_utc,
3122
4229
  tz_name=context.display_tz_name, speed=context.speed,
3123
4230
  )
3124
- if metadata_incomplete else build_codex_session_view(
3125
- entries, now_utc=context.now_utc,
4231
+ if metadata_incomplete else _cached_codex_session_view(
4232
+ entries,
4233
+ changed_old=changed_old_rows_by_account.get(key, ()),
4234
+ changed_new=changed_new_rows_by_account.get(key, ()),
4235
+ cache_key=("account", key),
4236
+ semantic_signature=scope_signature,
4237
+ now_utc=context.now_utc,
3126
4238
  tz_name=context.display_tz_name, speed=context.speed,
3127
4239
  )
3128
4240
  )
@@ -3165,16 +4277,31 @@ def _codex_account_scopes_wire(
3165
4277
  # quota window with no spend yet, and a retired one the reverse.
3166
4278
  "is_empty": not rows and not account_observations,
3167
4279
  "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,
4280
+ "daily": _period_wire(_cached_codex_period_view(
4281
+ entries,
4282
+ changed_old=changed_old_by_account.get(key, ()),
4283
+ changed_new=changed_new_by_account.get(key, ()),
4284
+ kind="daily", cache_key=("account", key),
4285
+ semantic_signature=scope_signature,
4286
+ now_utc=context.now_utc, tz_name=context.display_tz_name,
4287
+ speed=context.speed,
3171
4288
  )),
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,
4289
+ "monthly": _period_wire(_cached_codex_period_view(
4290
+ entries,
4291
+ changed_old=changed_old_by_account.get(key, ()),
4292
+ changed_new=changed_new_by_account.get(key, ()),
4293
+ kind="monthly", cache_key=("account", key),
4294
+ semantic_signature=scope_signature,
4295
+ now_utc=context.now_utc, tz_name=context.display_tz_name,
4296
+ speed=context.speed,
3175
4297
  )),
3176
- "weekly": _period_wire(_build_codex_native_weekly_view(
3177
- context.stats_conn, rows, source_root_keys=roots,
4298
+ "weekly": _period_wire(_cached_codex_native_weekly_view(
4299
+ context.stats_conn, rows,
4300
+ changed_old=changed_old_rows_by_account.get(key, ()),
4301
+ changed_new=changed_new_rows_by_account.get(key, ()),
4302
+ cache_key=("account", key),
4303
+ semantic_signature=scope_signature,
4304
+ source_root_keys=roots,
3178
4305
  active_cycle=cycle, now_utc=context.now_utc,
3179
4306
  display_tz_name=context.display_tz_name, speed=context.speed,
3180
4307
  account_key=key,
@@ -3186,18 +4313,26 @@ def _codex_account_scopes_wire(
3186
4313
  ),
3187
4314
  "projects": (
3188
4315
  _partial_projects_wire(rows, conversation_metadata)
3189
- if metadata_incomplete else _projects_wire(
4316
+ if metadata_incomplete else _cached_projects_wire(
3190
4317
  context, account_observations, rows,
4318
+ changed_old=changed_old_rows_by_account.get(key, ()),
4319
+ changed_new=changed_new_rows_by_account.get(key, ()),
3191
4320
  accounting_end=accounting_end,
4321
+ cache_key=("account", key),
4322
+ semantic_signature=scope_signature,
3192
4323
  )
3193
4324
  ),
3194
4325
  "cache_report": _codex_cache_report_wire(
3195
4326
  rows, metadata=conversation_metadata, now_utc=context.now_utc,
3196
4327
  display_tz_name=context.display_tz_name, speed=context.speed,
3197
4328
  anomaly_threshold_pp=context.cache_report_anomaly_threshold_pp,
4329
+ cache_key=("account", key),
4330
+ changed_old=changed_old_rows_by_account.get(key, ()),
4331
+ changed_new=changed_new_rows_by_account.get(key, ()),
4332
+ semantic_signature=scope_signature,
3198
4333
  ),
3199
4334
  "budget": {
3200
- "status": _configured_codex_budget_status(
4335
+ **_codex_budget_status_domain(
3201
4336
  context, rows,
3202
4337
  cost_events=budget_cost_events_by_account.get(key, ()),
3203
4338
  account_key=key,
@@ -3237,7 +4372,52 @@ def _codex_account_scopes_wire(
3237
4372
  (set(partition) | set(obs_partition) | _codex_block_account_keys(
3238
4373
  context.stats_conn, roots)) - set(ordered_keys)
3239
4374
  )
3240
- return {key: _for_account(key) for key in ordered_keys + residual_keys}
4375
+ result: dict[str, dict[str, object]] = {}
4376
+ live_keys = ordered_keys + residual_keys
4377
+ for key in live_keys:
4378
+ account_observations = tuple(obs_partition.get(key, ()))
4379
+ account_metadata = tuple(
4380
+ (identity, conversation_metadata.get(identity))
4381
+ for identity in sorted({
4382
+ (
4383
+ str(getattr(row, "source_root_key", "")),
4384
+ str(getattr(row, "source_path", "")),
4385
+ )
4386
+ for row in partition.get(key, ())
4387
+ })
4388
+ )
4389
+ signature = (
4390
+ scope_signature,
4391
+ account_observations,
4392
+ cycle_by_account.get(key),
4393
+ account_metadata,
4394
+ tuple(_codex_account_scoped_rows(alert_rows, key)),
4395
+ tuple(_codex_account_scoped_rows(budget_rows, key)),
4396
+ tuple(_codex_account_scoped_rows(projected_rows, key)),
4397
+ budget_cost_events_by_account.get(key, ()),
4398
+ context.codex_budget,
4399
+ context.codex_quota_actual_thresholds,
4400
+ context.codex_quota_projected_thresholds,
4401
+ context.cache_report_anomaly_threshold_pp,
4402
+ metadata_incomplete,
4403
+ hero_failure,
4404
+ )
4405
+ cached = _CODEX_ACCOUNT_SCOPE_CACHE.get(key)
4406
+ if (
4407
+ scope_signature is not None
4408
+ and key not in dirty_account_keys
4409
+ and cached is not None
4410
+ and cached[0] == signature
4411
+ ):
4412
+ result[key] = cached[1]
4413
+ continue
4414
+ value = _for_account(key)
4415
+ result[key] = value
4416
+ if scope_signature is not None:
4417
+ _CODEX_ACCOUNT_SCOPE_CACHE[key] = (signature, value)
4418
+ for stale_key in set(_CODEX_ACCOUNT_SCOPE_CACHE) - set(live_keys):
4419
+ _CODEX_ACCOUNT_SCOPE_CACHE.pop(stale_key, None)
4420
+ return result
3241
4421
 
3242
4422
 
3243
4423
  def _codex_block_account_keys(
@@ -3418,13 +4598,61 @@ def build_codex_source_state(
3418
4598
  No sync, rollout scan, CLI parser, or fallback is reachable from this
3419
4599
  adapter. Period and session arithmetic remains delegated to the shipped
3420
4600
  S3 view kernels, preserving the CLI's inclusive-token vocabulary.
4601
+
4602
+ The whole read runs under ONE ``codex_path_scope`` (#566 §5.1 item 1), so
4603
+ the merged parent view and every per-account child share a single session
4604
+ root resolution and a single parse per distinct session file. The scope is
4605
+ opened here rather than further out because this is the boundary that owns
4606
+ every Codex session view in the build, and it is discarded when the read
4607
+ returns.
3421
4608
  """
4609
+ # This memo deduplicates the several account/parent consumers inside ONE
4610
+ # coordinated source build. It may not cross that boundary: a caller can
4611
+ # deliberately request a fresh build after stats/account decoration changes
4612
+ # without advancing cache.db's quota ledger, and the established contract
4613
+ # requires one bounded physical load for that new build.
4614
+ reset_codex_quota_observation_cache()
4615
+ caches = (
4616
+ _CODEX_QUOTA_OBSERVATION_CACHE,
4617
+ _CODEX_PERIOD_VIEW_CACHE,
4618
+ _CODEX_CACHE_REPORT_ROWS,
4619
+ _CODEX_SESSION_VIEW_CACHE,
4620
+ _CODEX_PROJECT_LABEL_CACHE,
4621
+ _CODEX_PROJECT_WIRE_CACHE,
4622
+ _CODEX_ENTRY_ADAPTER_CACHE,
4623
+ _CODEX_WEEKLY_VIEW_CACHE,
4624
+ _CODEX_ACCOUNT_SCOPE_CACHE,
4625
+ )
4626
+ cache_checkpoint = tuple(dict(cache) for cache in caches)
4627
+ accounting_checkpoint = (
4628
+ _lib_snapshot_cache.checkpoint_codex_accounting_cache_state()
4629
+ )
4630
+ try:
4631
+ with codex_path_scope() as path_scope:
4632
+ return _build_codex_source_state(
4633
+ context, data_version=data_version, path_scope=path_scope,
4634
+ )
4635
+ except Exception:
4636
+ for cache, prior in zip(caches, cache_checkpoint):
4637
+ cache.clear()
4638
+ cache.update(prior)
4639
+ _lib_snapshot_cache.restore_codex_accounting_cache_state(
4640
+ accounting_checkpoint)
4641
+ raise
4642
+
4643
+
4644
+ def _build_codex_source_state(
4645
+ context: DashboardReadContext,
4646
+ *,
4647
+ data_version: str,
4648
+ path_scope: object,
4649
+ ) -> SourceDashboardState:
3422
4650
  active_roots = tuple(sorted(
3423
4651
  str(row[0]) for row in context.cache_conn.execute(
3424
4652
  "SELECT source_root_key FROM codex_source_roots"
3425
4653
  )
3426
4654
  ))
3427
- quota_observations = load_codex_quota_observations(
4655
+ quota_observations = _cached_codex_quota_observations(
3428
4656
  source_root_keys=active_roots,
3429
4657
  cache_conn=context.cache_conn,
3430
4658
  captured_at_or_after=(
@@ -3445,8 +4673,23 @@ def build_codex_source_state(
3445
4673
  accounting_end = context.now_utc + dt.timedelta(microseconds=1)
3446
4674
  accounting_start = context.range_start
3447
4675
  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)
4676
+ # #556 S5 Unit 2 (Unit 1 review R6, widened) — this is the SECOND
4677
+ # unguarded call to the window resolver, and unlike
4678
+ # `_codex_budget_cost_events` it had no boundary of its own. It sits
4679
+ # outside every other `try` in this function, so an unresolvable window
4680
+ # escaped into `_tui_build_source_bundle`'s `source_build_failed`
4681
+ # handler and destroyed the entire Codex provider's data — the exact
4682
+ # failure §3.5 exists to prevent, reached from a different line.
4683
+ #
4684
+ # Degrading to the un-widened accounting range cannot publish a false
4685
+ # figure: the same failure reaches `_codex_budget_status_domain`, which
4686
+ # nulls the status and names `budget_compute_failed`.
4687
+ try:
4688
+ _period, budget_start, _budget_end = _configured_codex_budget_window(context)
4689
+ except Exception:
4690
+ _warn_codex_budget_window_once("accounting_range")
4691
+ else:
4692
+ accounting_start = min(accounting_start, budget_start)
3450
4693
  health = load_codex_project_metadata_health(
3451
4694
  cache_conn=context.cache_conn,
3452
4695
  start=accounting_start,
@@ -3461,15 +4704,51 @@ def build_codex_source_state(
3461
4704
  "run `cctally cache-sync --source codex --rebuild`."
3462
4705
  )
3463
4706
  qualified_entries: tuple[object, ...] = ()
4707
+ accounting_dirty_accounts: tuple[str, ...] = ()
4708
+ accounting_changed_old: tuple[object, ...] = ()
4709
+ accounting_changed_new: tuple[object, ...] = ()
3464
4710
  if not metadata_incomplete:
3465
4711
  try:
3466
- qualified_entries = load_qualified_codex_entries(
3467
- accounting_start,
3468
- accounting_end,
3469
- speed=context.speed,
3470
- sync=False,
4712
+ cached_accounting = _lib_snapshot_cache.build_cached_codex_accounting(
3471
4713
  cache_conn=context.cache_conn,
4714
+ range_start=accounting_start,
4715
+ range_end=accounting_end,
4716
+ extra_signature=(
4717
+ context.speed,
4718
+ tuple(str(root) for root in path_scope.roots),
4719
+ active_roots,
4720
+ ),
4721
+ load_all=lambda: load_qualified_codex_entries(
4722
+ accounting_start,
4723
+ accounting_end,
4724
+ speed=context.speed,
4725
+ sync=False,
4726
+ cache_conn=context.cache_conn,
4727
+ ),
4728
+ load_paths=lambda identities: load_qualified_codex_entries(
4729
+ accounting_start,
4730
+ accounting_end,
4731
+ speed=context.speed,
4732
+ sync=False,
4733
+ cache_conn=context.cache_conn,
4734
+ source_identities=identities,
4735
+ ),
4736
+ path_of=lambda entry: (
4737
+ str(entry.source_root_key), str(entry.source_path),
4738
+ ),
4739
+ account_of=lambda entry: str(entry.account_key),
4740
+ order_key=lambda entry: (
4741
+ entry.timestamp,
4742
+ str(entry.source_root_key),
4743
+ str(entry.conversation_key),
4744
+ int(entry.cache_entry_id),
4745
+ ),
4746
+ identity_of=lambda entry: int(entry.cache_entry_id),
3472
4747
  )
4748
+ qualified_entries = cached_accounting.entries
4749
+ accounting_dirty_accounts = cached_accounting.dirty_accounts
4750
+ accounting_changed_old = cached_accounting.changed_old
4751
+ accounting_changed_new = cached_accounting.changed_new
3473
4752
  accounting_entries: tuple[object, ...] = qualified_entries
3474
4753
  except QualifiedMetadataUnavailable:
3475
4754
  # A cached read must be internally coherent, but retain accounting
@@ -3478,19 +4757,31 @@ def build_codex_source_state(
3478
4757
  "Codex qualified metadata read became unavailable; using cache-only accounting fallback"
3479
4758
  )
3480
4759
  metadata_incomplete = True
4760
+ _lib_snapshot_cache.reset_codex_accounting_cache_state()
3481
4761
  accounting_entries = load_cached_rooted_codex_accounting_entries(
3482
4762
  accounting_start,
3483
4763
  accounting_end,
3484
4764
  speed=context.speed,
3485
4765
  cache_conn=context.cache_conn,
3486
4766
  )
4767
+ accounting_dirty_accounts = tuple(sorted({
4768
+ str(getattr(entry, "account_key", "") or
4769
+ _lib_accounts.UNATTRIBUTED)
4770
+ for entry in accounting_entries
4771
+ }))
3487
4772
  else:
4773
+ _lib_snapshot_cache.reset_codex_accounting_cache_state()
3488
4774
  accounting_entries = load_cached_rooted_codex_accounting_entries(
3489
4775
  accounting_start,
3490
4776
  accounting_end,
3491
4777
  speed=context.speed,
3492
4778
  cache_conn=context.cache_conn,
3493
4779
  )
4780
+ accounting_dirty_accounts = tuple(sorted({
4781
+ str(getattr(entry, "account_key", "") or
4782
+ _lib_accounts.UNATTRIBUTED)
4783
+ for entry in accounting_entries
4784
+ }))
3494
4785
  budget_entries = _codex_entries_from_accounting(accounting_entries)
3495
4786
  cycles_all: list[CodexCycleBoundary] = []
3496
4787
  try:
@@ -3535,12 +4826,66 @@ def build_codex_source_state(
3535
4826
  entry for entry in accounting_entries
3536
4827
  if context.range_start <= getattr(entry, "timestamp").astimezone(UTC) < accounting_end
3537
4828
  )
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,
4829
+ # #566 §5.1 item 2: one pass over the visible rows produces the merged
4830
+ # population and both per-account partitions. The children below reuse
4831
+ # these instead of re-partitioning and re-adapting the same rows.
4832
+ entries, visible_rows_by_account, visible_entries_by_account = (
4833
+ _codex_fold_visible_rows(visible_accounting_entries)
4834
+ )
4835
+ changed_old_visible = tuple(
4836
+ entry for entry in accounting_changed_old
4837
+ if context.range_start <= entry.timestamp.astimezone(UTC) < accounting_end
4838
+ )
4839
+ changed_new_visible = tuple(
4840
+ entry for entry in accounting_changed_new
4841
+ if context.range_start <= entry.timestamp.astimezone(UTC) < accounting_end
4842
+ )
4843
+ changed_old_entries = tuple(_codex_entries_from_accounting(changed_old_visible))
4844
+ changed_new_entries = tuple(_codex_entries_from_accounting(changed_new_visible))
4845
+
4846
+ def _changed_by_account(rows, converted):
4847
+ grouped: dict[str, list[CodexEntry]] = {}
4848
+ for row, entry in zip(rows, converted):
4849
+ key = str(getattr(row, "account_key", "") or
4850
+ _lib_accounts.UNATTRIBUTED)
4851
+ grouped.setdefault(key, []).append(entry)
4852
+ return {key: tuple(values) for key, values in grouped.items()}
4853
+
4854
+ changed_old_by_account = _changed_by_account(
4855
+ changed_old_visible, changed_old_entries)
4856
+ changed_new_by_account = _changed_by_account(
4857
+ changed_new_visible, changed_new_entries)
4858
+
4859
+ def _changed_rows_by_account(rows):
4860
+ grouped: dict[str, list[object]] = {}
4861
+ for row in rows:
4862
+ key = str(getattr(row, "account_key", "") or
4863
+ _lib_accounts.UNATTRIBUTED)
4864
+ grouped.setdefault(key, []).append(row)
4865
+ return {key: tuple(values) for key, values in grouped.items()}
4866
+
4867
+ changed_old_rows_by_account = _changed_rows_by_account(changed_old_visible)
4868
+ changed_new_rows_by_account = _changed_rows_by_account(changed_new_visible)
4869
+ # The published provider version also carries quota/stat generations.
4870
+ # Those generations must rebuild quota domains, but they are not accounting
4871
+ # semantics: folding them into these cache keys made one fresh quota sample
4872
+ # discard every clean period/session/project/account group. The accounting
4873
+ # population cache above owns upper-bound, root and speed invalidation; the
4874
+ # individual builders add their own tz/speed/period/cycle dimensions.
4875
+ period_signature = (
4876
+ "codex-accounting-v1", context.range_start, metadata_incomplete,
4877
+ )
4878
+ daily = _cached_codex_period_view(
4879
+ entries, changed_old=changed_old_entries,
4880
+ changed_new=changed_new_entries, kind="daily", cache_key=("parent",),
4881
+ semantic_signature=period_signature, now_utc=context.now_utc,
4882
+ tz_name=context.display_tz_name, speed=context.speed,
3541
4883
  )
3542
- monthly = build_codex_monthly_view(
3543
- entries, now_utc=context.now_utc, tz_name=context.display_tz_name, speed=context.speed,
4884
+ monthly = _cached_codex_period_view(
4885
+ entries, changed_old=changed_old_entries,
4886
+ changed_new=changed_new_entries, kind="monthly", cache_key=("parent",),
4887
+ semantic_signature=period_signature, now_utc=context.now_utc,
4888
+ tz_name=context.display_tz_name, speed=context.speed,
3544
4889
  )
3545
4890
  # R8 gate, resolved once before the parent weekly projection so that only
3546
4891
  # a decorated merged row gains the additive account axis. Focused children
@@ -3551,9 +4896,13 @@ def build_codex_source_state(
3551
4896
  context.stats_conn, "codex")
3552
4897
  except Exception:
3553
4898
  _codex_decorated = False
3554
- weekly = _build_codex_native_weekly_view(
4899
+ weekly = _cached_codex_native_weekly_view(
3555
4900
  context.stats_conn,
3556
4901
  visible_accounting_entries,
4902
+ changed_old=changed_old_visible,
4903
+ changed_new=changed_new_visible,
4904
+ cache_key=("parent",),
4905
+ semantic_signature=period_signature,
3557
4906
  source_root_keys=active_roots,
3558
4907
  active_cycle=cycle,
3559
4908
  now_utc=context.now_utc,
@@ -3568,8 +4917,11 @@ def build_codex_source_state(
3568
4917
  tz_name=context.display_tz_name,
3569
4918
  speed=context.speed,
3570
4919
  )
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,
4920
+ if metadata_incomplete else _cached_codex_session_view(
4921
+ entries, changed_old=changed_old_visible,
4922
+ changed_new=changed_new_visible, cache_key=("parent",),
4923
+ semantic_signature=period_signature, now_utc=context.now_utc,
4924
+ tz_name=context.display_tz_name, speed=context.speed,
3573
4925
  )
3574
4926
  )
3575
4927
  quota = _quota_read_model(
@@ -3611,9 +4963,10 @@ def build_codex_source_state(
3611
4963
  projected_budget_rows = _projected_budget_wire(
3612
4964
  context.stats_conn, decorated=_codex_decorated)
3613
4965
  budget_cost_events = _codex_budget_cost_events(context, budget_entries)
3614
- configured_budget = _configured_codex_budget_status(
4966
+ configured_budget_domain = _codex_budget_status_domain(
3615
4967
  context, budget_entries, cost_events=budget_cost_events,
3616
4968
  )
4969
+ configured_budget = configured_budget_domain["status"]
3617
4970
  conversation_metadata = _codex_conversation_metadata(context.cache_conn)
3618
4971
  cache_report = _codex_cache_report_wire(
3619
4972
  visible_accounting_entries,
@@ -3622,14 +4975,22 @@ def build_codex_source_state(
3622
4975
  display_tz_name=context.display_tz_name,
3623
4976
  speed=context.speed,
3624
4977
  anomaly_threshold_pp=context.cache_report_anomaly_threshold_pp,
4978
+ cache_key=("parent",),
4979
+ changed_old=changed_old_visible,
4980
+ changed_new=changed_new_visible,
4981
+ semantic_signature=period_signature,
3625
4982
  )
3626
4983
  projects = (
3627
4984
  _partial_projects_wire(visible_accounting_entries, conversation_metadata)
3628
- if metadata_incomplete else _projects_wire(
4985
+ if metadata_incomplete else _cached_projects_wire(
3629
4986
  context,
3630
4987
  quota_observations,
3631
4988
  visible_accounting_entries,
4989
+ changed_old=changed_old_visible,
4990
+ changed_new=changed_new_visible,
3632
4991
  accounting_end=accounting_end,
4992
+ cache_key=("parent",),
4993
+ semantic_signature=period_signature,
3633
4994
  )
3634
4995
  )
3635
4996
  alerts = _alerts_wire(context.stats_conn, decorated=_codex_decorated)
@@ -3677,6 +5038,9 @@ def build_codex_source_state(
3677
5038
  accounts_wire: list[dict[str, object]] = []
3678
5039
  hero_cycles_wire: list[dict[str, object]] = []
3679
5040
  account_scopes: dict[str, dict[str, object]] = {}
5041
+ # #556 S5 §3.8: bound OUTSIDE the try, because the degrade path below has to
5042
+ # be able to clear it, and the retained `clock_data` reads it either way.
5043
+ budget_events_by_account: dict[str, tuple[tuple[dt.datetime, float], ...]] = {}
3680
5044
  if _codex_decorated:
3681
5045
  try:
3682
5046
  accounts_wire, hero_cycles_wire = _codex_accounts_wire(
@@ -3704,17 +5068,19 @@ def build_codex_source_state(
3704
5068
  # Budget cost events are frozen per account over the CONFIGURED
3705
5069
  # budget window, which can start before `range_start` — so they come
3706
5070
  # from the full `accounting_entries`, not the visible slice.
3707
- budget_events_by_account = {
5071
+ budget_events_by_account = ({
3708
5072
  key: _codex_budget_cost_events(context, rows)
3709
5073
  for key, rows in _codex_partition_by_account(
3710
5074
  accounting_entries).items()
3711
- } if context.codex_budget is not None else {}
5075
+ } if context.codex_budget is not None else {})
3712
5076
  account_scopes = _codex_account_scopes_wire(
3713
5077
  context,
3714
5078
  account_keys=[str(card["accountKey"]) for card in accounts_wire],
3715
5079
  quota_observations=quota_observations,
3716
5080
  cycle_by_account=cycle_by_account,
3717
5081
  visible_accounting_entries=visible_accounting_entries,
5082
+ visible_rows_by_account=visible_rows_by_account,
5083
+ visible_entries_by_account=visible_entries_by_account,
3718
5084
  active_roots=active_roots,
3719
5085
  accounting_end=accounting_end,
3720
5086
  metadata_incomplete=metadata_incomplete,
@@ -3725,6 +5091,17 @@ def build_codex_source_state(
3725
5091
  budget_cost_events_by_account=budget_events_by_account,
3726
5092
  private_session_labels=private_session_labels,
3727
5093
  hero_failure=hero_failure,
5094
+ dirty_accounts=accounting_dirty_accounts,
5095
+ changed_old_by_account=changed_old_by_account,
5096
+ changed_new_by_account=changed_new_by_account,
5097
+ changed_old_rows_by_account=changed_old_rows_by_account,
5098
+ changed_new_rows_by_account=changed_new_rows_by_account,
5099
+ # Quota/stat generations are already represented by each
5100
+ # child's quota observations, cycle and alert/budget rows in
5101
+ # `_codex_account_scopes_wire`'s outer signature. Reuse the
5102
+ # accounting-only semantic key here so an unrelated account's
5103
+ # fresh quota sample cannot evict every clean child.
5104
+ scope_signature=period_signature,
3728
5105
  )
3729
5106
  # #416 QA P1-A — the "All accounts" Blocks panel is the UNION of
3730
5107
  # every account's 5-hour blocks. `_quota_wire` filters
@@ -3787,6 +5164,7 @@ def build_codex_source_state(
3787
5164
  accounts_wire = []
3788
5165
  hero_cycles_wire = []
3789
5166
  account_scopes = {}
5167
+ budget_events_by_account = {}
3790
5168
  # #416 QA P0-A — the "All accounts" headline is the MERGED spend and tokens
3791
5169
  # (spec §6, decision D6). Everything above resolves the hero from ONE
3792
5170
  # representative cycle (`cycles_all[0]` plus that cycle's own
@@ -3800,11 +5178,11 @@ def build_codex_source_state(
3800
5178
  # blanks them with a pointer to the cards. The merge is a SUM OF THE CARDS
3801
5179
  # rather than a fresh query, so the headline can never disagree with the
3802
5180
  # 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.
5181
+ # what its own card shows, over the bounded fallback window that card
5182
+ # publishes — #564). Gated on `_codex_decorated`, so a <=1-real-account
5183
+ # install keeps the single-cycle hero byte-for-byte (R8); gated on
5184
+ # `hero_failure`, so an unavailable hero stays unavailable rather than
5185
+ # gaining totals the rest of the envelope says are absent.
3808
5186
  if _codex_decorated and accounts_wire and not hero_failure:
3809
5187
  cycle_cost_usd = stable_sum(
3810
5188
  float(card["spendUsd"]) for card in accounts_wire)
@@ -3893,7 +5271,7 @@ def build_codex_source_state(
3893
5271
  "sessions": sessions_wire,
3894
5272
  "quota": quota,
3895
5273
  "budget": {
3896
- "status": configured_budget,
5274
+ **configured_budget_domain,
3897
5275
  "milestones": budget_rows,
3898
5276
  "projected": projected_budget_rows,
3899
5277
  },
@@ -3932,6 +5310,11 @@ def build_codex_source_state(
3932
5310
  },
3933
5311
  clock_data={
3934
5312
  "codex_budget_cost_events": budget_cost_events,
5313
+ # #556 S5 §3.8: the per-account tuples were computed at build and
5314
+ # discarded, so idle refresh had nothing to reclock a child budget
5315
+ # from. Empty for every undecorated install, which keeps the
5316
+ # retained carrier byte-neutral there.
5317
+ "codex_budget_cost_events_by_account": budget_events_by_account,
3935
5318
  # #350 spec §3.3: when the tick passes this instant it must rebuild
3936
5319
  # Codex authoritatively instead of idle-clocking or reusing, because
3937
5320
  # weekly-cycle resolution can change on identical frozen evidence.