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.
Files changed (40) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/bin/_cctally_alerts.py +54 -2
  3. package/bin/_cctally_cache.py +644 -107
  4. package/bin/_cctally_cache_report.py +41 -17
  5. package/bin/_cctally_codex.py +109 -4
  6. package/bin/_cctally_config.py +368 -2
  7. package/bin/_cctally_core.py +168 -5
  8. package/bin/_cctally_dashboard.py +679 -5
  9. package/bin/_cctally_dashboard_conversation.py +9 -4
  10. package/bin/_cctally_dashboard_envelope.py +110 -1
  11. package/bin/_cctally_dashboard_share.py +557 -79
  12. package/bin/_cctally_db.py +303 -17
  13. package/bin/_cctally_diff.py +49 -16
  14. package/bin/_cctally_doctor.py +118 -0
  15. package/bin/_cctally_forecast.py +12 -4
  16. package/bin/_cctally_parser.py +298 -92
  17. package/bin/_cctally_project.py +24 -8
  18. package/bin/_cctally_quota.py +1381 -0
  19. package/bin/_cctally_record.py +319 -14
  20. package/bin/_cctally_refresh.py +105 -3
  21. package/bin/_cctally_reporting.py +7 -1
  22. package/bin/_cctally_setup.py +343 -113
  23. package/bin/_cctally_statusline.py +228 -0
  24. package/bin/_cctally_tui.py +787 -7
  25. package/bin/_lib_alert_axes.py +11 -0
  26. package/bin/_lib_codex_hooks.py +367 -0
  27. package/bin/_lib_conversation_query.py +156 -67
  28. package/bin/_lib_doctor.py +202 -0
  29. package/bin/_lib_jsonl.py +529 -162
  30. package/bin/_lib_quota.py +566 -0
  31. package/bin/_lib_share.py +152 -34
  32. package/bin/_lib_snapshot_cache.py +26 -0
  33. package/bin/_lib_source_identity.py +74 -0
  34. package/bin/cctally +324 -10
  35. package/dashboard/static/assets/index-CBbErI-P.js +80 -0
  36. package/dashboard/static/assets/index-kDDVOLa_.css +1 -0
  37. package/dashboard/static/dashboard.html +2 -2
  38. package/package.json +5 -1
  39. package/dashboard/static/assets/index-BybNp_Di.js +0 -80
  40. 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))