cctally 1.97.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,12 +31,15 @@ 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,
37
38
  SourceDashboardState,
38
39
  SourceDashboardWarning,
39
40
  assess_codex_projection_coherence,
41
+ canonical_alerted_at,
42
+ canonical_alerted_at_sql,
40
43
  dashboard_resource_key,
41
44
  )
42
45
  from _lib_quota import (
@@ -51,6 +54,7 @@ from _lib_quota import (
51
54
  stale_after_seconds,
52
55
  )
53
56
  from _lib_jsonl import CodexEntry
57
+ from _lib_codex_account_adoption import ACCOUNT_WEEKLY_WINDOW_MINUTES
54
58
  from _lib_codex_pools import (
55
59
  codex_history_is_model_scoped,
56
60
  codex_model_scoped_quota_pool,
@@ -58,9 +62,10 @@ from _lib_codex_pools import (
58
62
  )
59
63
  from _lib_codex_conversation import _display_title as _codex_display_title
60
64
  from _lib_fmt import stable_sum
61
- from _lib_aggregators import _aggregate_codex_buckets
65
+ from _lib_aggregators import _aggregate_codex_buckets, codex_path_scope
62
66
  from _lib_five_hour import _FIVE_HOUR_JITTER_FLOOR_SECONDS
63
67
  from _lib_source_analytics import (
68
+ assign_collision_safe_project_labels,
64
69
  build_codex_project_result,
65
70
  collision_safe_project_label_map,
66
71
  )
@@ -74,6 +79,70 @@ from _lib_view_models import (
74
79
 
75
80
 
76
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
77
146
  SOURCE_HISTORY_LIMIT = 250
78
147
  DASHBOARD_QUOTA_OBSERVATION_LIMIT = 1000
79
148
  DASHBOARD_QUOTA_RECENT_DAYS = 35
@@ -416,7 +485,7 @@ def resolve_codex_cycle_detail_identity(
416
485
  ))
417
486
  if not active_roots:
418
487
  return identity
419
- observations = load_codex_quota_observations(
488
+ observations = _cached_codex_quota_observations(
420
489
  source_root_keys=active_roots,
421
490
  cache_conn=cache_conn,
422
491
  captured_at_or_after=(
@@ -742,6 +811,12 @@ class DashboardSourceSemantics:
742
811
  cache_report_anomaly_threshold_pp: int
743
812
  claude_identity: str
744
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
745
820
 
746
821
 
747
822
  def resolve_dashboard_source_semantics(
@@ -820,6 +895,10 @@ def resolve_dashboard_source_semantics(
820
895
  cache_report_anomaly_threshold_pp=cache_threshold,
821
896
  claude_identity=claude_identity,
822
897
  codex_identity=codex_identity,
898
+ claude_budget=MappingProxyType({
899
+ name: value for name, value in budget_config.items()
900
+ if name != "codex"
901
+ }),
823
902
  )
824
903
 
825
904
 
@@ -860,6 +939,33 @@ _RESOURCE_ROWS = {
860
939
  "block": ("quota", "blocks"),
861
940
  }
862
941
 
942
+ # #556 S2: additional collections a resource may ALSO be routed through.
943
+ #
944
+ # The primary above stays the capability gate — its absence is still
945
+ # `SourceCapabilityUnavailable`. These are searched only after the primary
946
+ # misses, and their own absence is an ordinary not-found rather than a
947
+ # capability failure, because a provider legitimately need not publish them (a
948
+ # Codex source has no aggregate sibling, and a Claude source whose bounded fold
949
+ # failed publishes none either).
950
+ #
951
+ # Projects needs one because `projects.rows` is the current SUBSCRIPTION WEEK
952
+ # while `projects.aggregate.rows` is folded over the thirty-day shared range.
953
+ # Without this the aggregate ranking would publish rows the drill-down route
954
+ # answers 404 for — in the committed `all-combined` fixture, four of six.
955
+ _RESOURCE_EXTRA_ROWS: Mapping[str, tuple[tuple[str, ...], ...]] = MappingProxyType({
956
+ "project": (("projects", "aggregate", "rows"),),
957
+ })
958
+
959
+
960
+ def _rows_at(data: Mapping, path: "tuple[str, ...]") -> "list | tuple":
961
+ """Read one optional nested rows collection, or an empty tuple."""
962
+ node: object = data
963
+ for step in path:
964
+ if not isinstance(node, Mapping):
965
+ return ()
966
+ node = node.get(step)
967
+ return node if isinstance(node, (list, tuple)) else ()
968
+
863
969
 
864
970
  def _public_copy(value: object) -> object:
865
971
  """Detach a bounded source row from its immutable published state."""
@@ -910,6 +1016,21 @@ def source_detail_lookup(
910
1016
  row for row in rows
911
1017
  if isinstance(row, Mapping) and row.get("key") == key
912
1018
  ]
1019
+ if not key_matches:
1020
+ # Only after the primary collection misses, so every key that resolves
1021
+ # today keeps resolving to the same row. The account-ownership branch
1022
+ # below does NOT see an unchanged candidate set — a key that resolves
1023
+ # only here reaches it as a row the primary collection never carried.
1024
+ # That leaks nothing, because an aggregate row carries no
1025
+ # `account_key` and the ownership check refuses a row it cannot
1026
+ # attribute, but the set is genuinely wider than it was.
1027
+ for path in _RESOURCE_EXTRA_ROWS.get(resource, ()):
1028
+ key_matches = [
1029
+ row for row in _rows_at(data, path)
1030
+ if isinstance(row, Mapping) and row.get("key") == key
1031
+ ]
1032
+ if key_matches:
1033
+ break
913
1034
  if not key_matches:
914
1035
  raise SourceResourceNotFound()
915
1036
  if account is not None:
@@ -969,14 +1090,55 @@ def codex_projection_coherence(
969
1090
  )
970
1091
 
971
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
+
972
1122
  def _codex_budget_cost_events(
973
1123
  context: DashboardReadContext,
974
1124
  entries: Iterable[object],
975
1125
  ) -> tuple[tuple[dt.datetime, float], ...]:
976
- """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
+ """
977
1135
  if context.codex_budget is None:
978
1136
  return ()
979
- _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 ()
980
1142
  c = sys.modules["cctally"]
981
1143
  events: list[tuple[dt.datetime, float]] = []
982
1144
  for entry in entries:
@@ -986,8 +1148,10 @@ def _codex_budget_cost_events(
986
1148
  timestamp = timestamp.astimezone(UTC)
987
1149
  if not start_at <= timestamp < end_at:
988
1150
  continue
1151
+ loaded_cost = getattr(entry, "cost_usd", None)
989
1152
  events.append((
990
1153
  timestamp,
1154
+ float(loaded_cost) if loaded_cost is not None else
991
1155
  c._calculate_codex_entry_cost(
992
1156
  str(getattr(entry, "model")),
993
1157
  int(getattr(entry, "input_tokens")),
@@ -1035,6 +1199,251 @@ def _period_wire(view: Any) -> dict[str, object]:
1035
1199
  }
1036
1200
 
1037
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
+
1038
1447
  def _codex_cache_report_wire(
1039
1448
  entries: Iterable[object],
1040
1449
  *,
@@ -1044,6 +1453,10 @@ def _codex_cache_report_wire(
1044
1453
  speed: str,
1045
1454
  anomaly_threshold_pp: int = 15,
1046
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,
1047
1460
  ) -> dict[str, object]:
1048
1461
  """Compute the canonical cache report from Codex's inclusive counters.
1049
1462
 
@@ -1058,6 +1471,7 @@ def _codex_cache_report_wire(
1058
1471
  wire = c._load_sibling("_lib_cache_report_wire")
1059
1472
  display_tz = ZoneInfo(display_tz_name) if display_tz_name else None
1060
1473
  cutoff = now_utc - dt.timedelta(days=window_days)
1474
+ bucket_tz = crk._resolve_bucket_tz(display_tz)
1061
1475
 
1062
1476
  def _tiered_cost(tokens: int, pricing: Mapping[str, object], base: str, above: str) -> float:
1063
1477
  if tokens <= 0:
@@ -1069,11 +1483,10 @@ def _codex_cache_report_wire(
1069
1483
  return threshold * base_rate + (tokens - threshold) * float(above_rate)
1070
1484
  return tokens * base_rate
1071
1485
 
1072
- wrapped = []
1073
- for entry in entries:
1486
+ def _wrap_entry(entry: object) -> object | None:
1074
1487
  timestamp = getattr(entry, "timestamp", None)
1075
1488
  if not isinstance(timestamp, dt.datetime) or timestamp < cutoff:
1076
- continue
1489
+ return None
1077
1490
  model = str(getattr(entry, "model", "") or "unknown")
1078
1491
  input_tokens = int(getattr(entry, "input_tokens", 0))
1079
1492
  cached_tokens = min(input_tokens, int(getattr(entry, "cached_input_tokens", 0)))
@@ -1100,7 +1513,7 @@ def _codex_cache_report_wire(
1100
1513
  or str(item_metadata.get("project_label") or "").strip()
1101
1514
  or "(unknown)"
1102
1515
  )
1103
- wrapped.append(SimpleNamespace(
1516
+ return SimpleNamespace(
1104
1517
  timestamp=timestamp,
1105
1518
  model=model,
1106
1519
  cost_usd=float(getattr(entry, "cost_usd", 0.0)),
@@ -1112,22 +1525,189 @@ def _codex_cache_report_wire(
1112
1525
  cache_saved_usd=saved,
1113
1526
  cache_wasted_usd=0.0,
1114
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),
1115
1531
  usage={
1116
1532
  "input_tokens": uncached_tokens,
1117
1533
  "output_tokens": int(getattr(entry, "output_tokens", 0)),
1118
1534
  "cache_creation_input_tokens": 0,
1119
1535
  "cache_read_input_tokens": cached_tokens,
1120
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()
1121
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 = []
1122
1703
 
1123
1704
  # One current day per invocation (#443 S3 F23): the focal day and the
1124
1705
  # entry filter below both resolve through the SAME zone the kernel
1125
1706
  # buckets by. ``display_tz or UTC`` diverged from host-local bucketing
1126
1707
  # on every non-UTC host, which published a fabricated spotlight and let
1127
1708
  # the breakdowns draw from a different entry population than the days.
1128
- bucket_tz = crk._resolve_bucket_tz(display_tz)
1129
1709
  today_iso = now_utc.astimezone(bucket_tz).strftime("%Y-%m-%d")
1130
- if not wrapped:
1710
+ if not wrapped and not cached_days:
1131
1711
  # An empty store measured nothing, so ``observed`` is False and
1132
1712
  # every applicable predicate is unevaluated. The client
1133
1713
  # short-circuits on ``is_empty`` before reading either, so this
@@ -1150,16 +1730,29 @@ def _codex_cache_report_wire(
1150
1730
  fourteen_day_efficiency_ratio=0.0, is_empty=True,
1151
1731
  )
1152
1732
 
1153
- result = crk._build_cache_report(
1154
- wrapped,
1155
- now_utc=now_utc,
1156
- window_days=window_days,
1157
- anomaly_threshold_pp=anomaly_threshold_pp,
1158
- anomaly_window_days=window_days,
1159
- display_tz=display_tz,
1160
- pricing=c.CODEX_MODEL_PRICING,
1161
- cost_calculator=lambda _model, _usage, _mode, cost: float(cost or 0.0),
1162
- )
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
+ )
1163
1756
  raw_rows = sorted(result.rows, key=lambda row: row.date or "", reverse=True)
1164
1757
  today_row = next((row for row in raw_rows if row.date == today_iso), None)
1165
1758
  # #443 F13/F14 — both charts label their rightmost element "Today"
@@ -1199,18 +1792,65 @@ def _codex_cache_report_wire(
1199
1792
  baseline = result.today_baseline_median
1200
1793
  today_hit = today_row.cache_hit_percent if today_row else 0.0
1201
1794
  kept_dates = {row["date"] for row in days}
1202
- kept_entries = [
1203
- entry for entry in wrapped
1204
- if entry.timestamp.astimezone(bucket_tz).strftime("%Y-%m-%d") in kept_dates
1205
- ]
1206
- by_project = crk._aggregate_cache_breakdown(
1207
- kept_entries, key_fn=lambda entry: entry.project_path,
1208
- pricing=c.CODEX_MODEL_PRICING,
1209
- )
1210
- by_model = crk._aggregate_cache_breakdown(
1211
- kept_entries, key_fn=lambda entry: entry.model,
1212
- pricing=c.CODEX_MODEL_PRICING,
1213
- )
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")
1214
1854
  seven = days[:7]
1215
1855
  saved_total = stable_sum(float(row["saved_usd"]) for row in days)
1216
1856
  wasted_total = stable_sum(float(row["wasted_usd"]) for row in days)
@@ -1715,6 +2355,15 @@ def _configured_codex_budget_status(
1715
2355
  with no configured budget therefore has no budget status at all (``None``),
1716
2356
  exactly as an unconfigured vendor does; the merged vendor status stays on the
1717
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.
1718
2367
  """
1719
2368
  config = context.codex_budget
1720
2369
  if config is None:
@@ -1725,6 +2374,8 @@ def _configured_codex_budget_status(
1725
2374
  if not isinstance(per_account, Mapping) or account_key not in per_account:
1726
2375
  return None
1727
2376
  amount_usd = per_account[account_key]
2377
+ if amount_usd is None:
2378
+ return None
1728
2379
  c = sys.modules["cctally"]
1729
2380
  period, start_at, end_at = _configured_codex_budget_window(context)
1730
2381
 
@@ -1736,35 +2387,75 @@ def _configured_codex_budget_status(
1736
2387
  return sum(cost for timestamp, cost in resolved_events if start <= timestamp < end)
1737
2388
 
1738
2389
  recent_start = max(start_at, context.now_utc - dt.timedelta(hours=24))
1739
- inputs = c.BudgetInputs(
1740
- 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,
1741
2396
  spent_usd=_sum_cost(start_at, context.now_utc),
1742
2397
  recent_24h_usd=_sum_cost(recent_start, context.now_utc),
1743
- week_start_at=start_at,
1744
- week_end_at=end_at,
1745
2398
  now=context.now_utc,
1746
- alert_thresholds=tuple(config["alert_thresholds"]),
2399
+ alert_thresholds=config["alert_thresholds"],
1747
2400
  )
1748
- status = c.compute_budget_status(inputs)
1749
- return {
1750
- "period": period,
1751
- "budget_usd": inputs.target_usd,
1752
- "spent_usd": status.spent_usd,
1753
- "remaining_usd": status.remaining_usd,
1754
- "consumption_pct": status.consumption_pct,
1755
- "verdict": status.verdict,
1756
- "low_confidence": status.low_confidence,
1757
- "window_start_at": start_at.astimezone(UTC).isoformat(),
1758
- "window_end_at": end_at.astimezone(UTC).isoformat(),
1759
- "recent_24h_usd": inputs.recent_24h_usd,
1760
- "alert_thresholds": inputs.alert_thresholds,
1761
- "pace": {
1762
- "daily_usd": status.daily_pace_usd,
1763
- "projected_low_usd": status.projected_eow_low_usd,
1764
- "projected_high_usd": status.projected_eow_high_usd,
1765
- "week_avg_projection_usd": status.week_avg_projection_usd,
1766
- },
1767
- }
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
+ }
1768
2459
 
1769
2460
 
1770
2461
  def _configured_codex_budget_window(
@@ -1951,7 +2642,7 @@ def _quota_read_model(
1951
2642
  # with whichever ACCOUNT's 5h observation happened to sort last.
1952
2643
  correlated_five_hour = tuple(
1953
2644
  observation
1954
- for observation in load_codex_quota_observations(
2645
+ for observation in _cached_codex_quota_observations(
1955
2646
  source_root_keys={identity.source_root_key},
1956
2647
  cache_conn=context.cache_conn,
1957
2648
  captured_at_or_after=block.nominal_start_at,
@@ -2319,19 +3010,50 @@ def refresh_codex_source_clock(
2319
3010
  scopes = data.get("account_scopes")
2320
3011
  scopes_changed = False
2321
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
+ )
2322
3021
  rebuilt_scopes = dict(scopes)
2323
3022
  for scope_key, scope in scopes.items():
2324
3023
  if not isinstance(scope, Mapping):
2325
3024
  continue
3025
+ rebuilt_scope: dict[str, object] | None = None
2326
3026
  scope_quota = scope.get("quota")
2327
- if not isinstance(scope_quota, Mapping):
2328
- continue
2329
- reclocked_scope_quota = _reclock_quota_domain(
2330
- scope_quota, now_utc=now_utc)
2331
- 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:
2332
3056
  continue
2333
- rebuilt_scope = dict(scope)
2334
- rebuilt_scope["quota"] = reclocked_scope_quota
2335
3057
  rebuilt_scopes[scope_key] = rebuilt_scope
2336
3058
  scopes_changed = True
2337
3059
  if scopes_changed:
@@ -2430,6 +3152,12 @@ def refresh_codex_source_clock(
2430
3152
  # combined figure fail closed on an idle tick that changed nothing else.
2431
3153
  account_scope=state.account_scope,
2432
3154
  private_session_labels=state.private_session_labels,
3155
+ # #556 S2 §3.6: the aggregate carrier travels with the rows it
3156
+ # describes. This clock refreshes presentation axes only and publishes
3157
+ # the SAME rows, so it must carry the range that bounded them —
3158
+ # dropping it here would withhold the aggregate on any idle tick whose
3159
+ # clock moved.
3160
+ aggregate_scope=state.aggregate_scope,
2433
3161
  )
2434
3162
  return state if refreshed_state == state else refreshed_state
2435
3163
 
@@ -2475,39 +3203,55 @@ def _alerts_wire(
2475
3203
  "accountLabel": label,
2476
3204
  }
2477
3205
 
3206
+ # #556 S3 §2.1/§2.4: the firing instant is what this wire filters on, what
3207
+ # the panel orders by and what it prints, so it is what every leg selects,
3208
+ # orders by and publishes. Two legs previously ordered, truncated and
3209
+ # published the CROSSING instant instead, which is a different moment: a
3210
+ # row that fired most recently but crossed longest ago was dropped at the
3211
+ # LIMIT and never reached the panel at all. `created_at` stays as an
3212
+ # equal-valued compatibility alias for a client reading a pre-v7 envelope.
3213
+ canon = canonical_alerted_at_sql()
3214
+
3215
+ def _instants(raw: object) -> dict[str, str]:
3216
+ value = canonical_alerted_at(raw)
3217
+ return {"alerted_at": value, "created_at": value}
3218
+
2478
3219
  try:
2479
- for period, threshold, consumption_pct, crossed_at, account_key in stats_conn.execute(
2480
- "SELECT period, threshold, consumption_pct, crossed_at_utc, account_key "
3220
+ for period, threshold, consumption_pct, crossed_at, alerted_at, account_key in stats_conn.execute(
3221
+ "SELECT period, threshold, consumption_pct, crossed_at_utc, alerted_at, account_key "
2481
3222
  "FROM budget_milestones WHERE vendor='codex' AND alerted_at IS NOT NULL "
2482
- "ORDER BY crossed_at_utc DESC, threshold DESC LIMIT ?",
3223
+ f"ORDER BY {canon} DESC, threshold DESC LIMIT ?",
2483
3224
  (SOURCE_HISTORY_LIMIT,),
2484
3225
  ):
2485
3226
  rows.append({
3227
+ # The resource key keeps the crossing instant it has always
3228
+ # carried: it is an opaque identity, and re-keying every
3229
+ # historical Codex alert row is not this session's change.
2486
3230
  "key": dashboard_resource_key("alert", "codex", "codex_budget", period, threshold, crossed_at),
2487
3231
  "source": "codex",
2488
3232
  "axis": "codex_budget", "period": period, "threshold": threshold,
2489
- "value": consumption_pct, "created_at": crossed_at,
3233
+ "value": consumption_pct, **_instants(alerted_at),
2490
3234
  **_account(account_key),
2491
3235
  })
2492
- for period, threshold, projected_value, crossed_at, account_key in stats_conn.execute(
2493
- "SELECT period, threshold, projected_value, crossed_at_utc, account_key "
3236
+ for period, threshold, projected_value, crossed_at, alerted_at, account_key in stats_conn.execute(
3237
+ "SELECT period, threshold, projected_value, crossed_at_utc, alerted_at, account_key "
2494
3238
  "FROM projected_milestones WHERE metric='codex_budget_usd' AND alerted_at IS NOT NULL "
2495
- "ORDER BY crossed_at_utc DESC, threshold DESC LIMIT ?",
3239
+ f"ORDER BY {canon} DESC, threshold DESC LIMIT ?",
2496
3240
  (SOURCE_HISTORY_LIMIT,),
2497
3241
  ):
2498
3242
  rows.append({
2499
3243
  "key": dashboard_resource_key("alert", "codex", "projected", period, threshold, crossed_at),
2500
3244
  "source": "codex",
2501
3245
  "axis": "projected", "period": period, "threshold": threshold,
2502
- "value": projected_value, "created_at": crossed_at,
3246
+ "value": projected_value, **_instants(alerted_at),
2503
3247
  **_account(account_key),
2504
3248
  })
2505
3249
  for (root_key, logical_key, observed_slot, window_minutes, resets_at,
2506
- threshold, severity, created_at, account_key) in stats_conn.execute(
3250
+ threshold, severity, created_at, alerted_at, account_key) in stats_conn.execute(
2507
3251
  "SELECT source_root_key, logical_limit_key, observed_slot, window_minutes, resets_at_utc, "
2508
- "threshold, severity, created_at_utc, account_key FROM quota_threshold_events "
3252
+ "threshold, severity, created_at_utc, alerted_at, account_key FROM quota_threshold_events "
2509
3253
  "WHERE source='codex' AND disposition='alerted' AND orphaned_at IS NULL "
2510
- "ORDER BY created_at_utc DESC, source_root_key, logical_limit_key, observed_slot, threshold "
3254
+ f"ORDER BY {canon} DESC, source_root_key, logical_limit_key, observed_slot, threshold "
2511
3255
  "LIMIT ?",
2512
3256
  (SOURCE_HISTORY_LIMIT,),
2513
3257
  ):
@@ -2518,34 +3262,78 @@ def _alerts_wire(
2518
3262
  ),
2519
3263
  "source": "codex",
2520
3264
  "axis": "quota", "threshold": threshold, "severity": severity,
2521
- "created_at": created_at,
3265
+ **_instants(alerted_at),
2522
3266
  **_account(account_key),
2523
3267
  })
2524
3268
  except sqlite3.Error:
2525
3269
  return ()
2526
3270
  return tuple(sorted(
2527
3271
  rows,
2528
- key=lambda item: str(item.get("created_at") or ""),
3272
+ key=lambda item: canonical_alerted_at(item["alerted_at"]),
2529
3273
  reverse=True,
2530
3274
  )[:SOURCE_HISTORY_LIMIT])
2531
3275
 
2532
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
+
2533
3320
  def _projects_wire(
2534
3321
  context: DashboardReadContext,
2535
- quota_observations: Iterable[object],
3322
+ _quota_observations: Iterable[object],
2536
3323
  entries: Iterable[object],
2537
3324
  *,
2538
3325
  accounting_end: dt.datetime,
3326
+ cache_key: object | None = None,
2539
3327
  ) -> dict[str, object]:
2540
3328
  """Adapt S3's already-qualified attribution result without re-formulas."""
2541
- qualified_entries = tuple(entries)
3329
+ qualified_entries = _cached_project_labeled_entries(
3330
+ tuple(entries), cache_key,
3331
+ )
2542
3332
  result = build_codex_project_result(
2543
3333
  qualified_entries,
2544
3334
  range_start=context.range_start,
2545
3335
  range_end=accounting_end,
2546
- blocks=build_blocks(quota_observations),
2547
3336
  as_of=context.now_utc,
2548
- allocation_entries=qualified_entries,
2549
3337
  )
2550
3338
  data = result.data
2551
3339
  if data is None:
@@ -2570,6 +3358,125 @@ def _projects_wire(
2570
3358
  }
2571
3359
 
2572
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
+
2573
3480
  def _partial_projects_wire(
2574
3481
  entries: Iterable[object],
2575
3482
  metadata: Mapping[tuple[str, str], Mapping[str, object]],
@@ -2678,15 +3585,23 @@ def _partial_projects_wire(
2678
3585
  }
2679
3586
 
2680
3587
 
3588
+ _CODEX_ENTRY_ADAPTER_CACHE: dict[int, tuple[object, CodexEntry]] = {}
3589
+
3590
+
2681
3591
  def _codex_entries_from_accounting(entries: Iterable[object]) -> list[CodexEntry]:
2682
3592
  """Adapt coordinated accounting rows for the shipped non-project kernels."""
2683
3593
  converted: list[CodexEntry] = []
2684
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
2685
3600
  source_path = str(getattr(entry, "source_path", "") or "")
2686
3601
  session_id = str(getattr(entry, "session_id", "") or "")
2687
3602
  if not source_path or not session_id:
2688
3603
  raise SourceCapabilityUnavailable("Codex accounting lacks session identity")
2689
- converted.append(CodexEntry(
3604
+ value = CodexEntry(
2690
3605
  timestamp=getattr(entry, "timestamp"),
2691
3606
  session_id=session_id,
2692
3607
  model=str(getattr(entry, "model")),
@@ -2696,7 +3611,17 @@ def _codex_entries_from_accounting(entries: Iterable[object]) -> list[CodexEntry
2696
3611
  reasoning_output_tokens=int(getattr(entry, "reasoning_output_tokens")),
2697
3612
  total_tokens=int(getattr(entry, "total_tokens")),
2698
3613
  source_path=source_path,
2699
- ))
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)
2700
3625
  return converted
2701
3626
 
2702
3627
 
@@ -2801,6 +3726,136 @@ def _build_codex_native_weekly_view(
2801
3726
  )
2802
3727
 
2803
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
+
2804
3859
  def _codex_account_five_hour_percent(
2805
3860
  observations: Iterable[object],
2806
3861
  now_utc: dt.datetime,
@@ -2847,8 +3902,9 @@ def _codex_accounts_wire(
2847
3902
  so the envelope stays byte-identical, spec R8). Each account carries
2848
3903
  ``{accountKey, label, plan, active, weeklyPercent, fiveHourPercent, resetsAt,
2849
3904
  spendUsd, inputTokens, cachedInputTokens, outputTokens,
2850
- reasoningOutputTokens, totalTokens, unattributed?}``; ``hero_cycles_wire`` is
2851
- 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[]``).
2852
3908
  """
2853
3909
  import _cctally_account
2854
3910
  active_keys = _cctally_account.resolve_active_account_keys()
@@ -2866,11 +3922,39 @@ def _codex_accounts_wire(
2866
3922
  reg = _cctally_account.load_accounts(context.stats_conn, "codex")
2867
3923
  plan_by_key = {r["account_key"]: r.get("plan_type") for r in reg}
2868
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
+ }
2869
3943
  # Include unattributed last iff it has cycle/5h/spend evidence.
2870
3944
  unattributed_rows = load_cached_rooted_codex_accounting_entries(
2871
3945
  accounting_start, accounting_end, speed=context.speed,
2872
3946
  cache_conn=context.cache_conn, account_key=_lib_accounts.UNATTRIBUTED,
2873
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
+ )
2874
3958
  if (
2875
3959
  unattributed_rows
2876
3960
  or _lib_accounts.UNATTRIBUTED in cycle_by_account
@@ -2912,12 +3996,14 @@ def _codex_accounts_wire(
2912
3996
  )
2913
3997
  totals = _totals(rows)
2914
3998
  elif is_unattributed:
2915
- totals = _totals(unattributed_rows)
3999
+ totals = _totals(unattributed_window_rows)
2916
4000
  else:
2917
- # A real account without a live weekly cycle: totals over the
2918
- # 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.
2919
4005
  rows = load_cached_rooted_codex_accounting_entries(
2920
- accounting_start, accounting_end, speed=context.speed,
4006
+ fallback_start, accounting_end, speed=context.speed,
2921
4007
  cache_conn=context.cache_conn, account_key=key,
2922
4008
  )
2923
4009
  totals = _totals(rows)
@@ -2944,6 +4030,12 @@ def _codex_accounts_wire(
2944
4030
  # cannot speak for a fresh sibling, and staleness is disclosure
2945
4031
  # only — the retained percentage, reset and spend remain useful.
2946
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
2947
4039
  accounts_wire.append(card)
2948
4040
  if cyc is not None and not is_unattributed:
2949
4041
  hero_cycles_wire.append({
@@ -2988,6 +4080,66 @@ def _codex_partition_by_account(
2988
4080
  return {key: tuple(values) for key, values in buckets.items()}
2989
4081
 
2990
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
+
2991
4143
  def _codex_account_scopes_wire(
2992
4144
  context: DashboardReadContext,
2993
4145
  *,
@@ -2995,6 +4147,8 @@ def _codex_account_scopes_wire(
2995
4147
  quota_observations: Iterable[object],
2996
4148
  cycle_by_account: Mapping[str, "CodexCycleBoundary"],
2997
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,
2998
4152
  active_roots: Iterable[str],
2999
4153
  accounting_end: dt.datetime,
3000
4154
  metadata_incomplete: bool,
@@ -3005,6 +4159,12 @@ def _codex_account_scopes_wire(
3005
4159
  budget_cost_events_by_account: Mapping[str, tuple[tuple[dt.datetime, float], ...]],
3006
4160
  private_session_labels: dict[str, str],
3007
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,
3008
4168
  ) -> dict[str, dict[str, object]]:
3009
4169
  """The per-account CHILDREN of the merged Codex read model (spec §5.3).
3010
4170
 
@@ -3035,7 +4195,15 @@ def _codex_account_scopes_wire(
3035
4195
  """
3036
4196
  visible = tuple(visible_accounting_entries)
3037
4197
  observations = tuple(quota_observations)
3038
- 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
3039
4207
  obs_partition: dict[str, list[object]] = {}
3040
4208
  for observation in observations:
3041
4209
  obs_partition.setdefault(
@@ -3044,19 +4212,29 @@ def _codex_account_scopes_wire(
3044
4212
  budget_rows = tuple(budget_milestones)
3045
4213
  projected_rows = tuple(projected_budget_milestones)
3046
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 {}
3047
4220
 
3048
4221
  def _for_account(key: str) -> dict[str, object]:
3049
4222
  rows = partition.get(key, ())
3050
4223
  account_observations = tuple(obs_partition.get(key, ()))
3051
- entries = _codex_entries_from_accounting(rows)
4224
+ entries = list(entries_partition.get(key, ()))
3052
4225
  cycle = cycle_by_account.get(key)
3053
4226
  sessions_view = (
3054
4227
  build_rooted_codex_session_view(
3055
4228
  rows, now_utc=context.now_utc,
3056
4229
  tz_name=context.display_tz_name, speed=context.speed,
3057
4230
  )
3058
- if metadata_incomplete else build_codex_session_view(
3059
- 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,
3060
4238
  tz_name=context.display_tz_name, speed=context.speed,
3061
4239
  )
3062
4240
  )
@@ -3099,16 +4277,31 @@ def _codex_account_scopes_wire(
3099
4277
  # quota window with no spend yet, and a retired one the reverse.
3100
4278
  "is_empty": not rows and not account_observations,
3101
4279
  "periods": {
3102
- "daily": _period_wire(build_codex_daily_view(
3103
- entries, now_utc=context.now_utc,
3104
- 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,
3105
4288
  )),
3106
- "monthly": _period_wire(build_codex_monthly_view(
3107
- entries, now_utc=context.now_utc,
3108
- 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,
3109
4297
  )),
3110
- "weekly": _period_wire(_build_codex_native_weekly_view(
3111
- 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,
3112
4305
  active_cycle=cycle, now_utc=context.now_utc,
3113
4306
  display_tz_name=context.display_tz_name, speed=context.speed,
3114
4307
  account_key=key,
@@ -3120,18 +4313,26 @@ def _codex_account_scopes_wire(
3120
4313
  ),
3121
4314
  "projects": (
3122
4315
  _partial_projects_wire(rows, conversation_metadata)
3123
- if metadata_incomplete else _projects_wire(
4316
+ if metadata_incomplete else _cached_projects_wire(
3124
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, ()),
3125
4320
  accounting_end=accounting_end,
4321
+ cache_key=("account", key),
4322
+ semantic_signature=scope_signature,
3126
4323
  )
3127
4324
  ),
3128
4325
  "cache_report": _codex_cache_report_wire(
3129
4326
  rows, metadata=conversation_metadata, now_utc=context.now_utc,
3130
4327
  display_tz_name=context.display_tz_name, speed=context.speed,
3131
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,
3132
4333
  ),
3133
4334
  "budget": {
3134
- "status": _configured_codex_budget_status(
4335
+ **_codex_budget_status_domain(
3135
4336
  context, rows,
3136
4337
  cost_events=budget_cost_events_by_account.get(key, ()),
3137
4338
  account_key=key,
@@ -3171,7 +4372,52 @@ def _codex_account_scopes_wire(
3171
4372
  (set(partition) | set(obs_partition) | _codex_block_account_keys(
3172
4373
  context.stats_conn, roots)) - set(ordered_keys)
3173
4374
  )
3174
- 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
3175
4421
 
3176
4422
 
3177
4423
  def _codex_block_account_keys(
@@ -3352,13 +4598,61 @@ def build_codex_source_state(
3352
4598
  No sync, rollout scan, CLI parser, or fallback is reachable from this
3353
4599
  adapter. Period and session arithmetic remains delegated to the shipped
3354
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.
3355
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:
3356
4650
  active_roots = tuple(sorted(
3357
4651
  str(row[0]) for row in context.cache_conn.execute(
3358
4652
  "SELECT source_root_key FROM codex_source_roots"
3359
4653
  )
3360
4654
  ))
3361
- quota_observations = load_codex_quota_observations(
4655
+ quota_observations = _cached_codex_quota_observations(
3362
4656
  source_root_keys=active_roots,
3363
4657
  cache_conn=context.cache_conn,
3364
4658
  captured_at_or_after=(
@@ -3379,8 +4673,23 @@ def build_codex_source_state(
3379
4673
  accounting_end = context.now_utc + dt.timedelta(microseconds=1)
3380
4674
  accounting_start = context.range_start
3381
4675
  if context.codex_budget is not None:
3382
- _period, budget_start, _budget_end = _configured_codex_budget_window(context)
3383
- 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)
3384
4693
  health = load_codex_project_metadata_health(
3385
4694
  cache_conn=context.cache_conn,
3386
4695
  start=accounting_start,
@@ -3395,15 +4704,51 @@ def build_codex_source_state(
3395
4704
  "run `cctally cache-sync --source codex --rebuild`."
3396
4705
  )
3397
4706
  qualified_entries: tuple[object, ...] = ()
4707
+ accounting_dirty_accounts: tuple[str, ...] = ()
4708
+ accounting_changed_old: tuple[object, ...] = ()
4709
+ accounting_changed_new: tuple[object, ...] = ()
3398
4710
  if not metadata_incomplete:
3399
4711
  try:
3400
- qualified_entries = load_qualified_codex_entries(
3401
- accounting_start,
3402
- accounting_end,
3403
- speed=context.speed,
3404
- sync=False,
4712
+ cached_accounting = _lib_snapshot_cache.build_cached_codex_accounting(
3405
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),
3406
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
3407
4752
  accounting_entries: tuple[object, ...] = qualified_entries
3408
4753
  except QualifiedMetadataUnavailable:
3409
4754
  # A cached read must be internally coherent, but retain accounting
@@ -3412,19 +4757,31 @@ def build_codex_source_state(
3412
4757
  "Codex qualified metadata read became unavailable; using cache-only accounting fallback"
3413
4758
  )
3414
4759
  metadata_incomplete = True
4760
+ _lib_snapshot_cache.reset_codex_accounting_cache_state()
3415
4761
  accounting_entries = load_cached_rooted_codex_accounting_entries(
3416
4762
  accounting_start,
3417
4763
  accounting_end,
3418
4764
  speed=context.speed,
3419
4765
  cache_conn=context.cache_conn,
3420
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
+ }))
3421
4772
  else:
4773
+ _lib_snapshot_cache.reset_codex_accounting_cache_state()
3422
4774
  accounting_entries = load_cached_rooted_codex_accounting_entries(
3423
4775
  accounting_start,
3424
4776
  accounting_end,
3425
4777
  speed=context.speed,
3426
4778
  cache_conn=context.cache_conn,
3427
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
+ }))
3428
4785
  budget_entries = _codex_entries_from_accounting(accounting_entries)
3429
4786
  cycles_all: list[CodexCycleBoundary] = []
3430
4787
  try:
@@ -3469,12 +4826,66 @@ def build_codex_source_state(
3469
4826
  entry for entry in accounting_entries
3470
4827
  if context.range_start <= getattr(entry, "timestamp").astimezone(UTC) < accounting_end
3471
4828
  )
3472
- entries = _codex_entries_from_accounting(visible_accounting_entries)
3473
- daily = build_codex_daily_view(
3474
- 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,
3475
4883
  )
3476
- monthly = build_codex_monthly_view(
3477
- 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,
3478
4889
  )
3479
4890
  # R8 gate, resolved once before the parent weekly projection so that only
3480
4891
  # a decorated merged row gains the additive account axis. Focused children
@@ -3485,9 +4896,13 @@ def build_codex_source_state(
3485
4896
  context.stats_conn, "codex")
3486
4897
  except Exception:
3487
4898
  _codex_decorated = False
3488
- weekly = _build_codex_native_weekly_view(
4899
+ weekly = _cached_codex_native_weekly_view(
3489
4900
  context.stats_conn,
3490
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,
3491
4906
  source_root_keys=active_roots,
3492
4907
  active_cycle=cycle,
3493
4908
  now_utc=context.now_utc,
@@ -3502,8 +4917,11 @@ def build_codex_source_state(
3502
4917
  tz_name=context.display_tz_name,
3503
4918
  speed=context.speed,
3504
4919
  )
3505
- if metadata_incomplete else build_codex_session_view(
3506
- 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,
3507
4925
  )
3508
4926
  )
3509
4927
  quota = _quota_read_model(
@@ -3545,9 +4963,10 @@ def build_codex_source_state(
3545
4963
  projected_budget_rows = _projected_budget_wire(
3546
4964
  context.stats_conn, decorated=_codex_decorated)
3547
4965
  budget_cost_events = _codex_budget_cost_events(context, budget_entries)
3548
- configured_budget = _configured_codex_budget_status(
4966
+ configured_budget_domain = _codex_budget_status_domain(
3549
4967
  context, budget_entries, cost_events=budget_cost_events,
3550
4968
  )
4969
+ configured_budget = configured_budget_domain["status"]
3551
4970
  conversation_metadata = _codex_conversation_metadata(context.cache_conn)
3552
4971
  cache_report = _codex_cache_report_wire(
3553
4972
  visible_accounting_entries,
@@ -3556,14 +4975,22 @@ def build_codex_source_state(
3556
4975
  display_tz_name=context.display_tz_name,
3557
4976
  speed=context.speed,
3558
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,
3559
4982
  )
3560
4983
  projects = (
3561
4984
  _partial_projects_wire(visible_accounting_entries, conversation_metadata)
3562
- if metadata_incomplete else _projects_wire(
4985
+ if metadata_incomplete else _cached_projects_wire(
3563
4986
  context,
3564
4987
  quota_observations,
3565
4988
  visible_accounting_entries,
4989
+ changed_old=changed_old_visible,
4990
+ changed_new=changed_new_visible,
3566
4991
  accounting_end=accounting_end,
4992
+ cache_key=("parent",),
4993
+ semantic_signature=period_signature,
3567
4994
  )
3568
4995
  )
3569
4996
  alerts = _alerts_wire(context.stats_conn, decorated=_codex_decorated)
@@ -3611,6 +5038,9 @@ def build_codex_source_state(
3611
5038
  accounts_wire: list[dict[str, object]] = []
3612
5039
  hero_cycles_wire: list[dict[str, object]] = []
3613
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], ...]] = {}
3614
5044
  if _codex_decorated:
3615
5045
  try:
3616
5046
  accounts_wire, hero_cycles_wire = _codex_accounts_wire(
@@ -3638,17 +5068,19 @@ def build_codex_source_state(
3638
5068
  # Budget cost events are frozen per account over the CONFIGURED
3639
5069
  # budget window, which can start before `range_start` — so they come
3640
5070
  # from the full `accounting_entries`, not the visible slice.
3641
- budget_events_by_account = {
5071
+ budget_events_by_account = ({
3642
5072
  key: _codex_budget_cost_events(context, rows)
3643
5073
  for key, rows in _codex_partition_by_account(
3644
5074
  accounting_entries).items()
3645
- } if context.codex_budget is not None else {}
5075
+ } if context.codex_budget is not None else {})
3646
5076
  account_scopes = _codex_account_scopes_wire(
3647
5077
  context,
3648
5078
  account_keys=[str(card["accountKey"]) for card in accounts_wire],
3649
5079
  quota_observations=quota_observations,
3650
5080
  cycle_by_account=cycle_by_account,
3651
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,
3652
5084
  active_roots=active_roots,
3653
5085
  accounting_end=accounting_end,
3654
5086
  metadata_incomplete=metadata_incomplete,
@@ -3659,6 +5091,17 @@ def build_codex_source_state(
3659
5091
  budget_cost_events_by_account=budget_events_by_account,
3660
5092
  private_session_labels=private_session_labels,
3661
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,
3662
5105
  )
3663
5106
  # #416 QA P1-A — the "All accounts" Blocks panel is the UNION of
3664
5107
  # every account's 5-hour blocks. `_quota_wire` filters
@@ -3721,6 +5164,7 @@ def build_codex_source_state(
3721
5164
  accounts_wire = []
3722
5165
  hero_cycles_wire = []
3723
5166
  account_scopes = {}
5167
+ budget_events_by_account = {}
3724
5168
  # #416 QA P0-A — the "All accounts" headline is the MERGED spend and tokens
3725
5169
  # (spec §6, decision D6). Everything above resolves the hero from ONE
3726
5170
  # representative cycle (`cycles_all[0]` plus that cycle's own
@@ -3734,11 +5178,11 @@ def build_codex_source_state(
3734
5178
  # blanks them with a pointer to the cards. The merge is a SUM OF THE CARDS
3735
5179
  # rather than a fresh query, so the headline can never disagree with the
3736
5180
  # strip it sits above (an account without a live cycle contributes exactly
3737
- # what its own card shows, over the accounting range the card's documented
3738
- # fallback). Gated on `_codex_decorated`, so a <=1-real-account install
3739
- # keeps the single-cycle hero byte-for-byte (R8); gated on `hero_failure`,
3740
- # so an unavailable hero stays unavailable rather than gaining totals the
3741
- # 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.
3742
5186
  if _codex_decorated and accounts_wire and not hero_failure:
3743
5187
  cycle_cost_usd = stable_sum(
3744
5188
  float(card["spendUsd"]) for card in accounts_wire)
@@ -3827,7 +5271,7 @@ def build_codex_source_state(
3827
5271
  "sessions": sessions_wire,
3828
5272
  "quota": quota,
3829
5273
  "budget": {
3830
- "status": configured_budget,
5274
+ **configured_budget_domain,
3831
5275
  "milestones": budget_rows,
3832
5276
  "projected": projected_budget_rows,
3833
5277
  },
@@ -3866,6 +5310,11 @@ def build_codex_source_state(
3866
5310
  },
3867
5311
  clock_data={
3868
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,
3869
5318
  # #350 spec §3.3: when the tick passes this instant it must rebuild
3870
5319
  # Codex authoritatively instead of idle-clocking or reusing, because
3871
5320
  # weekly-cycle resolution can change on identical frozen evidence.