cctally 1.28.0 → 1.30.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 +30 -0
- package/bin/_cctally_cache.py +147 -59
- package/bin/_cctally_core.py +22 -49
- package/bin/_cctally_dashboard.py +239 -152
- package/bin/_cctally_db.py +211 -31
- package/bin/_cctally_milestones.py +126 -166
- package/bin/_cctally_record.py +161 -192
- package/bin/_lib_alert_axes.py +7 -4
- package/bin/_lib_conversation.py +59 -8
- package/bin/_lib_conversation_query.py +306 -52
- package/bin/_lib_jsonl.py +69 -50
- package/bin/cctally +5 -5
- package/dashboard/static/assets/index-4OxMhN7N.js +53 -0
- package/dashboard/static/assets/index-DEDO-eqP.css +1 -0
- package/dashboard/static/assets/newsreader-latin-400-italic-CEihAR-f.woff2 +0 -0
- package/dashboard/static/assets/newsreader-latin-400-italic-CNZoH1hn.woff +0 -0
- package/dashboard/static/assets/newsreader-latin-400-normal-BFBkh4jY.woff2 +0 -0
- package/dashboard/static/assets/newsreader-latin-400-normal-gRTjlS2D.woff +0 -0
- package/dashboard/static/assets/newsreader-latin-500-normal-B66TYsaK.woff2 +0 -0
- package/dashboard/static/assets/newsreader-latin-500-normal-DFwuUcdu.woff +0 -0
- package/dashboard/static/assets/newsreader-latin-600-normal-30OJ_TG_.woff2 +0 -0
- package/dashboard/static/assets/newsreader-latin-600-normal-DUnT2r2g.woff +0 -0
- package/dashboard/static/assets/newsreader-latin-ext-400-italic-BMTE_bNQ.woff2 +0 -0
- package/dashboard/static/assets/newsreader-latin-ext-400-italic-qdgKLcPG.woff +0 -0
- package/dashboard/static/assets/newsreader-latin-ext-400-normal-DYA1XoQK.woff +0 -0
- package/dashboard/static/assets/newsreader-latin-ext-400-normal-svq1FPys.woff2 +0 -0
- package/dashboard/static/assets/newsreader-latin-ext-500-normal-BNHmvKvI.woff2 +0 -0
- package/dashboard/static/assets/newsreader-latin-ext-500-normal-CZruMFou.woff +0 -0
- package/dashboard/static/assets/newsreader-latin-ext-600-normal-BXv5iMHi.woff2 +0 -0
- package/dashboard/static/assets/newsreader-latin-ext-600-normal-BrbfzHZ5.woff +0 -0
- package/dashboard/static/assets/newsreader-vietnamese-400-italic-QbB8kb5s.woff +0 -0
- package/dashboard/static/assets/newsreader-vietnamese-400-italic-bZegYFuM.woff2 +0 -0
- package/dashboard/static/assets/newsreader-vietnamese-400-normal-BekUZro8.woff +0 -0
- package/dashboard/static/assets/newsreader-vietnamese-400-normal-DdKr49mV.woff2 +0 -0
- package/dashboard/static/assets/newsreader-vietnamese-500-normal-BEAbKU8A.woff +0 -0
- package/dashboard/static/assets/newsreader-vietnamese-500-normal-CL6a8tp2.woff2 +0 -0
- package/dashboard/static/assets/newsreader-vietnamese-600-normal-CVAR0otO.woff +0 -0
- package/dashboard/static/assets/newsreader-vietnamese-600-normal-CaH84vfx.woff2 +0 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +1 -1
- package/dashboard/static/assets/index-Bj5ckRUE.css +0 -1
- package/dashboard/static/assets/index-Dw4G5FD9.js +0 -18
package/bin/_cctally_record.py
CHANGED
|
@@ -273,20 +273,20 @@ def _build_alert_payload_codex_budget(*args, **kwargs):
|
|
|
273
273
|
return sys.modules["cctally"]._build_alert_payload_codex_budget(*args, **kwargs)
|
|
274
274
|
|
|
275
275
|
|
|
276
|
-
def
|
|
277
|
-
return sys.modules["cctally"].
|
|
276
|
+
def _budget_crossings(*args, **kwargs):
|
|
277
|
+
return sys.modules["cctally"]._budget_crossings(*args, **kwargs)
|
|
278
278
|
|
|
279
279
|
|
|
280
|
-
def
|
|
281
|
-
return sys.modules["cctally"].
|
|
280
|
+
def _resolve_budget_window(*args, **kwargs):
|
|
281
|
+
return sys.modules["cctally"]._resolve_budget_window(*args, **kwargs)
|
|
282
282
|
|
|
283
283
|
|
|
284
|
-
def
|
|
285
|
-
return sys.modules["cctally"].
|
|
284
|
+
def _budget_spend_for_vendor(*args, **kwargs):
|
|
285
|
+
return sys.modules["cctally"]._budget_spend_for_vendor(*args, **kwargs)
|
|
286
286
|
|
|
287
287
|
|
|
288
|
-
def
|
|
289
|
-
return sys.modules["cctally"].
|
|
288
|
+
def _resolve_codex_budget_period_window(*args, **kwargs):
|
|
289
|
+
return sys.modules["cctally"]._resolve_codex_budget_period_window(*args, **kwargs)
|
|
290
290
|
|
|
291
291
|
|
|
292
292
|
def resolve_display_tz(*args, **kwargs):
|
|
@@ -325,14 +325,6 @@ def _resolve_claude_budget_window(*args, **kwargs):
|
|
|
325
325
|
return sys.modules["cctally"]._resolve_claude_budget_window(*args, **kwargs)
|
|
326
326
|
|
|
327
327
|
|
|
328
|
-
def _sum_cost_for_range(*args, **kwargs):
|
|
329
|
-
return sys.modules["cctally"]._sum_cost_for_range(*args, **kwargs)
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
def insert_budget_milestone(*args, **kwargs):
|
|
333
|
-
return sys.modules["cctally"].insert_budget_milestone(*args, **kwargs)
|
|
334
|
-
|
|
335
|
-
|
|
336
328
|
def insert_projected_milestone(*args, **kwargs):
|
|
337
329
|
return sys.modules["cctally"].insert_projected_milestone(*args, **kwargs)
|
|
338
330
|
|
|
@@ -756,10 +748,103 @@ def maybe_record_milestone(
|
|
|
756
748
|
conn.close()
|
|
757
749
|
|
|
758
750
|
|
|
751
|
+
def _record_budget_milestone_for_vendor(
|
|
752
|
+
*, vendor, target, thresholds, period, config, tz, build_payload
|
|
753
|
+
) -> None:
|
|
754
|
+
"""Shared budget-milestone firing core for both vendors (#143).
|
|
755
|
+
|
|
756
|
+
Hot-path ordering is preserved verbatim (spec §4.2 / [Pre-probe before
|
|
757
|
+
sync_cache]): ``open_db`` → cheap ``_resolve_budget_window(vendor=…)`` →
|
|
758
|
+
unified pre-probe (which configured thresholds are STILL un-recorded for this
|
|
759
|
+
window/period) → **skip the cost SUM entirely when nothing is pending** →
|
|
760
|
+
``_budget_spend_for_vendor(vendor=…)`` (the costly leg) →
|
|
761
|
+
``_budget_crossings(vendor=…)`` (INSERT-and-arm, set-then-dispatch,
|
|
762
|
+
fire-once via rowcount) → single durable commit → post-commit dispatch.
|
|
763
|
+
|
|
764
|
+
The pre-probe's ``period = ? OR period IS NULL`` arm (#137) makes a pre-011
|
|
765
|
+
NULL-period row for this window count as already-recorded (no spurious
|
|
766
|
+
upgrade re-fire); a row under the SAME concrete ``period`` also counts
|
|
767
|
+
(fire-once). The cost SUM is skipped ONLY when every threshold already has a
|
|
768
|
+
row — a partial prior run still forces the SUM for the remaining thresholds
|
|
769
|
+
([Dedup mustn't gate side effects]).
|
|
770
|
+
|
|
771
|
+
``build_payload`` is the vendor's at-fire payload adapter (keeps the dispatch
|
|
772
|
+
``id`` byte-stable per vendor); it is invoked with
|
|
773
|
+
``threshold`` / ``crossed_at_utc`` / ``period_key`` / ``period`` /
|
|
774
|
+
``budget_usd`` / ``spent_usd`` / ``consumption_pct`` keyword args.
|
|
775
|
+
"""
|
|
776
|
+
now_utc = _command_as_of()
|
|
777
|
+
pending_alerts: list[dict[str, Any]] = []
|
|
778
|
+
conn = open_db()
|
|
779
|
+
try:
|
|
780
|
+
start_at = _resolve_budget_window(
|
|
781
|
+
conn, vendor=vendor, now_utc=now_utc, period=period,
|
|
782
|
+
config=config, tz=tz,
|
|
783
|
+
)
|
|
784
|
+
if start_at is None:
|
|
785
|
+
return # no resolvable window yet (claude subscription-week pre-snapshot)
|
|
786
|
+
period_key = start_at.isoformat(timespec="seconds")
|
|
787
|
+
|
|
788
|
+
present = {
|
|
789
|
+
int(r[0]) for r in conn.execute(
|
|
790
|
+
"SELECT threshold FROM budget_milestones "
|
|
791
|
+
"WHERE vendor = ? AND period_start_at = ? "
|
|
792
|
+
" AND (period = ? OR period IS NULL)",
|
|
793
|
+
(vendor, period_key, period),
|
|
794
|
+
)
|
|
795
|
+
}
|
|
796
|
+
pending = [t for t in sorted(thresholds) if t not in present]
|
|
797
|
+
if not pending:
|
|
798
|
+
return # nothing left this window → skip the cost SUM
|
|
799
|
+
|
|
800
|
+
spent = _budget_spend_for_vendor(
|
|
801
|
+
conn, vendor=vendor, start_at=start_at, now_utc=now_utc
|
|
802
|
+
)
|
|
803
|
+
# Shared INSERT-and-arm core (set-then-dispatch, fire-once via rowcount);
|
|
804
|
+
# commit=False inside, so this conn owns the single durable commit below.
|
|
805
|
+
for t, crossed_at, sp, tg, pct in _budget_crossings(
|
|
806
|
+
conn,
|
|
807
|
+
vendor=vendor,
|
|
808
|
+
period_key=period_key,
|
|
809
|
+
period=period,
|
|
810
|
+
thresholds=pending,
|
|
811
|
+
target=target,
|
|
812
|
+
spent=spent,
|
|
813
|
+
now_utc=now_utc,
|
|
814
|
+
):
|
|
815
|
+
pending_alerts.append(build_payload(
|
|
816
|
+
threshold=t,
|
|
817
|
+
crossed_at_utc=crossed_at,
|
|
818
|
+
period_key=period_key,
|
|
819
|
+
period=period,
|
|
820
|
+
budget_usd=tg,
|
|
821
|
+
spent_usd=sp,
|
|
822
|
+
consumption_pct=pct,
|
|
823
|
+
))
|
|
824
|
+
# Single commit: every INSERT + its alerted_at marker durable together.
|
|
825
|
+
conn.commit()
|
|
826
|
+
except Exception as exc:
|
|
827
|
+
eprint(f"[budget-milestone:{vendor}] error recording budget milestone: {exc}")
|
|
828
|
+
finally:
|
|
829
|
+
conn.close()
|
|
830
|
+
|
|
831
|
+
# Dispatch AFTER commit; a dispatch failure NEVER rolls back the milestone
|
|
832
|
+
# (set-then-dispatch invariant — one queue attempt per crossing, deduped on
|
|
833
|
+
# the alerted_at column).
|
|
834
|
+
for payload in pending_alerts:
|
|
835
|
+
try:
|
|
836
|
+
_dispatch_alert_notification(payload, mode="real")
|
|
837
|
+
except Exception as dispatch_exc:
|
|
838
|
+
eprint(f"[budget-alerts:{vendor}] dispatch failed: {dispatch_exc}")
|
|
839
|
+
|
|
840
|
+
|
|
759
841
|
def maybe_record_budget_milestone(saved: dict[str, Any]) -> None:
|
|
760
|
-
"""Fire equiv-$ budget alerts on ACTUAL-spend threshold crossings
|
|
761
|
-
(
|
|
762
|
-
5h-% milestone helpers).
|
|
842
|
+
"""Fire Claude equiv-$ budget alerts on ACTUAL-spend threshold crossings
|
|
843
|
+
(axis ``budget`` — called from ``cmd_record_usage`` alongside the weekly-% /
|
|
844
|
+
5h-% milestone helpers). Thin vendor adapter over
|
|
845
|
+
:func:`_record_budget_milestone_for_vendor` (#143): reads the Claude budget
|
|
846
|
+
config block, gates, resolves ``target`` / ``thresholds`` / ``period``, and
|
|
847
|
+
passes the Claude payload builder. Gated, hot-path-cheap, set-then-dispatch,
|
|
763
848
|
fire-once. Errors are logged, not raised (the caller also wraps).
|
|
764
849
|
|
|
765
850
|
``saved`` is accepted for call-site symmetry with
|
|
@@ -782,109 +867,35 @@ def maybe_record_budget_milestone(saved: dict[str, Any]) -> None:
|
|
|
782
867
|
return
|
|
783
868
|
if not _budget_alerts_active(budget_cfg):
|
|
784
869
|
return
|
|
785
|
-
target = budget_cfg["weekly_usd"]
|
|
786
870
|
thresholds = budget_cfg["alert_thresholds"]
|
|
787
871
|
if not thresholds:
|
|
788
872
|
return
|
|
789
873
|
# Period generalization (spec §6): subscription-week resolves the snapshot-
|
|
790
874
|
# anchored window; a calendar period (calendar-week / calendar-month)
|
|
791
|
-
# resolves the window purely from `now` + the period
|
|
792
|
-
#
|
|
793
|
-
# misnomer). config/tz are resolved once for the calendar branch.
|
|
875
|
+
# resolves the window purely from `now` + the period. config/tz are
|
|
876
|
+
# resolved once for the calendar branch.
|
|
794
877
|
period = budget_cfg.get("period", "subscription-week")
|
|
795
|
-
|
|
796
878
|
tz = resolve_display_tz(argparse.Namespace(tz=None), config)
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
# re-fires a spurious alert against a historical crossing; a row stored
|
|
818
|
-
# under the SAME concrete `period` also counts (fire-once).
|
|
819
|
-
present = {
|
|
820
|
-
int(r[0]) for r in conn.execute(
|
|
821
|
-
"SELECT threshold FROM budget_milestones "
|
|
822
|
-
"WHERE week_start_at = ? AND (period = ? OR period IS NULL)",
|
|
823
|
-
(week_key, period),
|
|
824
|
-
)
|
|
825
|
-
}
|
|
826
|
-
pending = [t for t in sorted(thresholds) if t not in present]
|
|
827
|
-
if not pending:
|
|
828
|
-
return # nothing left to cross this week → skip the cost SUM
|
|
829
|
-
|
|
830
|
-
spent = _sum_cost_for_range(week_start_at, now_utc, mode="auto")
|
|
831
|
-
# target > 0 is guaranteed by _get_budget_config (weekly_usd None is
|
|
832
|
-
# excluded by _budget_alerts_active above); the else is belt-and-suspenders.
|
|
833
|
-
consumption_pct = (spent / target * 100.0) if target > 0 else 0.0
|
|
834
|
-
for t in pending:
|
|
835
|
-
# +1e-9 snap-up: spent/target*100 can land one ULP below an
|
|
836
|
-
# integer threshold (CLAUDE.md float-floor gotcha).
|
|
837
|
-
if consumption_pct + 1e-9 >= t:
|
|
838
|
-
inserted = insert_budget_milestone(
|
|
839
|
-
conn,
|
|
840
|
-
week_start_at=week_key,
|
|
841
|
-
period=period,
|
|
842
|
-
threshold=t,
|
|
843
|
-
budget_usd=target,
|
|
844
|
-
spent_usd=spent,
|
|
845
|
-
consumption_pct=consumption_pct,
|
|
846
|
-
commit=False,
|
|
847
|
-
)
|
|
848
|
-
# Only the genuine-new-crossing winner (rowcount==1) dispatches;
|
|
849
|
-
# a racing record-usage instance gets rowcount==0 and skips.
|
|
850
|
-
if inserted == 1:
|
|
851
|
-
crossed_at = now_utc_iso()
|
|
852
|
-
# set-then-dispatch: alerted_at lands on the row BEFORE
|
|
853
|
-
# the osascript Popen, sharing this transaction with the
|
|
854
|
-
# INSERT (commit=False) so a crash between them is
|
|
855
|
-
# impossible. The UPDATE keys on the CONCRETE `period` (#137)
|
|
856
|
-
# — never a NULL-period sibling. `alerted_at IS NULL` guard
|
|
857
|
-
# is write-once defense-in-depth.
|
|
858
|
-
conn.execute(
|
|
859
|
-
"UPDATE budget_milestones SET alerted_at = ? "
|
|
860
|
-
"WHERE week_start_at = ? AND period = ? AND threshold = ? "
|
|
861
|
-
" AND alerted_at IS NULL",
|
|
862
|
-
(crossed_at, week_key, period, t),
|
|
863
|
-
)
|
|
864
|
-
pending_alerts.append(_build_alert_payload_budget(
|
|
865
|
-
threshold=t,
|
|
866
|
-
crossed_at_utc=crossed_at,
|
|
867
|
-
week_start_at=week_key,
|
|
868
|
-
budget_usd=target,
|
|
869
|
-
spent_usd=spent,
|
|
870
|
-
consumption_pct=consumption_pct,
|
|
871
|
-
period=period,
|
|
872
|
-
))
|
|
873
|
-
# Single commit: every INSERT + its alerted_at marker durable together.
|
|
874
|
-
conn.commit()
|
|
875
|
-
except Exception as exc:
|
|
876
|
-
eprint(f"[budget-milestone] error recording budget milestone: {exc}")
|
|
877
|
-
finally:
|
|
878
|
-
conn.close()
|
|
879
|
-
|
|
880
|
-
# Dispatch AFTER commit; a dispatch failure NEVER rolls back the milestone
|
|
881
|
-
# (set-then-dispatch invariant — one queue attempt per crossing, deduped
|
|
882
|
-
# on the alerted_at column).
|
|
883
|
-
for payload in pending_alerts:
|
|
884
|
-
try:
|
|
885
|
-
_dispatch_alert_notification(payload, mode="real")
|
|
886
|
-
except Exception as dispatch_exc:
|
|
887
|
-
eprint(f"[budget-alerts] dispatch failed: {dispatch_exc}")
|
|
879
|
+
_record_budget_milestone_for_vendor(
|
|
880
|
+
vendor="claude",
|
|
881
|
+
target=budget_cfg["weekly_usd"],
|
|
882
|
+
thresholds=thresholds,
|
|
883
|
+
period=period,
|
|
884
|
+
config=config,
|
|
885
|
+
tz=tz,
|
|
886
|
+
# The Claude payload builder takes the legacy `week_start_at=` kwarg
|
|
887
|
+
# (its value is the resolved period-start instant, == period_key), so
|
|
888
|
+
# the at-fire dispatch id stays byte-stable `budget:<period_start_at>:<t>`.
|
|
889
|
+
build_payload=lambda **kw: _build_alert_payload_budget(
|
|
890
|
+
threshold=kw["threshold"],
|
|
891
|
+
crossed_at_utc=kw["crossed_at_utc"],
|
|
892
|
+
week_start_at=kw["period_key"],
|
|
893
|
+
budget_usd=kw["budget_usd"],
|
|
894
|
+
spent_usd=kw["spent_usd"],
|
|
895
|
+
consumption_pct=kw["consumption_pct"],
|
|
896
|
+
period=kw["period"],
|
|
897
|
+
),
|
|
898
|
+
)
|
|
888
899
|
|
|
889
900
|
|
|
890
901
|
def maybe_record_project_budget_milestone(saved: dict[str, Any]) -> None:
|
|
@@ -1056,24 +1067,26 @@ def maybe_record_codex_budget_milestone(saved: dict[str, Any]) -> None:
|
|
|
1056
1067
|
"""Fire Codex budget alerts on ACTUAL-Codex-spend threshold crossings (axis
|
|
1057
1068
|
``codex_budget``, calendar-period-codex-budgets spec §6 — the gap the Codex
|
|
1058
1069
|
spec review flagged: Codex usage never flows through ``record-usage``, so the
|
|
1059
|
-
Claude budget axes can't catch it).
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1070
|
+
Claude budget axes can't catch it). Thin vendor adapter over
|
|
1071
|
+
:func:`_record_budget_milestone_for_vendor` (#143): reads the ``budget.codex``
|
|
1072
|
+
config block, gates, resolves ``target`` / ``thresholds`` / ``period``, and
|
|
1073
|
+
passes the Codex payload builder.
|
|
1074
|
+
|
|
1075
|
+
Called from ``cmd_record_usage`` alongside the weekly-% / 5h-% / budget /
|
|
1076
|
+
project-budget milestone helpers AND opportunistically from ``cmd_budget``
|
|
1077
|
+
(the public name is kept so that call site is unchanged). Forward-only /
|
|
1078
|
+
fire-once, so the double-trigger never double-fires. Gated, hot-path-cheap,
|
|
1079
|
+
set-then-dispatch. Errors are logged, not raised (the caller also wraps).
|
|
1065
1080
|
|
|
1066
1081
|
Unlike the Claude budget axis, Codex has NO subscription week: the period
|
|
1067
1082
|
window is resolved purely from ``now`` + the configured calendar period
|
|
1068
|
-
(calendar-week / calendar-month)
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
``_codex_budget_crossings`` helper so this firing path and the
|
|
1072
|
-
``cctally budget`` opportunistic path stay byte-identical (plan §3.6).
|
|
1083
|
+
(calendar-week / calendar-month) — it NEVER touches
|
|
1084
|
+
``weekly_usage_snapshots`` (the shared core's ``_resolve_budget_window``
|
|
1085
|
+
dispatches to the pure calendar window for ``vendor='codex'``).
|
|
1073
1086
|
|
|
1074
1087
|
``saved`` is accepted for call-site symmetry with the sibling helpers but is
|
|
1075
|
-
unused: Codex spend is resolved from the cache DB
|
|
1076
|
-
|
|
1088
|
+
unused: Codex spend is resolved from the cache DB independent of the
|
|
1089
|
+
just-recorded 7d-% snapshot.
|
|
1077
1090
|
"""
|
|
1078
1091
|
# Gate FIRST (hot-path discipline): no Codex budget OR alerts off → zero
|
|
1079
1092
|
# overhead for non-Codex-budget users. `load_config()` is safe outside any
|
|
@@ -1093,71 +1106,27 @@ def maybe_record_codex_budget_milestone(saved: dict[str, Any]) -> None:
|
|
|
1093
1106
|
thresholds = codex_cfg.get("alert_thresholds") or []
|
|
1094
1107
|
if target is None or not thresholds:
|
|
1095
1108
|
return
|
|
1096
|
-
period = codex_cfg["period"]
|
|
1097
|
-
|
|
1098
1109
|
tz = resolve_display_tz(argparse.Namespace(tz=None), config)
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
#
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
)
|
|
1120
|
-
}
|
|
1121
|
-
pending = [t for t in sorted(thresholds) if t not in present]
|
|
1122
|
-
if not pending:
|
|
1123
|
-
return # nothing left to cross this period → skip the cost SUM
|
|
1124
|
-
|
|
1125
|
-
spent = _sum_codex_cost_for_range(start_at, now_utc)
|
|
1126
|
-
# Shared INSERT-and-arm core (set-then-dispatch, fire-once via rowcount);
|
|
1127
|
-
# commit=False inside, so this conn owns the single durable commit below.
|
|
1128
|
-
for t, crossed_at, sp, tg, pct in _codex_budget_crossings(
|
|
1129
|
-
conn,
|
|
1130
|
-
period_key=period_key,
|
|
1131
|
-
period=period,
|
|
1132
|
-
thresholds=pending,
|
|
1133
|
-
target=target,
|
|
1134
|
-
spent=spent,
|
|
1135
|
-
now_utc=now_utc,
|
|
1136
|
-
):
|
|
1137
|
-
pending_alerts.append(_build_alert_payload_codex_budget(
|
|
1138
|
-
threshold=t,
|
|
1139
|
-
crossed_at_utc=crossed_at,
|
|
1140
|
-
period_start_at=period_key,
|
|
1141
|
-
period=period,
|
|
1142
|
-
budget_usd=tg,
|
|
1143
|
-
spent_usd=sp,
|
|
1144
|
-
consumption_pct=pct,
|
|
1145
|
-
))
|
|
1146
|
-
# Single commit: every INSERT + its alerted_at marker durable together.
|
|
1147
|
-
conn.commit()
|
|
1148
|
-
except Exception as exc:
|
|
1149
|
-
eprint(f"[codex-budget-milestone] error recording codex budget milestone: {exc}")
|
|
1150
|
-
finally:
|
|
1151
|
-
conn.close()
|
|
1152
|
-
|
|
1153
|
-
# Dispatch AFTER commit; a dispatch failure NEVER rolls back the milestone
|
|
1154
|
-
# (set-then-dispatch invariant — one queue attempt per crossing, deduped on
|
|
1155
|
-
# the alerted_at column).
|
|
1156
|
-
for payload in pending_alerts:
|
|
1157
|
-
try:
|
|
1158
|
-
_dispatch_alert_notification(payload, mode="real")
|
|
1159
|
-
except Exception as dispatch_exc:
|
|
1160
|
-
eprint(f"[codex-budget-alerts] dispatch failed: {dispatch_exc}")
|
|
1110
|
+
_record_budget_milestone_for_vendor(
|
|
1111
|
+
vendor="codex",
|
|
1112
|
+
target=target,
|
|
1113
|
+
thresholds=thresholds,
|
|
1114
|
+
period=codex_cfg["period"],
|
|
1115
|
+
config=config,
|
|
1116
|
+
tz=tz,
|
|
1117
|
+
# The Codex payload builder takes `period_start_at=` directly (== the
|
|
1118
|
+
# resolved period-start instant, == period_key), so the at-fire dispatch
|
|
1119
|
+
# id stays byte-stable `codex_budget:<period_start_at>:<threshold>`.
|
|
1120
|
+
build_payload=lambda **kw: _build_alert_payload_codex_budget(
|
|
1121
|
+
threshold=kw["threshold"],
|
|
1122
|
+
crossed_at_utc=kw["crossed_at_utc"],
|
|
1123
|
+
period_start_at=kw["period_key"],
|
|
1124
|
+
period=kw["period"],
|
|
1125
|
+
budget_usd=kw["budget_usd"],
|
|
1126
|
+
spent_usd=kw["spent_usd"],
|
|
1127
|
+
consumption_pct=kw["consumption_pct"],
|
|
1128
|
+
),
|
|
1129
|
+
)
|
|
1161
1130
|
|
|
1162
1131
|
|
|
1163
1132
|
def _weekly_pct_week_avg_projection(conn, now_utc):
|
package/bin/_lib_alert_axes.py
CHANGED
|
@@ -41,12 +41,13 @@ class AlertAxisDescriptor:
|
|
|
41
41
|
chip_label: str # SHOUT form, byte-identical with alertAxis.ts AXIS_CHIP_LABEL
|
|
42
42
|
title_label: str # sentence-case form, byte-identical with AXIS_TITLE_LABEL
|
|
43
43
|
milestone_table: str # SQLite table the dashboard envelope SELECTs from
|
|
44
|
+
vendor: "str | None" = None # 'claude'|'codex' for budget-family axes; None otherwise (#143)
|
|
44
45
|
|
|
45
46
|
|
|
46
47
|
AXIS_REGISTRY: "tuple[AlertAxisDescriptor, ...]" = (
|
|
47
48
|
AlertAxisDescriptor("weekly", "WEEKLY", "Weekly", "percent_milestones"),
|
|
48
49
|
AlertAxisDescriptor("five_hour", "5H-BLOCK", "5h-block", "five_hour_milestones"),
|
|
49
|
-
AlertAxisDescriptor("budget", "BUDGET", "Budget", "budget_milestones"),
|
|
50
|
+
AlertAxisDescriptor("budget", "BUDGET", "Budget", "budget_milestones", vendor="claude"),
|
|
50
51
|
AlertAxisDescriptor("projected", "PROJECTED", "Projected", "projected_milestones"),
|
|
51
52
|
# Per-project weekly budget alerts (issue #19 / #121). Distinct "PROJECT"
|
|
52
53
|
# chip vs the global "BUDGET" chip; its own forward-only table.
|
|
@@ -55,10 +56,12 @@ AXIS_REGISTRY: "tuple[AlertAxisDescriptor, ...]" = (
|
|
|
55
56
|
),
|
|
56
57
|
# Per-vendor Codex budget alerts (calendar-period; calendar-period-codex-budgets
|
|
57
58
|
# feature). Distinct "CODEX" chip vs the global "BUDGET" / per-project
|
|
58
|
-
# "PROJECT" chips
|
|
59
|
-
#
|
|
59
|
+
# "PROJECT" chips. As of #143 it shares the unified vendor-tagged
|
|
60
|
+
# `budget_milestones` table with the Claude `budget` axis; the envelope
|
|
61
|
+
# mapper's `WHERE vendor=?` filter does the row-level split (keyed on the
|
|
62
|
+
# resolved period-window start instant period_start_at, threshold).
|
|
60
63
|
AlertAxisDescriptor(
|
|
61
|
-
"codex_budget", "CODEX", "Codex budget", "
|
|
64
|
+
"codex_budget", "CODEX", "Codex budget", "budget_milestones", vendor="codex"
|
|
62
65
|
),
|
|
63
66
|
)
|
|
64
67
|
|
package/bin/_lib_conversation.py
CHANGED
|
@@ -59,12 +59,27 @@ def iter_message_rows(fh, path_str):
|
|
|
59
59
|
obj = json.loads(s)
|
|
60
60
|
except json.JSONDecodeError:
|
|
61
61
|
continue
|
|
62
|
-
|
|
63
|
-
if
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
62
|
+
row = parse_message_row(obj, offset)
|
|
63
|
+
if row is not None:
|
|
64
|
+
yield row
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def parse_message_row(obj, offset):
|
|
68
|
+
"""Pure per-line message parser: given a parsed JSONL object and its byte
|
|
69
|
+
offset, return a ``MessageRow`` when it is a user/assistant turn carrying a
|
|
70
|
+
``uuid``, or ``None`` otherwise (summary / file-history-snapshot / uuid-less
|
|
71
|
+
lines). No I/O — the caller owns the readline()+tell() loop.
|
|
72
|
+
|
|
73
|
+
Extracted (#138) so ``iter_message_rows`` and the fused single-pass sync
|
|
74
|
+
walker (``_cctally_cache._iter_sync_entries``) share ONE classification —
|
|
75
|
+
each JSONL line is parsed once and the conversation index is no longer
|
|
76
|
+
populated by a separate second seek-and-walk over the same byte span."""
|
|
77
|
+
t = obj.get("type")
|
|
78
|
+
if t not in ("user", "assistant"):
|
|
79
|
+
return None
|
|
80
|
+
if not obj.get("uuid"):
|
|
81
|
+
return None
|
|
82
|
+
return _normalize(obj, t, offset)
|
|
68
83
|
|
|
69
84
|
|
|
70
85
|
def _normalize(obj, t, offset):
|
|
@@ -121,12 +136,15 @@ def _blocks_and_text(content):
|
|
|
121
136
|
blocks.append({"kind": "thinking", "text": b.get("thinking", "") or ""})
|
|
122
137
|
elif bt == "tool_use":
|
|
123
138
|
blocks.append({"kind": "tool_use", "name": b.get("name"),
|
|
124
|
-
"input_summary": _summarize(b.get("input"))
|
|
139
|
+
"input_summary": _summarize(b.get("input")),
|
|
140
|
+
"id": b.get("id"),
|
|
141
|
+
"preview": tool_preview(b.get("name"), b.get("input"))})
|
|
125
142
|
elif bt == "tool_result":
|
|
126
143
|
raw = _stringify(b.get("content"))
|
|
127
144
|
blocks.append({"kind": "tool_result", "text": raw[:_TOOL_RESULT_CAP],
|
|
128
145
|
"truncated": len(raw) > _TOOL_RESULT_CAP,
|
|
129
|
-
"is_error": bool(b.get("is_error"))
|
|
146
|
+
"is_error": bool(b.get("is_error")),
|
|
147
|
+
"tool_use_id": b.get("tool_use_id")})
|
|
130
148
|
elif bt in ("image", "document"):
|
|
131
149
|
blocks.append({"kind": bt, **_media(b.get("source"))})
|
|
132
150
|
elif bt == "tool_reference":
|
|
@@ -155,6 +173,39 @@ def _summarize(inp):
|
|
|
155
173
|
return s[:200]
|
|
156
174
|
|
|
157
175
|
|
|
176
|
+
_PREVIEW_FIELDS = {
|
|
177
|
+
"Read": "file_path", "Write": "file_path", "Edit": "file_path",
|
|
178
|
+
"MultiEdit": "file_path", "NotebookEdit": "file_path",
|
|
179
|
+
"Bash": "command", "Grep": "pattern", "Glob": "pattern",
|
|
180
|
+
"Task": "description", "WebFetch": "url", "WebSearch": "query",
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def tool_preview(name, inp):
|
|
185
|
+
"""One-line, full-fidelity preview for a tool call's collapsed chip (#164,
|
|
186
|
+
C5). Runs on the RAW input dict before _summarize truncates to 200 chars.
|
|
187
|
+
Known tools map to their primary arg; Bash takes the first command line;
|
|
188
|
+
Task falls back to subagent_type; unknown/mcp tools take the first
|
|
189
|
+
string-valued arg, else the tool name. Always returns a single-line str."""
|
|
190
|
+
if not isinstance(inp, dict):
|
|
191
|
+
return ""
|
|
192
|
+
field = _PREVIEW_FIELDS.get(name or "")
|
|
193
|
+
val = None
|
|
194
|
+
if field is not None:
|
|
195
|
+
val = inp.get(field)
|
|
196
|
+
if val is None and name == "Task":
|
|
197
|
+
val = inp.get("subagent_type")
|
|
198
|
+
if val is None:
|
|
199
|
+
# generic fallback: first string-valued arg, else the tool name
|
|
200
|
+
for v in inp.values():
|
|
201
|
+
if isinstance(v, str) and v:
|
|
202
|
+
val = v
|
|
203
|
+
break
|
|
204
|
+
if not isinstance(val, str) or not val:
|
|
205
|
+
return name or ""
|
|
206
|
+
return val.splitlines()[0]
|
|
207
|
+
|
|
208
|
+
|
|
158
209
|
def _media(source):
|
|
159
210
|
if not isinstance(source, dict):
|
|
160
211
|
return {"media_type": None, "bytes": 0}
|