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