cctally 1.80.4 → 1.82.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 +32 -0
- package/bin/_cctally_account.py +397 -0
- package/bin/_cctally_alerts.py +58 -2
- package/bin/_cctally_cache.py +696 -166
- package/bin/_cctally_cache_report.py +21 -3
- package/bin/_cctally_config.py +119 -8
- package/bin/_cctally_core.py +343 -48
- package/bin/_cctally_dashboard.py +32 -7
- package/bin/_cctally_dashboard_conversation.py +6 -2
- package/bin/_cctally_dashboard_envelope.py +49 -0
- package/bin/_cctally_dashboard_share.py +78 -1
- package/bin/_cctally_dashboard_sources.py +434 -48
- package/bin/_cctally_db.py +659 -152
- package/bin/_cctally_diff.py +9 -0
- package/bin/_cctally_doctor.py +201 -26
- package/bin/_cctally_five_hour.py +110 -69
- package/bin/_cctally_forecast.py +112 -43
- package/bin/_cctally_journal.py +591 -80
- package/bin/_cctally_milestones.py +180 -67
- package/bin/_cctally_parser.py +87 -0
- package/bin/_cctally_percent_breakdown.py +24 -15
- package/bin/_cctally_project.py +12 -1
- package/bin/_cctally_quota.py +150 -39
- package/bin/_cctally_record.py +491 -141
- package/bin/_cctally_reporting.py +66 -14
- package/bin/_cctally_setup.py +9 -1
- package/bin/_cctally_source_analytics.py +56 -7
- package/bin/_cctally_statusline.py +56 -8
- package/bin/_cctally_store.py +20 -8
- package/bin/_cctally_sync_week.py +30 -10
- package/bin/_cctally_tui.py +99 -18
- package/bin/_cctally_weekrefs.py +38 -15
- package/bin/_lib_accounts.py +325 -0
- package/bin/_lib_alerts_payload.py +25 -4
- package/bin/_lib_cache_writer_lock.py +77 -0
- package/bin/_lib_codex_hooks.py +40 -1
- package/bin/_lib_diff_kernel.py +28 -14
- package/bin/_lib_doctor.py +160 -0
- package/bin/_lib_journal.py +65 -2
- package/bin/_lib_pricing.py +27 -3
- package/bin/_lib_quota.py +81 -4
- package/bin/_lib_render.py +19 -5
- package/bin/_lib_share.py +25 -0
- package/bin/_lib_snapshot_cache.py +8 -0
- package/bin/_lib_subscription_weeks.py +16 -2
- package/bin/_lib_view_models.py +18 -9
- package/bin/cctally +43 -4
- package/dashboard/static/assets/index-DJP4gEB7.js +92 -0
- package/dashboard/static/assets/index-Dk1nplOz.css +1 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +4 -1
- package/dashboard/static/assets/index-BzWujBS3.js +0 -92
- package/dashboard/static/assets/index-Cc2ykqF_.css +0 -1
package/bin/_cctally_quota.py
CHANGED
|
@@ -18,6 +18,7 @@ from dataclasses import dataclass
|
|
|
18
18
|
from typing import Callable, Iterable, Mapping
|
|
19
19
|
|
|
20
20
|
import _cctally_core
|
|
21
|
+
import _lib_accounts
|
|
21
22
|
from _cctally_core import _command_as_of, eprint
|
|
22
23
|
from _lib_quota import (
|
|
23
24
|
QuotaBlock,
|
|
@@ -28,6 +29,7 @@ from _lib_quota import (
|
|
|
28
29
|
QuotaPercentMilestone,
|
|
29
30
|
QuotaRule,
|
|
30
31
|
QuotaWindowIdentity,
|
|
32
|
+
adopt_unidentified_observations,
|
|
31
33
|
build_blocks,
|
|
32
34
|
build_history,
|
|
33
35
|
forecast_quota,
|
|
@@ -285,6 +287,12 @@ def load_codex_quota_observations(
|
|
|
285
287
|
has_observed_model = has_columns(
|
|
286
288
|
"quota_window_snapshots", {"observed_model"},
|
|
287
289
|
)
|
|
290
|
+
# account_key (#341 Task 2): NULL ≡ unattributed on the read path; a
|
|
291
|
+
# pre-Task-2 cache lacking the column reads every window as unattributed.
|
|
292
|
+
has_account = has_columns("quota_window_snapshots", {"account_key"})
|
|
293
|
+
account_expr = (
|
|
294
|
+
"account_key" if has_account else "NULL AS account_key"
|
|
295
|
+
)
|
|
288
296
|
entry_lookup = (
|
|
289
297
|
"(SELECT entries.model FROM codex_session_entries AS entries "
|
|
290
298
|
"WHERE entries.source_path=quota_window_snapshots.source_path "
|
|
@@ -310,10 +318,10 @@ def load_codex_quota_observations(
|
|
|
310
318
|
captured_at_utc, observed_slot, logical_limit_key, limit_id,
|
|
311
319
|
limit_name, window_minutes, used_percent, resets_at_utc,
|
|
312
320
|
plan_type, individual_limit_json, reached_type,
|
|
313
|
-
{model_expr}
|
|
321
|
+
{model_expr}, {account_expr}
|
|
314
322
|
FROM quota_window_snapshots
|
|
315
323
|
WHERE source='codex' AND source_root_key IS NOT NULL
|
|
316
|
-
""".format(model_expr=model_expr)
|
|
324
|
+
""".format(model_expr=model_expr, account_expr=account_expr)
|
|
317
325
|
params: list[object] = []
|
|
318
326
|
if requested is not None:
|
|
319
327
|
if not requested:
|
|
@@ -368,9 +376,15 @@ def load_codex_quota_observations(
|
|
|
368
376
|
str(row["observed_slot"]), int(row["window_minutes"]),
|
|
369
377
|
str(row["observed_model"]),
|
|
370
378
|
)
|
|
379
|
+
raw_account = row["account_key"]
|
|
380
|
+
account_key = (
|
|
381
|
+
str(raw_account) if raw_account not in (None, "")
|
|
382
|
+
else _lib_accounts.UNATTRIBUTED
|
|
383
|
+
)
|
|
371
384
|
identity = QuotaWindowIdentity(
|
|
372
385
|
source=str(row["source"]),
|
|
373
386
|
source_root_key=str(row["source_root_key"]),
|
|
387
|
+
account_key=account_key,
|
|
374
388
|
logical_limit_key=logical_limit_key,
|
|
375
389
|
observed_slot=str(row["observed_slot"]),
|
|
376
390
|
window_minutes=int(row["window_minutes"]),
|
|
@@ -410,6 +424,13 @@ def load_codex_quota_observations(
|
|
|
410
424
|
):
|
|
411
425
|
continue
|
|
412
426
|
result.append(observation)
|
|
427
|
+
# Window-account continuity fold (#341 spec §2): adopt unidentified
|
|
428
|
+
# observations into a same-physical-window identified account (exactly
|
|
429
|
+
# one). Physical signatures above are account-independent (computed from
|
|
430
|
+
# the physical tuple), so folding after them is order-invariant; the
|
|
431
|
+
# recursion path below re-fetches + re-folds. Idempotent for an
|
|
432
|
+
# already-identified set — a single-account cache is a no-op.
|
|
433
|
+
result = list(adopt_unidentified_observations(result))
|
|
413
434
|
if physical_signatures is not None:
|
|
414
435
|
physical_signatures.clear()
|
|
415
436
|
roots = requested if requested is not None else set(signature_tuples)
|
|
@@ -499,19 +520,22 @@ def _block_params(block: QuotaBlock, generation: str) -> tuple[object, ...]:
|
|
|
499
520
|
identity.limit_name, _utc_iso(block.resets_at), _utc_iso(block.nominal_start_at),
|
|
500
521
|
_utc_iso(block.first_observed_at), _utc_iso(block.last_observed_at),
|
|
501
522
|
block.first_percent, block.current_percent, latest.source_path,
|
|
502
|
-
latest.line_offset, generation,
|
|
523
|
+
latest.line_offset, generation, identity.account_key,
|
|
503
524
|
)
|
|
504
525
|
|
|
505
526
|
|
|
527
|
+
# account_key (#341): part of the block/milestone identity — the ON CONFLICT
|
|
528
|
+
# target includes it so two accounts sharing one physical window are DISTINCT
|
|
529
|
+
# rows (never-combine), and each account's row upserts independently.
|
|
506
530
|
_BLOCK_UPSERT = """
|
|
507
531
|
INSERT INTO quota_window_blocks
|
|
508
532
|
(source, source_root_key, logical_limit_key, observed_slot,
|
|
509
533
|
window_minutes, limit_id, limit_name, resets_at_utc, nominal_start_at_utc,
|
|
510
534
|
first_observed_at_utc, last_observed_at_utc, first_percent, current_percent,
|
|
511
|
-
last_source_path, last_line_offset, generation, orphaned_at)
|
|
512
|
-
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,NULL)
|
|
513
|
-
ON CONFLICT(source, source_root_key,
|
|
514
|
-
window_minutes, resets_at_utc) DO UPDATE SET
|
|
535
|
+
last_source_path, last_line_offset, generation, orphaned_at, account_key)
|
|
536
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,NULL,?)
|
|
537
|
+
ON CONFLICT(source, source_root_key, account_key, logical_limit_key,
|
|
538
|
+
observed_slot, window_minutes, resets_at_utc) DO UPDATE SET
|
|
515
539
|
limit_id=excluded.limit_id, limit_name=excluded.limit_name,
|
|
516
540
|
nominal_start_at_utc=excluded.nominal_start_at_utc,
|
|
517
541
|
first_observed_at_utc=excluded.first_observed_at_utc,
|
|
@@ -526,10 +550,11 @@ _MILESTONE_UPSERT = """
|
|
|
526
550
|
INSERT INTO quota_percent_milestones
|
|
527
551
|
(source, source_root_key, logical_limit_key, observed_slot, window_minutes,
|
|
528
552
|
resets_at_utc, percent_threshold, captured_at_utc, source_path,
|
|
529
|
-
line_offset, high_water_percent, generation, orphaned_at)
|
|
530
|
-
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,NULL)
|
|
531
|
-
ON CONFLICT(source, source_root_key,
|
|
532
|
-
window_minutes, resets_at_utc, percent_threshold)
|
|
553
|
+
line_offset, high_water_percent, generation, orphaned_at, account_key)
|
|
554
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,NULL,?)
|
|
555
|
+
ON CONFLICT(source, source_root_key, account_key, logical_limit_key,
|
|
556
|
+
observed_slot, window_minutes, resets_at_utc, percent_threshold)
|
|
557
|
+
DO UPDATE SET
|
|
533
558
|
captured_at_utc=excluded.captured_at_utc, source_path=excluded.source_path,
|
|
534
559
|
line_offset=excluded.line_offset, high_water_percent=excluded.high_water_percent,
|
|
535
560
|
generation=excluded.generation, orphaned_at=NULL
|
|
@@ -545,6 +570,7 @@ def _milestone_params(
|
|
|
545
570
|
identity.observed_slot, identity.window_minutes, _utc_iso(block.resets_at),
|
|
546
571
|
milestone.percent, _utc_iso(milestone.captured_at), milestone.observation.source_path,
|
|
547
572
|
milestone.observation.line_offset, milestone.percent, generation,
|
|
573
|
+
identity.account_key,
|
|
548
574
|
)
|
|
549
575
|
|
|
550
576
|
|
|
@@ -568,11 +594,14 @@ def _orphan_unseen(conn: sqlite3.Connection, roots: set[str], generation: str, n
|
|
|
568
594
|
# this completed generation, so a cache rebuild that restores the exact
|
|
569
595
|
# block clears a transient prune marker without creating a new terminal
|
|
570
596
|
# claim.
|
|
597
|
+
# account_key (#341): join on the account too so account A's terminal event
|
|
598
|
+
# is never un-orphaned by account B's block sharing the physical window.
|
|
571
599
|
event_sql = f"""UPDATE quota_threshold_events AS events
|
|
572
600
|
SET orphaned_at=CASE WHEN EXISTS (
|
|
573
601
|
SELECT 1 FROM quota_window_blocks AS blocks
|
|
574
602
|
WHERE blocks.source=events.source
|
|
575
603
|
AND blocks.source_root_key=events.source_root_key
|
|
604
|
+
AND blocks.account_key=events.account_key
|
|
576
605
|
AND blocks.logical_limit_key=events.logical_limit_key
|
|
577
606
|
AND blocks.observed_slot=events.observed_slot
|
|
578
607
|
AND blocks.window_minutes=events.window_minutes
|
|
@@ -611,11 +640,11 @@ def _quota_alert_config() -> tuple[bool, bool, tuple[QuotaRule, ...], dict]:
|
|
|
611
640
|
def _arming_row(conn: sqlite3.Connection, identity: QuotaWindowIdentity) -> sqlite3.Row | None:
|
|
612
641
|
return conn.execute(
|
|
613
642
|
"""SELECT rule_fingerprint, activated_at_utc FROM quota_alert_arming
|
|
614
|
-
WHERE source=? AND source_root_key=? AND
|
|
615
|
-
AND observed_slot=? AND window_minutes=?""",
|
|
643
|
+
WHERE source=? AND source_root_key=? AND account_key=?
|
|
644
|
+
AND logical_limit_key=? AND observed_slot=? AND window_minutes=?""",
|
|
616
645
|
(
|
|
617
|
-
identity.source, identity.source_root_key, identity.
|
|
618
|
-
identity.observed_slot, identity.window_minutes,
|
|
646
|
+
identity.source, identity.source_root_key, identity.account_key,
|
|
647
|
+
identity.logical_limit_key, identity.observed_slot, identity.window_minutes,
|
|
619
648
|
),
|
|
620
649
|
).fetchone()
|
|
621
650
|
|
|
@@ -640,15 +669,16 @@ def _activate_quota_rule(
|
|
|
640
669
|
conn.execute(
|
|
641
670
|
"""INSERT INTO quota_alert_arming
|
|
642
671
|
(source, source_root_key, logical_limit_key, observed_slot,
|
|
643
|
-
window_minutes, rule_fingerprint, activated_at_utc)
|
|
644
|
-
VALUES (
|
|
645
|
-
ON CONFLICT(source, source_root_key, logical_limit_key,
|
|
672
|
+
window_minutes, rule_fingerprint, activated_at_utc, account_key)
|
|
673
|
+
VALUES (?,?,?,?,?,?,?,?)
|
|
674
|
+
ON CONFLICT(source, source_root_key, account_key, logical_limit_key,
|
|
646
675
|
observed_slot, window_minutes) DO UPDATE SET
|
|
647
676
|
rule_fingerprint=excluded.rule_fingerprint,
|
|
648
677
|
activated_at_utc=excluded.activated_at_utc""",
|
|
649
678
|
(
|
|
650
679
|
identity.source, identity.source_root_key, identity.logical_limit_key,
|
|
651
680
|
identity.observed_slot, identity.window_minutes, fingerprint, now_iso,
|
|
681
|
+
identity.account_key,
|
|
652
682
|
),
|
|
653
683
|
)
|
|
654
684
|
if journal_emit is not None:
|
|
@@ -670,14 +700,14 @@ def _insert_quota_terminal_event(
|
|
|
670
700
|
(source, source_root_key, logical_limit_key, observed_slot,
|
|
671
701
|
window_minutes, resets_at_utc, threshold, qualifying_kind,
|
|
672
702
|
qualifying_percent, projected_percent, severity, created_at_utc,
|
|
673
|
-
disposition, alerted_at, suppressed_at)
|
|
674
|
-
VALUES (
|
|
703
|
+
disposition, alerted_at, suppressed_at, account_key)
|
|
704
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
|
675
705
|
(
|
|
676
706
|
identity.source, identity.source_root_key, identity.logical_limit_key,
|
|
677
707
|
identity.observed_slot, identity.window_minutes, _utc_iso(resets_at),
|
|
678
708
|
threshold, kind, qualifying_percent, projected_percent,
|
|
679
709
|
_cctally().severity_for(threshold), now_iso, disposition,
|
|
680
|
-
alerted_at, suppressed_at,
|
|
710
|
+
alerted_at, suppressed_at, identity.account_key,
|
|
681
711
|
),
|
|
682
712
|
)
|
|
683
713
|
return cur.rowcount == 1
|
|
@@ -711,6 +741,7 @@ def _quota_alert_payload(
|
|
|
711
741
|
resets_at_utc=_utc_iso(resets_at), threshold=threshold, kind=kind,
|
|
712
742
|
crossed_at_utc=now_iso, qualifying_percent=qualifying_percent,
|
|
713
743
|
projected_percent=projected_percent,
|
|
744
|
+
account_key=identity.account_key,
|
|
714
745
|
)
|
|
715
746
|
|
|
716
747
|
|
|
@@ -864,17 +895,35 @@ def _apply_quota_projection_rows(
|
|
|
864
895
|
# The completion stamp is intentionally the final DML in the stats
|
|
865
896
|
# transaction. A pre-commit failure rolls all projection updates back;
|
|
866
897
|
# a retry sees the prior complete generation or rederives it.
|
|
898
|
+
#
|
|
899
|
+
# projection_state is (source_root_key, account_key)-keyed (#341 spec §2). The
|
|
900
|
+
# active account identities are derived from the FOLDED observations (so an
|
|
901
|
+
# adopted-unidentified window contributes its adopting account, not the raw
|
|
902
|
+
# `unattributed` stamp); a root with no observations still stamps one
|
|
903
|
+
# `unattributed` row so its coherence signature is present (byte-stable with
|
|
904
|
+
# the prior per-root behavior for a single-account install). The
|
|
905
|
+
# physical_signature stays PER-ROOT (account-independent) — it certifies the
|
|
906
|
+
# root's physical cache evidence and matches the per-root cache certificate.
|
|
907
|
+
accounts_by_root: dict[str, set[str]] = {root: set() for root in active_roots}
|
|
908
|
+
for observation in observations:
|
|
909
|
+
root = observation.identity.source_root_key
|
|
910
|
+
if root in accounts_by_root:
|
|
911
|
+
accounts_by_root[root].add(observation.identity.account_key)
|
|
867
912
|
for root_key in sorted(active_roots):
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
913
|
+
accounts = accounts_by_root[root_key] or {_lib_accounts.UNATTRIBUTED}
|
|
914
|
+
root_signature = _signature(observations, root_key)
|
|
915
|
+
for account_key in sorted(accounts):
|
|
916
|
+
conn.execute(
|
|
917
|
+
"""INSERT INTO quota_projection_state
|
|
918
|
+
(source_root_key, account_key, generation, physical_signature,
|
|
919
|
+
completed_at_utc)
|
|
920
|
+
VALUES (?,?,?,?,?)
|
|
921
|
+
ON CONFLICT(source_root_key, account_key) DO UPDATE SET
|
|
922
|
+
generation=excluded.generation,
|
|
923
|
+
physical_signature=excluded.physical_signature,
|
|
924
|
+
completed_at_utc=excluded.completed_at_utc""",
|
|
925
|
+
(root_key, account_key, generation, root_signature, now_iso),
|
|
926
|
+
)
|
|
878
927
|
if sink is not None:
|
|
879
928
|
# Set-then-dispatch: all claims committed with the cycle before the
|
|
880
929
|
# cycle's post-commit ALERT_DISPATCHER fires them (spec §5.2 step 6).
|
|
@@ -1027,16 +1076,22 @@ def reconcile_codex_quota_projection(
|
|
|
1027
1076
|
def _codex_leg(ctx):
|
|
1028
1077
|
def _emit_arming(identity, fingerprint, activated_at):
|
|
1029
1078
|
# Item 5: journal the arming state change (`quota_alert_arming` evt).
|
|
1079
|
+
# account_key (#341) is part of the qaa natural key (after the root):
|
|
1080
|
+
# two accounts arming the same physical window produce DISTINCT evt
|
|
1081
|
+
# ids + rows. This id MUST match the cutover export's natural_key_id
|
|
1082
|
+
# ordering in _cctally_journal._CUTOVER_SPECS so a live re-emission and
|
|
1083
|
+
# a cutover-exported arming for one identity converge as ONE record.
|
|
1030
1084
|
eid = _jl.evt_id(
|
|
1031
1085
|
"qaa", identity.source, identity.source_root_key,
|
|
1032
|
-
identity.
|
|
1033
|
-
identity.window_minutes,
|
|
1086
|
+
identity.account_key, identity.logical_limit_key,
|
|
1087
|
+
identity.observed_slot, identity.window_minutes,
|
|
1034
1088
|
)
|
|
1035
1089
|
_jr.append_record(_jl.make_evt(
|
|
1036
1090
|
kind="quota_alert_arming", id=eid, at=activated_at,
|
|
1037
1091
|
payload={
|
|
1038
1092
|
"source": identity.source,
|
|
1039
1093
|
"source_root_key": identity.source_root_key,
|
|
1094
|
+
"account_key": identity.account_key,
|
|
1040
1095
|
"logical_limit_key": identity.logical_limit_key,
|
|
1041
1096
|
"observed_slot": identity.observed_slot,
|
|
1042
1097
|
"window_minutes": identity.window_minutes,
|
|
@@ -1081,13 +1136,14 @@ def _load_active_milestones(
|
|
|
1081
1136
|
return list(stats.execute(
|
|
1082
1137
|
"""SELECT percent_threshold, captured_at_utc, source_path, line_offset
|
|
1083
1138
|
FROM quota_percent_milestones
|
|
1084
|
-
WHERE source=? AND source_root_key=? AND
|
|
1085
|
-
AND
|
|
1086
|
-
AND orphaned_at IS NULL
|
|
1139
|
+
WHERE source=? AND source_root_key=? AND account_key=?
|
|
1140
|
+
AND logical_limit_key=? AND observed_slot=? AND window_minutes=?
|
|
1141
|
+
AND resets_at_utc=? AND orphaned_at IS NULL
|
|
1087
1142
|
ORDER BY percent_threshold""",
|
|
1088
1143
|
(
|
|
1089
|
-
identity.source, identity.source_root_key, identity.
|
|
1090
|
-
identity.
|
|
1144
|
+
identity.source, identity.source_root_key, identity.account_key,
|
|
1145
|
+
identity.logical_limit_key, identity.observed_slot,
|
|
1146
|
+
identity.window_minutes, _utc_iso(resets_at),
|
|
1091
1147
|
),
|
|
1092
1148
|
))
|
|
1093
1149
|
finally:
|
|
@@ -1486,7 +1542,10 @@ def _sync_and_load(
|
|
|
1486
1542
|
if should_sync:
|
|
1487
1543
|
cache = c.open_cache_db()
|
|
1488
1544
|
try:
|
|
1489
|
-
c.
|
|
1545
|
+
cache_mod = c._load_sibling("_cctally_cache")
|
|
1546
|
+
_, cache = cache_mod._run_cache_operation_with_recovery(
|
|
1547
|
+
cache, lambda active_conn: c.sync_codex_cache(active_conn)
|
|
1548
|
+
)
|
|
1490
1549
|
finally:
|
|
1491
1550
|
cache.close()
|
|
1492
1551
|
# The nested quota leaves heal the durable projection on every read. The
|
|
@@ -1528,6 +1587,38 @@ def _command_context(
|
|
|
1528
1587
|
return as_of, since, until, selected
|
|
1529
1588
|
|
|
1530
1589
|
|
|
1590
|
+
def _resolve_account_and_scope(args, histories):
|
|
1591
|
+
"""(#341, spec §3) Resolve ``--account`` (provider=codex) and scope histories.
|
|
1592
|
+
|
|
1593
|
+
Returns ``(account_key | None, exit_code | None, filtered_histories)``:
|
|
1594
|
+
``None`` key = the merged view (byte-identical to today, R8); a resolved ref
|
|
1595
|
+
filters to that account's quota identities; a bad ref yields exit 2 (the
|
|
1596
|
+
``account: …`` diagnostic is printed by ``resolve_account_filter``).
|
|
1597
|
+
The native quota views read the projection, not the entry cache, so
|
|
1598
|
+
``needs_cache=False``.
|
|
1599
|
+
"""
|
|
1600
|
+
c = _cctally()
|
|
1601
|
+
account_key, acct_exit = c.resolve_account_filter(
|
|
1602
|
+
args, "codex", needs_cache=False)
|
|
1603
|
+
if acct_exit is not None:
|
|
1604
|
+
return None, acct_exit, histories
|
|
1605
|
+
if account_key is not None:
|
|
1606
|
+
histories = type(histories)(
|
|
1607
|
+
h for h in histories
|
|
1608
|
+
if getattr(h.identity, "account_key", _lib_accounts.UNATTRIBUTED)
|
|
1609
|
+
== account_key
|
|
1610
|
+
)
|
|
1611
|
+
return account_key, None, histories
|
|
1612
|
+
|
|
1613
|
+
|
|
1614
|
+
def _decorate_account(payload: dict[str, object], account_key: "str | None") -> dict:
|
|
1615
|
+
"""(#341 R8) Add ``accountKey``/``accountLabel`` to a quota payload under an
|
|
1616
|
+
explicit ``--account`` invocation; a no-flag render stays byte-identical."""
|
|
1617
|
+
if account_key is not None:
|
|
1618
|
+
payload.update(_cctally().account_json_fields(account_key))
|
|
1619
|
+
return payload
|
|
1620
|
+
|
|
1621
|
+
|
|
1531
1622
|
def _emit(args, payload: dict[str, object], text: str) -> int:
|
|
1532
1623
|
if getattr(args, "json", False):
|
|
1533
1624
|
print(json.dumps(stamp_schema_version(payload), ensure_ascii=False))
|
|
@@ -1547,6 +1638,9 @@ def cmd_codex_quota_history(args) -> int:
|
|
|
1547
1638
|
as_of, since, until, histories = _command_context(args, range_args=True)
|
|
1548
1639
|
except QuotaCLIError as exc:
|
|
1549
1640
|
return _command_error(exc)
|
|
1641
|
+
acct_key, acct_exit, histories = _resolve_account_and_scope(args, histories)
|
|
1642
|
+
if acct_exit is not None:
|
|
1643
|
+
return acct_exit
|
|
1550
1644
|
windows = []
|
|
1551
1645
|
text_rows = ["Codex quota history · local-rollout"]
|
|
1552
1646
|
for history in histories:
|
|
@@ -1572,6 +1666,7 @@ def cmd_codex_quota_history(args) -> int:
|
|
|
1572
1666
|
"source": "codex", "generatedAt": _iso_z(as_of),
|
|
1573
1667
|
"freshnessSource": "local-rollout", "windows": windows,
|
|
1574
1668
|
}
|
|
1669
|
+
_decorate_account(payload, acct_key) # #341 R8
|
|
1575
1670
|
if len(text_rows) == 1:
|
|
1576
1671
|
text_rows.append("No Codex quota history.")
|
|
1577
1672
|
return _emit(args, payload, "\n".join(text_rows))
|
|
@@ -1595,6 +1690,9 @@ def cmd_codex_quota_statusline(args) -> int:
|
|
|
1595
1690
|
as_of, _since, _until, histories = _command_context(args)
|
|
1596
1691
|
except QuotaCLIError as exc:
|
|
1597
1692
|
return _command_error(exc)
|
|
1693
|
+
acct_key, acct_exit, histories = _resolve_account_and_scope(args, histories)
|
|
1694
|
+
if acct_exit is not None:
|
|
1695
|
+
return acct_exit
|
|
1598
1696
|
windows = []
|
|
1599
1697
|
text_rows = []
|
|
1600
1698
|
for history in histories:
|
|
@@ -1622,6 +1720,7 @@ def cmd_codex_quota_statusline(args) -> int:
|
|
|
1622
1720
|
"source": "codex", "generatedAt": _iso_z(as_of),
|
|
1623
1721
|
"freshnessSource": "local-rollout", "windows": windows,
|
|
1624
1722
|
}
|
|
1723
|
+
_decorate_account(payload, acct_key) # #341 R8
|
|
1625
1724
|
return _emit(args, payload, "\n".join(text_rows) if text_rows else "Codex quota unavailable.")
|
|
1626
1725
|
|
|
1627
1726
|
|
|
@@ -1648,6 +1747,9 @@ def cmd_codex_quota_forecast(args) -> int:
|
|
|
1648
1747
|
as_of, _since, _until, histories = _command_context(args)
|
|
1649
1748
|
except QuotaCLIError as exc:
|
|
1650
1749
|
return _command_error(exc)
|
|
1750
|
+
acct_key, acct_exit, histories = _resolve_account_and_scope(args, histories)
|
|
1751
|
+
if acct_exit is not None:
|
|
1752
|
+
return acct_exit
|
|
1651
1753
|
forecasts = [_forecast_wire(history, as_of) for history in histories]
|
|
1652
1754
|
text_rows = ["Codex quota forecast · local-rollout"]
|
|
1653
1755
|
for history, forecast in zip(histories, forecasts):
|
|
@@ -1664,6 +1766,7 @@ def cmd_codex_quota_forecast(args) -> int:
|
|
|
1664
1766
|
"source": "codex", "generatedAt": _iso_z(as_of),
|
|
1665
1767
|
"freshnessSource": "local-rollout", "forecasts": forecasts,
|
|
1666
1768
|
}
|
|
1769
|
+
_decorate_account(payload, acct_key) # #341 R8
|
|
1667
1770
|
return _emit(args, payload, "\n".join(text_rows))
|
|
1668
1771
|
|
|
1669
1772
|
|
|
@@ -1673,6 +1776,9 @@ def cmd_codex_quota_blocks(args) -> int:
|
|
|
1673
1776
|
as_of, since, until, histories = _command_context(args, range_args=True)
|
|
1674
1777
|
except QuotaCLIError as exc:
|
|
1675
1778
|
return _command_error(exc)
|
|
1779
|
+
acct_key, acct_exit, histories = _resolve_account_and_scope(args, histories)
|
|
1780
|
+
if acct_exit is not None:
|
|
1781
|
+
return acct_exit
|
|
1676
1782
|
blocks = []
|
|
1677
1783
|
text_rows = ["Codex quota blocks · local-rollout"]
|
|
1678
1784
|
for block in build_blocks(
|
|
@@ -1698,6 +1804,7 @@ def cmd_codex_quota_blocks(args) -> int:
|
|
|
1698
1804
|
"source": "codex", "generatedAt": _iso_z(as_of),
|
|
1699
1805
|
"freshnessSource": "local-rollout", "blocks": blocks,
|
|
1700
1806
|
}
|
|
1807
|
+
_decorate_account(payload, acct_key) # #341 R8
|
|
1701
1808
|
if len(text_rows) == 1:
|
|
1702
1809
|
text_rows.append("No Codex quota blocks.")
|
|
1703
1810
|
return _emit(args, payload, "\n".join(text_rows))
|
|
@@ -1707,6 +1814,9 @@ def cmd_codex_quota_breakdown(args) -> int:
|
|
|
1707
1814
|
"""Render root-qualified, live-priced milestone deltas for one block."""
|
|
1708
1815
|
try:
|
|
1709
1816
|
as_of, _since, _until, histories = _command_context(args)
|
|
1817
|
+
acct_key, acct_exit, histories = _resolve_account_and_scope(args, histories)
|
|
1818
|
+
if acct_exit is not None:
|
|
1819
|
+
return acct_exit
|
|
1710
1820
|
if len(histories) != 1:
|
|
1711
1821
|
raise QuotaCLIError(
|
|
1712
1822
|
"breakdown requires selectors resolving to exactly one quota identity; candidates:\n"
|
|
@@ -1745,6 +1855,7 @@ def cmd_codex_quota_breakdown(args) -> int:
|
|
|
1745
1855
|
"block": {"resetAt": _iso_z(block.resets_at), "nominalStartAt": _iso_z(block.nominal_start_at)},
|
|
1746
1856
|
"speed": speed, "milestones": milestones,
|
|
1747
1857
|
}
|
|
1858
|
+
_decorate_account(payload, acct_key) # #341 R8
|
|
1748
1859
|
text_rows = [
|
|
1749
1860
|
f"Codex quota breakdown · {_identity_label(identity)}",
|
|
1750
1861
|
f"reset {_iso_z(block.resets_at)} · speed {speed}",
|