cctally 1.68.0 → 1.69.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 +56 -0
- package/bin/_cctally_alerts.py +54 -2
- package/bin/_cctally_cache.py +644 -107
- package/bin/_cctally_cache_report.py +41 -17
- package/bin/_cctally_codex.py +109 -4
- package/bin/_cctally_config.py +368 -2
- package/bin/_cctally_core.py +168 -5
- package/bin/_cctally_dashboard.py +679 -5
- package/bin/_cctally_dashboard_conversation.py +9 -4
- package/bin/_cctally_dashboard_envelope.py +110 -1
- package/bin/_cctally_dashboard_share.py +557 -79
- package/bin/_cctally_db.py +303 -17
- package/bin/_cctally_diff.py +49 -16
- package/bin/_cctally_doctor.py +118 -0
- package/bin/_cctally_forecast.py +12 -4
- package/bin/_cctally_parser.py +298 -92
- package/bin/_cctally_project.py +24 -8
- package/bin/_cctally_quota.py +1381 -0
- package/bin/_cctally_record.py +319 -14
- package/bin/_cctally_refresh.py +105 -3
- package/bin/_cctally_reporting.py +7 -1
- package/bin/_cctally_setup.py +343 -113
- package/bin/_cctally_statusline.py +228 -0
- package/bin/_cctally_tui.py +787 -7
- package/bin/_lib_alert_axes.py +11 -0
- package/bin/_lib_codex_hooks.py +367 -0
- package/bin/_lib_conversation_query.py +156 -67
- package/bin/_lib_doctor.py +202 -0
- package/bin/_lib_jsonl.py +529 -162
- package/bin/_lib_quota.py +566 -0
- package/bin/_lib_share.py +152 -34
- package/bin/_lib_snapshot_cache.py +26 -0
- package/bin/_lib_source_identity.py +74 -0
- package/bin/cctally +324 -10
- package/dashboard/static/assets/index-CBbErI-P.js +80 -0
- package/dashboard/static/assets/index-kDDVOLa_.css +1 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +5 -1
- package/dashboard/static/assets/index-BybNp_Di.js +0 -80
- package/dashboard/static/assets/index-DgAmLK65.css +0 -1
|
@@ -0,0 +1,1381 @@
|
|
|
1
|
+
"""Codex adapter for durable provider-neutral quota interpretation.
|
|
2
|
+
|
|
3
|
+
``quota_window_snapshots`` remains cache.db's physical, re-derivable evidence.
|
|
4
|
+
This module reads that committed cache after the S1 ingest lock releases and
|
|
5
|
+
reconciles an interpreted index in stats.db. The two databases are not and do
|
|
6
|
+
not pretend to be one atomic transaction: a retry always derives a complete new
|
|
7
|
+
stats generation from the physical cache.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import datetime as dt
|
|
12
|
+
import hashlib
|
|
13
|
+
import json
|
|
14
|
+
import secrets
|
|
15
|
+
import sqlite3
|
|
16
|
+
import sys
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from typing import Callable, Iterable, Mapping
|
|
19
|
+
|
|
20
|
+
import _cctally_core
|
|
21
|
+
from _cctally_core import _command_as_of, eprint
|
|
22
|
+
from _lib_quota import (
|
|
23
|
+
QuotaBlock,
|
|
24
|
+
QuotaForecast,
|
|
25
|
+
QuotaFreshness,
|
|
26
|
+
QuotaHistory,
|
|
27
|
+
QuotaObservation,
|
|
28
|
+
QuotaPercentMilestone,
|
|
29
|
+
QuotaRule,
|
|
30
|
+
QuotaWindowIdentity,
|
|
31
|
+
build_blocks,
|
|
32
|
+
build_history,
|
|
33
|
+
forecast_quota,
|
|
34
|
+
quota_freshness,
|
|
35
|
+
quota_rule_fingerprint,
|
|
36
|
+
quota_threshold_decisions,
|
|
37
|
+
percent_milestones,
|
|
38
|
+
resolve_quota_rule,
|
|
39
|
+
select_baseline,
|
|
40
|
+
source_path_key,
|
|
41
|
+
)
|
|
42
|
+
from _lib_json_envelope import stamp_schema_version
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
UTC = dt.timezone.utc
|
|
46
|
+
_DASHBOARD_PROJECTION_CERTIFICATE_KEY = "codex_quota_projection_certificate"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True)
|
|
50
|
+
class QuotaProjectionResult:
|
|
51
|
+
"""Counts from one completed reconciliation transaction."""
|
|
52
|
+
|
|
53
|
+
generation: str | None
|
|
54
|
+
blocks_upserted: int
|
|
55
|
+
milestones_upserted: int
|
|
56
|
+
blocks_orphaned: int
|
|
57
|
+
milestones_orphaned: int
|
|
58
|
+
roots_stamped: int
|
|
59
|
+
alerts_dispatched: int
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(frozen=True)
|
|
63
|
+
class CodexQuotaBreakdownRow:
|
|
64
|
+
"""One milestone correlated with root-qualified Codex accounting."""
|
|
65
|
+
|
|
66
|
+
percent: int
|
|
67
|
+
captured_at: dt.datetime
|
|
68
|
+
input_tokens: int
|
|
69
|
+
cached_input_tokens: int
|
|
70
|
+
output_tokens: int
|
|
71
|
+
reasoning_output_tokens: int
|
|
72
|
+
total_tokens: int
|
|
73
|
+
cost_usd: float
|
|
74
|
+
marginal_cost_usd: float
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _parse_utc(value: str, label: str) -> dt.datetime:
|
|
78
|
+
parsed = dt.datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
79
|
+
if parsed.tzinfo is None or parsed.utcoffset() is None:
|
|
80
|
+
raise ValueError(f"{label} must be timezone-aware")
|
|
81
|
+
return parsed.astimezone(UTC)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _utc_iso(value: dt.datetime) -> str:
|
|
85
|
+
return value.astimezone(UTC).isoformat()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _physical_tuple(observation: QuotaObservation) -> tuple[dt.datetime, str, int]:
|
|
89
|
+
return (observation.captured_at, observation.source_path, observation.line_offset)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _cache_connection() -> sqlite3.Connection:
|
|
93
|
+
"""Open a read-only cache connection without invoking or re-running ingest."""
|
|
94
|
+
path = _cctally_core.CACHE_DB_PATH
|
|
95
|
+
if not path.exists():
|
|
96
|
+
raise FileNotFoundError(path)
|
|
97
|
+
conn = sqlite3.connect(f"file:{path}?mode=ro", uri=True)
|
|
98
|
+
conn.row_factory = sqlite3.Row
|
|
99
|
+
return conn
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _cache_root_keys(conn: sqlite3.Connection) -> set[str]:
|
|
103
|
+
try:
|
|
104
|
+
return {
|
|
105
|
+
str(row[0]) for row in conn.execute(
|
|
106
|
+
"SELECT source_root_key FROM codex_source_roots"
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
except sqlite3.OperationalError:
|
|
110
|
+
return set()
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def codex_physical_mutation_seq(conn: sqlite3.Connection) -> int:
|
|
114
|
+
"""Return the cache-local Codex physical sequence without scanning history."""
|
|
115
|
+
try:
|
|
116
|
+
row = conn.execute(
|
|
117
|
+
"SELECT value FROM cache_meta WHERE key='codex_physical_mutation_seq'"
|
|
118
|
+
).fetchone()
|
|
119
|
+
return 0 if row is None else int(row[0])
|
|
120
|
+
except (sqlite3.Error, TypeError, ValueError):
|
|
121
|
+
return 0
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def load_codex_quota_projection_certificate(
|
|
125
|
+
conn: sqlite3.Connection,
|
|
126
|
+
) -> tuple[int, dict[str, str]] | None:
|
|
127
|
+
"""Read the post-reconciliation physical-signature certificate in O(1)."""
|
|
128
|
+
try:
|
|
129
|
+
row = conn.execute(
|
|
130
|
+
"SELECT value FROM cache_meta WHERE key=?",
|
|
131
|
+
(_DASHBOARD_PROJECTION_CERTIFICATE_KEY,),
|
|
132
|
+
).fetchone()
|
|
133
|
+
if row is None:
|
|
134
|
+
return None
|
|
135
|
+
payload = json.loads(str(row[0]))
|
|
136
|
+
sequence = int(payload["sequence"])
|
|
137
|
+
signatures = {
|
|
138
|
+
str(root_key): str(signature)
|
|
139
|
+
for root_key, signature in dict(payload["signatures"]).items()
|
|
140
|
+
}
|
|
141
|
+
except (sqlite3.Error, TypeError, ValueError, KeyError, json.JSONDecodeError):
|
|
142
|
+
return None
|
|
143
|
+
if sequence < 0 or any(len(signature) != 64 for signature in signatures.values()):
|
|
144
|
+
return None
|
|
145
|
+
return sequence, signatures
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _store_codex_quota_projection_certificate(
|
|
149
|
+
*,
|
|
150
|
+
sequence: int,
|
|
151
|
+
signatures: Mapping[str, str],
|
|
152
|
+
) -> None:
|
|
153
|
+
"""Stamp exact validated signatures only if cache physical state is unchanged.
|
|
154
|
+
|
|
155
|
+
The certificate is written after the independent stats transaction commits.
|
|
156
|
+
A later cache mutation necessarily advances ``sequence``, so a dashboard
|
|
157
|
+
reader fails coherence rather than combining new physical cache data with
|
|
158
|
+
the prior projection certificate.
|
|
159
|
+
"""
|
|
160
|
+
path = _cctally_core.CACHE_DB_PATH
|
|
161
|
+
if not path.exists():
|
|
162
|
+
return
|
|
163
|
+
try:
|
|
164
|
+
conn = sqlite3.connect(path)
|
|
165
|
+
try:
|
|
166
|
+
conn.execute("BEGIN IMMEDIATE")
|
|
167
|
+
if codex_physical_mutation_seq(conn) != sequence:
|
|
168
|
+
conn.rollback()
|
|
169
|
+
return
|
|
170
|
+
payload = json.dumps({
|
|
171
|
+
"sequence": sequence,
|
|
172
|
+
"signatures": dict(sorted(signatures.items())),
|
|
173
|
+
}, sort_keys=True, separators=(",", ":"))
|
|
174
|
+
conn.execute(
|
|
175
|
+
"INSERT INTO cache_meta(key, value) VALUES (?, ?) "
|
|
176
|
+
"ON CONFLICT(key) DO UPDATE SET value=excluded.value",
|
|
177
|
+
(_DASHBOARD_PROJECTION_CERTIFICATE_KEY, payload),
|
|
178
|
+
)
|
|
179
|
+
conn.commit()
|
|
180
|
+
finally:
|
|
181
|
+
conn.close()
|
|
182
|
+
except sqlite3.Error:
|
|
183
|
+
return
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def load_codex_quota_observations(
|
|
187
|
+
*,
|
|
188
|
+
source_root_keys: Iterable[str] | None = None,
|
|
189
|
+
cache_conn: sqlite3.Connection | None = None,
|
|
190
|
+
captured_at_or_after: dt.datetime | None = None,
|
|
191
|
+
active_at: dt.datetime | None = None,
|
|
192
|
+
max_rows: int | None = None,
|
|
193
|
+
physical_signatures: dict[str, str] | None = None,
|
|
194
|
+
) -> tuple[QuotaObservation, ...]:
|
|
195
|
+
"""Load only valid root-qualified S1 physical quota rows.
|
|
196
|
+
|
|
197
|
+
Invalid/legacy partial rows remain cache evidence but are not safe enough to
|
|
198
|
+
become a quota identity, so they are skipped window-by-window. This is a
|
|
199
|
+
projection reader only; it never parses rollout JSONL or mutates cache.db.
|
|
200
|
+
|
|
201
|
+
``cache_conn`` is caller-owned and lets a coordinated dashboard rebuild
|
|
202
|
+
read quota evidence from its exact accounting generation. Omitting it
|
|
203
|
+
preserves the established independent read-only connection behavior.
|
|
204
|
+
|
|
205
|
+
The optional time/cardinality bounds are dashboard read-model controls;
|
|
206
|
+
their defaults preserve the CLI's complete-history behavior. ``active_at``
|
|
207
|
+
retains reset windows that are still active even when their last capture is
|
|
208
|
+
older than ``captured_at_or_after``. When ``physical_signatures`` is
|
|
209
|
+
supplied, exact S2 signatures are accumulated from the same cursor before
|
|
210
|
+
presentation bounds are applied, so coherence validation does not require
|
|
211
|
+
a second unbounded observation load.
|
|
212
|
+
"""
|
|
213
|
+
for name, value in (
|
|
214
|
+
("captured_at_or_after", captured_at_or_after), ("active_at", active_at),
|
|
215
|
+
):
|
|
216
|
+
if value is not None:
|
|
217
|
+
if value.tzinfo is None or value.utcoffset() is None:
|
|
218
|
+
raise ValueError(f"{name} must be timezone-aware")
|
|
219
|
+
if name == "captured_at_or_after":
|
|
220
|
+
captured_at_or_after = value.astimezone(UTC)
|
|
221
|
+
else:
|
|
222
|
+
active_at = value.astimezone(UTC)
|
|
223
|
+
if max_rows is not None:
|
|
224
|
+
if not isinstance(max_rows, int) or isinstance(max_rows, bool) or max_rows <= 0:
|
|
225
|
+
raise ValueError("max_rows must be a positive integer or None")
|
|
226
|
+
requested = None if source_root_keys is None else {str(key) for key in source_root_keys}
|
|
227
|
+
owns_conn = cache_conn is None
|
|
228
|
+
if owns_conn:
|
|
229
|
+
try:
|
|
230
|
+
conn = _cache_connection()
|
|
231
|
+
except (FileNotFoundError, sqlite3.Error):
|
|
232
|
+
return ()
|
|
233
|
+
else:
|
|
234
|
+
conn = cache_conn
|
|
235
|
+
previous_row_factory = conn.row_factory
|
|
236
|
+
try:
|
|
237
|
+
conn.row_factory = sqlite3.Row
|
|
238
|
+
sql = """
|
|
239
|
+
SELECT source, source_root_key, source_path, line_offset,
|
|
240
|
+
captured_at_utc, observed_slot, logical_limit_key, limit_id,
|
|
241
|
+
limit_name, window_minutes, used_percent, resets_at_utc,
|
|
242
|
+
plan_type, individual_limit_json, reached_type
|
|
243
|
+
FROM quota_window_snapshots
|
|
244
|
+
WHERE source='codex' AND source_root_key IS NOT NULL
|
|
245
|
+
"""
|
|
246
|
+
params: list[object] = []
|
|
247
|
+
if requested is not None:
|
|
248
|
+
if not requested:
|
|
249
|
+
return ()
|
|
250
|
+
sql += " AND source_root_key IN (" + ",".join("?" for _ in requested) + ")"
|
|
251
|
+
params.extend(sorted(requested))
|
|
252
|
+
# When exact signatures are requested this first cursor must cover the
|
|
253
|
+
# complete root history. Otherwise apply dashboard presentation bounds
|
|
254
|
+
# in SQL so only the capped evidence crosses the SQLite/Python boundary.
|
|
255
|
+
sql_bounded = physical_signatures is None
|
|
256
|
+
if sql_bounded and captured_at_or_after is not None:
|
|
257
|
+
if active_at is not None:
|
|
258
|
+
sql += (
|
|
259
|
+
" AND (unixepoch(captured_at_utc) >= unixepoch(?) "
|
|
260
|
+
"OR unixepoch(resets_at_utc) > unixepoch(?))"
|
|
261
|
+
)
|
|
262
|
+
params.extend((_utc_iso(captured_at_or_after), _utc_iso(active_at)))
|
|
263
|
+
else:
|
|
264
|
+
sql += " AND unixepoch(captured_at_utc) >= unixepoch(?)"
|
|
265
|
+
params.append(_utc_iso(captured_at_or_after))
|
|
266
|
+
if sql_bounded and max_rows is not None:
|
|
267
|
+
if active_at is not None:
|
|
268
|
+
sql += (
|
|
269
|
+
" ORDER BY (unixepoch(resets_at_utc) > unixepoch(?)) DESC, "
|
|
270
|
+
"unixepoch(captured_at_utc) DESC, unixepoch(resets_at_utc) DESC, "
|
|
271
|
+
"source_path DESC, line_offset DESC"
|
|
272
|
+
)
|
|
273
|
+
params.append(_utc_iso(active_at))
|
|
274
|
+
else:
|
|
275
|
+
sql += (
|
|
276
|
+
" ORDER BY unixepoch(captured_at_utc) DESC, "
|
|
277
|
+
"unixepoch(resets_at_utc) DESC, source_path DESC, line_offset DESC"
|
|
278
|
+
)
|
|
279
|
+
sql += " LIMIT ?"
|
|
280
|
+
params.append(max_rows)
|
|
281
|
+
else:
|
|
282
|
+
sql += " ORDER BY source_root_key, captured_at_utc, resets_at_utc, source_path, line_offset"
|
|
283
|
+
result: list[QuotaObservation] = []
|
|
284
|
+
signature_tuples: dict[str, list[tuple[object, ...]]] = {}
|
|
285
|
+
for row in conn.execute(sql, tuple(params)):
|
|
286
|
+
required_text = (
|
|
287
|
+
"source", "source_root_key", "source_path", "captured_at_utc",
|
|
288
|
+
"observed_slot", "logical_limit_key", "resets_at_utc",
|
|
289
|
+
)
|
|
290
|
+
if any(row[name] is None or not str(row[name]).strip() for name in required_text):
|
|
291
|
+
continue
|
|
292
|
+
try:
|
|
293
|
+
identity = QuotaWindowIdentity(
|
|
294
|
+
source=str(row["source"]),
|
|
295
|
+
source_root_key=str(row["source_root_key"]),
|
|
296
|
+
logical_limit_key=str(row["logical_limit_key"]),
|
|
297
|
+
observed_slot=str(row["observed_slot"]),
|
|
298
|
+
window_minutes=int(row["window_minutes"]),
|
|
299
|
+
limit_id=row["limit_id"],
|
|
300
|
+
limit_name=row["limit_name"],
|
|
301
|
+
)
|
|
302
|
+
observation = QuotaObservation(
|
|
303
|
+
identity=identity,
|
|
304
|
+
captured_at=_parse_utc(str(row["captured_at_utc"]), "captured_at_utc"),
|
|
305
|
+
used_percent=float(row["used_percent"]),
|
|
306
|
+
resets_at=_parse_utc(str(row["resets_at_utc"]), "resets_at_utc"),
|
|
307
|
+
source_path=str(row["source_path"]),
|
|
308
|
+
line_offset=int(row["line_offset"]),
|
|
309
|
+
plan_type=row["plan_type"],
|
|
310
|
+
individual_limit_json=row["individual_limit_json"],
|
|
311
|
+
reached_type=row["reached_type"],
|
|
312
|
+
)
|
|
313
|
+
except (TypeError, ValueError, OverflowError):
|
|
314
|
+
# Physical retention is intentionally more permissive than the
|
|
315
|
+
# provider-neutral identity contract. One malformed window
|
|
316
|
+
# must not suppress unrelated valid windows or accounting.
|
|
317
|
+
continue
|
|
318
|
+
if physical_signatures is not None:
|
|
319
|
+
signature_tuples.setdefault(identity.source_root_key, []).append((
|
|
320
|
+
identity.source_root_key,
|
|
321
|
+
identity.logical_limit_key,
|
|
322
|
+
_utc_iso(observation.captured_at),
|
|
323
|
+
observation.source_path,
|
|
324
|
+
observation.line_offset,
|
|
325
|
+
observation.used_percent,
|
|
326
|
+
_utc_iso(observation.resets_at),
|
|
327
|
+
))
|
|
328
|
+
if (
|
|
329
|
+
captured_at_or_after is not None
|
|
330
|
+
and observation.captured_at < captured_at_or_after
|
|
331
|
+
and (active_at is None or observation.resets_at <= active_at)
|
|
332
|
+
):
|
|
333
|
+
continue
|
|
334
|
+
result.append(observation)
|
|
335
|
+
if physical_signatures is not None:
|
|
336
|
+
physical_signatures.clear()
|
|
337
|
+
roots = requested if requested is not None else set(signature_tuples)
|
|
338
|
+
for root_key in roots:
|
|
339
|
+
encoded = json.dumps(
|
|
340
|
+
sorted(signature_tuples.get(root_key, ())),
|
|
341
|
+
ensure_ascii=False,
|
|
342
|
+
separators=(",", ":"),
|
|
343
|
+
).encode("utf-8")
|
|
344
|
+
physical_signatures[root_key] = hashlib.sha256(encoded).hexdigest()
|
|
345
|
+
if captured_at_or_after is not None or max_rows is not None:
|
|
346
|
+
return load_codex_quota_observations(
|
|
347
|
+
source_root_keys=requested,
|
|
348
|
+
cache_conn=conn,
|
|
349
|
+
captured_at_or_after=captured_at_or_after,
|
|
350
|
+
active_at=active_at,
|
|
351
|
+
max_rows=max_rows,
|
|
352
|
+
)
|
|
353
|
+
if max_rows is not None and len(result) > max_rows:
|
|
354
|
+
result = sorted(
|
|
355
|
+
result,
|
|
356
|
+
key=lambda observation: (
|
|
357
|
+
1 if active_at is not None and observation.resets_at > active_at else 0,
|
|
358
|
+
observation.captured_at,
|
|
359
|
+
observation.resets_at,
|
|
360
|
+
observation.source_path,
|
|
361
|
+
observation.line_offset,
|
|
362
|
+
),
|
|
363
|
+
reverse=True,
|
|
364
|
+
)[:max_rows]
|
|
365
|
+
result.sort(key=lambda observation: (
|
|
366
|
+
observation.identity.source_root_key,
|
|
367
|
+
observation.captured_at,
|
|
368
|
+
observation.resets_at,
|
|
369
|
+
observation.source_path,
|
|
370
|
+
observation.line_offset,
|
|
371
|
+
))
|
|
372
|
+
return tuple(result)
|
|
373
|
+
finally:
|
|
374
|
+
if owns_conn:
|
|
375
|
+
conn.close()
|
|
376
|
+
else:
|
|
377
|
+
conn.row_factory = previous_row_factory
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _historic_root_keys(conn: sqlite3.Connection) -> set[str]:
|
|
381
|
+
roots: set[str] = set()
|
|
382
|
+
for table in (
|
|
383
|
+
"quota_window_blocks", "quota_percent_milestones",
|
|
384
|
+
"quota_threshold_events", "quota_projection_state",
|
|
385
|
+
):
|
|
386
|
+
try:
|
|
387
|
+
roots.update(str(row[0]) for row in conn.execute(
|
|
388
|
+
f"SELECT DISTINCT source_root_key FROM {table}"
|
|
389
|
+
) if row[0] is not None)
|
|
390
|
+
except sqlite3.OperationalError:
|
|
391
|
+
continue
|
|
392
|
+
return roots
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def _signature(observations: Iterable[QuotaObservation], source_root_key: str) -> str:
|
|
396
|
+
tuples = [
|
|
397
|
+
(
|
|
398
|
+
observation.identity.source_root_key,
|
|
399
|
+
observation.identity.logical_limit_key,
|
|
400
|
+
_utc_iso(observation.captured_at),
|
|
401
|
+
observation.source_path,
|
|
402
|
+
observation.line_offset,
|
|
403
|
+
observation.used_percent,
|
|
404
|
+
_utc_iso(observation.resets_at),
|
|
405
|
+
)
|
|
406
|
+
for observation in observations
|
|
407
|
+
if observation.identity.source_root_key == source_root_key
|
|
408
|
+
]
|
|
409
|
+
encoded = json.dumps(
|
|
410
|
+
sorted(tuples), ensure_ascii=False, separators=(",", ":"),
|
|
411
|
+
).encode("utf-8")
|
|
412
|
+
return hashlib.sha256(encoded).hexdigest()
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def _block_params(block: QuotaBlock, generation: str) -> tuple[object, ...]:
|
|
416
|
+
latest = block.observations[-1]
|
|
417
|
+
identity = block.identity
|
|
418
|
+
return (
|
|
419
|
+
identity.source, identity.source_root_key, identity.logical_limit_key,
|
|
420
|
+
identity.observed_slot, identity.window_minutes, identity.limit_id,
|
|
421
|
+
identity.limit_name, _utc_iso(block.resets_at), _utc_iso(block.nominal_start_at),
|
|
422
|
+
_utc_iso(block.first_observed_at), _utc_iso(block.last_observed_at),
|
|
423
|
+
block.first_percent, block.current_percent, latest.source_path,
|
|
424
|
+
latest.line_offset, generation,
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
_BLOCK_UPSERT = """
|
|
429
|
+
INSERT INTO quota_window_blocks
|
|
430
|
+
(source, source_root_key, logical_limit_key, observed_slot,
|
|
431
|
+
window_minutes, limit_id, limit_name, resets_at_utc, nominal_start_at_utc,
|
|
432
|
+
first_observed_at_utc, last_observed_at_utc, first_percent, current_percent,
|
|
433
|
+
last_source_path, last_line_offset, generation, orphaned_at)
|
|
434
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,NULL)
|
|
435
|
+
ON CONFLICT(source, source_root_key, logical_limit_key, observed_slot,
|
|
436
|
+
window_minutes, resets_at_utc) DO UPDATE SET
|
|
437
|
+
limit_id=excluded.limit_id, limit_name=excluded.limit_name,
|
|
438
|
+
nominal_start_at_utc=excluded.nominal_start_at_utc,
|
|
439
|
+
first_observed_at_utc=excluded.first_observed_at_utc,
|
|
440
|
+
last_observed_at_utc=excluded.last_observed_at_utc,
|
|
441
|
+
first_percent=excluded.first_percent, current_percent=excluded.current_percent,
|
|
442
|
+
last_source_path=excluded.last_source_path, last_line_offset=excluded.last_line_offset,
|
|
443
|
+
generation=excluded.generation, orphaned_at=NULL
|
|
444
|
+
"""
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
_MILESTONE_UPSERT = """
|
|
448
|
+
INSERT INTO quota_percent_milestones
|
|
449
|
+
(source, source_root_key, logical_limit_key, observed_slot, window_minutes,
|
|
450
|
+
resets_at_utc, percent_threshold, captured_at_utc, source_path,
|
|
451
|
+
line_offset, high_water_percent, generation, orphaned_at)
|
|
452
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,NULL)
|
|
453
|
+
ON CONFLICT(source, source_root_key, logical_limit_key, observed_slot,
|
|
454
|
+
window_minutes, resets_at_utc, percent_threshold) DO UPDATE SET
|
|
455
|
+
captured_at_utc=excluded.captured_at_utc, source_path=excluded.source_path,
|
|
456
|
+
line_offset=excluded.line_offset, high_water_percent=excluded.high_water_percent,
|
|
457
|
+
generation=excluded.generation, orphaned_at=NULL
|
|
458
|
+
"""
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def _milestone_params(
|
|
462
|
+
block: QuotaBlock, milestone: QuotaPercentMilestone, generation: str,
|
|
463
|
+
) -> tuple[object, ...]:
|
|
464
|
+
identity = block.identity
|
|
465
|
+
return (
|
|
466
|
+
identity.source, identity.source_root_key, identity.logical_limit_key,
|
|
467
|
+
identity.observed_slot, identity.window_minutes, _utc_iso(block.resets_at),
|
|
468
|
+
milestone.percent, _utc_iso(milestone.captured_at), milestone.observation.source_path,
|
|
469
|
+
milestone.observation.line_offset, milestone.percent, generation,
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def _orphan_unseen(conn: sqlite3.Connection, roots: set[str], generation: str, now_iso: str) -> tuple[int, int]:
|
|
474
|
+
if not roots:
|
|
475
|
+
return (0, 0)
|
|
476
|
+
placeholders = ",".join("?" for _ in roots)
|
|
477
|
+
args = (now_iso, *sorted(roots), generation)
|
|
478
|
+
blocks = conn.execute(
|
|
479
|
+
"UPDATE quota_window_blocks SET orphaned_at=COALESCE(orphaned_at, ?) "
|
|
480
|
+
"WHERE source='codex' AND source_root_key IN (" + placeholders + ") "
|
|
481
|
+
"AND generation<>?", args,
|
|
482
|
+
).rowcount
|
|
483
|
+
milestones = conn.execute(
|
|
484
|
+
"UPDATE quota_percent_milestones SET orphaned_at=COALESCE(orphaned_at, ?) "
|
|
485
|
+
"WHERE source='codex' AND source_root_key IN (" + placeholders + ") "
|
|
486
|
+
"AND generation<>?", args,
|
|
487
|
+
).rowcount
|
|
488
|
+
# Threshold events are terminal evidence and are never recreated here.
|
|
489
|
+
# Their orphan marker tracks whether the stable source block is present in
|
|
490
|
+
# this completed generation, so a cache rebuild that restores the exact
|
|
491
|
+
# block clears a transient prune marker without creating a new terminal
|
|
492
|
+
# claim.
|
|
493
|
+
event_sql = f"""UPDATE quota_threshold_events AS events
|
|
494
|
+
SET orphaned_at=CASE WHEN EXISTS (
|
|
495
|
+
SELECT 1 FROM quota_window_blocks AS blocks
|
|
496
|
+
WHERE blocks.source=events.source
|
|
497
|
+
AND blocks.source_root_key=events.source_root_key
|
|
498
|
+
AND blocks.logical_limit_key=events.logical_limit_key
|
|
499
|
+
AND blocks.observed_slot=events.observed_slot
|
|
500
|
+
AND blocks.window_minutes=events.window_minutes
|
|
501
|
+
AND blocks.resets_at_utc=events.resets_at_utc
|
|
502
|
+
AND blocks.generation=?
|
|
503
|
+
) THEN NULL ELSE COALESCE(events.orphaned_at, ?) END
|
|
504
|
+
WHERE events.source='codex'
|
|
505
|
+
AND events.source_root_key IN ({placeholders})
|
|
506
|
+
"""
|
|
507
|
+
conn.execute(
|
|
508
|
+
event_sql,
|
|
509
|
+
(generation, now_iso, *sorted(roots)),
|
|
510
|
+
)
|
|
511
|
+
return (int(blocks), int(milestones))
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def _quota_alert_config() -> tuple[bool, bool, tuple[QuotaRule, ...], dict]:
|
|
515
|
+
"""Resolve global + quota gates and exact JSON-shaped overrides once."""
|
|
516
|
+
c = _cctally()
|
|
517
|
+
config = c.load_config()
|
|
518
|
+
alerts = _cctally_core._get_alerts_config(config)
|
|
519
|
+
quota = c._get_quota_alerts_config(config)
|
|
520
|
+
rules = tuple(
|
|
521
|
+
QuotaRule(
|
|
522
|
+
source=rule["source"],
|
|
523
|
+
source_root_key=rule["source_root_key"],
|
|
524
|
+
logical_limit_key=rule["logical_limit_key"],
|
|
525
|
+
actual_thresholds=tuple(rule["actual_thresholds"]),
|
|
526
|
+
projected_thresholds=tuple(rule["projected_thresholds"]),
|
|
527
|
+
)
|
|
528
|
+
for rule in quota["rules"]
|
|
529
|
+
)
|
|
530
|
+
return bool(alerts["enabled"]), bool(quota["enabled"]), rules, quota
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def _arming_row(conn: sqlite3.Connection, identity: QuotaWindowIdentity) -> sqlite3.Row | None:
|
|
534
|
+
return conn.execute(
|
|
535
|
+
"""SELECT rule_fingerprint, activated_at_utc FROM quota_alert_arming
|
|
536
|
+
WHERE source=? AND source_root_key=? AND logical_limit_key=?
|
|
537
|
+
AND observed_slot=? AND window_minutes=?""",
|
|
538
|
+
(
|
|
539
|
+
identity.source, identity.source_root_key, identity.logical_limit_key,
|
|
540
|
+
identity.observed_slot, identity.window_minutes,
|
|
541
|
+
),
|
|
542
|
+
).fetchone()
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def _activate_quota_rule(
|
|
546
|
+
conn: sqlite3.Connection, identity: QuotaWindowIdentity, fingerprint: str, now_iso: str,
|
|
547
|
+
) -> tuple[bool, dt.datetime]:
|
|
548
|
+
"""Persist one identity's resolved rule boundary, returning (changed, at)."""
|
|
549
|
+
row = _arming_row(conn, identity)
|
|
550
|
+
if row is not None and row["rule_fingerprint"] == fingerprint:
|
|
551
|
+
return False, _parse_utc(str(row["activated_at_utc"]), "activated_at_utc")
|
|
552
|
+
conn.execute(
|
|
553
|
+
"""INSERT INTO quota_alert_arming
|
|
554
|
+
(source, source_root_key, logical_limit_key, observed_slot,
|
|
555
|
+
window_minutes, rule_fingerprint, activated_at_utc)
|
|
556
|
+
VALUES (?,?,?,?,?,?,?)
|
|
557
|
+
ON CONFLICT(source, source_root_key, logical_limit_key,
|
|
558
|
+
observed_slot, window_minutes) DO UPDATE SET
|
|
559
|
+
rule_fingerprint=excluded.rule_fingerprint,
|
|
560
|
+
activated_at_utc=excluded.activated_at_utc""",
|
|
561
|
+
(
|
|
562
|
+
identity.source, identity.source_root_key, identity.logical_limit_key,
|
|
563
|
+
identity.observed_slot, identity.window_minutes, fingerprint, now_iso,
|
|
564
|
+
),
|
|
565
|
+
)
|
|
566
|
+
return True, _parse_utc(now_iso, "activated_at_utc")
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def _insert_quota_terminal_event(
|
|
570
|
+
conn: sqlite3.Connection,
|
|
571
|
+
*, identity: QuotaWindowIdentity, resets_at: dt.datetime,
|
|
572
|
+
threshold: int, kind: str, qualifying_percent: float | None,
|
|
573
|
+
projected_percent: float | None, disposition: str, now_iso: str,
|
|
574
|
+
) -> bool:
|
|
575
|
+
"""Claim one durable threshold lifecycle row; unique-key races converge."""
|
|
576
|
+
alerted_at = now_iso if disposition == "alerted" else None
|
|
577
|
+
suppressed_at = now_iso if disposition == "suppressed_backfill" else None
|
|
578
|
+
cur = conn.execute(
|
|
579
|
+
"""INSERT OR IGNORE INTO quota_threshold_events
|
|
580
|
+
(source, source_root_key, logical_limit_key, observed_slot,
|
|
581
|
+
window_minutes, resets_at_utc, threshold, qualifying_kind,
|
|
582
|
+
qualifying_percent, projected_percent, severity, created_at_utc,
|
|
583
|
+
disposition, alerted_at, suppressed_at)
|
|
584
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
|
585
|
+
(
|
|
586
|
+
identity.source, identity.source_root_key, identity.logical_limit_key,
|
|
587
|
+
identity.observed_slot, identity.window_minutes, _utc_iso(resets_at),
|
|
588
|
+
threshold, kind, qualifying_percent, projected_percent,
|
|
589
|
+
_cctally().severity_for(threshold), now_iso, disposition,
|
|
590
|
+
alerted_at, suppressed_at,
|
|
591
|
+
),
|
|
592
|
+
)
|
|
593
|
+
return cur.rowcount == 1
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def _block_observations_at_or_before(
|
|
597
|
+
block: QuotaBlock, at: dt.datetime,
|
|
598
|
+
) -> tuple[QuotaObservation, ...]:
|
|
599
|
+
return tuple(point for point in block.observations if point.captured_at <= at)
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def _quota_projection_for_block(
|
|
603
|
+
history: QuotaHistory, block: QuotaBlock, now: dt.datetime,
|
|
604
|
+
) -> float | None:
|
|
605
|
+
"""Return a fresh projected percent only for the current native block."""
|
|
606
|
+
forecast = forecast_quota(history.physical_observations, now)
|
|
607
|
+
if forecast.status != "ok" or forecast.resets_at != block.resets_at:
|
|
608
|
+
return None
|
|
609
|
+
return forecast.projected_percent
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
def _quota_alert_payload(
|
|
613
|
+
*, identity: QuotaWindowIdentity, resets_at: dt.datetime, threshold: int,
|
|
614
|
+
kind: str, now_iso: str, qualifying_percent: float | None,
|
|
615
|
+
projected_percent: float | None,
|
|
616
|
+
) -> dict:
|
|
617
|
+
return _cctally()._build_alert_payload_quota(
|
|
618
|
+
source=identity.source, source_root_key=identity.source_root_key,
|
|
619
|
+
logical_limit_key=identity.logical_limit_key,
|
|
620
|
+
observed_slot=identity.observed_slot, window_minutes=identity.window_minutes,
|
|
621
|
+
resets_at_utc=_utc_iso(resets_at), threshold=threshold, kind=kind,
|
|
622
|
+
crossed_at_utc=now_iso, qualifying_percent=qualifying_percent,
|
|
623
|
+
projected_percent=projected_percent,
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def _evaluate_quota_alerts(
|
|
628
|
+
conn: sqlite3.Connection,
|
|
629
|
+
*, observations: tuple[QuotaObservation, ...], alert_eligible_roots: set[str],
|
|
630
|
+
now: dt.datetime, now_iso: str,
|
|
631
|
+
) -> list[dict]:
|
|
632
|
+
"""Arm or claim quota alerts within the caller's stats transaction.
|
|
633
|
+
|
|
634
|
+
A fresh fingerprint writes only terminal backfill suppressions. Later
|
|
635
|
+
eligible observations can claim an alerted row. No non-terminal state is
|
|
636
|
+
stored: the arming boundary plus unique terminal event key is sufficient.
|
|
637
|
+
"""
|
|
638
|
+
if not alert_eligible_roots:
|
|
639
|
+
return []
|
|
640
|
+
global_enabled, quota_enabled, rules, config = _quota_alert_config()
|
|
641
|
+
# Disabled delivery is entirely inert: it must not leave an arming
|
|
642
|
+
# boundary that could turn disabled-period evidence into a later alert.
|
|
643
|
+
if not (global_enabled and quota_enabled):
|
|
644
|
+
placeholders = ", ".join("?" for _ in alert_eligible_roots)
|
|
645
|
+
conn.execute(
|
|
646
|
+
f"""DELETE FROM quota_alert_arming
|
|
647
|
+
WHERE source='codex' AND source_root_key IN ({placeholders})""",
|
|
648
|
+
tuple(sorted(alert_eligible_roots)),
|
|
649
|
+
)
|
|
650
|
+
return []
|
|
651
|
+
histories = build_history(observations)
|
|
652
|
+
queued: list[dict] = []
|
|
653
|
+
for history in histories:
|
|
654
|
+
identity = history.identity
|
|
655
|
+
if identity.source_root_key not in alert_eligible_roots:
|
|
656
|
+
continue
|
|
657
|
+
resolved = resolve_quota_rule(
|
|
658
|
+
identity,
|
|
659
|
+
default_actual_thresholds=config["actual_thresholds"],
|
|
660
|
+
default_projected_thresholds=config["projected_thresholds"],
|
|
661
|
+
rules=rules,
|
|
662
|
+
)
|
|
663
|
+
fingerprint = quota_rule_fingerprint(
|
|
664
|
+
identity, resolved, global_enabled=global_enabled,
|
|
665
|
+
quota_enabled=quota_enabled,
|
|
666
|
+
)
|
|
667
|
+
changed, activated_at = _activate_quota_rule(conn, identity, fingerprint, now_iso)
|
|
668
|
+
|
|
669
|
+
# Future evidence is never a threshold qualifier (including a first
|
|
670
|
+
# activation backfill). A later well-clocked observation creates the
|
|
671
|
+
# appropriate normal activation/claim path.
|
|
672
|
+
freshness = quota_freshness(history.physical_observations, now)
|
|
673
|
+
if freshness.state == "future":
|
|
674
|
+
continue
|
|
675
|
+
blocks = tuple(
|
|
676
|
+
block for block in build_blocks(history.observations)
|
|
677
|
+
if block.identity == identity
|
|
678
|
+
)
|
|
679
|
+
for block in blocks:
|
|
680
|
+
present = _block_observations_at_or_before(block, now)
|
|
681
|
+
if not present:
|
|
682
|
+
continue
|
|
683
|
+
if changed:
|
|
684
|
+
actual_percent = max(point.used_percent for point in present)
|
|
685
|
+
projected_percent = _quota_projection_for_block(history, block, now)
|
|
686
|
+
for decision in quota_threshold_decisions(
|
|
687
|
+
current_percent=actual_percent,
|
|
688
|
+
projected_percent=projected_percent,
|
|
689
|
+
actual_thresholds=resolved.actual_thresholds,
|
|
690
|
+
projected_thresholds=resolved.projected_thresholds,
|
|
691
|
+
):
|
|
692
|
+
_insert_quota_terminal_event(
|
|
693
|
+
conn, identity=identity, resets_at=block.resets_at,
|
|
694
|
+
threshold=decision.threshold, kind=decision.kind,
|
|
695
|
+
qualifying_percent=(actual_percent if decision.kind == "actual" else None),
|
|
696
|
+
projected_percent=(
|
|
697
|
+
projected_percent if decision.kind == "projected" else None
|
|
698
|
+
),
|
|
699
|
+
disposition="suppressed_backfill", now_iso=now_iso,
|
|
700
|
+
)
|
|
701
|
+
continue
|
|
702
|
+
later = tuple(point for point in present if point.captured_at > activated_at)
|
|
703
|
+
if not later:
|
|
704
|
+
continue
|
|
705
|
+
actual_percent = max(point.used_percent for point in later)
|
|
706
|
+
projected_percent = None
|
|
707
|
+
baseline = select_baseline(history.observations, now)
|
|
708
|
+
if (
|
|
709
|
+
freshness.state != "stale" and baseline is not None
|
|
710
|
+
and baseline.resets_at == block.resets_at
|
|
711
|
+
and baseline.captured_at > activated_at
|
|
712
|
+
):
|
|
713
|
+
projected_percent = _quota_projection_for_block(history, block, now)
|
|
714
|
+
for decision in quota_threshold_decisions(
|
|
715
|
+
current_percent=actual_percent,
|
|
716
|
+
projected_percent=projected_percent,
|
|
717
|
+
actual_thresholds=resolved.actual_thresholds,
|
|
718
|
+
projected_thresholds=resolved.projected_thresholds,
|
|
719
|
+
):
|
|
720
|
+
qualifying = actual_percent if decision.kind == "actual" else None
|
|
721
|
+
projected = projected_percent if decision.kind == "projected" else None
|
|
722
|
+
if _insert_quota_terminal_event(
|
|
723
|
+
conn, identity=identity, resets_at=block.resets_at,
|
|
724
|
+
threshold=decision.threshold, kind=decision.kind,
|
|
725
|
+
qualifying_percent=qualifying, projected_percent=projected,
|
|
726
|
+
disposition="alerted", now_iso=now_iso,
|
|
727
|
+
):
|
|
728
|
+
queued.append(_quota_alert_payload(
|
|
729
|
+
identity=identity, resets_at=block.resets_at,
|
|
730
|
+
threshold=decision.threshold, kind=decision.kind,
|
|
731
|
+
now_iso=now_iso, qualifying_percent=qualifying,
|
|
732
|
+
projected_percent=projected,
|
|
733
|
+
))
|
|
734
|
+
return queued
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
def reconcile_codex_quota_projection(
|
|
738
|
+
*,
|
|
739
|
+
source_root_keys: Iterable[str] | None = None,
|
|
740
|
+
alert_eligible_root_keys: Iterable[str] = (),
|
|
741
|
+
now: dt.datetime | None = None,
|
|
742
|
+
_before_stats_commit: Callable[[], None] | None = None,
|
|
743
|
+
_after_stats_commit: Callable[[], None] | None = None,
|
|
744
|
+
) -> QuotaProjectionResult:
|
|
745
|
+
"""Reconcile every active Codex root into one stats transaction.
|
|
746
|
+
|
|
747
|
+
Reporting reconciles every configured root. Threshold evaluation is limited
|
|
748
|
+
to explicitly lifecycle-eligible roots, so read-only quota commands pass an
|
|
749
|
+
empty set and never create an alert claim or activation boundary.
|
|
750
|
+
"""
|
|
751
|
+
if now is None:
|
|
752
|
+
now = dt.datetime.now(UTC)
|
|
753
|
+
if now.tzinfo is None or now.utcoffset() is None:
|
|
754
|
+
raise ValueError("now must be timezone-aware")
|
|
755
|
+
now_iso = _utc_iso(now)
|
|
756
|
+
|
|
757
|
+
try:
|
|
758
|
+
cache = _cache_connection()
|
|
759
|
+
except (FileNotFoundError, sqlite3.Error):
|
|
760
|
+
return QuotaProjectionResult(None, 0, 0, 0, 0, 0, 0)
|
|
761
|
+
try:
|
|
762
|
+
active_roots = (
|
|
763
|
+
_cache_root_keys(cache)
|
|
764
|
+
if source_root_keys is None else {str(key) for key in source_root_keys}
|
|
765
|
+
)
|
|
766
|
+
physical_sequence = codex_physical_mutation_seq(cache)
|
|
767
|
+
observations = load_codex_quota_observations(
|
|
768
|
+
source_root_keys=active_roots, cache_conn=cache,
|
|
769
|
+
)
|
|
770
|
+
finally:
|
|
771
|
+
cache.close()
|
|
772
|
+
alert_eligible_roots = {str(key) for key in alert_eligible_root_keys}
|
|
773
|
+
|
|
774
|
+
# No configured roots and no existing interpreted history means there is no
|
|
775
|
+
# stats work. This preserves the existing empty-Codex sync fast path.
|
|
776
|
+
stats = _cctally_core.open_db()
|
|
777
|
+
try:
|
|
778
|
+
historic_roots = _historic_root_keys(stats)
|
|
779
|
+
roots_to_reconcile = active_roots | historic_roots
|
|
780
|
+
if not roots_to_reconcile:
|
|
781
|
+
return QuotaProjectionResult(None, 0, 0, 0, 0, 0, 0)
|
|
782
|
+
|
|
783
|
+
generation = secrets.token_hex(16)
|
|
784
|
+
blocks = build_blocks(observations)
|
|
785
|
+
queued_alerts: list[dict] = []
|
|
786
|
+
stats.execute("BEGIN IMMEDIATE")
|
|
787
|
+
try:
|
|
788
|
+
for block in blocks:
|
|
789
|
+
stats.execute(_BLOCK_UPSERT, _block_params(block, generation))
|
|
790
|
+
for milestone in percent_milestones(block):
|
|
791
|
+
stats.execute(
|
|
792
|
+
_MILESTONE_UPSERT,
|
|
793
|
+
_milestone_params(block, milestone, generation),
|
|
794
|
+
)
|
|
795
|
+
blocks_orphaned, milestones_orphaned = _orphan_unseen(
|
|
796
|
+
stats, roots_to_reconcile, generation, now_iso,
|
|
797
|
+
)
|
|
798
|
+
queued_alerts = _evaluate_quota_alerts(
|
|
799
|
+
stats, observations=observations,
|
|
800
|
+
alert_eligible_roots=alert_eligible_roots & active_roots,
|
|
801
|
+
now=now, now_iso=now_iso,
|
|
802
|
+
)
|
|
803
|
+
# The completion stamp is intentionally the final DML in the stats
|
|
804
|
+
# transaction. A pre-commit failure rolls all projection updates
|
|
805
|
+
# back; a retry sees the prior complete generation or rederives it.
|
|
806
|
+
for root_key in sorted(active_roots):
|
|
807
|
+
stats.execute(
|
|
808
|
+
"""INSERT INTO quota_projection_state
|
|
809
|
+
(source_root_key, generation, physical_signature, completed_at_utc)
|
|
810
|
+
VALUES (?,?,?,?)
|
|
811
|
+
ON CONFLICT(source_root_key) DO UPDATE SET
|
|
812
|
+
generation=excluded.generation,
|
|
813
|
+
physical_signature=excluded.physical_signature,
|
|
814
|
+
completed_at_utc=excluded.completed_at_utc""",
|
|
815
|
+
(root_key, generation, _signature(observations, root_key), now_iso),
|
|
816
|
+
)
|
|
817
|
+
if _before_stats_commit is not None:
|
|
818
|
+
_before_stats_commit()
|
|
819
|
+
stats.commit()
|
|
820
|
+
except Exception:
|
|
821
|
+
stats.rollback()
|
|
822
|
+
raise
|
|
823
|
+
finally:
|
|
824
|
+
stats.close()
|
|
825
|
+
|
|
826
|
+
if _after_stats_commit is not None:
|
|
827
|
+
_after_stats_commit()
|
|
828
|
+
_store_codex_quota_projection_certificate(
|
|
829
|
+
sequence=physical_sequence,
|
|
830
|
+
signatures={root_key: _signature(observations, root_key) for root_key in active_roots},
|
|
831
|
+
)
|
|
832
|
+
# Set-then-dispatch: all alert claims committed before this best-effort I/O.
|
|
833
|
+
# The dispatch helper never raises on notifier/FS failures; retain the
|
|
834
|
+
# defensive guard so an injected test double cannot reopen a refire path.
|
|
835
|
+
for payload in queued_alerts:
|
|
836
|
+
try:
|
|
837
|
+
_cctally()._dispatch_alert_notification(payload, mode="real")
|
|
838
|
+
except Exception:
|
|
839
|
+
pass
|
|
840
|
+
return QuotaProjectionResult(
|
|
841
|
+
generation=generation,
|
|
842
|
+
blocks_upserted=len(blocks),
|
|
843
|
+
milestones_upserted=sum(len(percent_milestones(block)) for block in blocks),
|
|
844
|
+
blocks_orphaned=blocks_orphaned,
|
|
845
|
+
milestones_orphaned=milestones_orphaned,
|
|
846
|
+
roots_stamped=len(active_roots),
|
|
847
|
+
alerts_dispatched=len(queued_alerts),
|
|
848
|
+
)
|
|
849
|
+
|
|
850
|
+
|
|
851
|
+
def _load_active_milestones(
|
|
852
|
+
identity: QuotaWindowIdentity, resets_at: dt.datetime,
|
|
853
|
+
) -> list[sqlite3.Row]:
|
|
854
|
+
stats = _cctally_core.open_db()
|
|
855
|
+
try:
|
|
856
|
+
return list(stats.execute(
|
|
857
|
+
"""SELECT percent_threshold, captured_at_utc, source_path, line_offset
|
|
858
|
+
FROM quota_percent_milestones
|
|
859
|
+
WHERE source=? AND source_root_key=? AND logical_limit_key=?
|
|
860
|
+
AND observed_slot=? AND window_minutes=? AND resets_at_utc=?
|
|
861
|
+
AND orphaned_at IS NULL
|
|
862
|
+
ORDER BY percent_threshold""",
|
|
863
|
+
(
|
|
864
|
+
identity.source, identity.source_root_key, identity.logical_limit_key,
|
|
865
|
+
identity.observed_slot, identity.window_minutes, _utc_iso(resets_at),
|
|
866
|
+
),
|
|
867
|
+
))
|
|
868
|
+
finally:
|
|
869
|
+
stats.close()
|
|
870
|
+
|
|
871
|
+
|
|
872
|
+
def _matching_block_observations(
|
|
873
|
+
identity: QuotaWindowIdentity, resets_at: dt.datetime,
|
|
874
|
+
) -> tuple[QuotaObservation, ...]:
|
|
875
|
+
return tuple(
|
|
876
|
+
observation for observation in load_codex_quota_observations(
|
|
877
|
+
source_root_keys={identity.source_root_key},
|
|
878
|
+
)
|
|
879
|
+
if observation.identity == identity and observation.resets_at == resets_at
|
|
880
|
+
)
|
|
881
|
+
|
|
882
|
+
|
|
883
|
+
def codex_quota_breakdown(
|
|
884
|
+
identity: QuotaWindowIdentity,
|
|
885
|
+
resets_at: str | dt.datetime,
|
|
886
|
+
*, speed: str = "auto",
|
|
887
|
+
) -> tuple[CodexQuotaBreakdownRow, ...]:
|
|
888
|
+
"""Correlate durable milestone boundaries with live-priced cache accounting.
|
|
889
|
+
|
|
890
|
+
Each comparison is the full physical tuple ``(timestamp, path, offset)`` so
|
|
891
|
+
same-timestamp records stay deterministic. Pricing is deliberately read
|
|
892
|
+
now rather than materialized in stats.db, keeping a pricing refresh
|
|
893
|
+
immediately effective for historical quota breakdowns.
|
|
894
|
+
"""
|
|
895
|
+
reset = _parse_utc(resets_at, "resets_at") if isinstance(resets_at, str) else resets_at
|
|
896
|
+
if reset.tzinfo is None or reset.utcoffset() is None:
|
|
897
|
+
raise ValueError("resets_at must be timezone-aware")
|
|
898
|
+
reset = reset.astimezone(UTC)
|
|
899
|
+
points = sorted(_matching_block_observations(identity, reset), key=_physical_tuple)
|
|
900
|
+
if not points:
|
|
901
|
+
return ()
|
|
902
|
+
milestones = _load_active_milestones(identity, reset)
|
|
903
|
+
if not milestones:
|
|
904
|
+
return ()
|
|
905
|
+
try:
|
|
906
|
+
cache = _cache_connection()
|
|
907
|
+
except (FileNotFoundError, sqlite3.Error):
|
|
908
|
+
return ()
|
|
909
|
+
try:
|
|
910
|
+
entries = []
|
|
911
|
+
for row in cache.execute(
|
|
912
|
+
"""SELECT timestamp_utc, source_path, line_offset, model,
|
|
913
|
+
input_tokens, cached_input_tokens, output_tokens,
|
|
914
|
+
reasoning_output_tokens, total_tokens
|
|
915
|
+
FROM codex_session_entries
|
|
916
|
+
WHERE source_root_key=?""",
|
|
917
|
+
(identity.source_root_key,),
|
|
918
|
+
):
|
|
919
|
+
try:
|
|
920
|
+
physical = (_parse_utc(str(row["timestamp_utc"]), "timestamp_utc"),
|
|
921
|
+
str(row["source_path"]), int(row["line_offset"]))
|
|
922
|
+
except (TypeError, ValueError):
|
|
923
|
+
continue
|
|
924
|
+
entries.append((physical, row))
|
|
925
|
+
finally:
|
|
926
|
+
cache.close()
|
|
927
|
+
entries.sort(key=lambda pair: pair[0])
|
|
928
|
+
resolved_speed = sys.modules["cctally"]._resolve_codex_speed(speed)
|
|
929
|
+
calculate_cost = sys.modules["cctally"]._calculate_codex_entry_cost
|
|
930
|
+
start = _physical_tuple(points[0])
|
|
931
|
+
prior_cumulative = 0.0
|
|
932
|
+
cumulative_input = 0
|
|
933
|
+
cumulative_cached = 0
|
|
934
|
+
cumulative_output = 0
|
|
935
|
+
cumulative_reasoning = 0
|
|
936
|
+
cumulative_total = 0
|
|
937
|
+
result: list[CodexQuotaBreakdownRow] = []
|
|
938
|
+
for milestone in milestones:
|
|
939
|
+
end = (
|
|
940
|
+
_parse_utc(str(milestone["captured_at_utc"]), "captured_at_utc"),
|
|
941
|
+
str(milestone["source_path"]), int(milestone["line_offset"]),
|
|
942
|
+
)
|
|
943
|
+
selected = [row for physical, row in entries if start < physical <= end]
|
|
944
|
+
input_tokens = sum(int(row["input_tokens"]) for row in selected)
|
|
945
|
+
cached = sum(int(row["cached_input_tokens"]) for row in selected)
|
|
946
|
+
output = sum(int(row["output_tokens"]) for row in selected)
|
|
947
|
+
reasoning = sum(int(row["reasoning_output_tokens"]) for row in selected)
|
|
948
|
+
total = sum(int(row["total_tokens"]) for row in selected)
|
|
949
|
+
marginal = sum(
|
|
950
|
+
calculate_cost(
|
|
951
|
+
str(row["model"]), int(row["input_tokens"]),
|
|
952
|
+
int(row["cached_input_tokens"]), int(row["output_tokens"]),
|
|
953
|
+
int(row["reasoning_output_tokens"]), speed=resolved_speed,
|
|
954
|
+
)
|
|
955
|
+
for row in selected
|
|
956
|
+
)
|
|
957
|
+
cumulative = prior_cumulative + marginal
|
|
958
|
+
cumulative_input += input_tokens
|
|
959
|
+
cumulative_cached += cached
|
|
960
|
+
cumulative_output += output
|
|
961
|
+
cumulative_reasoning += reasoning
|
|
962
|
+
cumulative_total += total
|
|
963
|
+
result.append(CodexQuotaBreakdownRow(
|
|
964
|
+
percent=int(milestone["percent_threshold"]), captured_at=end[0],
|
|
965
|
+
input_tokens=cumulative_input, cached_input_tokens=cumulative_cached,
|
|
966
|
+
output_tokens=cumulative_output, reasoning_output_tokens=cumulative_reasoning,
|
|
967
|
+
total_tokens=cumulative_total, cost_usd=cumulative,
|
|
968
|
+
marginal_cost_usd=marginal,
|
|
969
|
+
))
|
|
970
|
+
prior_cumulative = cumulative
|
|
971
|
+
start = end
|
|
972
|
+
return tuple(result)
|
|
973
|
+
|
|
974
|
+
|
|
975
|
+
# === Canonical nested `cctally codex quota` CLI ===========================
|
|
976
|
+
|
|
977
|
+
|
|
978
|
+
class QuotaCLIError(ValueError):
|
|
979
|
+
"""A cctally-native quota CLI validation failure (exit 2)."""
|
|
980
|
+
|
|
981
|
+
|
|
982
|
+
def _cctally():
|
|
983
|
+
"""Resolve the current main module at call time for test isolation."""
|
|
984
|
+
return sys.modules["cctally"]
|
|
985
|
+
|
|
986
|
+
|
|
987
|
+
def _iso_z(value: dt.datetime | None) -> str | None:
|
|
988
|
+
if value is None:
|
|
989
|
+
return None
|
|
990
|
+
return value.astimezone(UTC).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
991
|
+
|
|
992
|
+
|
|
993
|
+
def _identity_wire(identity: QuotaWindowIdentity) -> dict[str, object]:
|
|
994
|
+
return {
|
|
995
|
+
"source": identity.source,
|
|
996
|
+
"sourceRootKey": identity.source_root_key,
|
|
997
|
+
"logicalLimitKey": identity.logical_limit_key,
|
|
998
|
+
"observedSlot": identity.observed_slot,
|
|
999
|
+
"windowMinutes": identity.window_minutes,
|
|
1000
|
+
"limitId": identity.limit_id,
|
|
1001
|
+
"limitName": identity.limit_name,
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
|
|
1005
|
+
def _freshness_wire(freshness: QuotaFreshness) -> dict[str, object]:
|
|
1006
|
+
return {
|
|
1007
|
+
"state": freshness.state,
|
|
1008
|
+
"source": "local-rollout",
|
|
1009
|
+
"capturedAt": _iso_z(freshness.captured_at),
|
|
1010
|
+
"ageSeconds": freshness.age_seconds,
|
|
1011
|
+
"staleAfterSeconds": freshness.stale_after_seconds,
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
|
|
1015
|
+
def _observation_wire(observation: QuotaObservation) -> dict[str, object]:
|
|
1016
|
+
return {
|
|
1017
|
+
"capturedAt": _iso_z(observation.captured_at),
|
|
1018
|
+
"usedPercent": observation.used_percent,
|
|
1019
|
+
"resetsAt": _iso_z(observation.resets_at),
|
|
1020
|
+
"sourcePathKey": source_path_key(observation.source_path),
|
|
1021
|
+
"lineOffset": observation.line_offset,
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
|
|
1025
|
+
def _duration_label(minutes: int) -> str:
|
|
1026
|
+
hours, mins = divmod(minutes, 60)
|
|
1027
|
+
if hours and mins:
|
|
1028
|
+
return f"{hours}h {mins}m"
|
|
1029
|
+
if hours:
|
|
1030
|
+
return f"{hours}h"
|
|
1031
|
+
return f"{mins}m"
|
|
1032
|
+
|
|
1033
|
+
|
|
1034
|
+
def _identity_label(identity: QuotaWindowIdentity) -> str:
|
|
1035
|
+
return (
|
|
1036
|
+
f"{identity.observed_slot} · {_duration_label(identity.window_minutes)}"
|
|
1037
|
+
f" · root={identity.source_root_key} · limit={identity.logical_limit_key}"
|
|
1038
|
+
)
|
|
1039
|
+
|
|
1040
|
+
|
|
1041
|
+
def _parse_as_of(value: str | None) -> dt.datetime:
|
|
1042
|
+
if value is None:
|
|
1043
|
+
return _command_as_of().astimezone(UTC)
|
|
1044
|
+
try:
|
|
1045
|
+
parsed = dt.datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
1046
|
+
except ValueError as exc:
|
|
1047
|
+
raise QuotaCLIError(f"invalid --as-of timestamp: {value!r}") from exc
|
|
1048
|
+
if parsed.tzinfo is None or parsed.utcoffset() is None:
|
|
1049
|
+
parsed = parsed.replace(tzinfo=UTC)
|
|
1050
|
+
return parsed.astimezone(UTC)
|
|
1051
|
+
|
|
1052
|
+
|
|
1053
|
+
def _parse_reset_at(value: str) -> dt.datetime:
|
|
1054
|
+
if "T" not in value and "t" not in value:
|
|
1055
|
+
raise QuotaCLIError("--reset-at rejects date-only input; include HH:MM")
|
|
1056
|
+
try:
|
|
1057
|
+
parsed = dt.datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
1058
|
+
except ValueError as exc:
|
|
1059
|
+
raise QuotaCLIError(f"invalid --reset-at timestamp: {value!r}") from exc
|
|
1060
|
+
if parsed.tzinfo is None or parsed.utcoffset() is None:
|
|
1061
|
+
parsed = parsed.replace(tzinfo=UTC)
|
|
1062
|
+
return parsed.astimezone(UTC)
|
|
1063
|
+
|
|
1064
|
+
|
|
1065
|
+
def _parse_range_bound(value: str | None, *, display_tz, option: str) -> dt.datetime | None:
|
|
1066
|
+
if value is None:
|
|
1067
|
+
return None
|
|
1068
|
+
try:
|
|
1069
|
+
if "T" not in value and "t" not in value:
|
|
1070
|
+
date = dt.date.fromisoformat(value)
|
|
1071
|
+
return dt.datetime.combine(date, dt.time.min, tzinfo=display_tz).astimezone(UTC)
|
|
1072
|
+
parsed = dt.datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
1073
|
+
except ValueError as exc:
|
|
1074
|
+
raise QuotaCLIError(f"invalid {option} timestamp: {value!r}") from exc
|
|
1075
|
+
if parsed.tzinfo is None or parsed.utcoffset() is None:
|
|
1076
|
+
raise QuotaCLIError(f"{option} datetime must include an offset (or use a date-only value)")
|
|
1077
|
+
return parsed.astimezone(UTC)
|
|
1078
|
+
|
|
1079
|
+
|
|
1080
|
+
def _history_in_range(
|
|
1081
|
+
history: QuotaHistory, *, since: dt.datetime | None, until: dt.datetime | None,
|
|
1082
|
+
) -> tuple[QuotaObservation, ...]:
|
|
1083
|
+
return tuple(
|
|
1084
|
+
observation for observation in history.physical_observations
|
|
1085
|
+
if (since is None or observation.captured_at >= since)
|
|
1086
|
+
and (until is None or observation.captured_at < until)
|
|
1087
|
+
)
|
|
1088
|
+
|
|
1089
|
+
|
|
1090
|
+
def _candidate_text(histories: tuple[QuotaHistory, ...]) -> str:
|
|
1091
|
+
if not histories:
|
|
1092
|
+
return " (no active Codex quota identities)"
|
|
1093
|
+
return "\n".join(
|
|
1094
|
+
" root-key={root} limit-key={limit}".format(
|
|
1095
|
+
root=history.identity.source_root_key,
|
|
1096
|
+
limit=history.identity.logical_limit_key,
|
|
1097
|
+
)
|
|
1098
|
+
for history in histories
|
|
1099
|
+
)
|
|
1100
|
+
|
|
1101
|
+
|
|
1102
|
+
def _select_histories(
|
|
1103
|
+
histories: tuple[QuotaHistory, ...], *, root_key: str | None, limit_key: str | None,
|
|
1104
|
+
) -> tuple[QuotaHistory, ...]:
|
|
1105
|
+
selected = tuple(
|
|
1106
|
+
history for history in histories
|
|
1107
|
+
if (root_key is None or history.identity.source_root_key == root_key)
|
|
1108
|
+
and (limit_key is None or history.identity.logical_limit_key == limit_key)
|
|
1109
|
+
)
|
|
1110
|
+
if (root_key is not None or limit_key is not None) and not selected:
|
|
1111
|
+
raise QuotaCLIError(
|
|
1112
|
+
"no quota identity matches the exact selectors; candidates:\n"
|
|
1113
|
+
+ _candidate_text(histories)
|
|
1114
|
+
)
|
|
1115
|
+
return selected
|
|
1116
|
+
|
|
1117
|
+
|
|
1118
|
+
def _sync_and_load(args, as_of: dt.datetime) -> tuple[QuotaHistory, ...]:
|
|
1119
|
+
c = _cctally()
|
|
1120
|
+
if not getattr(args, "no_sync", False):
|
|
1121
|
+
cache = c.open_cache_db()
|
|
1122
|
+
try:
|
|
1123
|
+
c.sync_codex_cache(cache)
|
|
1124
|
+
finally:
|
|
1125
|
+
cache.close()
|
|
1126
|
+
# All five CLI leaves use the single durable projection reconciler. It
|
|
1127
|
+
# gives breakdown its milestone index and heals a cache/stats interruption
|
|
1128
|
+
# without ever reinterpreting or mutating physical cache evidence here.
|
|
1129
|
+
reconcile_codex_quota_projection(now=as_of)
|
|
1130
|
+
observations = load_codex_quota_observations()
|
|
1131
|
+
return build_history(observations)
|
|
1132
|
+
|
|
1133
|
+
|
|
1134
|
+
def _command_context(args, *, range_args: bool = False):
|
|
1135
|
+
c = _cctally()
|
|
1136
|
+
config = c._load_claude_config_for_args(args)
|
|
1137
|
+
display_tz = c._resolve_display_tz_obj(config)
|
|
1138
|
+
as_of = _parse_as_of(getattr(args, "as_of", None))
|
|
1139
|
+
since = until = None
|
|
1140
|
+
if range_args:
|
|
1141
|
+
since = _parse_range_bound(getattr(args, "since", None), display_tz=display_tz, option="--since")
|
|
1142
|
+
until = _parse_range_bound(getattr(args, "until", None), display_tz=display_tz, option="--until")
|
|
1143
|
+
if since is not None and until is not None and until <= since:
|
|
1144
|
+
raise QuotaCLIError("--until must be after --since")
|
|
1145
|
+
histories = _sync_and_load(args, as_of)
|
|
1146
|
+
selected = _select_histories(
|
|
1147
|
+
histories,
|
|
1148
|
+
root_key=getattr(args, "root_key", None),
|
|
1149
|
+
limit_key=getattr(args, "limit_key", None),
|
|
1150
|
+
)
|
|
1151
|
+
return as_of, since, until, selected
|
|
1152
|
+
|
|
1153
|
+
|
|
1154
|
+
def _emit(args, payload: dict[str, object], text: str) -> int:
|
|
1155
|
+
if getattr(args, "json", False):
|
|
1156
|
+
print(json.dumps(stamp_schema_version(payload), ensure_ascii=False))
|
|
1157
|
+
else:
|
|
1158
|
+
print(text)
|
|
1159
|
+
return 0
|
|
1160
|
+
|
|
1161
|
+
|
|
1162
|
+
def _command_error(exc: QuotaCLIError) -> int:
|
|
1163
|
+
eprint(f"cctally codex quota: {exc}")
|
|
1164
|
+
return 2
|
|
1165
|
+
|
|
1166
|
+
|
|
1167
|
+
def cmd_codex_quota_history(args) -> int:
|
|
1168
|
+
"""Render root-qualified physical local-rollout quota history."""
|
|
1169
|
+
try:
|
|
1170
|
+
as_of, since, until, histories = _command_context(args, range_args=True)
|
|
1171
|
+
except QuotaCLIError as exc:
|
|
1172
|
+
return _command_error(exc)
|
|
1173
|
+
windows = []
|
|
1174
|
+
text_rows = ["Codex quota history · local-rollout"]
|
|
1175
|
+
for history in histories:
|
|
1176
|
+
shown = _history_in_range(history, since=since, until=until)
|
|
1177
|
+
if not shown:
|
|
1178
|
+
continue
|
|
1179
|
+
freshness = quota_freshness(history.physical_observations, as_of)
|
|
1180
|
+
windows.append({
|
|
1181
|
+
"identity": _identity_wire(history.identity),
|
|
1182
|
+
"freshness": _freshness_wire(freshness),
|
|
1183
|
+
"orphaned": False,
|
|
1184
|
+
"observations": [_observation_wire(observation) for observation in shown],
|
|
1185
|
+
})
|
|
1186
|
+
text_rows.append(_identity_label(history.identity))
|
|
1187
|
+
text_rows.extend(
|
|
1188
|
+
" {at} {percent:.1f}% reset {reset} path {path}".format(
|
|
1189
|
+
at=_iso_z(observation.captured_at), percent=observation.used_percent,
|
|
1190
|
+
reset=_iso_z(observation.resets_at), path=source_path_key(observation.source_path),
|
|
1191
|
+
)
|
|
1192
|
+
for observation in shown
|
|
1193
|
+
)
|
|
1194
|
+
payload = {
|
|
1195
|
+
"source": "codex", "generatedAt": _iso_z(as_of),
|
|
1196
|
+
"freshnessSource": "local-rollout", "windows": windows,
|
|
1197
|
+
}
|
|
1198
|
+
if len(text_rows) == 1:
|
|
1199
|
+
text_rows.append("No Codex quota history.")
|
|
1200
|
+
return _emit(args, payload, "\n".join(text_rows))
|
|
1201
|
+
|
|
1202
|
+
|
|
1203
|
+
def _statusline_status(history: QuotaHistory, as_of: dt.datetime) -> tuple[str, QuotaObservation | None, QuotaFreshness]:
|
|
1204
|
+
freshness = quota_freshness(history.physical_observations, as_of)
|
|
1205
|
+
current = select_baseline(history.observations, as_of)
|
|
1206
|
+
if freshness.state == "future":
|
|
1207
|
+
return "future", current, freshness
|
|
1208
|
+
if current is None:
|
|
1209
|
+
return "unavailable", None, freshness
|
|
1210
|
+
if freshness.state == "stale":
|
|
1211
|
+
return "stale", current, freshness
|
|
1212
|
+
return "ok", current, freshness
|
|
1213
|
+
|
|
1214
|
+
|
|
1215
|
+
def cmd_codex_quota_statusline(args) -> int:
|
|
1216
|
+
"""Render one truthful native status segment for every selected identity."""
|
|
1217
|
+
try:
|
|
1218
|
+
as_of, _since, _until, histories = _command_context(args)
|
|
1219
|
+
except QuotaCLIError as exc:
|
|
1220
|
+
return _command_error(exc)
|
|
1221
|
+
windows = []
|
|
1222
|
+
text_rows = []
|
|
1223
|
+
for history in histories:
|
|
1224
|
+
status, current, freshness = _statusline_status(history, as_of)
|
|
1225
|
+
label = _identity_label(history.identity)
|
|
1226
|
+
windows.append({
|
|
1227
|
+
"identity": _identity_wire(history.identity),
|
|
1228
|
+
"freshness": _freshness_wire(freshness),
|
|
1229
|
+
"status": status,
|
|
1230
|
+
"current": None if current is None else {
|
|
1231
|
+
"usedPercent": current.used_percent, "resetsAt": _iso_z(current.resets_at),
|
|
1232
|
+
},
|
|
1233
|
+
"label": label,
|
|
1234
|
+
})
|
|
1235
|
+
if current is None:
|
|
1236
|
+
row = f"{label} · unavailable"
|
|
1237
|
+
else:
|
|
1238
|
+
row = f"{label} · {current.used_percent:.1f}% · resets {_iso_z(current.resets_at)}"
|
|
1239
|
+
if status == "future":
|
|
1240
|
+
row += " · FUTURE DATA"
|
|
1241
|
+
elif status == "stale":
|
|
1242
|
+
row += " · STALE"
|
|
1243
|
+
text_rows.append(row)
|
|
1244
|
+
payload = {
|
|
1245
|
+
"source": "codex", "generatedAt": _iso_z(as_of),
|
|
1246
|
+
"freshnessSource": "local-rollout", "windows": windows,
|
|
1247
|
+
}
|
|
1248
|
+
return _emit(args, payload, "\n".join(text_rows) if text_rows else "Codex quota unavailable.")
|
|
1249
|
+
|
|
1250
|
+
|
|
1251
|
+
def _forecast_wire(history: QuotaHistory, as_of: dt.datetime) -> dict[str, object]:
|
|
1252
|
+
forecast: QuotaForecast = forecast_quota(history.physical_observations, as_of)
|
|
1253
|
+
freshness = quota_freshness(history.physical_observations, as_of)
|
|
1254
|
+
return {
|
|
1255
|
+
"identity": _identity_wire(history.identity),
|
|
1256
|
+
"freshness": _freshness_wire(freshness), "status": forecast.status,
|
|
1257
|
+
"currentPercent": forecast.current_percent,
|
|
1258
|
+
"ratePercentPerHour": forecast.rate_percent_per_hour,
|
|
1259
|
+
"projectedPercent": forecast.projected_percent,
|
|
1260
|
+
"resetsAt": _iso_z(forecast.resets_at),
|
|
1261
|
+
"remainingSeconds": forecast.remaining_seconds,
|
|
1262
|
+
"sampleCount": forecast.sample_count,
|
|
1263
|
+
"sampleSpanSeconds": forecast.sample_span_seconds,
|
|
1264
|
+
"confidence": forecast.confidence,
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
|
|
1268
|
+
def cmd_codex_quota_forecast(args) -> int:
|
|
1269
|
+
"""Render independent native-window forecasts without quota blending."""
|
|
1270
|
+
try:
|
|
1271
|
+
as_of, _since, _until, histories = _command_context(args)
|
|
1272
|
+
except QuotaCLIError as exc:
|
|
1273
|
+
return _command_error(exc)
|
|
1274
|
+
forecasts = [_forecast_wire(history, as_of) for history in histories]
|
|
1275
|
+
text_rows = ["Codex quota forecast · local-rollout"]
|
|
1276
|
+
for history, forecast in zip(histories, forecasts):
|
|
1277
|
+
label = _identity_label(history.identity)
|
|
1278
|
+
current = forecast["currentPercent"]
|
|
1279
|
+
projected = forecast["projectedPercent"]
|
|
1280
|
+
row = f"{label} · {forecast['status']}"
|
|
1281
|
+
if current is not None:
|
|
1282
|
+
row += f" · current {float(current):.1f}%"
|
|
1283
|
+
if projected is not None:
|
|
1284
|
+
row += f" · projected {float(projected):.1f}%"
|
|
1285
|
+
text_rows.append(row)
|
|
1286
|
+
payload = {
|
|
1287
|
+
"source": "codex", "generatedAt": _iso_z(as_of),
|
|
1288
|
+
"freshnessSource": "local-rollout", "forecasts": forecasts,
|
|
1289
|
+
}
|
|
1290
|
+
return _emit(args, payload, "\n".join(text_rows))
|
|
1291
|
+
|
|
1292
|
+
|
|
1293
|
+
def cmd_codex_quota_blocks(args) -> int:
|
|
1294
|
+
"""Render reset-native quota blocks from the provider-neutral kernel."""
|
|
1295
|
+
try:
|
|
1296
|
+
as_of, since, until, histories = _command_context(args, range_args=True)
|
|
1297
|
+
except QuotaCLIError as exc:
|
|
1298
|
+
return _command_error(exc)
|
|
1299
|
+
blocks = []
|
|
1300
|
+
text_rows = ["Codex quota blocks · local-rollout"]
|
|
1301
|
+
for block in build_blocks(
|
|
1302
|
+
observation for history in histories for observation in history.physical_observations
|
|
1303
|
+
):
|
|
1304
|
+
if since is not None and block.last_observed_at < since:
|
|
1305
|
+
continue
|
|
1306
|
+
if until is not None and block.first_observed_at >= until:
|
|
1307
|
+
continue
|
|
1308
|
+
blocks.append({
|
|
1309
|
+
"identity": _identity_wire(block.identity), "resetAt": _iso_z(block.resets_at),
|
|
1310
|
+
"nominalStartAt": _iso_z(block.nominal_start_at),
|
|
1311
|
+
"firstObservedAt": _iso_z(block.first_observed_at),
|
|
1312
|
+
"lastObservedAt": _iso_z(block.last_observed_at),
|
|
1313
|
+
"firstPercent": block.first_percent, "currentPercent": block.current_percent,
|
|
1314
|
+
"orphaned": False,
|
|
1315
|
+
})
|
|
1316
|
+
text_rows.append(
|
|
1317
|
+
f"{_identity_label(block.identity)} · {block.first_percent:.1f}% → "
|
|
1318
|
+
f"{block.current_percent:.1f}% · reset {_iso_z(block.resets_at)}"
|
|
1319
|
+
)
|
|
1320
|
+
payload = {
|
|
1321
|
+
"source": "codex", "generatedAt": _iso_z(as_of),
|
|
1322
|
+
"freshnessSource": "local-rollout", "blocks": blocks,
|
|
1323
|
+
}
|
|
1324
|
+
if len(text_rows) == 1:
|
|
1325
|
+
text_rows.append("No Codex quota blocks.")
|
|
1326
|
+
return _emit(args, payload, "\n".join(text_rows))
|
|
1327
|
+
|
|
1328
|
+
|
|
1329
|
+
def cmd_codex_quota_breakdown(args) -> int:
|
|
1330
|
+
"""Render root-qualified, live-priced milestone deltas for one block."""
|
|
1331
|
+
try:
|
|
1332
|
+
as_of, _since, _until, histories = _command_context(args)
|
|
1333
|
+
if len(histories) != 1:
|
|
1334
|
+
raise QuotaCLIError(
|
|
1335
|
+
"breakdown requires selectors resolving to exactly one quota identity; candidates:\n"
|
|
1336
|
+
+ _candidate_text(histories)
|
|
1337
|
+
)
|
|
1338
|
+
reset_at = _parse_reset_at(args.reset_at)
|
|
1339
|
+
identity = histories[0].identity
|
|
1340
|
+
matching = [
|
|
1341
|
+
block for block in build_blocks(histories[0].physical_observations)
|
|
1342
|
+
if block.resets_at == reset_at
|
|
1343
|
+
]
|
|
1344
|
+
if len(matching) != 1:
|
|
1345
|
+
raise QuotaCLIError(
|
|
1346
|
+
"--reset-at matches no unique native quota block; candidates:\n"
|
|
1347
|
+
+ _candidate_text(histories)
|
|
1348
|
+
)
|
|
1349
|
+
except QuotaCLIError as exc:
|
|
1350
|
+
return _command_error(exc)
|
|
1351
|
+
c = _cctally()
|
|
1352
|
+
speed = c._resolve_codex_speed(args.speed)
|
|
1353
|
+
rows = codex_quota_breakdown(identity, reset_at, speed=speed)
|
|
1354
|
+
block = matching[0]
|
|
1355
|
+
milestones = [
|
|
1356
|
+
{
|
|
1357
|
+
"percent": row.percent, "capturedAt": _iso_z(row.captured_at),
|
|
1358
|
+
"inputTokens": row.input_tokens, "cachedInputTokens": row.cached_input_tokens,
|
|
1359
|
+
"outputTokens": row.output_tokens, "reasoningOutputTokens": row.reasoning_output_tokens,
|
|
1360
|
+
"totalTokens": row.total_tokens, "costUSD": row.cost_usd,
|
|
1361
|
+
"marginalCostUSD": row.marginal_cost_usd,
|
|
1362
|
+
}
|
|
1363
|
+
for row in rows
|
|
1364
|
+
]
|
|
1365
|
+
payload = {
|
|
1366
|
+
"source": "codex", "generatedAt": _iso_z(as_of),
|
|
1367
|
+
"freshnessSource": "local-rollout", "identity": _identity_wire(identity),
|
|
1368
|
+
"block": {"resetAt": _iso_z(block.resets_at), "nominalStartAt": _iso_z(block.nominal_start_at)},
|
|
1369
|
+
"speed": speed, "milestones": milestones,
|
|
1370
|
+
}
|
|
1371
|
+
text_rows = [
|
|
1372
|
+
f"Codex quota breakdown · {_identity_label(identity)}",
|
|
1373
|
+
f"reset {_iso_z(block.resets_at)} · speed {speed}",
|
|
1374
|
+
]
|
|
1375
|
+
text_rows.extend(
|
|
1376
|
+
f"{row.percent:>3}% {row.total_tokens:>8} tokens ${row.cost_usd:.6f} Δ${row.marginal_cost_usd:.6f}"
|
|
1377
|
+
for row in rows
|
|
1378
|
+
)
|
|
1379
|
+
if not rows:
|
|
1380
|
+
text_rows.append("No percent milestones.")
|
|
1381
|
+
return _emit(args, payload, "\n".join(text_rows))
|