cctally 1.82.0 → 1.83.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 +70 -0
- package/README.md +52 -74
- package/bin/_cctally_alerts.py +8 -1
- package/bin/_cctally_cache.py +963 -149
- package/bin/_cctally_config.py +43 -4
- package/bin/_cctally_core.py +933 -759
- package/bin/_cctally_dashboard.py +157 -47
- package/bin/_cctally_dashboard_cache_report.py +13 -6
- package/bin/_cctally_dashboard_conversation.py +1 -0
- package/bin/_cctally_dashboard_envelope.py +186 -8
- package/bin/_cctally_dashboard_share.py +60 -20
- package/bin/_cctally_dashboard_sources.py +427 -128
- package/bin/_cctally_db.py +605 -128
- package/bin/_cctally_doctor.py +413 -28
- package/bin/_cctally_five_hour.py +12 -5
- package/bin/_cctally_journal.py +2050 -156
- package/bin/_cctally_journal_repair.py +519 -0
- package/bin/_cctally_milestone_history.py +142 -56
- package/bin/_cctally_milestones.py +179 -111
- package/bin/_cctally_parser.py +42 -0
- package/bin/_cctally_project.py +24 -18
- package/bin/_cctally_quota.py +139 -25
- package/bin/_cctally_record.py +279 -108
- package/bin/_cctally_rederive.py +1052 -0
- package/bin/_cctally_reporting.py +58 -53
- package/bin/_cctally_setup.py +1 -0
- package/bin/_cctally_source_analytics.py +4 -1
- package/bin/_cctally_statusline.py +11 -11
- package/bin/_cctally_store.py +1039 -31
- package/bin/_cctally_sync_week.py +17 -8
- package/bin/_cctally_tui.py +421 -54
- package/bin/_cctally_update.py +133 -8
- package/bin/_cctally_weekrefs.py +14 -0
- package/bin/_lib_aggregators.py +10 -6
- package/bin/_lib_cache_report.py +101 -9
- package/bin/_lib_codex_pools.py +82 -0
- package/bin/_lib_conversation_query.py +126 -33
- package/bin/_lib_dashboard_sources.py +126 -1
- package/bin/_lib_diff_kernel.py +28 -15
- package/bin/_lib_doctor.py +342 -4
- package/bin/_lib_journal.py +924 -2
- package/bin/_lib_jsonl.py +43 -14
- package/bin/_lib_pricing.py +140 -21
- package/bin/_lib_readme_refresh.py +401 -0
- package/bin/_lib_rederive.py +395 -0
- package/bin/_lib_share.py +58 -2
- package/bin/cctally +56 -8
- package/dashboard/static/assets/{index-DJP4gEB7.js → index-3bgCMVHb.js} +52 -52
- package/dashboard/static/assets/index-D27EIHEI.css +1 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +6 -1
- package/dashboard/static/assets/index-Dk1nplOz.css +0 -1
package/bin/_cctally_quota.py
CHANGED
|
@@ -43,7 +43,12 @@ from _lib_quota import (
|
|
|
43
43
|
source_path_key,
|
|
44
44
|
)
|
|
45
45
|
from _lib_json_envelope import stamp_schema_version
|
|
46
|
-
from _lib_jsonl import _codex_logical_limit_key
|
|
46
|
+
from _lib_jsonl import _codex_logical_limit_key
|
|
47
|
+
from _lib_codex_pools import (
|
|
48
|
+
codex_history_is_model_scoped,
|
|
49
|
+
codex_model_scoped_quota_pool,
|
|
50
|
+
is_model_scoped_codex_quota,
|
|
51
|
+
)
|
|
47
52
|
|
|
48
53
|
|
|
49
54
|
UTC = dt.timezone.utc
|
|
@@ -748,7 +753,7 @@ def _quota_alert_payload(
|
|
|
748
753
|
def _evaluate_quota_alerts(
|
|
749
754
|
conn: sqlite3.Connection,
|
|
750
755
|
*, observations: tuple[QuotaObservation, ...], alert_eligible_roots: set[str],
|
|
751
|
-
now: dt.datetime, now_iso: str, journal_emit=None,
|
|
756
|
+
now: dt.datetime, now_iso: str, journal_emit=None, journal_disarm=None,
|
|
752
757
|
) -> list[dict]:
|
|
753
758
|
"""Arm or claim quota alerts within the caller's stats transaction.
|
|
754
759
|
|
|
@@ -756,18 +761,42 @@ def _evaluate_quota_alerts(
|
|
|
756
761
|
eligible observations can claim an alerted row. No non-terminal state is
|
|
757
762
|
stored: the arming boundary plus unique terminal event key is sufficient.
|
|
758
763
|
"""
|
|
759
|
-
if not alert_eligible_roots:
|
|
760
|
-
return []
|
|
761
764
|
global_enabled, quota_enabled, rules, config = _quota_alert_config()
|
|
762
765
|
# Disabled delivery is entirely inert: it must not leave an arming
|
|
763
766
|
# boundary that could turn disabled-period evidence into a later alert.
|
|
767
|
+
# This gate deliberately precedes the lifecycle-eligibility fast path:
|
|
768
|
+
# read-only report reconciles carry no eligible roots, but they must still
|
|
769
|
+
# durably disarm state after either user-facing switch is turned off.
|
|
764
770
|
if not (global_enabled and quota_enabled):
|
|
765
|
-
|
|
771
|
+
# Scratch rebuild/rederive replay has no alert sink and must preserve
|
|
772
|
+
# the provider-owned state selected from the journal verbatim. Only the
|
|
773
|
+
# live single-flight reconcile supplies the durable disarm emitter.
|
|
774
|
+
if journal_disarm is None:
|
|
775
|
+
return []
|
|
776
|
+
rows = conn.execute(
|
|
777
|
+
"""SELECT source, source_root_key, account_key,
|
|
778
|
+
logical_limit_key, observed_slot, window_minutes
|
|
779
|
+
FROM quota_alert_arming
|
|
780
|
+
WHERE source='codex'"""
|
|
781
|
+
).fetchall()
|
|
766
782
|
conn.execute(
|
|
767
|
-
|
|
768
|
-
WHERE source='codex' AND source_root_key IN ({placeholders})""",
|
|
769
|
-
tuple(sorted(alert_eligible_roots)),
|
|
783
|
+
"DELETE FROM quota_alert_arming WHERE source='codex'"
|
|
770
784
|
)
|
|
785
|
+
if journal_disarm is not None:
|
|
786
|
+
for row in rows:
|
|
787
|
+
journal_disarm(
|
|
788
|
+
QuotaWindowIdentity(
|
|
789
|
+
source=str(row["source"]),
|
|
790
|
+
source_root_key=str(row["source_root_key"]),
|
|
791
|
+
account_key=str(row["account_key"]),
|
|
792
|
+
logical_limit_key=str(row["logical_limit_key"]),
|
|
793
|
+
observed_slot=str(row["observed_slot"]),
|
|
794
|
+
window_minutes=int(row["window_minutes"]),
|
|
795
|
+
),
|
|
796
|
+
now_iso,
|
|
797
|
+
)
|
|
798
|
+
return []
|
|
799
|
+
if not alert_eligible_roots:
|
|
771
800
|
return []
|
|
772
801
|
histories = build_history(observations)
|
|
773
802
|
queued: list[dict] = []
|
|
@@ -858,7 +887,8 @@ def _evaluate_quota_alerts(
|
|
|
858
887
|
|
|
859
888
|
def _apply_quota_projection_rows(
|
|
860
889
|
conn, *, observations, active_roots, now, now_iso,
|
|
861
|
-
sink, alert_eligible_roots, journal_emit=None,
|
|
890
|
+
sink, alert_eligible_roots, journal_emit=None, journal_disarm=None,
|
|
891
|
+
holder=None,
|
|
862
892
|
):
|
|
863
893
|
"""Transaction-neutral quota projection apply (spec §5.3 "projection").
|
|
864
894
|
|
|
@@ -891,6 +921,7 @@ def _apply_quota_projection_rows(
|
|
|
891
921
|
conn, observations=observations,
|
|
892
922
|
alert_eligible_roots=alert_eligible_roots & active_roots,
|
|
893
923
|
now=now, now_iso=now_iso, journal_emit=journal_emit,
|
|
924
|
+
journal_disarm=journal_disarm,
|
|
894
925
|
)
|
|
895
926
|
# The completion stamp is intentionally the final DML in the stats
|
|
896
927
|
# transaction. A pre-commit failure rolls all projection updates back;
|
|
@@ -1001,6 +1032,10 @@ def reconcile_codex_quota_projection(
|
|
|
1001
1032
|
now_iso = _utc_iso(now)
|
|
1002
1033
|
|
|
1003
1034
|
alert_eligible_roots = {str(key) for key in alert_eligible_root_keys}
|
|
1035
|
+
global_alerts_enabled, quota_alerts_enabled, _rules, _config = (
|
|
1036
|
+
_quota_alert_config()
|
|
1037
|
+
)
|
|
1038
|
+
delivery_enabled = global_alerts_enabled and quota_alerts_enabled
|
|
1004
1039
|
|
|
1005
1040
|
try:
|
|
1006
1041
|
cache = _cache_connection()
|
|
@@ -1031,8 +1066,16 @@ def reconcile_codex_quota_projection(
|
|
|
1031
1066
|
if physical_sequence == cert_seq and active_roots <= set(cert_sigs):
|
|
1032
1067
|
stats_conn = _cctally_core.open_db()
|
|
1033
1068
|
try:
|
|
1034
|
-
|
|
1035
|
-
|
|
1069
|
+
has_arming = bool(stats_conn.execute(
|
|
1070
|
+
"SELECT 1 FROM quota_alert_arming "
|
|
1071
|
+
"WHERE source='codex' LIMIT 1"
|
|
1072
|
+
).fetchone())
|
|
1073
|
+
can_skip_delivery = delivery_enabled or not has_arming
|
|
1074
|
+
if (
|
|
1075
|
+
can_skip_delivery
|
|
1076
|
+
and _stats_projection_signatures_match(
|
|
1077
|
+
stats_conn, active_roots, cert_sigs
|
|
1078
|
+
)
|
|
1036
1079
|
):
|
|
1037
1080
|
return QuotaProjectionResult(None, 0, 0, 0, 0, 0, 0)
|
|
1038
1081
|
finally:
|
|
@@ -1058,7 +1101,9 @@ def reconcile_codex_quota_projection(
|
|
|
1058
1101
|
"signatures": None,
|
|
1059
1102
|
}
|
|
1060
1103
|
|
|
1061
|
-
def _apply_projection(
|
|
1104
|
+
def _apply_projection(
|
|
1105
|
+
conn, sink, *, journal_emit=None, journal_disarm=None,
|
|
1106
|
+
):
|
|
1062
1107
|
# No configured roots and no existing interpreted history means there is
|
|
1063
1108
|
# no stats work. This preserves the existing empty-Codex fast path.
|
|
1064
1109
|
# Delegates to the shared module-level apply so the live leg and the
|
|
@@ -1067,7 +1112,8 @@ def reconcile_codex_quota_projection(
|
|
|
1067
1112
|
conn, observations=observations, active_roots=active_roots,
|
|
1068
1113
|
now=now, now_iso=now_iso, sink=sink,
|
|
1069
1114
|
alert_eligible_roots=alert_eligible_roots,
|
|
1070
|
-
journal_emit=journal_emit,
|
|
1115
|
+
journal_emit=journal_emit, journal_disarm=journal_disarm,
|
|
1116
|
+
holder=holder,
|
|
1071
1117
|
)
|
|
1072
1118
|
|
|
1073
1119
|
import _cctally_journal as _jr
|
|
@@ -1076,15 +1122,16 @@ def reconcile_codex_quota_projection(
|
|
|
1076
1122
|
def _codex_leg(ctx):
|
|
1077
1123
|
def _emit_arming(identity, fingerprint, activated_at):
|
|
1078
1124
|
# Item 5: journal the arming state change (`quota_alert_arming` evt).
|
|
1079
|
-
# account_key (#341) is part of the qaa
|
|
1080
|
-
#
|
|
1081
|
-
#
|
|
1082
|
-
#
|
|
1083
|
-
#
|
|
1125
|
+
# account_key (#341) is part of the qaa state identity (after the
|
|
1126
|
+
# root), while fingerprint + activation boundary make each real
|
|
1127
|
+
# state transition a distinct rev-0 event (#372). Exact re-emission
|
|
1128
|
+
# of one state still converges on one id. This order MUST match the
|
|
1129
|
+
# cutover export's natural_key_id.
|
|
1084
1130
|
eid = _jl.evt_id(
|
|
1085
1131
|
"qaa", identity.source, identity.source_root_key,
|
|
1086
1132
|
identity.account_key, identity.logical_limit_key,
|
|
1087
1133
|
identity.observed_slot, identity.window_minutes,
|
|
1134
|
+
fingerprint, activated_at,
|
|
1088
1135
|
)
|
|
1089
1136
|
_jr.append_record(_jl.make_evt(
|
|
1090
1137
|
kind="quota_alert_arming", id=eid, at=activated_at,
|
|
@@ -1097,11 +1144,40 @@ def reconcile_codex_quota_projection(
|
|
|
1097
1144
|
"window_minutes": identity.window_minutes,
|
|
1098
1145
|
"rule_fingerprint": fingerprint,
|
|
1099
1146
|
"activated_at_utc": activated_at,
|
|
1147
|
+
"journal_identity_version": 2,
|
|
1148
|
+
},
|
|
1149
|
+
))
|
|
1150
|
+
ctx.events_emitted += 1
|
|
1151
|
+
|
|
1152
|
+
def _emit_disarm(identity, disarmed_at):
|
|
1153
|
+
eid = _jl.evt_id(
|
|
1154
|
+
"qaa", identity.source, identity.source_root_key,
|
|
1155
|
+
identity.account_key, identity.logical_limit_key,
|
|
1156
|
+
identity.observed_slot, identity.window_minutes,
|
|
1157
|
+
"disarmed", disarmed_at,
|
|
1158
|
+
)
|
|
1159
|
+
_jr.append_record(_jl.make_evt(
|
|
1160
|
+
kind="quota_alert_arming", id=eid, at=disarmed_at,
|
|
1161
|
+
payload={
|
|
1162
|
+
"source": identity.source,
|
|
1163
|
+
"source_root_key": identity.source_root_key,
|
|
1164
|
+
"account_key": identity.account_key,
|
|
1165
|
+
"logical_limit_key": identity.logical_limit_key,
|
|
1166
|
+
"observed_slot": identity.observed_slot,
|
|
1167
|
+
"window_minutes": identity.window_minutes,
|
|
1168
|
+
"state": "disarmed",
|
|
1169
|
+
"disarmed_at_utc": disarmed_at,
|
|
1170
|
+
"journal_identity_version": 2,
|
|
1100
1171
|
},
|
|
1101
1172
|
))
|
|
1102
1173
|
ctx.events_emitted += 1
|
|
1103
1174
|
|
|
1104
|
-
_apply_projection(
|
|
1175
|
+
_apply_projection(
|
|
1176
|
+
ctx.conn,
|
|
1177
|
+
ctx.pending_alerts,
|
|
1178
|
+
journal_emit=_emit_arming,
|
|
1179
|
+
journal_disarm=_emit_disarm,
|
|
1180
|
+
)
|
|
1105
1181
|
# `_before_stats_commit` fires INSIDE the cycle txn, before COMMIT — a
|
|
1106
1182
|
# raise rolls the whole cycle back, so the projection updates undo
|
|
1107
1183
|
# together (the crash-consistency contract the callers test).
|
|
@@ -1321,7 +1397,19 @@ def codex_five_hour_percent_at_crossing(
|
|
|
1321
1397
|
observations: Iterable[QuotaObservation] | None = None,
|
|
1322
1398
|
*, cache_conn: sqlite3.Connection | None = None,
|
|
1323
1399
|
) -> float | None:
|
|
1324
|
-
"""Return the latest matching native five-hour percent at one crossing.
|
|
1400
|
+
"""Return the latest matching native five-hour percent at one crossing.
|
|
1401
|
+
|
|
1402
|
+
#373: the 5h window must share the target identity's MODEL SCOPE, not just
|
|
1403
|
+
its ``limit_id``. A separate model pool can reuse ``limit_id="codex"`` and
|
|
1404
|
+
spell itself only in the interpreted key's ``modelPool`` or in
|
|
1405
|
+
``limit_name``, so the pre-existing ``limit_id`` equality lets it through
|
|
1406
|
+
and a standard weekly crossing gets annotated with a foreign pool's 5h
|
|
1407
|
+
percent. The rule is symmetric: a Spark weekly still correlates with Spark
|
|
1408
|
+
5h rows, and never with standard ones.
|
|
1409
|
+
"""
|
|
1410
|
+
target_model_scoped = is_model_scoped_codex_quota(
|
|
1411
|
+
identity.logical_limit_key, identity.limit_name,
|
|
1412
|
+
)
|
|
1325
1413
|
if observations is not None:
|
|
1326
1414
|
eligible = tuple(
|
|
1327
1415
|
observation for observation in observations
|
|
@@ -1329,6 +1417,10 @@ def codex_five_hour_percent_at_crossing(
|
|
|
1329
1417
|
and observation.identity.window_minutes == 300
|
|
1330
1418
|
and observation.identity.observed_slot == identity.observed_slot
|
|
1331
1419
|
and observation.identity.limit_id == identity.limit_id
|
|
1420
|
+
and is_model_scoped_codex_quota(
|
|
1421
|
+
observation.identity.logical_limit_key,
|
|
1422
|
+
observation.identity.limit_name,
|
|
1423
|
+
) == target_model_scoped
|
|
1332
1424
|
and observation.captured_at <= captured_at < observation.resets_at
|
|
1333
1425
|
)
|
|
1334
1426
|
if not eligible:
|
|
@@ -1356,7 +1448,7 @@ def codex_five_hour_percent_at_crossing(
|
|
|
1356
1448
|
try:
|
|
1357
1449
|
rows = cache.execute(
|
|
1358
1450
|
"""SELECT captured_at_utc, resets_at_utc, source_path, line_offset,
|
|
1359
|
-
used_percent
|
|
1451
|
+
used_percent, logical_limit_key, limit_name
|
|
1360
1452
|
FROM quota_window_snapshots
|
|
1361
1453
|
WHERE source='codex' AND source_root_key=?
|
|
1362
1454
|
AND window_minutes=300 AND observed_slot=? AND limit_id IS ?
|
|
@@ -1378,6 +1470,10 @@ def codex_five_hour_percent_at_crossing(
|
|
|
1378
1470
|
used_percent = float(row[4])
|
|
1379
1471
|
except (TypeError, ValueError, OverflowError):
|
|
1380
1472
|
continue
|
|
1473
|
+
# Same model-scope rule as the in-memory branch above; the two must not
|
|
1474
|
+
# disagree about which 5h window belongs to this identity's pool.
|
|
1475
|
+
if is_model_scoped_codex_quota(row[5], row[6]) != target_model_scoped:
|
|
1476
|
+
continue
|
|
1381
1477
|
if observed_at <= captured_at < resets_at:
|
|
1382
1478
|
eligible.append((physical, used_percent))
|
|
1383
1479
|
if not eligible:
|
|
@@ -1544,7 +1640,9 @@ def _sync_and_load(
|
|
|
1544
1640
|
try:
|
|
1545
1641
|
cache_mod = c._load_sibling("_cctally_cache")
|
|
1546
1642
|
_, cache = cache_mod._run_cache_operation_with_recovery(
|
|
1547
|
-
cache,
|
|
1643
|
+
cache,
|
|
1644
|
+
lambda active_conn: c.sync_codex_cache(active_conn),
|
|
1645
|
+
origin="codex.quota.sync",
|
|
1548
1646
|
)
|
|
1549
1647
|
finally:
|
|
1550
1648
|
cache.close()
|
|
@@ -1890,12 +1988,28 @@ def cmd_codex_percent_breakdown(args) -> int:
|
|
|
1890
1988
|
if block.resets_at == reset_at
|
|
1891
1989
|
)
|
|
1892
1990
|
else:
|
|
1991
|
+
# #373: a separate model pool is not account-level standard quota,
|
|
1992
|
+
# so it must not make the DEFAULT (no-selector) command ambiguous.
|
|
1993
|
+
# Scoped to this branch only — `--reset-at` still reaches a foreign
|
|
1994
|
+
# pool's retained history — and it falls back to the unfiltered set
|
|
1995
|
+
# when no standard candidate survives, so a lone foreign pool (or an
|
|
1996
|
+
# explicit `--limit-key` naming one) still renders rather than
|
|
1997
|
+
# turning into an exit-2 for a window the user genuinely has.
|
|
1998
|
+
# The baseline is resolved first because it is the §7.1 label
|
|
1999
|
+
# authority for classification.
|
|
2000
|
+
candidates = tuple(
|
|
2001
|
+
(history, select_baseline(history.observations, as_of))
|
|
2002
|
+
for history in histories
|
|
2003
|
+
)
|
|
2004
|
+
standard = tuple(
|
|
2005
|
+
(history, baseline) for history, baseline in candidates
|
|
2006
|
+
if not codex_history_is_model_scoped(history, baseline=baseline)
|
|
2007
|
+
)
|
|
1893
2008
|
matching = tuple(
|
|
1894
2009
|
(history, block)
|
|
1895
|
-
for history in
|
|
2010
|
+
for history, baseline in (standard or candidates)
|
|
1896
2011
|
if (
|
|
1897
|
-
|
|
1898
|
-
is not None
|
|
2012
|
+
baseline is not None
|
|
1899
2013
|
and baseline.resets_at > as_of
|
|
1900
2014
|
and quota_freshness(
|
|
1901
2015
|
history.physical_observations, as_of,
|