cctally 1.99.1 → 1.101.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 +88 -0
- package/bin/_cctally_alerts.py +13 -2
- package/bin/_cctally_cache.py +3 -1
- package/bin/_cctally_cache_report.py +103 -6
- package/bin/_cctally_dashboard.py +2083 -356
- package/bin/_cctally_dashboard_envelope.py +115 -31
- package/bin/_cctally_dashboard_perf.py +433 -0
- package/bin/_cctally_dashboard_share.py +101 -29
- package/bin/_cctally_dashboard_sources.py +1072 -214
- package/bin/_cctally_db.py +30 -14
- package/bin/_cctally_diff.py +20 -0
- package/bin/_cctally_doctor.py +1331 -1148
- package/bin/_cctally_forecast.py +304 -96
- package/bin/_cctally_milestone_history.py +10 -2
- package/bin/_cctally_parser.py +70 -1
- package/bin/_cctally_project.py +155 -47
- package/bin/_cctally_quota.py +40 -21
- package/bin/_cctally_record.py +44 -3
- package/bin/_cctally_refresh.py +60 -10
- package/bin/_cctally_share.py +9 -2
- package/bin/_cctally_source_analytics.py +40 -4
- package/bin/_cctally_statusline.py +53 -7
- package/bin/_cctally_tui.py +723 -232
- package/bin/_cctally_update.py +28 -22
- package/bin/_lib_alert_scope.py +685 -0
- package/bin/_lib_alerts_payload.py +112 -7
- package/bin/_lib_cache_report.py +110 -1
- package/bin/_lib_codex_pools.py +20 -8
- package/bin/_lib_dashboard_sources.py +237 -30
- package/bin/_lib_doctor.py +37 -0
- package/bin/_lib_forecast.py +12 -4
- package/bin/_lib_jsonl.py +4 -2
- package/bin/_lib_perf.py +132 -3
- package/bin/_lib_pricing.py +8 -7
- package/bin/_lib_render.py +31 -3
- package/bin/_lib_share_templates.py +150 -55
- package/bin/_lib_snapshot_cache.py +71 -13
- package/bin/_lib_source_analytics.py +2 -2
- package/bin/_lib_source_identity.py +50 -2
- package/bin/_lib_subscription_weeks.py +65 -0
- package/bin/_lib_tick_stats.py +538 -0
- package/bin/cctally +29 -7
- package/dashboard/static/assets/dashboardStream.shared-worker-1XTMV3nr.js +1 -0
- package/dashboard/static/assets/index-D6Eb9KDn.js +97 -0
- package/dashboard/static/assets/index-i3g7g8zo.css +1 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +4 -1
- package/dashboard/static/assets/index-C5NBB2w9.js +0 -97
- package/dashboard/static/assets/index-hJP4wlIO.css +0 -1
|
@@ -15,6 +15,7 @@ from zoneinfo import ZoneInfo
|
|
|
15
15
|
|
|
16
16
|
from _cctally_core import get_week_start_name
|
|
17
17
|
from _cctally_quota import (
|
|
18
|
+
QuotaProjectionIncomplete,
|
|
18
19
|
assert_projection_readable,
|
|
19
20
|
codex_five_hour_percent_at_crossing,
|
|
20
21
|
codex_quota_breakdown,
|
|
@@ -40,6 +41,9 @@ from _lib_dashboard_sources import (
|
|
|
40
41
|
assess_codex_projection_coherence,
|
|
41
42
|
canonical_alerted_at,
|
|
42
43
|
canonical_alerted_at_sql,
|
|
44
|
+
claude_stats_digest,
|
|
45
|
+
combined_accounting_version,
|
|
46
|
+
codex_stats_digest,
|
|
43
47
|
dashboard_resource_key,
|
|
44
48
|
)
|
|
45
49
|
from _lib_quota import (
|
|
@@ -69,6 +73,7 @@ from _lib_source_analytics import (
|
|
|
69
73
|
build_codex_project_result,
|
|
70
74
|
collision_safe_project_label_map,
|
|
71
75
|
)
|
|
76
|
+
from _lib_source_identity import identity_path
|
|
72
77
|
from _lib_view_models import (
|
|
73
78
|
CodexWeeklyView,
|
|
74
79
|
build_codex_daily_view,
|
|
@@ -85,44 +90,184 @@ _CODEX_QUOTA_OBSERVATION_CACHE: dict[object, tuple[object, ...]] = {}
|
|
|
85
90
|
|
|
86
91
|
|
|
87
92
|
def reset_codex_quota_observation_cache() -> None:
|
|
88
|
-
"""
|
|
93
|
+
"""Discard #582's value-only quota-read memo. TEST HOOK — no production caller.
|
|
94
|
+
|
|
95
|
+
Since #583 S5 removed the per-build clear, nothing in the shipped code path
|
|
96
|
+
calls this: the memo is invalidated by its key moving, never by a caller
|
|
97
|
+
emptying it. It survives as the isolation hook the pytest fixtures and the
|
|
98
|
+
cold-reference comparisons use, and `_codex_source_caches` is what the
|
|
99
|
+
build's own checkpoint/restore goes through.
|
|
100
|
+
"""
|
|
89
101
|
_CODEX_QUOTA_OBSERVATION_CACHE.clear()
|
|
90
102
|
|
|
91
103
|
|
|
92
|
-
def
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
104
|
+
def _codex_quota_reuse_identity(
|
|
105
|
+
cache_conn, stats_conn, *, stats_identity: tuple | None = None,
|
|
106
|
+
) -> tuple | None:
|
|
107
|
+
"""The full invalidation basis for the cross-build quota memo (#583 S5).
|
|
108
|
+
|
|
109
|
+
The cache legs alone are not sufficient. ``_build_codex_source_state`` used
|
|
110
|
+
to discard this memo on every build precisely because a stats-side
|
|
111
|
+
decoration or account-registry change advances no cache.db leg, so a key
|
|
112
|
+
built only from cache.db would not move for a mutation that changes the
|
|
113
|
+
answer. Returning ``None`` means "cannot establish identity", and every
|
|
114
|
+
caller must treat that as a cold read rather than as a cache hit.
|
|
115
|
+
|
|
116
|
+
Six legs, in two halves. The cache.db half is the quota change ledger's
|
|
117
|
+
high-water sequence, the window-attribution revision and the ``main``
|
|
118
|
+
database path -- an unreadable ledger or an absent ledger table is not an
|
|
119
|
+
idle ledger, so both return ``None``. The stats.db half is the three
|
|
120
|
+
digests the build already computes for its own composite signature
|
|
121
|
+
(``bin/_cctally_tui.py``), reused rather than replaced by a fourth
|
|
122
|
+
identity.
|
|
123
|
+
|
|
124
|
+
``stats_identity`` is that reuse, made literal: the caller passes
|
|
125
|
+
``(codex_stats_digest, accounts_identity_digest, claude_stats_digest)``
|
|
126
|
+
already computed for its own signature and this function does not recompute
|
|
127
|
+
them. Deriving them here is not cheap -- the three helpers issue twelve
|
|
128
|
+
stats.db statements, several of them whole-relation scans with an ``ORDER
|
|
129
|
+
BY``, plus one uncached filesystem read per configured provider root once
|
|
130
|
+
the ``accounts`` registry is non-empty. Measured on a copy of the real store
|
|
131
|
+
(276,391 retained Codex quota rows), warm, five samples after one untimed
|
|
132
|
+
warm-up: 35.2 ms median, range 33.1-35.7 ms per derivation.
|
|
133
|
+
|
|
134
|
+
Without a supplied identity the digests are derived here, and then the
|
|
135
|
+
projection gate applies. ``codex_stats_digest``'s relations include
|
|
136
|
+
``quota_projection_state`` and ``quota_window_blocks``, whose read site is
|
|
137
|
+
classified ``gate_at_caller`` in ``_cctally_quota.PROJECTION_READ_SITE_
|
|
138
|
+
ACTIONS`` precisely because that kernel may not import the gate; this
|
|
139
|
+
function is therefore named in ``PROJECTION_GATE_CALLERS`` and runs it. An
|
|
140
|
+
incomplete projection returns ``None`` rather than propagating, because the
|
|
141
|
+
honest answer for a REUSE IDENTITY is "cannot establish identity" and the
|
|
142
|
+
fail-safe for that is a cold read. Nothing is suppressed by swallowing it:
|
|
143
|
+
every caller of this memo runs a gated projection read of its own within a
|
|
144
|
+
few statements -- ``codex_projection_coherence`` on the build path -- so the
|
|
145
|
+
retry signal still reaches whoever owns the transaction.
|
|
146
|
+
|
|
147
|
+
A ``stats_identity`` of the wrong shape is the one input that RAISES instead
|
|
148
|
+
of returning ``None``, and the two cases are different in kind. Everything
|
|
149
|
+
above is an environmental inability to establish an identity -- an absent
|
|
150
|
+
ledger table, an unreadable database, an incomplete projection -- for which
|
|
151
|
+
a cold read is the correct degrade. A caller that hands this function a
|
|
152
|
+
two-element tuple has a defect in the caller, and taking the cold path for
|
|
153
|
+
it would leave the memo permanently and silently disabled. It is unreachable
|
|
154
|
+
from the production callers, all of which pass either ``None`` or
|
|
155
|
+
``DashboardReadContext.stats_identity``.
|
|
156
|
+
"""
|
|
157
|
+
if not isinstance(cache_conn, sqlite3.Connection):
|
|
158
|
+
return None
|
|
159
|
+
if stats_identity is not None:
|
|
160
|
+
if not isinstance(stats_identity, tuple) or len(stats_identity) != 3:
|
|
161
|
+
raise ValueError(
|
|
162
|
+
"stats_identity must be the three-digest tuple or None")
|
|
163
|
+
elif not isinstance(stats_conn, sqlite3.Connection):
|
|
164
|
+
return None
|
|
97
165
|
try:
|
|
98
|
-
seq_row =
|
|
166
|
+
seq_row = cache_conn.execute(
|
|
99
167
|
"SELECT seq FROM sqlite_sequence "
|
|
100
168
|
"WHERE name='quota_window_change_log'"
|
|
101
169
|
).fetchone()
|
|
102
|
-
table =
|
|
170
|
+
table = cache_conn.execute(
|
|
103
171
|
"SELECT 1 FROM sqlite_master WHERE type='table' "
|
|
104
172
|
"AND name='quota_window_change_log'"
|
|
105
173
|
).fetchone()
|
|
106
|
-
revision_row =
|
|
174
|
+
revision_row = cache_conn.execute(
|
|
107
175
|
"SELECT value FROM cache_meta "
|
|
108
176
|
"WHERE key='codex_window_attribution_revision'"
|
|
109
177
|
).fetchone()
|
|
110
178
|
db_path = next(
|
|
111
|
-
str(row[2]) for row in
|
|
179
|
+
str(row[2]) for row in cache_conn.execute("PRAGMA database_list")
|
|
112
180
|
if str(row[1]) == "main"
|
|
113
181
|
)
|
|
114
182
|
except (sqlite3.Error, StopIteration):
|
|
115
|
-
return
|
|
183
|
+
return None
|
|
116
184
|
if table is None:
|
|
185
|
+
return None
|
|
186
|
+
if stats_identity is None:
|
|
187
|
+
try:
|
|
188
|
+
assert_projection_readable(stats_conn)
|
|
189
|
+
stats_identity = (
|
|
190
|
+
codex_stats_digest(stats_conn),
|
|
191
|
+
accounts_identity_digest(stats_conn),
|
|
192
|
+
claude_stats_digest(stats_conn),
|
|
193
|
+
)
|
|
194
|
+
except QuotaProjectionIncomplete:
|
|
195
|
+
return None
|
|
196
|
+
except sqlite3.Error:
|
|
197
|
+
return None
|
|
198
|
+
return (
|
|
199
|
+
db_path,
|
|
200
|
+
0 if seq_row is None else int(seq_row[0]),
|
|
201
|
+
"" if revision_row is None else str(revision_row[0]),
|
|
202
|
+
*stats_identity,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _cached_codex_quota_observations(**kwargs) -> tuple[object, ...]:
|
|
207
|
+
"""Reuse bounded quota reads while their dedicated mutation stream is idle.
|
|
208
|
+
|
|
209
|
+
#583 S5: the memo now survives across source builds, so the key must carry
|
|
210
|
+
the stats-side identity as well as the cache.db legs. ``stats_conn``,
|
|
211
|
+
``stats_identity`` and ``memoize`` are memo-only inputs; none is forwarded
|
|
212
|
+
to the loader. A caller that already holds the three digests passes
|
|
213
|
+
``stats_identity`` so they are not recomputed once per memo call.
|
|
214
|
+
|
|
215
|
+
``memoize=False`` is the opt-out for a key that CANNOT match, and it is a
|
|
216
|
+
cost decision rather than a correctness one. A caller whose key carries the
|
|
217
|
+
tick instant -- ``active_at=now`` and a ``now``-derived
|
|
218
|
+
``captured_at_or_after`` -- moves both terms on every call, so it can never
|
|
219
|
+
read a prior entry. Going through the memo anyway would establish an
|
|
220
|
+
identity it cannot use (35 ms of stats.db work when no ``stats_identity`` is
|
|
221
|
+
supplied) and retain a result nobody can hit, which also evicts the
|
|
222
|
+
clock-free entries that DO reuse, because the memo is discarded whole on
|
|
223
|
+
reaching 32 entries.
|
|
224
|
+
|
|
225
|
+
THE GENERATION CONTRACT IS WEAKER THAN IT WAS, and the next reader of
|
|
226
|
+
``_tui_build_source_bundle``'s pin needs to know exactly how. This dict is a
|
|
227
|
+
plain module global with no ``_assert_owner``, and the per-build clear that
|
|
228
|
+
used to stand in ``_build_codex_source_state`` is gone, so an entry
|
|
229
|
+
populated on a non-builder path -- the share render or the per-request
|
|
230
|
+
cycle-detail route, from an unpinned connection at whatever cache generation
|
|
231
|
+
it saw -- can now be served to the builder INSIDE its pinned read
|
|
232
|
+
transaction. Before #583 S5 every cache-backed read inside that pin was
|
|
233
|
+
physically issued on the pinned connection.
|
|
234
|
+
|
|
235
|
+
What still holds, and why that is safe:
|
|
236
|
+
|
|
237
|
+
* Every read the builder issues ITSELF still runs on the pinned connection
|
|
238
|
+
and sees the frozen generation. Only a memo HIT returns rows another
|
|
239
|
+
caller loaded, and after the ``memoize=False`` sites above the sole read
|
|
240
|
+
that can hit inside a build is the five-hour correlation read.
|
|
241
|
+
* A hit requires the whole key to match, and the key is the invalidation
|
|
242
|
+
basis: the ``main`` database path, ``quota_window_change_log``'s
|
|
243
|
+
high-water sequence, ``codex_window_attribution_revision`` and the three
|
|
244
|
+
stats digests. Every ``quota_window_snapshots`` mutation is recorded by
|
|
245
|
+
the three ledger triggers in ``bin/_cctally_db.py``
|
|
246
|
+
(``trg_qws_ledger_ins`` / ``_del`` / ``_upd``, the last firing on the
|
|
247
|
+
semantic column set the loader interprets), and the attribution overlay
|
|
248
|
+
bumps its revision in the same transaction as the rows it applies
|
|
249
|
+
(``bin/_cctally_journal.py``). A retained value is therefore served only
|
|
250
|
+
while the evidence it was derived from has not moved, whatever generation
|
|
251
|
+
the connection that loaded it was on.
|
|
252
|
+
* The value is a tuple of frozen ``QuotaObservation``s, so a hit hands back
|
|
253
|
+
no cursor, connection or lazily-read state belonging to the other caller.
|
|
254
|
+
"""
|
|
255
|
+
stats_conn = kwargs.pop("stats_conn", None)
|
|
256
|
+
stats_identity = kwargs.pop("stats_identity", None)
|
|
257
|
+
memoize = bool(kwargs.pop("memoize", True))
|
|
258
|
+
conn = kwargs.get("cache_conn")
|
|
259
|
+
if not memoize or not isinstance(conn, sqlite3.Connection):
|
|
260
|
+
return load_codex_quota_observations(**kwargs)
|
|
261
|
+
identity = _codex_quota_reuse_identity(
|
|
262
|
+
conn, stats_conn, stats_identity=stats_identity)
|
|
263
|
+
if identity is None:
|
|
117
264
|
return load_codex_quota_observations(**kwargs)
|
|
118
265
|
roots = kwargs.get("source_root_keys")
|
|
119
266
|
physical_signatures = kwargs.get("physical_signatures")
|
|
120
267
|
physical_groups = kwargs.get("physical_groups")
|
|
121
268
|
key = (
|
|
122
269
|
id(load_codex_quota_observations),
|
|
123
|
-
|
|
124
|
-
0 if seq_row is None else int(seq_row[0]),
|
|
125
|
-
"" if revision_row is None else str(revision_row[0]),
|
|
270
|
+
identity,
|
|
126
271
|
None if roots is None else tuple(sorted(str(root) for root in roots)),
|
|
127
272
|
kwargs.get("captured_at_or_after"),
|
|
128
273
|
kwargs.get("active_at"),
|
|
@@ -139,6 +284,27 @@ def _cached_codex_quota_observations(**kwargs) -> tuple[object, ...]:
|
|
|
139
284
|
if cached is not None:
|
|
140
285
|
return cached
|
|
141
286
|
loaded = tuple(load_codex_quota_observations(**kwargs))
|
|
287
|
+
# The written memory bound (#583 S5 spec §2.1 "Memory"). Now that this memo
|
|
288
|
+
# survives a build, "32 entries" is a bound on ENTRIES and says nothing on
|
|
289
|
+
# its own about rows, so the worst-case retained row count is stated here:
|
|
290
|
+
#
|
|
291
|
+
# * at most 32 results are retained, and the whole memo is discarded on
|
|
292
|
+
# reaching that count rather than evicted one entry at a time;
|
|
293
|
+
# * a bounded dashboard read contributes at most
|
|
294
|
+
# `DASHBOARD_QUOTA_OBSERVATION_LIMIT` (1,000) `QuotaObservation`s;
|
|
295
|
+
# * the five-hour correlation read in `_quota_read_model` carries NO
|
|
296
|
+
# `max_rows`. It is bounded only by one root's retained observations
|
|
297
|
+
# captured at or after one weekly block's nominal start, which is the
|
|
298
|
+
# term that actually sizes this cache, and it grows with the block's
|
|
299
|
+
# age because an older block's nominal start reaches further back.
|
|
300
|
+
# Measured on a store holding 276,391 Codex quota rows, over the seven
|
|
301
|
+
# weekly blocks live at that instant: 15,129 observations for the
|
|
302
|
+
# newest and 36,943 for the oldest. The worst case is therefore 32
|
|
303
|
+
# times a population in the tens of thousands, not 32 times 1,000.
|
|
304
|
+
# * the doctor's unbounded `latest_per_identity` read is NOT retained
|
|
305
|
+
# here. `bin/_cctally_doctor.py` calls `load_codex_quota_observations`
|
|
306
|
+
# directly rather than through this memo, so its all-history population
|
|
307
|
+
# never enters this dict.
|
|
142
308
|
if len(_CODEX_QUOTA_OBSERVATION_CACHE) >= 32:
|
|
143
309
|
_CODEX_QUOTA_OBSERVATION_CACHE.clear()
|
|
144
310
|
_CODEX_QUOTA_OBSERVATION_CACHE[key] = loaded
|
|
@@ -327,10 +493,10 @@ def _resolve_codex_weekly_cycle(
|
|
|
327
493
|
|
|
328
494
|
#350 — FRESH-FIRST ranking (spec §3.2). Codex has no background quota poll,
|
|
329
495
|
so ``stale_after_seconds(10_080) == 3600`` makes an idle weekly observation
|
|
330
|
-
stale after exactly one hour. Discarding a stale-but-
|
|
496
|
+
stale after exactly one hour. Discarding a stale-but-ACTIVE boundary blanked
|
|
331
497
|
the hero's backward-looking actuals even though the spend was never lost, so
|
|
332
|
-
each account's
|
|
333
|
-
and a stale set and ranked:
|
|
498
|
+
each account's weekly boundaries that contain ``now`` are collected into a
|
|
499
|
+
fresh set and a stale set and ranked:
|
|
334
500
|
|
|
335
501
|
1. exactly one FRESH boundary -> valid, cycle fresh;
|
|
336
502
|
2. else, no fresh boundaries and exactly one STALE boundary -> valid, cycle
|
|
@@ -363,7 +529,15 @@ def _resolve_codex_weekly_cycle(
|
|
|
363
529
|
# current-cycle milestone filter matches `resets_at` exactly, so the
|
|
364
530
|
# ladder empties for precisely the jittered cycles canonicalization
|
|
365
531
|
# exists to collapse. Liveness rides the same instant the hero shows.
|
|
366
|
-
|
|
532
|
+
# #599: a future reset is not active when its nominal start is also in
|
|
533
|
+
# the future; admitting it makes the bounded accounting range empty.
|
|
534
|
+
if (
|
|
535
|
+
baseline is None
|
|
536
|
+
or baseline.canonical_resets_at <= now_utc
|
|
537
|
+
or baseline.canonical_resets_at - dt.timedelta(
|
|
538
|
+
minutes=history.identity.window_minutes
|
|
539
|
+
) >= now_utc
|
|
540
|
+
):
|
|
367
541
|
continue
|
|
368
542
|
state = quota_freshness(history.physical_observations, now_utc).state
|
|
369
543
|
boundary = (history.identity.window_minutes, baseline.canonical_resets_at)
|
|
@@ -435,6 +609,7 @@ def resolve_codex_cycle_detail_identity(
|
|
|
435
609
|
source_root_keys: Iterable[str],
|
|
436
610
|
now_utc: dt.datetime,
|
|
437
611
|
account_key: str | None = None,
|
|
612
|
+
stats_conn=None,
|
|
438
613
|
):
|
|
439
614
|
"""The live-cycle identity for a per-request Codex cycle-DETAIL read (#373).
|
|
440
615
|
|
|
@@ -488,6 +663,10 @@ def resolve_codex_cycle_detail_identity(
|
|
|
488
663
|
observations = _cached_codex_quota_observations(
|
|
489
664
|
source_root_keys=active_roots,
|
|
490
665
|
cache_conn=cache_conn,
|
|
666
|
+
stats_conn=stats_conn,
|
|
667
|
+
# Both bounds are derived from this request's own instant, so this
|
|
668
|
+
# key can never match a retained entry. See `memoize` there.
|
|
669
|
+
memoize=False,
|
|
491
670
|
captured_at_or_after=(
|
|
492
671
|
now_utc - dt.timedelta(days=DASHBOARD_QUOTA_RECENT_DAYS)
|
|
493
672
|
),
|
|
@@ -918,6 +1097,14 @@ class DashboardReadContext:
|
|
|
918
1097
|
codex_quota_actual_thresholds: tuple[int, ...] = ()
|
|
919
1098
|
codex_quota_projected_thresholds: tuple[int, ...] = ()
|
|
920
1099
|
cache_report_anomaly_threshold_pp: int = 15
|
|
1100
|
+
#: ``(codex_stats_digest, accounts_identity_digest, claude_stats_digest)``
|
|
1101
|
+
#: when the caller already computed them for its own composite signature,
|
|
1102
|
+
#: so the quota memo's reuse identity does not derive a second copy once
|
|
1103
|
+
#: per call (#583 S5 spec §2.1 item 1, Preserve 2). ``None`` means the
|
|
1104
|
+
#: identity derives them itself under the projection gate; that is the
|
|
1105
|
+
#: share path and the per-request cycle-detail route, neither of which
|
|
1106
|
+
#: computes a build signature.
|
|
1107
|
+
stats_identity: tuple[str, str, str] | None = None
|
|
921
1108
|
|
|
922
1109
|
def __post_init__(self) -> None:
|
|
923
1110
|
for name in ("range_start", "now_utc"):
|
|
@@ -2073,7 +2260,9 @@ def _session_wire(
|
|
|
2073
2260
|
# session paths from distinct $CODEX_HOME roots. The opaque detail
|
|
2074
2261
|
# key must use that same grouping identity or two visible rows route
|
|
2075
2262
|
# to one another's detail payload.
|
|
2076
|
-
root_identity =
|
|
2263
|
+
root_identity = (
|
|
2264
|
+
identity_path(row.codex_root) if row.codex_root else "single-root"
|
|
2265
|
+
)
|
|
2077
2266
|
row_metadata = (metadata or {}).get((str(row.codex_root or ""), str(row.session_id_path)))
|
|
2078
2267
|
if row_metadata is None and metadata is not None:
|
|
2079
2268
|
row_metadata = next((
|
|
@@ -2430,11 +2619,25 @@ def _codex_budget_status_domain(
|
|
|
2430
2619
|
``{"status": None}`` for it made the client render "No budget set." beside
|
|
2431
2620
|
the command that sets one — to a user who has budgets set. It publishes the
|
|
2432
2621
|
same ``not_configured.disposition`` the Claude half publishes, which the
|
|
2433
|
-
client already handles.
|
|
2434
|
-
``
|
|
2435
|
-
|
|
2622
|
+
client already handles. #586 gives the genuinely-unset ACCOUNT-SCOPED read
|
|
2623
|
+
its own ``account_budget_unset`` disposition plus immutable account key, so
|
|
2624
|
+
the card can name ``budget.codex.accounts`` rather than the unrelated
|
|
2625
|
+
vendor-wide command.
|
|
2436
2626
|
"""
|
|
2437
2627
|
config = context.codex_budget
|
|
2628
|
+
if account_key is not None:
|
|
2629
|
+
per_account = config.get("accounts") if isinstance(config, Mapping) else None
|
|
2630
|
+
if not isinstance(per_account, Mapping) or account_key not in per_account:
|
|
2631
|
+
return {
|
|
2632
|
+
"status": None,
|
|
2633
|
+
"not_configured": {
|
|
2634
|
+
"disposition": "account_budget_unset",
|
|
2635
|
+
"account_key": account_key,
|
|
2636
|
+
# `config set` replaces this map wholesale. Preserve every
|
|
2637
|
+
# sibling in the client-rendered example command (#586).
|
|
2638
|
+
"configured_accounts": dict(per_account or {}),
|
|
2639
|
+
},
|
|
2640
|
+
}
|
|
2438
2641
|
if (
|
|
2439
2642
|
account_key is None
|
|
2440
2643
|
and isinstance(config, Mapping)
|
|
@@ -2506,6 +2709,86 @@ def _codex_account_admits(scope_key: str | None, row_key: object) -> bool:
|
|
|
2506
2709
|
return key in (scope_key, _lib_accounts.UNATTRIBUTED)
|
|
2507
2710
|
|
|
2508
2711
|
|
|
2712
|
+
def _quota_breakdown_capture_key(
|
|
2713
|
+
account_key: str | None,
|
|
2714
|
+
identity: QuotaWindowIdentity,
|
|
2715
|
+
resets_at: dt.datetime,
|
|
2716
|
+
) -> tuple[object, ...]:
|
|
2717
|
+
return (
|
|
2718
|
+
account_key,
|
|
2719
|
+
identity.source_root_key,
|
|
2720
|
+
identity.logical_limit_key,
|
|
2721
|
+
identity.observed_slot,
|
|
2722
|
+
identity.window_minutes,
|
|
2723
|
+
identity.account_key,
|
|
2724
|
+
resets_at.astimezone(UTC).isoformat(),
|
|
2725
|
+
)
|
|
2726
|
+
|
|
2727
|
+
|
|
2728
|
+
def _capture_quota_breakdown_evidence(
|
|
2729
|
+
context: DashboardReadContext,
|
|
2730
|
+
observations: Iterable[object],
|
|
2731
|
+
) -> Mapping[tuple[object, ...], tuple[tuple, tuple]]:
|
|
2732
|
+
"""Capture every fold-dependent weekly read from one cache generation.
|
|
2733
|
+
|
|
2734
|
+
The account partitions and `build_blocks` fold are intentionally cheap and
|
|
2735
|
+
may run inside the pin: their only purpose is to enumerate the bounded
|
|
2736
|
+
identity set whose durable breakdown and 5h-correlation reads cannot be
|
|
2737
|
+
issued after rollback. Expensive row aggregation remains in
|
|
2738
|
+
`_quota_read_model` and consumes these values later.
|
|
2739
|
+
"""
|
|
2740
|
+
values = tuple(observations)
|
|
2741
|
+
partitions: list[tuple[str | None, tuple[object, ...]]] = [(None, values)]
|
|
2742
|
+
account_keys = sorted({
|
|
2743
|
+
str(observation.identity.account_key) for observation in values
|
|
2744
|
+
})
|
|
2745
|
+
partitions.extend((key, tuple(
|
|
2746
|
+
observation for observation in values
|
|
2747
|
+
if str(observation.identity.account_key) == key
|
|
2748
|
+
)) for key in account_keys)
|
|
2749
|
+
captured: dict[tuple[object, ...], tuple[tuple, tuple]] = {}
|
|
2750
|
+
for account_key, partition in partitions:
|
|
2751
|
+
for block in build_blocks(partition):
|
|
2752
|
+
identity = block.identity
|
|
2753
|
+
if identity.window_minutes != 10_080 or block.resets_at <= context.now_utc:
|
|
2754
|
+
continue
|
|
2755
|
+
key = _quota_breakdown_capture_key(
|
|
2756
|
+
account_key, identity, block.resets_at)
|
|
2757
|
+
try:
|
|
2758
|
+
canonical_rows = codex_quota_breakdown(
|
|
2759
|
+
identity,
|
|
2760
|
+
block.resets_at,
|
|
2761
|
+
speed=context.speed,
|
|
2762
|
+
cache_conn=context.cache_conn,
|
|
2763
|
+
stats_conn=context.stats_conn,
|
|
2764
|
+
account_key=account_key,
|
|
2765
|
+
)
|
|
2766
|
+
except sqlite3.Error:
|
|
2767
|
+
canonical_rows = ()
|
|
2768
|
+
correlated_five_hour: tuple = ()
|
|
2769
|
+
if canonical_rows:
|
|
2770
|
+
try:
|
|
2771
|
+
correlated_five_hour = tuple(
|
|
2772
|
+
observation
|
|
2773
|
+
for observation in _cached_codex_quota_observations(
|
|
2774
|
+
source_root_keys={identity.source_root_key},
|
|
2775
|
+
cache_conn=context.cache_conn,
|
|
2776
|
+
stats_conn=context.stats_conn,
|
|
2777
|
+
stats_identity=context.stats_identity,
|
|
2778
|
+
captured_at_or_after=block.nominal_start_at,
|
|
2779
|
+
)
|
|
2780
|
+
if observation.identity.window_minutes == 300
|
|
2781
|
+
and observation.identity.observed_slot == identity.observed_slot
|
|
2782
|
+
and observation.identity.limit_id == identity.limit_id
|
|
2783
|
+
and _codex_account_admits(
|
|
2784
|
+
account_key, observation.identity.account_key)
|
|
2785
|
+
)
|
|
2786
|
+
except sqlite3.Error:
|
|
2787
|
+
correlated_five_hour = ()
|
|
2788
|
+
captured[key] = (tuple(canonical_rows), correlated_five_hour)
|
|
2789
|
+
return MappingProxyType(captured)
|
|
2790
|
+
|
|
2791
|
+
|
|
2509
2792
|
def _quota_read_model(
|
|
2510
2793
|
context: DashboardReadContext,
|
|
2511
2794
|
observations: Iterable[object],
|
|
@@ -2513,6 +2796,7 @@ def _quota_read_model(
|
|
|
2513
2796
|
accounting_entries: Iterable[object] = (),
|
|
2514
2797
|
account_key: str | None = None,
|
|
2515
2798
|
decorated: bool,
|
|
2799
|
+
captured_breakdowns: "Mapping[tuple[object, ...], tuple[tuple, tuple]] | None" = None,
|
|
2516
2800
|
) -> dict[str, object]:
|
|
2517
2801
|
"""Use S2's pure history/block/forecast kernels over cache evidence.
|
|
2518
2802
|
|
|
@@ -2625,41 +2909,52 @@ def _quota_read_model(
|
|
|
2625
2909
|
< block.resets_at
|
|
2626
2910
|
)
|
|
2627
2911
|
canonical_rows = ()
|
|
2912
|
+
correlated_five_hour = ()
|
|
2628
2913
|
if identity.window_minutes == 10_080 and block.resets_at > context.now_utc:
|
|
2629
|
-
|
|
2630
|
-
canonical_rows =
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
cache_conn=context.cache_conn,
|
|
2635
|
-
stats_conn=context.stats_conn,
|
|
2636
|
-
account_key=account_key,
|
|
2914
|
+
if captured_breakdowns is not None:
|
|
2915
|
+
canonical_rows, correlated_five_hour = captured_breakdowns.get(
|
|
2916
|
+
_quota_breakdown_capture_key(
|
|
2917
|
+
account_key, identity, block.resets_at),
|
|
2918
|
+
((), ()),
|
|
2637
2919
|
)
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
try:
|
|
2645
|
-
# #416 Slice 3A review F4: this load is bounded by root, slot
|
|
2646
|
-
# and `limit_id` only, so under focus the crossing was annotated
|
|
2647
|
-
# with whichever ACCOUNT's 5h observation happened to sort last.
|
|
2648
|
-
correlated_five_hour = tuple(
|
|
2649
|
-
observation
|
|
2650
|
-
for observation in _cached_codex_quota_observations(
|
|
2651
|
-
source_root_keys={identity.source_root_key},
|
|
2920
|
+
else:
|
|
2921
|
+
try:
|
|
2922
|
+
canonical_rows = codex_quota_breakdown(
|
|
2923
|
+
identity,
|
|
2924
|
+
block.resets_at,
|
|
2925
|
+
speed=context.speed,
|
|
2652
2926
|
cache_conn=context.cache_conn,
|
|
2653
|
-
|
|
2927
|
+
stats_conn=context.stats_conn,
|
|
2928
|
+
account_key=account_key,
|
|
2654
2929
|
)
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2930
|
+
except sqlite3.Error:
|
|
2931
|
+
# Older or partially migrated stores retain the bounded
|
|
2932
|
+
# observation-derived fallback below. A coherent current
|
|
2933
|
+
# store always has the durable projection used by the CLI.
|
|
2934
|
+
canonical_rows = ()
|
|
2935
|
+
if canonical_rows:
|
|
2936
|
+
if captured_breakdowns is None:
|
|
2937
|
+
try:
|
|
2938
|
+
# #416 Slice 3A review F4: this load is bounded by root,
|
|
2939
|
+
# slot and `limit_id` only, so under focus the crossing was
|
|
2940
|
+
# once annotated with whichever account sorted last.
|
|
2941
|
+
correlated_five_hour = tuple(
|
|
2942
|
+
observation
|
|
2943
|
+
for observation in _cached_codex_quota_observations(
|
|
2944
|
+
source_root_keys={identity.source_root_key},
|
|
2945
|
+
cache_conn=context.cache_conn,
|
|
2946
|
+
stats_conn=context.stats_conn,
|
|
2947
|
+
stats_identity=context.stats_identity,
|
|
2948
|
+
captured_at_or_after=block.nominal_start_at,
|
|
2949
|
+
)
|
|
2950
|
+
if observation.identity.window_minutes == 300
|
|
2951
|
+
and observation.identity.observed_slot == identity.observed_slot
|
|
2952
|
+
and observation.identity.limit_id == identity.limit_id
|
|
2953
|
+
and _codex_account_admits(
|
|
2954
|
+
account_key, observation.identity.account_key)
|
|
2955
|
+
)
|
|
2956
|
+
except sqlite3.Error:
|
|
2957
|
+
correlated_five_hour = ()
|
|
2663
2958
|
|
|
2664
2959
|
for row in canonical_rows:
|
|
2665
2960
|
milestone_rows.append({
|
|
@@ -3133,8 +3428,8 @@ def refresh_codex_source_clock(
|
|
|
3133
3428
|
availability = "partial"
|
|
3134
3429
|
# #556 S1 §4.1: an EXPIRED boundary is exactly the state the
|
|
3135
3430
|
# accounting axis reports as stale. Build time can never see it
|
|
3136
|
-
# (`_resolve_codex_weekly_cycle` retains only
|
|
3137
|
-
# so this clock is the only writer of that value.
|
|
3431
|
+
# (`_resolve_codex_weekly_cycle` retains only boundaries containing
|
|
3432
|
+
# `now`), so this clock is the only writer of that value.
|
|
3138
3433
|
domain_freshness["hero"] = "stale"
|
|
3139
3434
|
cycle_changed = True
|
|
3140
3435
|
# 3. budget last
|
|
@@ -3158,6 +3453,7 @@ def refresh_codex_source_clock(
|
|
|
3158
3453
|
# omission silently drops the authoritative account count and makes the
|
|
3159
3454
|
# combined figure fail closed on an idle tick that changed nothing else.
|
|
3160
3455
|
account_scope=state.account_scope,
|
|
3456
|
+
combined_accounting=state.combined_accounting,
|
|
3161
3457
|
private_session_labels=state.private_session_labels,
|
|
3162
3458
|
# #556 S2 §3.6: the aggregate carrier travels with the rows it
|
|
3163
3459
|
# describes. This clock refreshes presentation axes only and publishes
|
|
@@ -3901,8 +4197,20 @@ def _codex_accounts_wire(
|
|
|
3901
4197
|
cycles: list["CodexCycleBoundary"],
|
|
3902
4198
|
accounting_start: dt.datetime,
|
|
3903
4199
|
accounting_end: dt.datetime,
|
|
3904
|
-
|
|
3905
|
-
|
|
4200
|
+
population: tuple[object, ...],
|
|
4201
|
+
) -> tuple[
|
|
4202
|
+
list[dict[str, object]], list[dict[str, object]], dict[str, object]
|
|
4203
|
+
]:
|
|
4204
|
+
"""Return cards, hero cycles, and their server-only spend certificate.
|
|
4205
|
+
|
|
4206
|
+
``population`` is the merged, encounter-ordered accounting read every card
|
|
4207
|
+
is derived from (#583 S5 change 2). This function issues no accounting SQL
|
|
4208
|
+
of its own. It walks the population ONCE, partitioning it by account in
|
|
4209
|
+
encounter order, and then range-filters each card's own partition, so
|
|
4210
|
+
neither the read count nor the in-memory work grows with the account count.
|
|
4211
|
+
The caller loads it over a range that starts no later than any card's own
|
|
4212
|
+
range, which on the share path is EARLIER than the published accounting
|
|
4213
|
+
range.
|
|
3906
4214
|
|
|
3907
4215
|
Caller must gate on ``provider_is_decorated(stats_conn, "codex")`` — this
|
|
3908
4216
|
builds nothing for a <=1-real-account install (the whole surface is absent,
|
|
@@ -3947,11 +4255,79 @@ def _codex_accounts_wire(
|
|
|
3947
4255
|
"startAt": fallback_start.astimezone(UTC).isoformat(),
|
|
3948
4256
|
"endAt": context.now_utc.astimezone(UTC).isoformat(),
|
|
3949
4257
|
}
|
|
4258
|
+
# ONE encounter-ordered pass over the population, in place of one pass per
|
|
4259
|
+
# card (#583 S5 spec §2.2). `_codex_partition_by_account` applies the same
|
|
4260
|
+
# `or UNATTRIBUTED` normalization each card's filter used to apply itself,
|
|
4261
|
+
# so the buckets are the sets those filters selected from, and it preserves
|
|
4262
|
+
# encounter order, which `_aggregate_codex_buckets` depends on for a
|
|
4263
|
+
# bucket's first-seen model order.
|
|
4264
|
+
by_account = _codex_partition_by_account(population)
|
|
4265
|
+
|
|
4266
|
+
def _slice(
|
|
4267
|
+
account_key: str,
|
|
4268
|
+
start: dt.datetime,
|
|
4269
|
+
end: dt.datetime,
|
|
4270
|
+
source_root_keys: Iterable[str] | None = None,
|
|
4271
|
+
) -> tuple[object, ...]:
|
|
4272
|
+
"""One card's rows, range-filtered from that account's partition.
|
|
4273
|
+
|
|
4274
|
+
The predicates match the reader these filters replaced, member for
|
|
4275
|
+
member: half-open ``[start, end)`` on a UTC-normalized timestamp, the
|
|
4276
|
+
reserved sentinel standing for a NULL stamp, and a root set that selects
|
|
4277
|
+
nothing when it is supplied empty. Encounter order is the partition's,
|
|
4278
|
+
which is the population's, which is the reader's ``ORDER BY`` over a
|
|
4279
|
+
wider range, so a bucket's first-seen model order is unchanged.
|
|
4280
|
+
|
|
4281
|
+
Two comparisons are nevertheless not the SAME comparison, and both are
|
|
4282
|
+
equivalent only because ingest cannot write the row that separates them.
|
|
4283
|
+
Stated here so the next reader does not have to re-derive it, and
|
|
4284
|
+
asserted at the producer by
|
|
4285
|
+
``test_ingest_writes_only_canonical_account_and_timestamp_values``.
|
|
4286
|
+
|
|
4287
|
+
1. The replaced per-card query passed ``account_key=<key>`` to
|
|
4288
|
+
``load_cached_rooted_codex_accounting_entries``, whose sentinel
|
|
4289
|
+
predicate is ``account_key IS NULL OR account_key = 'unattributed'``.
|
|
4290
|
+
A row stored as the EMPTY STRING matched neither that nor any real
|
|
4291
|
+
account's ``account_key = ?``, so it appeared on no card at all,
|
|
4292
|
+
while the reader maps ``''`` to the sentinel and this filter
|
|
4293
|
+
therefore includes it. No shipped writer emits it: the three
|
|
4294
|
+
statements that set this column (`bin/_cctally_cache.py` -- the
|
|
4295
|
+
ingest insert, the window spend-adoption update and the attribution
|
|
4296
|
+
restore) each write NULL or a resolved 32-hex account key, and the
|
|
4297
|
+
ingest value comes from ``codex_file_accounts.account_key``, itself
|
|
4298
|
+
written only by two statements with the same two outcomes. Measured:
|
|
4299
|
+
zero empty strings among 163,424 rows of the maintainer's store.
|
|
4300
|
+
Note also that every OTHER account-partitioned surface already files
|
|
4301
|
+
such a row under the sentinel -- ``_codex_partition_by_account``
|
|
4302
|
+
applies the identical ``or _lib_accounts.UNATTRIBUTED`` -- so the
|
|
4303
|
+
card query was the sole outlier rather than the rule.
|
|
4304
|
+
2. The reader bounds ``timestamp_utc`` by comparing ISO TEXT inside
|
|
4305
|
+
SQLite; this filter compares parsed UTC datetimes. For the canonical
|
|
4306
|
+
``...+00:00`` spelling the two orders coincide, including against a
|
|
4307
|
+
bound carrying microseconds, because ``+`` sorts before ``.``. They
|
|
4308
|
+
diverge only for a stamp carrying a different valid offset, and a
|
|
4309
|
+
NAIVE stamp is not a divergence at all: ``_parse_timestamp`` refuses
|
|
4310
|
+
it, so such a row fails the whole read on the old path and the new
|
|
4311
|
+
one alike. Codex ingest writes
|
|
4312
|
+
``entry.timestamp.astimezone(UTC).isoformat()``, always ``+00:00``.
|
|
4313
|
+
Measured: 163,424 of 163,424 rows canonical.
|
|
4314
|
+
"""
|
|
4315
|
+
roots = (
|
|
4316
|
+
None if source_root_keys is None
|
|
4317
|
+
else {key for key in source_root_keys
|
|
4318
|
+
if isinstance(key, str) and key}
|
|
4319
|
+
)
|
|
4320
|
+
if roots is not None and not roots:
|
|
4321
|
+
return ()
|
|
4322
|
+
return tuple(
|
|
4323
|
+
row for row in by_account.get(account_key, ())
|
|
4324
|
+
if start <= row.timestamp < end
|
|
4325
|
+
and (roots is None or row.source_root_key in roots)
|
|
4326
|
+
)
|
|
4327
|
+
|
|
3950
4328
|
# Include unattributed last iff it has cycle/5h/spend evidence.
|
|
3951
|
-
unattributed_rows =
|
|
3952
|
-
accounting_start, accounting_end
|
|
3953
|
-
cache_conn=context.cache_conn, account_key=_lib_accounts.UNATTRIBUTED,
|
|
3954
|
-
)
|
|
4329
|
+
unattributed_rows = _slice(
|
|
4330
|
+
_lib_accounts.UNATTRIBUTED, accounting_start, accounting_end)
|
|
3955
4331
|
# Existence is decided over the accounting range so a sentinel holding only
|
|
3956
4332
|
# older spend keeps its card; the totals below cover the bounded window, so
|
|
3957
4333
|
# a resolved $0.00 is an honest empty state rather than an absence (#564).
|
|
@@ -3991,15 +4367,15 @@ def _codex_accounts_wire(
|
|
|
3991
4367
|
|
|
3992
4368
|
accounts_wire: list[dict[str, object]] = []
|
|
3993
4369
|
hero_cycles_wire: list[dict[str, object]] = []
|
|
4370
|
+
contributions: list[dict[str, object]] = []
|
|
3994
4371
|
for key in ordered_keys:
|
|
3995
4372
|
cyc = cycle_by_account.get(key)
|
|
3996
4373
|
is_unattributed = key == _lib_accounts.UNATTRIBUTED
|
|
3997
4374
|
if cyc is not None and not is_unattributed:
|
|
3998
4375
|
cycle_end = min(accounting_end, cyc.resets_at)
|
|
3999
|
-
rows =
|
|
4000
|
-
cyc.start_at, cycle_end,
|
|
4001
|
-
|
|
4002
|
-
source_root_keys=cyc.source_root_keys, account_key=key,
|
|
4376
|
+
rows = _slice(
|
|
4377
|
+
key, cyc.start_at, cycle_end,
|
|
4378
|
+
source_root_keys=cyc.source_root_keys,
|
|
4003
4379
|
)
|
|
4004
4380
|
totals = _totals(rows)
|
|
4005
4381
|
elif is_unattributed:
|
|
@@ -4009,10 +4385,7 @@ def _codex_accounts_wire(
|
|
|
4009
4385
|
# native cycle width ending now, so this card can be summed into a
|
|
4010
4386
|
# week-labelled headline without overstating it (#564). No bars or
|
|
4011
4387
|
# reset, because there is no live cycle to describe.
|
|
4012
|
-
rows =
|
|
4013
|
-
fallback_start, accounting_end, speed=context.speed,
|
|
4014
|
-
cache_conn=context.cache_conn, account_key=key,
|
|
4015
|
-
)
|
|
4388
|
+
rows = _slice(key, fallback_start, accounting_end)
|
|
4016
4389
|
totals = _totals(rows)
|
|
4017
4390
|
card: dict[str, object] = {
|
|
4018
4391
|
"accountKey": key,
|
|
@@ -4044,6 +4417,23 @@ def _codex_accounts_wire(
|
|
|
4044
4417
|
# which is true of several unrelated states (#564 D3).
|
|
4045
4418
|
card["spendWindow"] = fallback_window
|
|
4046
4419
|
accounts_wire.append(card)
|
|
4420
|
+
if cyc is not None and not is_unattributed:
|
|
4421
|
+
contribution_period = {
|
|
4422
|
+
"kind": "native_7_day_cycle",
|
|
4423
|
+
"start_at": cyc.start_at.astimezone(UTC).isoformat(),
|
|
4424
|
+
"end_at": cyc.resets_at.astimezone(UTC).isoformat(),
|
|
4425
|
+
}
|
|
4426
|
+
else:
|
|
4427
|
+
contribution_period = {
|
|
4428
|
+
"kind": "trailing_7_day_cycle",
|
|
4429
|
+
"start_at": fallback_start.astimezone(UTC).isoformat(),
|
|
4430
|
+
"end_at": context.now_utc.astimezone(UTC).isoformat(),
|
|
4431
|
+
}
|
|
4432
|
+
contributions.append({
|
|
4433
|
+
"account_key": key,
|
|
4434
|
+
"cost_usd": totals["spendUsd"],
|
|
4435
|
+
"period": contribution_period,
|
|
4436
|
+
})
|
|
4047
4437
|
if cyc is not None and not is_unattributed:
|
|
4048
4438
|
hero_cycles_wire.append({
|
|
4049
4439
|
"accountKey": key,
|
|
@@ -4054,7 +4444,11 @@ def _codex_accounts_wire(
|
|
|
4054
4444
|
"cost_usd": totals["spendUsd"],
|
|
4055
4445
|
"total_tokens": totals["totalTokens"],
|
|
4056
4446
|
})
|
|
4057
|
-
return accounts_wire, hero_cycles_wire
|
|
4447
|
+
return accounts_wire, hero_cycles_wire, {
|
|
4448
|
+
"scope": "account_cycles",
|
|
4449
|
+
"status": "certified",
|
|
4450
|
+
"contributions": tuple(contributions),
|
|
4451
|
+
}
|
|
4058
4452
|
|
|
4059
4453
|
|
|
4060
4454
|
def _codex_partition_by_account(
|
|
@@ -4172,6 +4566,7 @@ def _codex_account_scopes_wire(
|
|
|
4172
4566
|
changed_new_by_account: Mapping[str, tuple[CodexEntry, ...]] | None = None,
|
|
4173
4567
|
changed_old_rows_by_account: Mapping[str, tuple[object, ...]] | None = None,
|
|
4174
4568
|
changed_new_rows_by_account: Mapping[str, tuple[object, ...]] | None = None,
|
|
4569
|
+
captured_breakdowns: "Mapping[tuple[object, ...], tuple[tuple, tuple]] | None" = None,
|
|
4175
4570
|
) -> dict[str, dict[str, object]]:
|
|
4176
4571
|
"""The per-account CHILDREN of the merged Codex read model (spec §5.3).
|
|
4177
4572
|
|
|
@@ -4251,6 +4646,7 @@ def _codex_account_scopes_wire(
|
|
|
4251
4646
|
# #429 §4.4: this wire is only ever reached under decoration — the
|
|
4252
4647
|
# caller gates the whole `account_scopes` surface on it.
|
|
4253
4648
|
decorated=True,
|
|
4649
|
+
captured_breakdowns=captured_breakdowns,
|
|
4254
4650
|
)
|
|
4255
4651
|
# Each decorated child owns the only honest cycle index for that
|
|
4256
4652
|
# account. Reusing a merged parent index here would render account A's
|
|
@@ -4480,7 +4876,7 @@ def _claude_accounts_wire(
|
|
|
4480
4876
|
stats_conn: sqlite3.Connection,
|
|
4481
4877
|
*,
|
|
4482
4878
|
now_utc: dt.datetime,
|
|
4483
|
-
) -> list[dict[str, object]]:
|
|
4879
|
+
) -> tuple[list[dict[str, object]], dict[str, object]]:
|
|
4484
4880
|
"""Per-account Claude hero cards (#341 Task 4, Ruling C).
|
|
4485
4881
|
|
|
4486
4882
|
Symmetric with ``_codex_accounts_wire``: the caller gates on
|
|
@@ -4509,13 +4905,38 @@ def _claude_accounts_wire(
|
|
|
4509
4905
|
(key,),
|
|
4510
4906
|
).fetchone()
|
|
4511
4907
|
|
|
4512
|
-
def _latest_cost(key: str)
|
|
4908
|
+
def _latest_cost(key: str):
|
|
4513
4909
|
row = stats_conn.execute(
|
|
4514
|
-
"SELECT cost_usd
|
|
4910
|
+
"SELECT cost_usd, week_start_at, week_end_at "
|
|
4911
|
+
"FROM weekly_cost_snapshots WHERE account_key=? "
|
|
4515
4912
|
"ORDER BY captured_at_utc DESC LIMIT 1",
|
|
4516
4913
|
(key,),
|
|
4517
4914
|
).fetchone()
|
|
4518
|
-
|
|
4915
|
+
if row is None or row[0] is None:
|
|
4916
|
+
return 0.0, None, "account_cost_unresolved"
|
|
4917
|
+
try:
|
|
4918
|
+
start = dt.datetime.fromisoformat(str(row[1]).replace("Z", "+00:00"))
|
|
4919
|
+
end = dt.datetime.fromisoformat(str(row[2]).replace("Z", "+00:00"))
|
|
4920
|
+
except (TypeError, ValueError):
|
|
4921
|
+
return float(row[0]), None, "account_cycle_unresolved"
|
|
4922
|
+
if (
|
|
4923
|
+
start.tzinfo is None or start.utcoffset() is None
|
|
4924
|
+
or end.tzinfo is None or end.utcoffset() is None
|
|
4925
|
+
or start >= end
|
|
4926
|
+
):
|
|
4927
|
+
return float(row[0]), None, "account_cycle_unresolved"
|
|
4928
|
+
start_at = start.astimezone(UTC).isoformat().replace("+00:00", "Z")
|
|
4929
|
+
end_at = end.astimezone(UTC).isoformat().replace("+00:00", "Z")
|
|
4930
|
+
contribution = {
|
|
4931
|
+
"account_key": key,
|
|
4932
|
+
"cost_usd": float(row[0]),
|
|
4933
|
+
"period": {
|
|
4934
|
+
"kind": "subscription_week",
|
|
4935
|
+
"start_at": start_at,
|
|
4936
|
+
"end_at": end_at,
|
|
4937
|
+
},
|
|
4938
|
+
}
|
|
4939
|
+
return float(row[0]), contribution, None
|
|
4519
4940
|
|
|
4520
4941
|
_claude_label_map = _cctally_account.display_label_map(stats_conn, "claude")
|
|
4521
4942
|
|
|
@@ -4529,12 +4950,20 @@ def _claude_accounts_wire(
|
|
|
4529
4950
|
ordered_keys.append(_lib_accounts.UNATTRIBUTED)
|
|
4530
4951
|
|
|
4531
4952
|
cards: list[dict[str, object]] = []
|
|
4953
|
+
contributions: list[dict[str, object]] = []
|
|
4954
|
+
unresolved_causes: set[str] = set()
|
|
4532
4955
|
for key in ordered_keys:
|
|
4533
4956
|
is_unattributed = key == _lib_accounts.UNATTRIBUTED
|
|
4534
4957
|
usage = _latest_usage(key)
|
|
4535
4958
|
weekly_pct = usage[0] if usage is not None else None
|
|
4536
4959
|
five_hour_pct = usage[1] if usage is not None else None
|
|
4537
4960
|
resets_at = usage[2] if usage is not None else None
|
|
4961
|
+
spend_usd, contribution, unresolved_cause = _latest_cost(key)
|
|
4962
|
+
if contribution is None:
|
|
4963
|
+
unresolved_causes.add(
|
|
4964
|
+
unresolved_cause or "account_reconciliation_failed")
|
|
4965
|
+
else:
|
|
4966
|
+
contributions.append(contribution)
|
|
4538
4967
|
card: dict[str, object] = {
|
|
4539
4968
|
"accountKey": key,
|
|
4540
4969
|
"label": _claude_label_map.get(key) or _cctally_account.account_label(stats_conn, key),
|
|
@@ -4543,12 +4972,30 @@ def _claude_accounts_wire(
|
|
|
4543
4972
|
"weeklyPercent": None if is_unattributed else weekly_pct,
|
|
4544
4973
|
"fiveHourPercent": None if is_unattributed else five_hour_pct,
|
|
4545
4974
|
"resetsAt": None if is_unattributed else resets_at,
|
|
4546
|
-
"spendUsd":
|
|
4975
|
+
"spendUsd": spend_usd,
|
|
4547
4976
|
}
|
|
4548
4977
|
if is_unattributed:
|
|
4549
4978
|
card["unattributed"] = True
|
|
4979
|
+
if contribution is not None:
|
|
4980
|
+
period = contribution["period"]
|
|
4981
|
+
card["spendWindow"] = {
|
|
4982
|
+
"kind": "subscription-week",
|
|
4983
|
+
"startAt": period["start_at"],
|
|
4984
|
+
"endAt": period["end_at"],
|
|
4985
|
+
}
|
|
4550
4986
|
cards.append(card)
|
|
4551
|
-
|
|
4987
|
+
certificate: dict[str, object] = {
|
|
4988
|
+
"scope": "account_cycles",
|
|
4989
|
+
"status": "unresolved" if unresolved_causes else "certified",
|
|
4990
|
+
"contributions": tuple(contributions),
|
|
4991
|
+
}
|
|
4992
|
+
if unresolved_causes:
|
|
4993
|
+
certificate["cause"] = (
|
|
4994
|
+
"account_cost_unresolved"
|
|
4995
|
+
if "account_cost_unresolved" in unresolved_causes
|
|
4996
|
+
else "account_cycle_unresolved"
|
|
4997
|
+
)
|
|
4998
|
+
return cards, certificate
|
|
4552
4999
|
|
|
4553
5000
|
|
|
4554
5001
|
def _codex_ingest_backlog_wire(
|
|
@@ -4595,31 +5042,15 @@ def _codex_ingest_backlog_wire(
|
|
|
4595
5042
|
}
|
|
4596
5043
|
|
|
4597
5044
|
|
|
4598
|
-
def
|
|
4599
|
-
|
|
4600
|
-
*,
|
|
4601
|
-
data_version: str,
|
|
4602
|
-
) -> SourceDashboardState:
|
|
4603
|
-
"""Build Codex data strictly from the coordinated cache/stats reads.
|
|
4604
|
-
|
|
4605
|
-
No sync, rollout scan, CLI parser, or fallback is reachable from this
|
|
4606
|
-
adapter. Period and session arithmetic remains delegated to the shipped
|
|
4607
|
-
S3 view kernels, preserving the CLI's inclusive-token vocabulary.
|
|
5045
|
+
def _codex_source_caches() -> tuple[dict, ...]:
|
|
5046
|
+
"""Every process cache a Codex source build reuses across builds.
|
|
4608
5047
|
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
every Codex session view in the build, and it is discarded when the read
|
|
4614
|
-
returns.
|
|
5048
|
+
ONE definition, so the set the build checkpoints and the set a cold
|
|
5049
|
+
reference discards cannot drift apart. A cold reference that discards only
|
|
5050
|
+
some of them compares two partly-warm builds and calls the second cold
|
|
5051
|
+
(#583 S5 criterion 10).
|
|
4615
5052
|
"""
|
|
4616
|
-
|
|
4617
|
-
# coordinated source build. It may not cross that boundary: a caller can
|
|
4618
|
-
# deliberately request a fresh build after stats/account decoration changes
|
|
4619
|
-
# without advancing cache.db's quota ledger, and the established contract
|
|
4620
|
-
# requires one bounded physical load for that new build.
|
|
4621
|
-
reset_codex_quota_observation_cache()
|
|
4622
|
-
caches = (
|
|
5053
|
+
return (
|
|
4623
5054
|
_CODEX_QUOTA_OBSERVATION_CACHE,
|
|
4624
5055
|
_CODEX_PERIOD_VIEW_CACHE,
|
|
4625
5056
|
_CODEX_CACHE_REPORT_ROWS,
|
|
@@ -4630,90 +5061,45 @@ def build_codex_source_state(
|
|
|
4630
5061
|
_CODEX_WEEKLY_VIEW_CACHE,
|
|
4631
5062
|
_CODEX_ACCOUNT_SCOPE_CACHE,
|
|
4632
5063
|
)
|
|
4633
|
-
cache_checkpoint = tuple(dict(cache) for cache in caches)
|
|
4634
|
-
accounting_checkpoint = (
|
|
4635
|
-
_lib_snapshot_cache.checkpoint_codex_accounting_cache_state()
|
|
4636
|
-
)
|
|
4637
|
-
try:
|
|
4638
|
-
with codex_path_scope() as path_scope:
|
|
4639
|
-
return _build_codex_source_state(
|
|
4640
|
-
context, data_version=data_version, path_scope=path_scope,
|
|
4641
|
-
)
|
|
4642
|
-
except Exception:
|
|
4643
|
-
for cache, prior in zip(caches, cache_checkpoint):
|
|
4644
|
-
cache.clear()
|
|
4645
|
-
cache.update(prior)
|
|
4646
|
-
_lib_snapshot_cache.restore_codex_accounting_cache_state(
|
|
4647
|
-
accounting_checkpoint)
|
|
4648
|
-
raise
|
|
4649
5064
|
|
|
4650
5065
|
|
|
4651
|
-
def
|
|
5066
|
+
def reset_codex_source_caches() -> None:
|
|
5067
|
+
"""Discard every reuse a Codex source build could draw on.
|
|
5068
|
+
|
|
5069
|
+
The cold reference for a warm-versus-cold equality check: after this, the
|
|
5070
|
+
next build reconstructs from the databases alone. It covers the accounting
|
|
5071
|
+
cache state too, which lives in ``_lib_snapshot_cache`` rather than in a
|
|
5072
|
+
dict here and is the other half of what a build retains.
|
|
5073
|
+
"""
|
|
5074
|
+
for cache in _codex_source_caches():
|
|
5075
|
+
cache.clear()
|
|
5076
|
+
_lib_snapshot_cache.reset_codex_accounting_cache_state()
|
|
5077
|
+
|
|
5078
|
+
|
|
5079
|
+
@dataclass(frozen=True)
|
|
5080
|
+
class _CodexAccountingCapture:
|
|
5081
|
+
qualified_entries: tuple[object, ...]
|
|
5082
|
+
dirty_accounts: tuple[str, ...]
|
|
5083
|
+
changed_old: tuple[object, ...]
|
|
5084
|
+
changed_new: tuple[object, ...]
|
|
5085
|
+
entries: tuple[object, ...]
|
|
5086
|
+
metadata_incomplete: bool
|
|
5087
|
+
|
|
5088
|
+
|
|
5089
|
+
def _capture_codex_accounting(
|
|
4652
5090
|
context: DashboardReadContext,
|
|
4653
5091
|
*,
|
|
4654
|
-
|
|
5092
|
+
accounting_start: dt.datetime,
|
|
5093
|
+
accounting_end: dt.datetime,
|
|
5094
|
+
active_roots: tuple[str, ...],
|
|
4655
5095
|
path_scope: object,
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
"SELECT source_root_key FROM codex_source_roots"
|
|
4660
|
-
)
|
|
4661
|
-
))
|
|
4662
|
-
quota_observations = _cached_codex_quota_observations(
|
|
4663
|
-
source_root_keys=active_roots,
|
|
4664
|
-
cache_conn=context.cache_conn,
|
|
4665
|
-
captured_at_or_after=(
|
|
4666
|
-
context.now_utc - dt.timedelta(days=DASHBOARD_QUOTA_RECENT_DAYS)
|
|
4667
|
-
),
|
|
4668
|
-
active_at=context.now_utc,
|
|
4669
|
-
max_rows=DASHBOARD_QUOTA_OBSERVATION_LIMIT,
|
|
4670
|
-
)
|
|
4671
|
-
coherence = codex_projection_coherence(
|
|
4672
|
-
context,
|
|
4673
|
-
)
|
|
4674
|
-
projection_incoherent = not coherence.coherent
|
|
4675
|
-
ingest_backlog = _codex_ingest_backlog_wire(context.cache_conn)
|
|
4676
|
-
# The cache reader's established report surface treats the ``now`` instant
|
|
4677
|
-
# as inclusive. The qualified adapter is half-open, so extend only its
|
|
4678
|
-
# query/result boundary by one microsecond and keep all live budget sums
|
|
4679
|
-
# explicitly half-open at ``now`` below.
|
|
4680
|
-
accounting_end = context.now_utc + dt.timedelta(microseconds=1)
|
|
4681
|
-
accounting_start = context.range_start
|
|
4682
|
-
if context.codex_budget is not None:
|
|
4683
|
-
# #556 S5 Unit 2 (Unit 1 review R6, widened) — this is the SECOND
|
|
4684
|
-
# unguarded call to the window resolver, and unlike
|
|
4685
|
-
# `_codex_budget_cost_events` it had no boundary of its own. It sits
|
|
4686
|
-
# outside every other `try` in this function, so an unresolvable window
|
|
4687
|
-
# escaped into `_tui_build_source_bundle`'s `source_build_failed`
|
|
4688
|
-
# handler and destroyed the entire Codex provider's data — the exact
|
|
4689
|
-
# failure §3.5 exists to prevent, reached from a different line.
|
|
4690
|
-
#
|
|
4691
|
-
# Degrading to the un-widened accounting range cannot publish a false
|
|
4692
|
-
# figure: the same failure reaches `_codex_budget_status_domain`, which
|
|
4693
|
-
# nulls the status and names `budget_compute_failed`.
|
|
4694
|
-
try:
|
|
4695
|
-
_period, budget_start, _budget_end = _configured_codex_budget_window(context)
|
|
4696
|
-
except Exception:
|
|
4697
|
-
_warn_codex_budget_window_once("accounting_range")
|
|
4698
|
-
else:
|
|
4699
|
-
accounting_start = min(accounting_start, budget_start)
|
|
4700
|
-
health = load_codex_project_metadata_health(
|
|
4701
|
-
cache_conn=context.cache_conn,
|
|
4702
|
-
start=accounting_start,
|
|
4703
|
-
end=accounting_end,
|
|
4704
|
-
)
|
|
4705
|
-
metadata_incomplete = health.incomplete_rows > 0
|
|
4706
|
-
metadata_warning_message = (
|
|
4707
|
-
f"{health.incomplete_rows} Codex accounting row(s) lack project metadata; "
|
|
4708
|
-
"run `cctally cache-sync --source codex --rebuild`."
|
|
4709
|
-
if metadata_incomplete
|
|
4710
|
-
else "Codex project metadata could not be read; "
|
|
4711
|
-
"run `cctally cache-sync --source codex --rebuild`."
|
|
4712
|
-
)
|
|
5096
|
+
metadata_incomplete: bool,
|
|
5097
|
+
) -> _CodexAccountingCapture:
|
|
5098
|
+
"""Read the incremental accounting carrier without folding public views."""
|
|
4713
5099
|
qualified_entries: tuple[object, ...] = ()
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
5100
|
+
dirty_accounts: tuple[str, ...] = ()
|
|
5101
|
+
changed_old: tuple[object, ...] = ()
|
|
5102
|
+
changed_new: tuple[object, ...] = ()
|
|
4717
5103
|
if not metadata_incomplete:
|
|
4718
5104
|
try:
|
|
4719
5105
|
cached_accounting = _lib_snapshot_cache.build_cached_codex_accounting(
|
|
@@ -4753,42 +5139,425 @@ def _build_codex_source_state(
|
|
|
4753
5139
|
identity_of=lambda entry: int(entry.cache_entry_id),
|
|
4754
5140
|
)
|
|
4755
5141
|
qualified_entries = cached_accounting.entries
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
|
|
4759
|
-
|
|
5142
|
+
dirty_accounts = cached_accounting.dirty_accounts
|
|
5143
|
+
changed_old = cached_accounting.changed_old
|
|
5144
|
+
changed_new = cached_accounting.changed_new
|
|
5145
|
+
entries: tuple[object, ...] = qualified_entries
|
|
4760
5146
|
except QualifiedMetadataUnavailable:
|
|
4761
|
-
# A cached read must be internally coherent, but retain accounting
|
|
4762
|
-
# once if a defensive race or malformed row violates that premise.
|
|
4763
5147
|
_lib_log.get_logger("dashboard").warning(
|
|
4764
5148
|
"Codex qualified metadata read became unavailable; using cache-only accounting fallback"
|
|
4765
5149
|
)
|
|
4766
5150
|
metadata_incomplete = True
|
|
4767
5151
|
_lib_snapshot_cache.reset_codex_accounting_cache_state()
|
|
4768
|
-
|
|
5152
|
+
entries = load_cached_rooted_codex_accounting_entries(
|
|
4769
5153
|
accounting_start,
|
|
4770
5154
|
accounting_end,
|
|
4771
5155
|
speed=context.speed,
|
|
4772
5156
|
cache_conn=context.cache_conn,
|
|
4773
5157
|
)
|
|
4774
|
-
|
|
5158
|
+
dirty_accounts = tuple(sorted({
|
|
4775
5159
|
str(getattr(entry, "account_key", "") or
|
|
4776
5160
|
_lib_accounts.UNATTRIBUTED)
|
|
4777
|
-
for entry in
|
|
5161
|
+
for entry in entries
|
|
4778
5162
|
}))
|
|
4779
5163
|
else:
|
|
4780
5164
|
_lib_snapshot_cache.reset_codex_accounting_cache_state()
|
|
4781
|
-
|
|
5165
|
+
entries = load_cached_rooted_codex_accounting_entries(
|
|
4782
5166
|
accounting_start,
|
|
4783
5167
|
accounting_end,
|
|
4784
5168
|
speed=context.speed,
|
|
4785
5169
|
cache_conn=context.cache_conn,
|
|
4786
5170
|
)
|
|
4787
|
-
|
|
5171
|
+
dirty_accounts = tuple(sorted({
|
|
4788
5172
|
str(getattr(entry, "account_key", "") or
|
|
4789
5173
|
_lib_accounts.UNATTRIBUTED)
|
|
4790
|
-
for entry in
|
|
5174
|
+
for entry in entries
|
|
4791
5175
|
}))
|
|
5176
|
+
return _CodexAccountingCapture(
|
|
5177
|
+
qualified_entries=qualified_entries,
|
|
5178
|
+
dirty_accounts=dirty_accounts,
|
|
5179
|
+
changed_old=changed_old,
|
|
5180
|
+
changed_new=changed_new,
|
|
5181
|
+
entries=tuple(entries),
|
|
5182
|
+
metadata_incomplete=metadata_incomplete,
|
|
5183
|
+
)
|
|
5184
|
+
|
|
5185
|
+
|
|
5186
|
+
@dataclass(frozen=True)
|
|
5187
|
+
class CodexSourceCapture:
|
|
5188
|
+
"""Every cache-backed input for a post-transaction Codex source build."""
|
|
5189
|
+
|
|
5190
|
+
context: DashboardReadContext
|
|
5191
|
+
active_roots: tuple[str, ...]
|
|
5192
|
+
quota_observations: tuple[object, ...]
|
|
5193
|
+
projection_incoherent: bool
|
|
5194
|
+
ingest_backlog: object
|
|
5195
|
+
accounting_start: dt.datetime
|
|
5196
|
+
accounting_end: dt.datetime
|
|
5197
|
+
health: object
|
|
5198
|
+
accounting: _CodexAccountingCapture
|
|
5199
|
+
accounting_exists: bool
|
|
5200
|
+
accounting_exists_error: Exception | None
|
|
5201
|
+
conversation_metadata: Mapping
|
|
5202
|
+
decorated: bool
|
|
5203
|
+
card_population: tuple[object, ...]
|
|
5204
|
+
breakdowns: Mapping
|
|
5205
|
+
cache_checkpoint: tuple[dict, ...]
|
|
5206
|
+
accounting_checkpoint: object
|
|
5207
|
+
|
|
5208
|
+
|
|
5209
|
+
def _restore_codex_source_capture(capture: CodexSourceCapture) -> None:
|
|
5210
|
+
for cache, prior in zip(_codex_source_caches(), capture.cache_checkpoint):
|
|
5211
|
+
cache.clear()
|
|
5212
|
+
cache.update(prior)
|
|
5213
|
+
_lib_snapshot_cache.restore_codex_accounting_cache_state(
|
|
5214
|
+
capture.accounting_checkpoint)
|
|
5215
|
+
|
|
5216
|
+
|
|
5217
|
+
def capture_codex_source_state(
|
|
5218
|
+
context: DashboardReadContext,
|
|
5219
|
+
*,
|
|
5220
|
+
path_scope: object,
|
|
5221
|
+
) -> CodexSourceCapture:
|
|
5222
|
+
"""Capture Codex cache evidence without constructing public view models."""
|
|
5223
|
+
caches = _codex_source_caches()
|
|
5224
|
+
cache_checkpoint = tuple(dict(cache) for cache in caches)
|
|
5225
|
+
accounting_checkpoint = (
|
|
5226
|
+
_lib_snapshot_cache.checkpoint_codex_accounting_cache_state()
|
|
5227
|
+
)
|
|
5228
|
+
try:
|
|
5229
|
+
active_roots = tuple(sorted(
|
|
5230
|
+
str(row[0]) for row in context.cache_conn.execute(
|
|
5231
|
+
"SELECT source_root_key FROM codex_source_roots"
|
|
5232
|
+
)
|
|
5233
|
+
))
|
|
5234
|
+
quota_observations = tuple(_cached_codex_quota_observations(
|
|
5235
|
+
source_root_keys=active_roots,
|
|
5236
|
+
cache_conn=context.cache_conn,
|
|
5237
|
+
stats_conn=context.stats_conn,
|
|
5238
|
+
stats_identity=context.stats_identity,
|
|
5239
|
+
memoize=False,
|
|
5240
|
+
captured_at_or_after=(
|
|
5241
|
+
context.now_utc - dt.timedelta(days=DASHBOARD_QUOTA_RECENT_DAYS)
|
|
5242
|
+
),
|
|
5243
|
+
active_at=context.now_utc,
|
|
5244
|
+
max_rows=DASHBOARD_QUOTA_OBSERVATION_LIMIT,
|
|
5245
|
+
))
|
|
5246
|
+
projection_incoherent = not codex_projection_coherence(context).coherent
|
|
5247
|
+
ingest_backlog = _codex_ingest_backlog_wire(context.cache_conn)
|
|
5248
|
+
accounting_end = context.now_utc + dt.timedelta(microseconds=1)
|
|
5249
|
+
accounting_start = context.range_start
|
|
5250
|
+
if context.codex_budget is not None:
|
|
5251
|
+
try:
|
|
5252
|
+
_period, budget_start, _budget_end = (
|
|
5253
|
+
_configured_codex_budget_window(context)
|
|
5254
|
+
)
|
|
5255
|
+
except Exception:
|
|
5256
|
+
_warn_codex_budget_window_once("accounting_range")
|
|
5257
|
+
else:
|
|
5258
|
+
accounting_start = min(accounting_start, budget_start)
|
|
5259
|
+
health = load_codex_project_metadata_health(
|
|
5260
|
+
cache_conn=context.cache_conn,
|
|
5261
|
+
start=accounting_start,
|
|
5262
|
+
end=accounting_end,
|
|
5263
|
+
)
|
|
5264
|
+
accounting = _capture_codex_accounting(
|
|
5265
|
+
context,
|
|
5266
|
+
accounting_start=accounting_start,
|
|
5267
|
+
accounting_end=accounting_end,
|
|
5268
|
+
active_roots=active_roots,
|
|
5269
|
+
path_scope=path_scope,
|
|
5270
|
+
metadata_incomplete=health.incomplete_rows > 0,
|
|
5271
|
+
)
|
|
5272
|
+
try:
|
|
5273
|
+
accounting_exists = has_cached_codex_accounting_entries(
|
|
5274
|
+
cache_conn=context.cache_conn)
|
|
5275
|
+
accounting_exists_error = None
|
|
5276
|
+
except Exception as exc:
|
|
5277
|
+
# This probe is semantically relevant only when quota evidence
|
|
5278
|
+
# cannot resolve a native cycle. Capture the fault beside the
|
|
5279
|
+
# value, then defer it until the build knows that branch is live.
|
|
5280
|
+
accounting_exists = False
|
|
5281
|
+
accounting_exists_error = exc
|
|
5282
|
+
conversation_metadata = MappingProxyType(
|
|
5283
|
+
_codex_conversation_metadata(context.cache_conn))
|
|
5284
|
+
try:
|
|
5285
|
+
import _cctally_account
|
|
5286
|
+
decorated = _cctally_account.provider_is_decorated(
|
|
5287
|
+
context.stats_conn, "codex")
|
|
5288
|
+
except Exception:
|
|
5289
|
+
decorated = False
|
|
5290
|
+
card_population_start = accounting_start
|
|
5291
|
+
if decorated:
|
|
5292
|
+
card_population_start = min(
|
|
5293
|
+
context.range_start,
|
|
5294
|
+
context.now_utc - dt.timedelta(
|
|
5295
|
+
minutes=ACCOUNT_WEEKLY_WINDOW_MINUTES),
|
|
5296
|
+
accounting_start,
|
|
5297
|
+
)
|
|
5298
|
+
else:
|
|
5299
|
+
# The undecorated parent has no account cards. It needs an earlier
|
|
5300
|
+
# population only when a healthy representative cycle actually
|
|
5301
|
+
# resolves; an unresolved narrow share must not pay for a seven-day
|
|
5302
|
+
# read whose rows no published domain can consume.
|
|
5303
|
+
try:
|
|
5304
|
+
captured_cycles = _resolve_codex_weekly_cycle(
|
|
5305
|
+
quota_observations, context.now_utc)
|
|
5306
|
+
except CodexCycleUnavailable:
|
|
5307
|
+
captured_cycles = ()
|
|
5308
|
+
if captured_cycles:
|
|
5309
|
+
card_population_start = min(
|
|
5310
|
+
accounting_start, captured_cycles[0].start_at)
|
|
5311
|
+
card_population = (
|
|
5312
|
+
accounting.entries
|
|
5313
|
+
if card_population_start == accounting_start
|
|
5314
|
+
else load_cached_rooted_codex_accounting_entries(
|
|
5315
|
+
card_population_start,
|
|
5316
|
+
accounting_end,
|
|
5317
|
+
speed=context.speed,
|
|
5318
|
+
cache_conn=context.cache_conn,
|
|
5319
|
+
)
|
|
5320
|
+
)
|
|
5321
|
+
breakdowns = _capture_quota_breakdown_evidence(
|
|
5322
|
+
context, quota_observations)
|
|
5323
|
+
return CodexSourceCapture(
|
|
5324
|
+
context=context,
|
|
5325
|
+
active_roots=active_roots,
|
|
5326
|
+
quota_observations=quota_observations,
|
|
5327
|
+
projection_incoherent=projection_incoherent,
|
|
5328
|
+
ingest_backlog=ingest_backlog,
|
|
5329
|
+
accounting_start=accounting_start,
|
|
5330
|
+
accounting_end=accounting_end,
|
|
5331
|
+
health=health,
|
|
5332
|
+
accounting=accounting,
|
|
5333
|
+
accounting_exists=accounting_exists,
|
|
5334
|
+
accounting_exists_error=accounting_exists_error,
|
|
5335
|
+
conversation_metadata=conversation_metadata,
|
|
5336
|
+
decorated=decorated,
|
|
5337
|
+
card_population=tuple(card_population),
|
|
5338
|
+
breakdowns=breakdowns,
|
|
5339
|
+
cache_checkpoint=cache_checkpoint,
|
|
5340
|
+
accounting_checkpoint=accounting_checkpoint,
|
|
5341
|
+
)
|
|
5342
|
+
except Exception:
|
|
5343
|
+
for cache, prior in zip(caches, cache_checkpoint):
|
|
5344
|
+
cache.clear()
|
|
5345
|
+
cache.update(prior)
|
|
5346
|
+
_lib_snapshot_cache.restore_codex_accounting_cache_state(
|
|
5347
|
+
accounting_checkpoint)
|
|
5348
|
+
raise
|
|
5349
|
+
|
|
5350
|
+
|
|
5351
|
+
def build_codex_source_state_from_capture(
|
|
5352
|
+
capture: CodexSourceCapture,
|
|
5353
|
+
*,
|
|
5354
|
+
data_version: str,
|
|
5355
|
+
path_scope: object,
|
|
5356
|
+
) -> SourceDashboardState:
|
|
5357
|
+
"""Fold one captured Codex generation without issuing cache SQL."""
|
|
5358
|
+
try:
|
|
5359
|
+
return _build_codex_source_state(
|
|
5360
|
+
capture.context,
|
|
5361
|
+
data_version=data_version,
|
|
5362
|
+
path_scope=path_scope,
|
|
5363
|
+
capture=capture,
|
|
5364
|
+
)
|
|
5365
|
+
except Exception:
|
|
5366
|
+
_restore_codex_source_capture(capture)
|
|
5367
|
+
raise
|
|
5368
|
+
|
|
5369
|
+
|
|
5370
|
+
def build_codex_source_state(
|
|
5371
|
+
context: DashboardReadContext,
|
|
5372
|
+
*,
|
|
5373
|
+
data_version: str,
|
|
5374
|
+
) -> SourceDashboardState:
|
|
5375
|
+
"""Build Codex data strictly from the coordinated cache/stats reads.
|
|
5376
|
+
|
|
5377
|
+
No sync, rollout scan, CLI parser, or fallback is reachable from this
|
|
5378
|
+
adapter. Period and session arithmetic remains delegated to the shipped
|
|
5379
|
+
S3 view kernels, preserving the CLI's inclusive-token vocabulary.
|
|
5380
|
+
|
|
5381
|
+
The whole read runs under ONE ``codex_path_scope`` (#566 §5.1 item 1), so
|
|
5382
|
+
the merged parent view and every per-account child share a single session
|
|
5383
|
+
root resolution and a single parse per distinct session file. The scope is
|
|
5384
|
+
opened here rather than further out because this is the boundary that owns
|
|
5385
|
+
every Codex session view in the build, and it is discarded when the read
|
|
5386
|
+
returns.
|
|
5387
|
+
"""
|
|
5388
|
+
# #583 S5: this memo now survives across builds. It may do so ONLY because
|
|
5389
|
+
# its key carries the stats-side identity as well as the cache legs -- see
|
|
5390
|
+
# `_codex_quota_reuse_identity`. The clear that used to stand here existed
|
|
5391
|
+
# because a decoration or account-registry change advances no cache.db leg
|
|
5392
|
+
# and so could not move a cache-only key. Do not narrow that identity.
|
|
5393
|
+
with codex_path_scope() as path_scope:
|
|
5394
|
+
capture = capture_codex_source_state(
|
|
5395
|
+
context, path_scope=path_scope)
|
|
5396
|
+
return build_codex_source_state_from_capture(
|
|
5397
|
+
capture, data_version=data_version, path_scope=path_scope)
|
|
5398
|
+
|
|
5399
|
+
|
|
5400
|
+
def _build_codex_cycle_accounting(
|
|
5401
|
+
rows: Iterable[object],
|
|
5402
|
+
*,
|
|
5403
|
+
now_utc: dt.datetime,
|
|
5404
|
+
display_tz_name: str | None,
|
|
5405
|
+
speed: str,
|
|
5406
|
+
) -> tuple[list[CodexEntry], float]:
|
|
5407
|
+
"""Price the hero's already-captured native-cycle accounting rows."""
|
|
5408
|
+
entries = _codex_entries_from_accounting(rows)
|
|
5409
|
+
cost_usd = build_codex_daily_view(
|
|
5410
|
+
entries,
|
|
5411
|
+
now_utc=now_utc,
|
|
5412
|
+
tz_name=display_tz_name,
|
|
5413
|
+
speed=speed,
|
|
5414
|
+
).total_cost_usd
|
|
5415
|
+
return entries, cost_usd
|
|
5416
|
+
|
|
5417
|
+
|
|
5418
|
+
def _build_codex_source_state(
|
|
5419
|
+
context: DashboardReadContext,
|
|
5420
|
+
*,
|
|
5421
|
+
data_version: str,
|
|
5422
|
+
path_scope: object,
|
|
5423
|
+
capture: "CodexSourceCapture | None" = None,
|
|
5424
|
+
) -> SourceDashboardState:
|
|
5425
|
+
active_roots = (
|
|
5426
|
+
capture.active_roots
|
|
5427
|
+
if capture is not None
|
|
5428
|
+
else tuple(sorted(
|
|
5429
|
+
str(row[0]) for row in context.cache_conn.execute(
|
|
5430
|
+
"SELECT source_root_key FROM codex_source_roots"
|
|
5431
|
+
)
|
|
5432
|
+
))
|
|
5433
|
+
)
|
|
5434
|
+
# This read is a function of the tick instant, so it does NOT reuse across
|
|
5435
|
+
# ticks and the cross-build memo above cannot help it. Both bounds move
|
|
5436
|
+
# with `context.now_utc`, which in production is the tick's wall clock
|
|
5437
|
+
# (`bin/_cctally_tui.py`: `now_utc or dt.datetime.now(dt.timezone.utc)`).
|
|
5438
|
+
#
|
|
5439
|
+
# It cannot be made reusable by loading a memoizable superset and filtering
|
|
5440
|
+
# in memory, and the reason is a property of the loader rather than of this
|
|
5441
|
+
# call: `max_rows` is a SQL `LIMIT` under an `ORDER BY` whose leading term
|
|
5442
|
+
# is `resets_at_utc > active_at`, so a superset truncates a DIFFERENT set of
|
|
5443
|
+
# rows; and `_apply_codex_window_attribution_overlay` plus
|
|
5444
|
+
# `adopt_unidentified_observations` are population-dependent folds run after
|
|
5445
|
+
# the bound, so a wider population can change the account a retained
|
|
5446
|
+
# observation carries. Filtering a superset would therefore change the
|
|
5447
|
+
# published answer, not just its cost.
|
|
5448
|
+
#
|
|
5449
|
+
# Measured on a copy of the real store (276,391 retained Codex quota rows),
|
|
5450
|
+
# warm, five samples after one untimed warm-up: 222.5 ms median per tick,
|
|
5451
|
+
# range 210.7-231.3 ms.
|
|
5452
|
+
#
|
|
5453
|
+
# As a share, that is 13.9% of 1.60 s, and the denominator has to be named:
|
|
5454
|
+
# 1.60 s is the sum of the three quota call sites when each is timed ALONE
|
|
5455
|
+
# and UN-profiled on that store (this bounded read 222.5 ms, the five-hour
|
|
5456
|
+
# correlation read 209.6 ms, the doctor's all-history `latest_per_identity`
|
|
5457
|
+
# read 1,164.7 ms). The S5 measurement record's 2.571 s for
|
|
5458
|
+
# `load_codex_quota_observations` is a cProfile total instead, so putting an
|
|
5459
|
+
# un-profiled numerator over it reads about 8% and understates the share.
|
|
5460
|
+
# Either way the site is well under the threshold that would have required a
|
|
5461
|
+
# superset fix, which is why the design conclusion above stands. The
|
|
5462
|
+
# doctor's read is about 73% of the un-profiled total and this memo never
|
|
5463
|
+
# sees it.
|
|
5464
|
+
quota_observations = (
|
|
5465
|
+
capture.quota_observations
|
|
5466
|
+
if capture is not None
|
|
5467
|
+
else _cached_codex_quota_observations(
|
|
5468
|
+
source_root_keys=active_roots,
|
|
5469
|
+
cache_conn=context.cache_conn,
|
|
5470
|
+
stats_conn=context.stats_conn,
|
|
5471
|
+
stats_identity=context.stats_identity,
|
|
5472
|
+
# A key carrying the tick instant cannot match a retained entry, so the
|
|
5473
|
+
# memo is bypassed rather than asked. On the share path
|
|
5474
|
+
# `stats_identity` is None as well, and establishing an identity for an
|
|
5475
|
+
# unreachable key cost 35 ms of stats.db work per render.
|
|
5476
|
+
memoize=False,
|
|
5477
|
+
captured_at_or_after=(
|
|
5478
|
+
context.now_utc - dt.timedelta(days=DASHBOARD_QUOTA_RECENT_DAYS)
|
|
5479
|
+
),
|
|
5480
|
+
active_at=context.now_utc,
|
|
5481
|
+
max_rows=DASHBOARD_QUOTA_OBSERVATION_LIMIT,
|
|
5482
|
+
)
|
|
5483
|
+
)
|
|
5484
|
+
projection_incoherent = (
|
|
5485
|
+
capture.projection_incoherent
|
|
5486
|
+
if capture is not None
|
|
5487
|
+
else not codex_projection_coherence(context).coherent
|
|
5488
|
+
)
|
|
5489
|
+
ingest_backlog = (
|
|
5490
|
+
capture.ingest_backlog
|
|
5491
|
+
if capture is not None
|
|
5492
|
+
else _codex_ingest_backlog_wire(context.cache_conn)
|
|
5493
|
+
)
|
|
5494
|
+
# The cache reader's established report surface treats the ``now`` instant
|
|
5495
|
+
# as inclusive. The qualified adapter is half-open, so extend only its
|
|
5496
|
+
# query/result boundary by one microsecond and keep all live budget sums
|
|
5497
|
+
# explicitly half-open at ``now`` below.
|
|
5498
|
+
accounting_end = (
|
|
5499
|
+
capture.accounting_end
|
|
5500
|
+
if capture is not None
|
|
5501
|
+
else context.now_utc + dt.timedelta(microseconds=1)
|
|
5502
|
+
)
|
|
5503
|
+
accounting_start = (
|
|
5504
|
+
capture.accounting_start
|
|
5505
|
+
if capture is not None
|
|
5506
|
+
else context.range_start
|
|
5507
|
+
)
|
|
5508
|
+
if capture is None and context.codex_budget is not None:
|
|
5509
|
+
# #556 S5 Unit 2 (Unit 1 review R6, widened) — this is the SECOND
|
|
5510
|
+
# unguarded call to the window resolver, and unlike
|
|
5511
|
+
# `_codex_budget_cost_events` it had no boundary of its own. It sits
|
|
5512
|
+
# outside every other `try` in this function, so an unresolvable window
|
|
5513
|
+
# escaped into `_tui_build_source_bundle`'s `source_build_failed`
|
|
5514
|
+
# handler and destroyed the entire Codex provider's data — the exact
|
|
5515
|
+
# failure §3.5 exists to prevent, reached from a different line.
|
|
5516
|
+
#
|
|
5517
|
+
# Degrading to the un-widened accounting range cannot publish a false
|
|
5518
|
+
# figure: the same failure reaches `_codex_budget_status_domain`, which
|
|
5519
|
+
# nulls the status and names `budget_compute_failed`.
|
|
5520
|
+
try:
|
|
5521
|
+
_period, budget_start, _budget_end = _configured_codex_budget_window(context)
|
|
5522
|
+
except Exception:
|
|
5523
|
+
_warn_codex_budget_window_once("accounting_range")
|
|
5524
|
+
else:
|
|
5525
|
+
accounting_start = min(accounting_start, budget_start)
|
|
5526
|
+
health = (
|
|
5527
|
+
capture.health
|
|
5528
|
+
if capture is not None
|
|
5529
|
+
else load_codex_project_metadata_health(
|
|
5530
|
+
cache_conn=context.cache_conn,
|
|
5531
|
+
start=accounting_start,
|
|
5532
|
+
end=accounting_end,
|
|
5533
|
+
)
|
|
5534
|
+
)
|
|
5535
|
+
metadata_incomplete = health.incomplete_rows > 0
|
|
5536
|
+
metadata_warning_message = (
|
|
5537
|
+
f"{health.incomplete_rows} Codex accounting row(s) lack project metadata; "
|
|
5538
|
+
"run `cctally cache-sync --source codex --rebuild`."
|
|
5539
|
+
if metadata_incomplete
|
|
5540
|
+
else "Codex project metadata could not be read; "
|
|
5541
|
+
"run `cctally cache-sync --source codex --rebuild`."
|
|
5542
|
+
)
|
|
5543
|
+
accounting_capture = (
|
|
5544
|
+
capture.accounting
|
|
5545
|
+
if capture is not None
|
|
5546
|
+
else _capture_codex_accounting(
|
|
5547
|
+
context,
|
|
5548
|
+
accounting_start=accounting_start,
|
|
5549
|
+
accounting_end=accounting_end,
|
|
5550
|
+
active_roots=active_roots,
|
|
5551
|
+
path_scope=path_scope,
|
|
5552
|
+
metadata_incomplete=metadata_incomplete,
|
|
5553
|
+
)
|
|
5554
|
+
)
|
|
5555
|
+
qualified_entries = accounting_capture.qualified_entries
|
|
5556
|
+
accounting_dirty_accounts = accounting_capture.dirty_accounts
|
|
5557
|
+
accounting_changed_old = accounting_capture.changed_old
|
|
5558
|
+
accounting_changed_new = accounting_capture.changed_new
|
|
5559
|
+
accounting_entries = accounting_capture.entries
|
|
5560
|
+
metadata_incomplete = accounting_capture.metadata_incomplete
|
|
4792
5561
|
budget_entries = _codex_entries_from_accounting(accounting_entries)
|
|
4793
5562
|
cycles_all: list[CodexCycleBoundary] = []
|
|
4794
5563
|
try:
|
|
@@ -4806,29 +5575,43 @@ def _build_codex_source_state(
|
|
|
4806
5575
|
# here leaves `cycle` unresolved, which `cycle_failure` below turns
|
|
4807
5576
|
# into the hero failure the accounting axis actually reports.
|
|
4808
5577
|
cycle = None
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
5578
|
+
if cycle is None and capture is not None:
|
|
5579
|
+
if capture.accounting_exists_error is not None:
|
|
5580
|
+
raise capture.accounting_exists_error
|
|
5581
|
+
accounting_exists = capture.accounting_exists
|
|
5582
|
+
elif cycle is None:
|
|
5583
|
+
accounting_exists = has_cached_codex_accounting_entries(
|
|
5584
|
+
cache_conn=context.cache_conn)
|
|
5585
|
+
else:
|
|
5586
|
+
accounting_exists = False
|
|
5587
|
+
cycle_failure = cycle is None and accounting_exists
|
|
4812
5588
|
hero_failure = projection_incoherent or cycle_failure
|
|
4813
5589
|
if cycle is None or hero_failure:
|
|
4814
5590
|
cycle_entries: list[CodexEntry] = []
|
|
4815
5591
|
cycle_cost_usd: float | None = None if hero_failure else 0.0
|
|
4816
5592
|
else:
|
|
4817
5593
|
cycle_end = min(accounting_end, cycle.resets_at)
|
|
4818
|
-
cycle_rows =
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
|
|
5594
|
+
cycle_rows = (
|
|
5595
|
+
tuple(
|
|
5596
|
+
row for row in capture.card_population
|
|
5597
|
+
if cycle.start_at <= row.timestamp.astimezone(UTC) < cycle_end
|
|
5598
|
+
and str(row.source_root_key) in cycle.source_root_keys
|
|
5599
|
+
)
|
|
5600
|
+
if capture is not None
|
|
5601
|
+
else load_cached_rooted_codex_accounting_entries(
|
|
5602
|
+
cycle.start_at,
|
|
5603
|
+
cycle_end,
|
|
5604
|
+
speed=context.speed,
|
|
5605
|
+
cache_conn=context.cache_conn,
|
|
5606
|
+
source_root_keys=cycle.source_root_keys,
|
|
5607
|
+
)
|
|
4824
5608
|
)
|
|
4825
|
-
cycle_entries =
|
|
4826
|
-
|
|
4827
|
-
cycle_entries,
|
|
5609
|
+
cycle_entries, cycle_cost_usd = _build_codex_cycle_accounting(
|
|
5610
|
+
cycle_rows,
|
|
4828
5611
|
now_utc=context.now_utc,
|
|
4829
|
-
|
|
5612
|
+
display_tz_name=context.display_tz_name,
|
|
4830
5613
|
speed=context.speed,
|
|
4831
|
-
)
|
|
5614
|
+
)
|
|
4832
5615
|
visible_accounting_entries = tuple(
|
|
4833
5616
|
entry for entry in accounting_entries
|
|
4834
5617
|
if context.range_start <= getattr(entry, "timestamp").astimezone(UTC) < accounting_end
|
|
@@ -4897,12 +5680,15 @@ def _build_codex_source_state(
|
|
|
4897
5680
|
# R8 gate, resolved once before the parent weekly projection so that only
|
|
4898
5681
|
# a decorated merged row gains the additive account axis. Focused children
|
|
4899
5682
|
# and <=1-real-account providers remain byte-identical.
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
|
|
5683
|
+
if capture is not None:
|
|
5684
|
+
_codex_decorated = capture.decorated
|
|
5685
|
+
else:
|
|
5686
|
+
try:
|
|
5687
|
+
import _cctally_account
|
|
5688
|
+
_codex_decorated = _cctally_account.provider_is_decorated(
|
|
5689
|
+
context.stats_conn, "codex")
|
|
5690
|
+
except Exception:
|
|
5691
|
+
_codex_decorated = False
|
|
4906
5692
|
weekly = _cached_codex_native_weekly_view(
|
|
4907
5693
|
context.stats_conn,
|
|
4908
5694
|
visible_accounting_entries,
|
|
@@ -4936,6 +5722,8 @@ def _build_codex_source_state(
|
|
|
4936
5722
|
quota_observations,
|
|
4937
5723
|
accounting_entries=visible_accounting_entries,
|
|
4938
5724
|
decorated=_codex_decorated,
|
|
5725
|
+
captured_breakdowns=(
|
|
5726
|
+
capture.breakdowns if capture is not None else None),
|
|
4939
5727
|
)
|
|
4940
5728
|
# R8 gate, resolved ONCE and threaded (#341 Task 4 / #416 §5.8). Every
|
|
4941
5729
|
# per-account decoration below — block/alert/budget `account_key`, the
|
|
@@ -4974,7 +5762,11 @@ def _build_codex_source_state(
|
|
|
4974
5762
|
context, budget_entries, cost_events=budget_cost_events,
|
|
4975
5763
|
)
|
|
4976
5764
|
configured_budget = configured_budget_domain["status"]
|
|
4977
|
-
conversation_metadata =
|
|
5765
|
+
conversation_metadata = (
|
|
5766
|
+
capture.conversation_metadata
|
|
5767
|
+
if capture is not None
|
|
5768
|
+
else _codex_conversation_metadata(context.cache_conn)
|
|
5769
|
+
)
|
|
4978
5770
|
cache_report = _codex_cache_report_wire(
|
|
4979
5771
|
visible_accounting_entries,
|
|
4980
5772
|
metadata=conversation_metadata,
|
|
@@ -5044,18 +5836,73 @@ def _build_codex_source_state(
|
|
|
5044
5836
|
# projection (client-side chip filter); the hero renders per-account cards.
|
|
5045
5837
|
accounts_wire: list[dict[str, object]] = []
|
|
5046
5838
|
hero_cycles_wire: list[dict[str, object]] = []
|
|
5839
|
+
combined_accounting: dict[str, object] | None = None
|
|
5047
5840
|
account_scopes: dict[str, dict[str, object]] = {}
|
|
5048
5841
|
# #556 S5 §3.8: bound OUTSIDE the try, because the degrade path below has to
|
|
5049
5842
|
# be able to clear it, and the retained `clock_data` reads it either way.
|
|
5050
5843
|
budget_events_by_account: dict[str, tuple[tuple[dt.datetime, float], ...]] = {}
|
|
5051
5844
|
if _codex_decorated:
|
|
5052
5845
|
try:
|
|
5053
|
-
|
|
5846
|
+
# #583 S5 change 2: ONE population the cards are partitioned from,
|
|
5847
|
+
# in place of one accounting read per card. The PUBLISHED accounting
|
|
5848
|
+
# range is unchanged -- widening it would move the envelope.
|
|
5849
|
+
#
|
|
5850
|
+
# This derivation population starts earlier than the published range
|
|
5851
|
+
# on purpose. On the dashboard path `accounting_start` is already a
|
|
5852
|
+
# superset of every card's own range, because it is the 30-day
|
|
5853
|
+
# shared range and a live cycle's `start_at` is inside
|
|
5854
|
+
# `(now - 7d, now)` by the two filters in
|
|
5855
|
+
# `_resolve_codex_weekly_cycle`. On the SHARE path it is not:
|
|
5856
|
+
# `bin/_cctally_dashboard_share.py` passes a user-selected
|
|
5857
|
+
# `range_start` verbatim, so a one-day custom share would truncate a
|
|
5858
|
+
# card whose cycle began six days earlier -- silently, with no error
|
|
5859
|
+
# and no moved golden.
|
|
5860
|
+
card_population_start = min(
|
|
5861
|
+
context.range_start,
|
|
5862
|
+
context.now_utc - dt.timedelta(
|
|
5863
|
+
minutes=ACCOUNT_WEEKLY_WINDOW_MINUTES),
|
|
5864
|
+
accounting_start,
|
|
5865
|
+
)
|
|
5866
|
+
if capture is not None:
|
|
5867
|
+
card_population = capture.card_population
|
|
5868
|
+
elif card_population_start == accounting_start:
|
|
5869
|
+
# The build already holds this exact population, so reading it
|
|
5870
|
+
# again would be one extra full-range pass over every retained
|
|
5871
|
+
# Codex accounting row, per tick. The equality is the ordinary
|
|
5872
|
+
# dashboard case rather than an edge: `card_population_start` is
|
|
5873
|
+
# a `min` that includes `accounting_start`, so it can only be
|
|
5874
|
+
# earlier or equal, and it is equal whenever `accounting_start`
|
|
5875
|
+
# is already at or before both `range_start` and `now - 7d` --
|
|
5876
|
+
# which the 30-day shared range always is. Only a custom share
|
|
5877
|
+
# narrower than one native cycle takes the other branch.
|
|
5878
|
+
#
|
|
5879
|
+
# `accounting_entries` is `QualifiedCodexEntry` on the normal
|
|
5880
|
+
# path and `RootedCodexAccountingEntry` on the
|
|
5881
|
+
# `metadata_incomplete` fallback. Both carry the three
|
|
5882
|
+
# attributes `_slice` reads (`account_key`, `timestamp`,
|
|
5883
|
+
# `source_root_key`), both normalize a NULL or empty stored
|
|
5884
|
+
# account to the sentinel, both price through
|
|
5885
|
+
# `_calculate_codex_entry_cost` at this `speed`, and both are
|
|
5886
|
+
# ordered by (timestamp, source_root_key, conversation_key, id)
|
|
5887
|
+
# over the same half-open range -- the qualified read adds only
|
|
5888
|
+
# a LEFT JOIN onto `codex_conversation_threads`, whose
|
|
5889
|
+
# `conversation_key` is that table's PRIMARY KEY and so cannot
|
|
5890
|
+
# duplicate a row.
|
|
5891
|
+
card_population = accounting_entries
|
|
5892
|
+
else:
|
|
5893
|
+
card_population = load_cached_rooted_codex_accounting_entries(
|
|
5894
|
+
card_population_start,
|
|
5895
|
+
accounting_end,
|
|
5896
|
+
speed=context.speed,
|
|
5897
|
+
cache_conn=context.cache_conn,
|
|
5898
|
+
)
|
|
5899
|
+
accounts_wire, hero_cycles_wire, combined_accounting = _codex_accounts_wire(
|
|
5054
5900
|
context,
|
|
5055
5901
|
quota_observations=quota_observations,
|
|
5056
5902
|
cycles=cycles_all,
|
|
5057
5903
|
accounting_start=accounting_start,
|
|
5058
5904
|
accounting_end=accounting_end,
|
|
5905
|
+
population=card_population,
|
|
5059
5906
|
)
|
|
5060
5907
|
# #416 §5.3: the per-account CHILDREN beside the merged parent. The
|
|
5061
5908
|
# scope set is exactly the card set, so every chip the client can
|
|
@@ -5103,6 +5950,8 @@ def _build_codex_source_state(
|
|
|
5103
5950
|
changed_new_by_account=changed_new_by_account,
|
|
5104
5951
|
changed_old_rows_by_account=changed_old_rows_by_account,
|
|
5105
5952
|
changed_new_rows_by_account=changed_new_rows_by_account,
|
|
5953
|
+
captured_breakdowns=(
|
|
5954
|
+
capture.breakdowns if capture is not None else None),
|
|
5106
5955
|
# Quota/stat generations are already represented by each
|
|
5107
5956
|
# child's quota observations, cycle and alert/budget rows in
|
|
5108
5957
|
# `_codex_account_scopes_wire`'s outer signature. Reuse the
|
|
@@ -5170,6 +6019,12 @@ def _build_codex_source_state(
|
|
|
5170
6019
|
# degrade to the byte-stable undecorated shape.
|
|
5171
6020
|
accounts_wire = []
|
|
5172
6021
|
hero_cycles_wire = []
|
|
6022
|
+
combined_accounting = {
|
|
6023
|
+
"scope": "account_cycles",
|
|
6024
|
+
"status": "unresolved",
|
|
6025
|
+
"cause": "account_cost_unresolved",
|
|
6026
|
+
"contributions": (),
|
|
6027
|
+
}
|
|
5173
6028
|
account_scopes = {}
|
|
5174
6029
|
budget_events_by_account = {}
|
|
5175
6030
|
# #416 QA P0-A — the "All accounts" headline is the MERGED spend and tokens
|
|
@@ -5206,7 +6061,9 @@ def _build_codex_source_state(
|
|
|
5206
6061
|
# observation age and weekly-cycle evidence live on their own axes.
|
|
5207
6062
|
freshness="fresh",
|
|
5208
6063
|
warnings=tuple(warnings),
|
|
5209
|
-
data_version=
|
|
6064
|
+
data_version=combined_accounting_version(
|
|
6065
|
+
data_version, combined_accounting,
|
|
6066
|
+
),
|
|
5210
6067
|
last_success_at=context.now_utc,
|
|
5211
6068
|
capabilities={
|
|
5212
6069
|
"hero": (
|
|
@@ -5305,8 +6162,8 @@ def _build_codex_source_state(
|
|
|
5305
6162
|
# while nothing about the accounting changed. The percent age is
|
|
5306
6163
|
# already carried by `quota` below and by the additive hero-local
|
|
5307
6164
|
# `cycle_freshness` field. `_resolve_codex_weekly_cycle` retains
|
|
5308
|
-
# only boundaries with `
|
|
5309
|
-
# never expired at build time; the idle clock owns expiry.
|
|
6165
|
+
# only boundaries with `start_at < now < resets_at`, so a resolved
|
|
6166
|
+
# cycle is never expired at build time; the idle clock owns expiry.
|
|
5310
6167
|
"hero": "stale" if hero_failure else "fresh",
|
|
5311
6168
|
"quota": (
|
|
5312
6169
|
"stale"
|
|
@@ -5330,4 +6187,5 @@ def _build_codex_source_state(
|
|
|
5330
6187
|
),
|
|
5331
6188
|
},
|
|
5332
6189
|
private_session_labels=private_session_labels,
|
|
6190
|
+
combined_accounting=combined_accounting,
|
|
5333
6191
|
)
|