cctally 1.99.1 → 1.100.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 +44 -0
- package/bin/_cctally_dashboard.py +1370 -190
- package/bin/_cctally_dashboard_envelope.py +98 -4
- package/bin/_cctally_dashboard_perf.py +433 -0
- package/bin/_cctally_dashboard_sources.py +441 -54
- package/bin/_cctally_db.py +30 -14
- package/bin/_cctally_doctor.py +1331 -1148
- package/bin/_cctally_parser.py +70 -1
- package/bin/_cctally_quota.py +40 -21
- package/bin/_cctally_record.py +37 -2
- package/bin/_cctally_refresh.py +60 -10
- package/bin/_cctally_statusline.py +53 -7
- package/bin/_cctally_tui.py +328 -28
- package/bin/_cctally_update.py +28 -22
- package/bin/_lib_dashboard_sources.py +26 -7
- package/bin/_lib_doctor.py +37 -0
- package/bin/_lib_jsonl.py +4 -2
- package/bin/_lib_perf.py +132 -3
- package/bin/_lib_source_analytics.py +2 -2
- package/bin/_lib_tick_stats.py +538 -0
- package/bin/cctally +19 -7
- package/dashboard/static/assets/index-B5YfQEtn.css +1 -0
- package/dashboard/static/assets/index-Bt59nMMO.js +97 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +3 -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,8 @@ 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
|
+
codex_stats_digest,
|
|
43
46
|
dashboard_resource_key,
|
|
44
47
|
)
|
|
45
48
|
from _lib_quota import (
|
|
@@ -85,44 +88,184 @@ _CODEX_QUOTA_OBSERVATION_CACHE: dict[object, tuple[object, ...]] = {}
|
|
|
85
88
|
|
|
86
89
|
|
|
87
90
|
def reset_codex_quota_observation_cache() -> None:
|
|
88
|
-
"""
|
|
91
|
+
"""Discard #582's value-only quota-read memo. TEST HOOK — no production caller.
|
|
92
|
+
|
|
93
|
+
Since #583 S5 removed the per-build clear, nothing in the shipped code path
|
|
94
|
+
calls this: the memo is invalidated by its key moving, never by a caller
|
|
95
|
+
emptying it. It survives as the isolation hook the pytest fixtures and the
|
|
96
|
+
cold-reference comparisons use, and `_codex_source_caches` is what the
|
|
97
|
+
build's own checkpoint/restore goes through.
|
|
98
|
+
"""
|
|
89
99
|
_CODEX_QUOTA_OBSERVATION_CACHE.clear()
|
|
90
100
|
|
|
91
101
|
|
|
92
|
-
def
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
102
|
+
def _codex_quota_reuse_identity(
|
|
103
|
+
cache_conn, stats_conn, *, stats_identity: tuple | None = None,
|
|
104
|
+
) -> tuple | None:
|
|
105
|
+
"""The full invalidation basis for the cross-build quota memo (#583 S5).
|
|
106
|
+
|
|
107
|
+
The cache legs alone are not sufficient. ``_build_codex_source_state`` used
|
|
108
|
+
to discard this memo on every build precisely because a stats-side
|
|
109
|
+
decoration or account-registry change advances no cache.db leg, so a key
|
|
110
|
+
built only from cache.db would not move for a mutation that changes the
|
|
111
|
+
answer. Returning ``None`` means "cannot establish identity", and every
|
|
112
|
+
caller must treat that as a cold read rather than as a cache hit.
|
|
113
|
+
|
|
114
|
+
Six legs, in two halves. The cache.db half is the quota change ledger's
|
|
115
|
+
high-water sequence, the window-attribution revision and the ``main``
|
|
116
|
+
database path -- an unreadable ledger or an absent ledger table is not an
|
|
117
|
+
idle ledger, so both return ``None``. The stats.db half is the three
|
|
118
|
+
digests the build already computes for its own composite signature
|
|
119
|
+
(``bin/_cctally_tui.py``), reused rather than replaced by a fourth
|
|
120
|
+
identity.
|
|
121
|
+
|
|
122
|
+
``stats_identity`` is that reuse, made literal: the caller passes
|
|
123
|
+
``(codex_stats_digest, accounts_identity_digest, claude_stats_digest)``
|
|
124
|
+
already computed for its own signature and this function does not recompute
|
|
125
|
+
them. Deriving them here is not cheap -- the three helpers issue twelve
|
|
126
|
+
stats.db statements, several of them whole-relation scans with an ``ORDER
|
|
127
|
+
BY``, plus one uncached filesystem read per configured provider root once
|
|
128
|
+
the ``accounts`` registry is non-empty. Measured on a copy of the real store
|
|
129
|
+
(276,391 retained Codex quota rows), warm, five samples after one untimed
|
|
130
|
+
warm-up: 35.2 ms median, range 33.1-35.7 ms per derivation.
|
|
131
|
+
|
|
132
|
+
Without a supplied identity the digests are derived here, and then the
|
|
133
|
+
projection gate applies. ``codex_stats_digest``'s relations include
|
|
134
|
+
``quota_projection_state`` and ``quota_window_blocks``, whose read site is
|
|
135
|
+
classified ``gate_at_caller`` in ``_cctally_quota.PROJECTION_READ_SITE_
|
|
136
|
+
ACTIONS`` precisely because that kernel may not import the gate; this
|
|
137
|
+
function is therefore named in ``PROJECTION_GATE_CALLERS`` and runs it. An
|
|
138
|
+
incomplete projection returns ``None`` rather than propagating, because the
|
|
139
|
+
honest answer for a REUSE IDENTITY is "cannot establish identity" and the
|
|
140
|
+
fail-safe for that is a cold read. Nothing is suppressed by swallowing it:
|
|
141
|
+
every caller of this memo runs a gated projection read of its own within a
|
|
142
|
+
few statements -- ``codex_projection_coherence`` on the build path -- so the
|
|
143
|
+
retry signal still reaches whoever owns the transaction.
|
|
144
|
+
|
|
145
|
+
A ``stats_identity`` of the wrong shape is the one input that RAISES instead
|
|
146
|
+
of returning ``None``, and the two cases are different in kind. Everything
|
|
147
|
+
above is an environmental inability to establish an identity -- an absent
|
|
148
|
+
ledger table, an unreadable database, an incomplete projection -- for which
|
|
149
|
+
a cold read is the correct degrade. A caller that hands this function a
|
|
150
|
+
two-element tuple has a defect in the caller, and taking the cold path for
|
|
151
|
+
it would leave the memo permanently and silently disabled. It is unreachable
|
|
152
|
+
from the production callers, all of which pass either ``None`` or
|
|
153
|
+
``DashboardReadContext.stats_identity``.
|
|
154
|
+
"""
|
|
155
|
+
if not isinstance(cache_conn, sqlite3.Connection):
|
|
156
|
+
return None
|
|
157
|
+
if stats_identity is not None:
|
|
158
|
+
if not isinstance(stats_identity, tuple) or len(stats_identity) != 3:
|
|
159
|
+
raise ValueError(
|
|
160
|
+
"stats_identity must be the three-digest tuple or None")
|
|
161
|
+
elif not isinstance(stats_conn, sqlite3.Connection):
|
|
162
|
+
return None
|
|
97
163
|
try:
|
|
98
|
-
seq_row =
|
|
164
|
+
seq_row = cache_conn.execute(
|
|
99
165
|
"SELECT seq FROM sqlite_sequence "
|
|
100
166
|
"WHERE name='quota_window_change_log'"
|
|
101
167
|
).fetchone()
|
|
102
|
-
table =
|
|
168
|
+
table = cache_conn.execute(
|
|
103
169
|
"SELECT 1 FROM sqlite_master WHERE type='table' "
|
|
104
170
|
"AND name='quota_window_change_log'"
|
|
105
171
|
).fetchone()
|
|
106
|
-
revision_row =
|
|
172
|
+
revision_row = cache_conn.execute(
|
|
107
173
|
"SELECT value FROM cache_meta "
|
|
108
174
|
"WHERE key='codex_window_attribution_revision'"
|
|
109
175
|
).fetchone()
|
|
110
176
|
db_path = next(
|
|
111
|
-
str(row[2]) for row in
|
|
177
|
+
str(row[2]) for row in cache_conn.execute("PRAGMA database_list")
|
|
112
178
|
if str(row[1]) == "main"
|
|
113
179
|
)
|
|
114
180
|
except (sqlite3.Error, StopIteration):
|
|
115
|
-
return
|
|
181
|
+
return None
|
|
116
182
|
if table is None:
|
|
183
|
+
return None
|
|
184
|
+
if stats_identity is None:
|
|
185
|
+
try:
|
|
186
|
+
assert_projection_readable(stats_conn)
|
|
187
|
+
stats_identity = (
|
|
188
|
+
codex_stats_digest(stats_conn),
|
|
189
|
+
accounts_identity_digest(stats_conn),
|
|
190
|
+
claude_stats_digest(stats_conn),
|
|
191
|
+
)
|
|
192
|
+
except QuotaProjectionIncomplete:
|
|
193
|
+
return None
|
|
194
|
+
except sqlite3.Error:
|
|
195
|
+
return None
|
|
196
|
+
return (
|
|
197
|
+
db_path,
|
|
198
|
+
0 if seq_row is None else int(seq_row[0]),
|
|
199
|
+
"" if revision_row is None else str(revision_row[0]),
|
|
200
|
+
*stats_identity,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _cached_codex_quota_observations(**kwargs) -> tuple[object, ...]:
|
|
205
|
+
"""Reuse bounded quota reads while their dedicated mutation stream is idle.
|
|
206
|
+
|
|
207
|
+
#583 S5: the memo now survives across source builds, so the key must carry
|
|
208
|
+
the stats-side identity as well as the cache.db legs. ``stats_conn``,
|
|
209
|
+
``stats_identity`` and ``memoize`` are memo-only inputs; none is forwarded
|
|
210
|
+
to the loader. A caller that already holds the three digests passes
|
|
211
|
+
``stats_identity`` so they are not recomputed once per memo call.
|
|
212
|
+
|
|
213
|
+
``memoize=False`` is the opt-out for a key that CANNOT match, and it is a
|
|
214
|
+
cost decision rather than a correctness one. A caller whose key carries the
|
|
215
|
+
tick instant -- ``active_at=now`` and a ``now``-derived
|
|
216
|
+
``captured_at_or_after`` -- moves both terms on every call, so it can never
|
|
217
|
+
read a prior entry. Going through the memo anyway would establish an
|
|
218
|
+
identity it cannot use (35 ms of stats.db work when no ``stats_identity`` is
|
|
219
|
+
supplied) and retain a result nobody can hit, which also evicts the
|
|
220
|
+
clock-free entries that DO reuse, because the memo is discarded whole on
|
|
221
|
+
reaching 32 entries.
|
|
222
|
+
|
|
223
|
+
THE GENERATION CONTRACT IS WEAKER THAN IT WAS, and the next reader of
|
|
224
|
+
``_tui_build_source_bundle``'s pin needs to know exactly how. This dict is a
|
|
225
|
+
plain module global with no ``_assert_owner``, and the per-build clear that
|
|
226
|
+
used to stand in ``_build_codex_source_state`` is gone, so an entry
|
|
227
|
+
populated on a non-builder path -- the share render or the per-request
|
|
228
|
+
cycle-detail route, from an unpinned connection at whatever cache generation
|
|
229
|
+
it saw -- can now be served to the builder INSIDE its pinned read
|
|
230
|
+
transaction. Before #583 S5 every cache-backed read inside that pin was
|
|
231
|
+
physically issued on the pinned connection.
|
|
232
|
+
|
|
233
|
+
What still holds, and why that is safe:
|
|
234
|
+
|
|
235
|
+
* Every read the builder issues ITSELF still runs on the pinned connection
|
|
236
|
+
and sees the frozen generation. Only a memo HIT returns rows another
|
|
237
|
+
caller loaded, and after the ``memoize=False`` sites above the sole read
|
|
238
|
+
that can hit inside a build is the five-hour correlation read.
|
|
239
|
+
* A hit requires the whole key to match, and the key is the invalidation
|
|
240
|
+
basis: the ``main`` database path, ``quota_window_change_log``'s
|
|
241
|
+
high-water sequence, ``codex_window_attribution_revision`` and the three
|
|
242
|
+
stats digests. Every ``quota_window_snapshots`` mutation is recorded by
|
|
243
|
+
the three ledger triggers in ``bin/_cctally_db.py``
|
|
244
|
+
(``trg_qws_ledger_ins`` / ``_del`` / ``_upd``, the last firing on the
|
|
245
|
+
semantic column set the loader interprets), and the attribution overlay
|
|
246
|
+
bumps its revision in the same transaction as the rows it applies
|
|
247
|
+
(``bin/_cctally_journal.py``). A retained value is therefore served only
|
|
248
|
+
while the evidence it was derived from has not moved, whatever generation
|
|
249
|
+
the connection that loaded it was on.
|
|
250
|
+
* The value is a tuple of frozen ``QuotaObservation``s, so a hit hands back
|
|
251
|
+
no cursor, connection or lazily-read state belonging to the other caller.
|
|
252
|
+
"""
|
|
253
|
+
stats_conn = kwargs.pop("stats_conn", None)
|
|
254
|
+
stats_identity = kwargs.pop("stats_identity", None)
|
|
255
|
+
memoize = bool(kwargs.pop("memoize", True))
|
|
256
|
+
conn = kwargs.get("cache_conn")
|
|
257
|
+
if not memoize or not isinstance(conn, sqlite3.Connection):
|
|
258
|
+
return load_codex_quota_observations(**kwargs)
|
|
259
|
+
identity = _codex_quota_reuse_identity(
|
|
260
|
+
conn, stats_conn, stats_identity=stats_identity)
|
|
261
|
+
if identity is None:
|
|
117
262
|
return load_codex_quota_observations(**kwargs)
|
|
118
263
|
roots = kwargs.get("source_root_keys")
|
|
119
264
|
physical_signatures = kwargs.get("physical_signatures")
|
|
120
265
|
physical_groups = kwargs.get("physical_groups")
|
|
121
266
|
key = (
|
|
122
267
|
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]),
|
|
268
|
+
identity,
|
|
126
269
|
None if roots is None else tuple(sorted(str(root) for root in roots)),
|
|
127
270
|
kwargs.get("captured_at_or_after"),
|
|
128
271
|
kwargs.get("active_at"),
|
|
@@ -139,6 +282,27 @@ def _cached_codex_quota_observations(**kwargs) -> tuple[object, ...]:
|
|
|
139
282
|
if cached is not None:
|
|
140
283
|
return cached
|
|
141
284
|
loaded = tuple(load_codex_quota_observations(**kwargs))
|
|
285
|
+
# The written memory bound (#583 S5 spec §2.1 "Memory"). Now that this memo
|
|
286
|
+
# survives a build, "32 entries" is a bound on ENTRIES and says nothing on
|
|
287
|
+
# its own about rows, so the worst-case retained row count is stated here:
|
|
288
|
+
#
|
|
289
|
+
# * at most 32 results are retained, and the whole memo is discarded on
|
|
290
|
+
# reaching that count rather than evicted one entry at a time;
|
|
291
|
+
# * a bounded dashboard read contributes at most
|
|
292
|
+
# `DASHBOARD_QUOTA_OBSERVATION_LIMIT` (1,000) `QuotaObservation`s;
|
|
293
|
+
# * the five-hour correlation read in `_quota_read_model` carries NO
|
|
294
|
+
# `max_rows`. It is bounded only by one root's retained observations
|
|
295
|
+
# captured at or after one weekly block's nominal start, which is the
|
|
296
|
+
# term that actually sizes this cache, and it grows with the block's
|
|
297
|
+
# age because an older block's nominal start reaches further back.
|
|
298
|
+
# Measured on a store holding 276,391 Codex quota rows, over the seven
|
|
299
|
+
# weekly blocks live at that instant: 15,129 observations for the
|
|
300
|
+
# newest and 36,943 for the oldest. The worst case is therefore 32
|
|
301
|
+
# times a population in the tens of thousands, not 32 times 1,000.
|
|
302
|
+
# * the doctor's unbounded `latest_per_identity` read is NOT retained
|
|
303
|
+
# here. `bin/_cctally_doctor.py` calls `load_codex_quota_observations`
|
|
304
|
+
# directly rather than through this memo, so its all-history population
|
|
305
|
+
# never enters this dict.
|
|
142
306
|
if len(_CODEX_QUOTA_OBSERVATION_CACHE) >= 32:
|
|
143
307
|
_CODEX_QUOTA_OBSERVATION_CACHE.clear()
|
|
144
308
|
_CODEX_QUOTA_OBSERVATION_CACHE[key] = loaded
|
|
@@ -327,10 +491,10 @@ def _resolve_codex_weekly_cycle(
|
|
|
327
491
|
|
|
328
492
|
#350 — FRESH-FIRST ranking (spec §3.2). Codex has no background quota poll,
|
|
329
493
|
so ``stale_after_seconds(10_080) == 3600`` makes an idle weekly observation
|
|
330
|
-
stale after exactly one hour. Discarding a stale-but-
|
|
494
|
+
stale after exactly one hour. Discarding a stale-but-ACTIVE boundary blanked
|
|
331
495
|
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:
|
|
496
|
+
each account's weekly boundaries that contain ``now`` are collected into a
|
|
497
|
+
fresh set and a stale set and ranked:
|
|
334
498
|
|
|
335
499
|
1. exactly one FRESH boundary -> valid, cycle fresh;
|
|
336
500
|
2. else, no fresh boundaries and exactly one STALE boundary -> valid, cycle
|
|
@@ -363,7 +527,15 @@ def _resolve_codex_weekly_cycle(
|
|
|
363
527
|
# current-cycle milestone filter matches `resets_at` exactly, so the
|
|
364
528
|
# ladder empties for precisely the jittered cycles canonicalization
|
|
365
529
|
# exists to collapse. Liveness rides the same instant the hero shows.
|
|
366
|
-
|
|
530
|
+
# #599: a future reset is not active when its nominal start is also in
|
|
531
|
+
# the future; admitting it makes the bounded accounting range empty.
|
|
532
|
+
if (
|
|
533
|
+
baseline is None
|
|
534
|
+
or baseline.canonical_resets_at <= now_utc
|
|
535
|
+
or baseline.canonical_resets_at - dt.timedelta(
|
|
536
|
+
minutes=history.identity.window_minutes
|
|
537
|
+
) >= now_utc
|
|
538
|
+
):
|
|
367
539
|
continue
|
|
368
540
|
state = quota_freshness(history.physical_observations, now_utc).state
|
|
369
541
|
boundary = (history.identity.window_minutes, baseline.canonical_resets_at)
|
|
@@ -435,6 +607,7 @@ def resolve_codex_cycle_detail_identity(
|
|
|
435
607
|
source_root_keys: Iterable[str],
|
|
436
608
|
now_utc: dt.datetime,
|
|
437
609
|
account_key: str | None = None,
|
|
610
|
+
stats_conn=None,
|
|
438
611
|
):
|
|
439
612
|
"""The live-cycle identity for a per-request Codex cycle-DETAIL read (#373).
|
|
440
613
|
|
|
@@ -488,6 +661,10 @@ def resolve_codex_cycle_detail_identity(
|
|
|
488
661
|
observations = _cached_codex_quota_observations(
|
|
489
662
|
source_root_keys=active_roots,
|
|
490
663
|
cache_conn=cache_conn,
|
|
664
|
+
stats_conn=stats_conn,
|
|
665
|
+
# Both bounds are derived from this request's own instant, so this
|
|
666
|
+
# key can never match a retained entry. See `memoize` there.
|
|
667
|
+
memoize=False,
|
|
491
668
|
captured_at_or_after=(
|
|
492
669
|
now_utc - dt.timedelta(days=DASHBOARD_QUOTA_RECENT_DAYS)
|
|
493
670
|
),
|
|
@@ -918,6 +1095,14 @@ class DashboardReadContext:
|
|
|
918
1095
|
codex_quota_actual_thresholds: tuple[int, ...] = ()
|
|
919
1096
|
codex_quota_projected_thresholds: tuple[int, ...] = ()
|
|
920
1097
|
cache_report_anomaly_threshold_pp: int = 15
|
|
1098
|
+
#: ``(codex_stats_digest, accounts_identity_digest, claude_stats_digest)``
|
|
1099
|
+
#: when the caller already computed them for its own composite signature,
|
|
1100
|
+
#: so the quota memo's reuse identity does not derive a second copy once
|
|
1101
|
+
#: per call (#583 S5 spec §2.1 item 1, Preserve 2). ``None`` means the
|
|
1102
|
+
#: identity derives them itself under the projection gate; that is the
|
|
1103
|
+
#: share path and the per-request cycle-detail route, neither of which
|
|
1104
|
+
#: computes a build signature.
|
|
1105
|
+
stats_identity: tuple[str, str, str] | None = None
|
|
921
1106
|
|
|
922
1107
|
def __post_init__(self) -> None:
|
|
923
1108
|
for name in ("range_start", "now_utc"):
|
|
@@ -2430,11 +2615,25 @@ def _codex_budget_status_domain(
|
|
|
2430
2615
|
``{"status": None}`` for it made the client render "No budget set." beside
|
|
2431
2616
|
the command that sets one — to a user who has budgets set. It publishes the
|
|
2432
2617
|
same ``not_configured.disposition`` the Claude half publishes, which the
|
|
2433
|
-
client already handles.
|
|
2434
|
-
``
|
|
2435
|
-
|
|
2618
|
+
client already handles. #586 gives the genuinely-unset ACCOUNT-SCOPED read
|
|
2619
|
+
its own ``account_budget_unset`` disposition plus immutable account key, so
|
|
2620
|
+
the card can name ``budget.codex.accounts`` rather than the unrelated
|
|
2621
|
+
vendor-wide command.
|
|
2436
2622
|
"""
|
|
2437
2623
|
config = context.codex_budget
|
|
2624
|
+
if account_key is not None:
|
|
2625
|
+
per_account = config.get("accounts") if isinstance(config, Mapping) else None
|
|
2626
|
+
if not isinstance(per_account, Mapping) or account_key not in per_account:
|
|
2627
|
+
return {
|
|
2628
|
+
"status": None,
|
|
2629
|
+
"not_configured": {
|
|
2630
|
+
"disposition": "account_budget_unset",
|
|
2631
|
+
"account_key": account_key,
|
|
2632
|
+
# `config set` replaces this map wholesale. Preserve every
|
|
2633
|
+
# sibling in the client-rendered example command (#586).
|
|
2634
|
+
"configured_accounts": dict(per_account or {}),
|
|
2635
|
+
},
|
|
2636
|
+
}
|
|
2438
2637
|
if (
|
|
2439
2638
|
account_key is None
|
|
2440
2639
|
and isinstance(config, Mapping)
|
|
@@ -2650,6 +2849,8 @@ def _quota_read_model(
|
|
|
2650
2849
|
for observation in _cached_codex_quota_observations(
|
|
2651
2850
|
source_root_keys={identity.source_root_key},
|
|
2652
2851
|
cache_conn=context.cache_conn,
|
|
2852
|
+
stats_conn=context.stats_conn,
|
|
2853
|
+
stats_identity=context.stats_identity,
|
|
2653
2854
|
captured_at_or_after=block.nominal_start_at,
|
|
2654
2855
|
)
|
|
2655
2856
|
if observation.identity.window_minutes == 300
|
|
@@ -3133,8 +3334,8 @@ def refresh_codex_source_clock(
|
|
|
3133
3334
|
availability = "partial"
|
|
3134
3335
|
# #556 S1 §4.1: an EXPIRED boundary is exactly the state the
|
|
3135
3336
|
# 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.
|
|
3337
|
+
# (`_resolve_codex_weekly_cycle` retains only boundaries containing
|
|
3338
|
+
# `now`), so this clock is the only writer of that value.
|
|
3138
3339
|
domain_freshness["hero"] = "stale"
|
|
3139
3340
|
cycle_changed = True
|
|
3140
3341
|
# 3. budget last
|
|
@@ -3901,9 +4102,19 @@ def _codex_accounts_wire(
|
|
|
3901
4102
|
cycles: list["CodexCycleBoundary"],
|
|
3902
4103
|
accounting_start: dt.datetime,
|
|
3903
4104
|
accounting_end: dt.datetime,
|
|
4105
|
+
population: tuple[object, ...],
|
|
3904
4106
|
) -> tuple[list[dict[str, object]], list[dict[str, object]]]:
|
|
3905
4107
|
"""Return ``(accounts_wire, hero_cycles_wire)`` for a decorated Codex source.
|
|
3906
4108
|
|
|
4109
|
+
``population`` is the merged, encounter-ordered accounting read every card
|
|
4110
|
+
is derived from (#583 S5 change 2). This function issues no accounting SQL
|
|
4111
|
+
of its own. It walks the population ONCE, partitioning it by account in
|
|
4112
|
+
encounter order, and then range-filters each card's own partition, so
|
|
4113
|
+
neither the read count nor the in-memory work grows with the account count.
|
|
4114
|
+
The caller loads it over a range that starts no later than any card's own
|
|
4115
|
+
range, which on the share path is EARLIER than the published accounting
|
|
4116
|
+
range.
|
|
4117
|
+
|
|
3907
4118
|
Caller must gate on ``provider_is_decorated(stats_conn, "codex")`` — this
|
|
3908
4119
|
builds nothing for a <=1-real-account install (the whole surface is absent,
|
|
3909
4120
|
so the envelope stays byte-identical, spec R8). Each account carries
|
|
@@ -3947,11 +4158,79 @@ def _codex_accounts_wire(
|
|
|
3947
4158
|
"startAt": fallback_start.astimezone(UTC).isoformat(),
|
|
3948
4159
|
"endAt": context.now_utc.astimezone(UTC).isoformat(),
|
|
3949
4160
|
}
|
|
4161
|
+
# ONE encounter-ordered pass over the population, in place of one pass per
|
|
4162
|
+
# card (#583 S5 spec §2.2). `_codex_partition_by_account` applies the same
|
|
4163
|
+
# `or UNATTRIBUTED` normalization each card's filter used to apply itself,
|
|
4164
|
+
# so the buckets are the sets those filters selected from, and it preserves
|
|
4165
|
+
# encounter order, which `_aggregate_codex_buckets` depends on for a
|
|
4166
|
+
# bucket's first-seen model order.
|
|
4167
|
+
by_account = _codex_partition_by_account(population)
|
|
4168
|
+
|
|
4169
|
+
def _slice(
|
|
4170
|
+
account_key: str,
|
|
4171
|
+
start: dt.datetime,
|
|
4172
|
+
end: dt.datetime,
|
|
4173
|
+
source_root_keys: Iterable[str] | None = None,
|
|
4174
|
+
) -> tuple[object, ...]:
|
|
4175
|
+
"""One card's rows, range-filtered from that account's partition.
|
|
4176
|
+
|
|
4177
|
+
The predicates match the reader these filters replaced, member for
|
|
4178
|
+
member: half-open ``[start, end)`` on a UTC-normalized timestamp, the
|
|
4179
|
+
reserved sentinel standing for a NULL stamp, and a root set that selects
|
|
4180
|
+
nothing when it is supplied empty. Encounter order is the partition's,
|
|
4181
|
+
which is the population's, which is the reader's ``ORDER BY`` over a
|
|
4182
|
+
wider range, so a bucket's first-seen model order is unchanged.
|
|
4183
|
+
|
|
4184
|
+
Two comparisons are nevertheless not the SAME comparison, and both are
|
|
4185
|
+
equivalent only because ingest cannot write the row that separates them.
|
|
4186
|
+
Stated here so the next reader does not have to re-derive it, and
|
|
4187
|
+
asserted at the producer by
|
|
4188
|
+
``test_ingest_writes_only_canonical_account_and_timestamp_values``.
|
|
4189
|
+
|
|
4190
|
+
1. The replaced per-card query passed ``account_key=<key>`` to
|
|
4191
|
+
``load_cached_rooted_codex_accounting_entries``, whose sentinel
|
|
4192
|
+
predicate is ``account_key IS NULL OR account_key = 'unattributed'``.
|
|
4193
|
+
A row stored as the EMPTY STRING matched neither that nor any real
|
|
4194
|
+
account's ``account_key = ?``, so it appeared on no card at all,
|
|
4195
|
+
while the reader maps ``''`` to the sentinel and this filter
|
|
4196
|
+
therefore includes it. No shipped writer emits it: the three
|
|
4197
|
+
statements that set this column (`bin/_cctally_cache.py` -- the
|
|
4198
|
+
ingest insert, the window spend-adoption update and the attribution
|
|
4199
|
+
restore) each write NULL or a resolved 32-hex account key, and the
|
|
4200
|
+
ingest value comes from ``codex_file_accounts.account_key``, itself
|
|
4201
|
+
written only by two statements with the same two outcomes. Measured:
|
|
4202
|
+
zero empty strings among 163,424 rows of the maintainer's store.
|
|
4203
|
+
Note also that every OTHER account-partitioned surface already files
|
|
4204
|
+
such a row under the sentinel -- ``_codex_partition_by_account``
|
|
4205
|
+
applies the identical ``or _lib_accounts.UNATTRIBUTED`` -- so the
|
|
4206
|
+
card query was the sole outlier rather than the rule.
|
|
4207
|
+
2. The reader bounds ``timestamp_utc`` by comparing ISO TEXT inside
|
|
4208
|
+
SQLite; this filter compares parsed UTC datetimes. For the canonical
|
|
4209
|
+
``...+00:00`` spelling the two orders coincide, including against a
|
|
4210
|
+
bound carrying microseconds, because ``+`` sorts before ``.``. They
|
|
4211
|
+
diverge only for a stamp carrying a different valid offset, and a
|
|
4212
|
+
NAIVE stamp is not a divergence at all: ``_parse_timestamp`` refuses
|
|
4213
|
+
it, so such a row fails the whole read on the old path and the new
|
|
4214
|
+
one alike. Codex ingest writes
|
|
4215
|
+
``entry.timestamp.astimezone(UTC).isoformat()``, always ``+00:00``.
|
|
4216
|
+
Measured: 163,424 of 163,424 rows canonical.
|
|
4217
|
+
"""
|
|
4218
|
+
roots = (
|
|
4219
|
+
None if source_root_keys is None
|
|
4220
|
+
else {key for key in source_root_keys
|
|
4221
|
+
if isinstance(key, str) and key}
|
|
4222
|
+
)
|
|
4223
|
+
if roots is not None and not roots:
|
|
4224
|
+
return ()
|
|
4225
|
+
return tuple(
|
|
4226
|
+
row for row in by_account.get(account_key, ())
|
|
4227
|
+
if start <= row.timestamp < end
|
|
4228
|
+
and (roots is None or row.source_root_key in roots)
|
|
4229
|
+
)
|
|
4230
|
+
|
|
3950
4231
|
# 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
|
-
)
|
|
4232
|
+
unattributed_rows = _slice(
|
|
4233
|
+
_lib_accounts.UNATTRIBUTED, accounting_start, accounting_end)
|
|
3955
4234
|
# Existence is decided over the accounting range so a sentinel holding only
|
|
3956
4235
|
# older spend keeps its card; the totals below cover the bounded window, so
|
|
3957
4236
|
# a resolved $0.00 is an honest empty state rather than an absence (#564).
|
|
@@ -3996,10 +4275,9 @@ def _codex_accounts_wire(
|
|
|
3996
4275
|
is_unattributed = key == _lib_accounts.UNATTRIBUTED
|
|
3997
4276
|
if cyc is not None and not is_unattributed:
|
|
3998
4277
|
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,
|
|
4278
|
+
rows = _slice(
|
|
4279
|
+
key, cyc.start_at, cycle_end,
|
|
4280
|
+
source_root_keys=cyc.source_root_keys,
|
|
4003
4281
|
)
|
|
4004
4282
|
totals = _totals(rows)
|
|
4005
4283
|
elif is_unattributed:
|
|
@@ -4009,10 +4287,7 @@ def _codex_accounts_wire(
|
|
|
4009
4287
|
# native cycle width ending now, so this card can be summed into a
|
|
4010
4288
|
# week-labelled headline without overstating it (#564). No bars or
|
|
4011
4289
|
# 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
|
-
)
|
|
4290
|
+
rows = _slice(key, fallback_start, accounting_end)
|
|
4016
4291
|
totals = _totals(rows)
|
|
4017
4292
|
card: dict[str, object] = {
|
|
4018
4293
|
"accountKey": key,
|
|
@@ -4595,6 +4870,40 @@ def _codex_ingest_backlog_wire(
|
|
|
4595
4870
|
}
|
|
4596
4871
|
|
|
4597
4872
|
|
|
4873
|
+
def _codex_source_caches() -> tuple[dict, ...]:
|
|
4874
|
+
"""Every process cache a Codex source build reuses across builds.
|
|
4875
|
+
|
|
4876
|
+
ONE definition, so the set the build checkpoints and the set a cold
|
|
4877
|
+
reference discards cannot drift apart. A cold reference that discards only
|
|
4878
|
+
some of them compares two partly-warm builds and calls the second cold
|
|
4879
|
+
(#583 S5 criterion 10).
|
|
4880
|
+
"""
|
|
4881
|
+
return (
|
|
4882
|
+
_CODEX_QUOTA_OBSERVATION_CACHE,
|
|
4883
|
+
_CODEX_PERIOD_VIEW_CACHE,
|
|
4884
|
+
_CODEX_CACHE_REPORT_ROWS,
|
|
4885
|
+
_CODEX_SESSION_VIEW_CACHE,
|
|
4886
|
+
_CODEX_PROJECT_LABEL_CACHE,
|
|
4887
|
+
_CODEX_PROJECT_WIRE_CACHE,
|
|
4888
|
+
_CODEX_ENTRY_ADAPTER_CACHE,
|
|
4889
|
+
_CODEX_WEEKLY_VIEW_CACHE,
|
|
4890
|
+
_CODEX_ACCOUNT_SCOPE_CACHE,
|
|
4891
|
+
)
|
|
4892
|
+
|
|
4893
|
+
|
|
4894
|
+
def reset_codex_source_caches() -> None:
|
|
4895
|
+
"""Discard every reuse a Codex source build could draw on.
|
|
4896
|
+
|
|
4897
|
+
The cold reference for a warm-versus-cold equality check: after this, the
|
|
4898
|
+
next build reconstructs from the databases alone. It covers the accounting
|
|
4899
|
+
cache state too, which lives in ``_lib_snapshot_cache`` rather than in a
|
|
4900
|
+
dict here and is the other half of what a build retains.
|
|
4901
|
+
"""
|
|
4902
|
+
for cache in _codex_source_caches():
|
|
4903
|
+
cache.clear()
|
|
4904
|
+
_lib_snapshot_cache.reset_codex_accounting_cache_state()
|
|
4905
|
+
|
|
4906
|
+
|
|
4598
4907
|
def build_codex_source_state(
|
|
4599
4908
|
context: DashboardReadContext,
|
|
4600
4909
|
*,
|
|
@@ -4613,23 +4922,12 @@ def build_codex_source_state(
|
|
|
4613
4922
|
every Codex session view in the build, and it is discarded when the read
|
|
4614
4923
|
returns.
|
|
4615
4924
|
"""
|
|
4616
|
-
#
|
|
4617
|
-
#
|
|
4618
|
-
#
|
|
4619
|
-
#
|
|
4620
|
-
#
|
|
4621
|
-
|
|
4622
|
-
caches = (
|
|
4623
|
-
_CODEX_QUOTA_OBSERVATION_CACHE,
|
|
4624
|
-
_CODEX_PERIOD_VIEW_CACHE,
|
|
4625
|
-
_CODEX_CACHE_REPORT_ROWS,
|
|
4626
|
-
_CODEX_SESSION_VIEW_CACHE,
|
|
4627
|
-
_CODEX_PROJECT_LABEL_CACHE,
|
|
4628
|
-
_CODEX_PROJECT_WIRE_CACHE,
|
|
4629
|
-
_CODEX_ENTRY_ADAPTER_CACHE,
|
|
4630
|
-
_CODEX_WEEKLY_VIEW_CACHE,
|
|
4631
|
-
_CODEX_ACCOUNT_SCOPE_CACHE,
|
|
4632
|
-
)
|
|
4925
|
+
# #583 S5: this memo now survives across builds. It may do so ONLY because
|
|
4926
|
+
# its key carries the stats-side identity as well as the cache legs -- see
|
|
4927
|
+
# `_codex_quota_reuse_identity`. The clear that used to stand here existed
|
|
4928
|
+
# because a decoration or account-registry change advances no cache.db leg
|
|
4929
|
+
# and so could not move a cache-only key. Do not narrow that identity.
|
|
4930
|
+
caches = _codex_source_caches()
|
|
4633
4931
|
cache_checkpoint = tuple(dict(cache) for cache in caches)
|
|
4634
4932
|
accounting_checkpoint = (
|
|
4635
4933
|
_lib_snapshot_cache.checkpoint_codex_accounting_cache_state()
|
|
@@ -4659,9 +4957,46 @@ def _build_codex_source_state(
|
|
|
4659
4957
|
"SELECT source_root_key FROM codex_source_roots"
|
|
4660
4958
|
)
|
|
4661
4959
|
))
|
|
4960
|
+
# This read is a function of the tick instant, so it does NOT reuse across
|
|
4961
|
+
# ticks and the cross-build memo above cannot help it. Both bounds move
|
|
4962
|
+
# with `context.now_utc`, which in production is the tick's wall clock
|
|
4963
|
+
# (`bin/_cctally_tui.py`: `now_utc or dt.datetime.now(dt.timezone.utc)`).
|
|
4964
|
+
#
|
|
4965
|
+
# It cannot be made reusable by loading a memoizable superset and filtering
|
|
4966
|
+
# in memory, and the reason is a property of the loader rather than of this
|
|
4967
|
+
# call: `max_rows` is a SQL `LIMIT` under an `ORDER BY` whose leading term
|
|
4968
|
+
# is `resets_at_utc > active_at`, so a superset truncates a DIFFERENT set of
|
|
4969
|
+
# rows; and `_apply_codex_window_attribution_overlay` plus
|
|
4970
|
+
# `adopt_unidentified_observations` are population-dependent folds run after
|
|
4971
|
+
# the bound, so a wider population can change the account a retained
|
|
4972
|
+
# observation carries. Filtering a superset would therefore change the
|
|
4973
|
+
# published answer, not just its cost.
|
|
4974
|
+
#
|
|
4975
|
+
# Measured on a copy of the real store (276,391 retained Codex quota rows),
|
|
4976
|
+
# warm, five samples after one untimed warm-up: 222.5 ms median per tick,
|
|
4977
|
+
# range 210.7-231.3 ms.
|
|
4978
|
+
#
|
|
4979
|
+
# As a share, that is 13.9% of 1.60 s, and the denominator has to be named:
|
|
4980
|
+
# 1.60 s is the sum of the three quota call sites when each is timed ALONE
|
|
4981
|
+
# and UN-profiled on that store (this bounded read 222.5 ms, the five-hour
|
|
4982
|
+
# correlation read 209.6 ms, the doctor's all-history `latest_per_identity`
|
|
4983
|
+
# read 1,164.7 ms). The S5 measurement record's 2.571 s for
|
|
4984
|
+
# `load_codex_quota_observations` is a cProfile total instead, so putting an
|
|
4985
|
+
# un-profiled numerator over it reads about 8% and understates the share.
|
|
4986
|
+
# Either way the site is well under the threshold that would have required a
|
|
4987
|
+
# superset fix, which is why the design conclusion above stands. The
|
|
4988
|
+
# doctor's read is about 73% of the un-profiled total and this memo never
|
|
4989
|
+
# sees it.
|
|
4662
4990
|
quota_observations = _cached_codex_quota_observations(
|
|
4663
4991
|
source_root_keys=active_roots,
|
|
4664
4992
|
cache_conn=context.cache_conn,
|
|
4993
|
+
stats_conn=context.stats_conn,
|
|
4994
|
+
stats_identity=context.stats_identity,
|
|
4995
|
+
# A key carrying the tick instant cannot match a retained entry, so the
|
|
4996
|
+
# memo is bypassed rather than asked. On the share path
|
|
4997
|
+
# `stats_identity` is None as well, and establishing an identity for an
|
|
4998
|
+
# unreachable key cost 35 ms of stats.db work per render.
|
|
4999
|
+
memoize=False,
|
|
4665
5000
|
captured_at_or_after=(
|
|
4666
5001
|
context.now_utc - dt.timedelta(days=DASHBOARD_QUOTA_RECENT_DAYS)
|
|
4667
5002
|
),
|
|
@@ -5050,12 +5385,64 @@ def _build_codex_source_state(
|
|
|
5050
5385
|
budget_events_by_account: dict[str, tuple[tuple[dt.datetime, float], ...]] = {}
|
|
5051
5386
|
if _codex_decorated:
|
|
5052
5387
|
try:
|
|
5388
|
+
# #583 S5 change 2: ONE population the cards are partitioned from,
|
|
5389
|
+
# in place of one accounting read per card. The PUBLISHED accounting
|
|
5390
|
+
# range is unchanged -- widening it would move the envelope.
|
|
5391
|
+
#
|
|
5392
|
+
# This derivation population starts earlier than the published range
|
|
5393
|
+
# on purpose. On the dashboard path `accounting_start` is already a
|
|
5394
|
+
# superset of every card's own range, because it is the 30-day
|
|
5395
|
+
# shared range and a live cycle's `start_at` is inside
|
|
5396
|
+
# `(now - 7d, now)` by the two filters in
|
|
5397
|
+
# `_resolve_codex_weekly_cycle`. On the SHARE path it is not:
|
|
5398
|
+
# `bin/_cctally_dashboard_share.py` passes a user-selected
|
|
5399
|
+
# `range_start` verbatim, so a one-day custom share would truncate a
|
|
5400
|
+
# card whose cycle began six days earlier -- silently, with no error
|
|
5401
|
+
# and no moved golden.
|
|
5402
|
+
card_population_start = min(
|
|
5403
|
+
context.range_start,
|
|
5404
|
+
context.now_utc - dt.timedelta(
|
|
5405
|
+
minutes=ACCOUNT_WEEKLY_WINDOW_MINUTES),
|
|
5406
|
+
accounting_start,
|
|
5407
|
+
)
|
|
5408
|
+
if card_population_start == accounting_start:
|
|
5409
|
+
# The build already holds this exact population, so reading it
|
|
5410
|
+
# again would be one extra full-range pass over every retained
|
|
5411
|
+
# Codex accounting row, per tick. The equality is the ordinary
|
|
5412
|
+
# dashboard case rather than an edge: `card_population_start` is
|
|
5413
|
+
# a `min` that includes `accounting_start`, so it can only be
|
|
5414
|
+
# earlier or equal, and it is equal whenever `accounting_start`
|
|
5415
|
+
# is already at or before both `range_start` and `now - 7d` --
|
|
5416
|
+
# which the 30-day shared range always is. Only a custom share
|
|
5417
|
+
# narrower than one native cycle takes the other branch.
|
|
5418
|
+
#
|
|
5419
|
+
# `accounting_entries` is `QualifiedCodexEntry` on the normal
|
|
5420
|
+
# path and `RootedCodexAccountingEntry` on the
|
|
5421
|
+
# `metadata_incomplete` fallback. Both carry the three
|
|
5422
|
+
# attributes `_slice` reads (`account_key`, `timestamp`,
|
|
5423
|
+
# `source_root_key`), both normalize a NULL or empty stored
|
|
5424
|
+
# account to the sentinel, both price through
|
|
5425
|
+
# `_calculate_codex_entry_cost` at this `speed`, and both are
|
|
5426
|
+
# ordered by (timestamp, source_root_key, conversation_key, id)
|
|
5427
|
+
# over the same half-open range -- the qualified read adds only
|
|
5428
|
+
# a LEFT JOIN onto `codex_conversation_threads`, whose
|
|
5429
|
+
# `conversation_key` is that table's PRIMARY KEY and so cannot
|
|
5430
|
+
# duplicate a row.
|
|
5431
|
+
card_population = accounting_entries
|
|
5432
|
+
else:
|
|
5433
|
+
card_population = load_cached_rooted_codex_accounting_entries(
|
|
5434
|
+
card_population_start,
|
|
5435
|
+
accounting_end,
|
|
5436
|
+
speed=context.speed,
|
|
5437
|
+
cache_conn=context.cache_conn,
|
|
5438
|
+
)
|
|
5053
5439
|
accounts_wire, hero_cycles_wire = _codex_accounts_wire(
|
|
5054
5440
|
context,
|
|
5055
5441
|
quota_observations=quota_observations,
|
|
5056
5442
|
cycles=cycles_all,
|
|
5057
5443
|
accounting_start=accounting_start,
|
|
5058
5444
|
accounting_end=accounting_end,
|
|
5445
|
+
population=card_population,
|
|
5059
5446
|
)
|
|
5060
5447
|
# #416 §5.3: the per-account CHILDREN beside the merged parent. The
|
|
5061
5448
|
# scope set is exactly the card set, so every chip the client can
|
|
@@ -5305,8 +5692,8 @@ def _build_codex_source_state(
|
|
|
5305
5692
|
# while nothing about the accounting changed. The percent age is
|
|
5306
5693
|
# already carried by `quota` below and by the additive hero-local
|
|
5307
5694
|
# `cycle_freshness` field. `_resolve_codex_weekly_cycle` retains
|
|
5308
|
-
# only boundaries with `
|
|
5309
|
-
# never expired at build time; the idle clock owns expiry.
|
|
5695
|
+
# only boundaries with `start_at < now < resets_at`, so a resolved
|
|
5696
|
+
# cycle is never expired at build time; the idle clock owns expiry.
|
|
5310
5697
|
"hero": "stale" if hero_failure else "fresh",
|
|
5311
5698
|
"quota": (
|
|
5312
5699
|
"stale"
|