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