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