cctally 1.106.0 → 1.108.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 +68 -0
- package/bin/_cctally_alerts.py +147 -6
- package/bin/_cctally_cache.py +553 -44
- package/bin/_cctally_core.py +316 -151
- package/bin/_cctally_dashboard.py +70 -73
- package/bin/_cctally_dashboard_envelope.py +119 -95
- package/bin/_cctally_dashboard_share.py +1 -7
- package/bin/_cctally_db.py +6 -133
- package/bin/_cctally_diff.py +15 -8
- package/bin/_cctally_doctor.py +162 -43
- package/bin/_cctally_five_hour.py +6 -53
- package/bin/_cctally_forecast.py +73 -203
- package/bin/_cctally_journal.py +305 -243
- package/bin/_cctally_milestone_history.py +58 -141
- package/bin/_cctally_milestones.py +0 -72
- package/bin/_cctally_parser.py +15 -15
- package/bin/_cctally_percent_breakdown.py +292 -32
- package/bin/_cctally_project.py +3 -5
- package/bin/_cctally_quota.py +16 -30
- package/bin/_cctally_quota_model.py +160 -27
- package/bin/_cctally_record.py +631 -1377
- package/bin/_cctally_rederive.py +0 -2
- package/bin/_cctally_reporting.py +1 -19
- package/bin/_cctally_setup.py +272 -110
- package/bin/_cctally_share.py +45 -32
- package/bin/_cctally_statusline.py +41 -16
- package/bin/_cctally_tui.py +56 -233
- package/bin/_cctally_weekrefs.py +404 -317
- package/bin/_lib_aggregators.py +7 -2
- package/bin/_lib_alerts_payload.py +48 -1
- package/bin/_lib_cache_report.py +5 -2
- package/bin/_lib_codex_hooks.py +694 -83
- package/bin/_lib_credit.py +2 -9
- package/bin/_lib_dashboard_sources.py +4 -16
- package/bin/_lib_diff_kernel.py +33 -10
- package/bin/_lib_doctor.py +361 -24
- package/bin/_lib_journal.py +64 -0
- package/bin/_lib_meter_rate_change.py +132 -3
- package/bin/_lib_pricing.py +66 -5
- package/bin/_lib_pricing_check.py +5 -4
- package/bin/_lib_quota_model.py +54 -44
- package/bin/_lib_record.py +9 -41
- package/bin/_lib_rederive.py +0 -8
- package/bin/_lib_render.py +19 -80
- package/bin/_lib_snapshot_cache.py +15 -198
- package/bin/_lib_subscription_weeks.py +209 -135
- package/bin/_lib_view_models.py +86 -133
- package/bin/cctally +14 -10
- package/dashboard/static/assets/ConversationsView-BOSaBRtu.js +72 -0
- package/dashboard/static/assets/{DoctorModal-5rvkyksy.js → DoctorModal-CWn3U4Wl.js} +1 -1
- package/dashboard/static/assets/ModalRoot-SR070V6c.js +1 -0
- package/dashboard/static/assets/{ProjectsDrillPanel-CsR1bjJu.js → ProjectsDrillPanel-QLI9i5mZ.js} +1 -1
- package/dashboard/static/assets/{SourceDetailModal-BnTIwt-R.js → SourceDetailModal-pv0wlFxl.js} +1 -1
- package/dashboard/static/assets/{UpdateModal-D_RfssnW.js → UpdateModal-D3m8GG6V.js} +3 -3
- package/dashboard/static/assets/index-BHu4mxd8.css +1 -0
- package/dashboard/static/assets/index-CIWsbux3.js +13 -0
- package/dashboard/static/assets/{outlineNavigation-C7J9lhGB.js → outlineNavigation-CJvKqmLV.js} +1 -1
- package/dashboard/static/assets/useKeymap-ffqsS5G0.js +1 -0
- package/dashboard/static/dashboard.html +3 -3
- package/package.json +1 -3
- package/bin/_lib_credit_identity.py +0 -159
- package/bin/_lib_credit_selection.py +0 -342
- package/dashboard/static/assets/ConversationsView-Dst33C0n.js +0 -72
- package/dashboard/static/assets/ModalRoot-BJH02VzE.js +0 -1
- package/dashboard/static/assets/index-rzXw99Hy.js +0 -13
- package/dashboard/static/assets/index-u3wLfjt2.css +0 -1
- package/dashboard/static/assets/useKeymap-B3KCc_c7.js +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,74 @@ based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [1.108.0] - 2026-09-05
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Codex reporting prices `gpt-6-astra` from OpenAI's published standard, cached-input, long-context and Fast-mode rates instead of the legacy `gpt-5` fallback.
|
|
12
|
+
- `cctally alerts test` gains `--axis quota` and `--axis meter-rate-change`, so you can rehearse those two notifications instead of waiting for a real crossing or rate change. `--threshold` does not apply to `meter-rate-change`.
|
|
13
|
+
- `cctally percent-breakdown` and `cctally report --detail` name a run of percent thresholds that one observation recorded, and print `observation_gap` where the marginal cost showed a bare `n/a`. `--json` adds `marginalCostWithheldCause`.
|
|
14
|
+
- On an install with more than one account for a provider, every alert toast names the account it belongs to, as the dashboard's Recent Alerts list already did.
|
|
15
|
+
- `cctally doctor` gains `hooks.codex_liveness_7d`, which FAILs when an enabled Codex hook has not succeeded in seven days. It reads the per-root success markers, so log rotation cannot hide a hook that stopped firing.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- `cctally weekly`, `cctally report` and `cctally project` render a week credited twice as three rows, one per billing cycle. A dictionary keyed on the week alone now keeps only the last cycle; key on the week and its start instant together.
|
|
19
|
+
- `cctally weekly`, `cctally report` and `cctally project` start the first cycle of a week whose boundary moved and was then credited at the moved boundary. The previous week keeps its own spend, which was counted into the credited week.
|
|
20
|
+
- Upgrading from 1.107.0 or earlier rebuilds the disposable stats index once, because a weekly reset now records the observation it came from and a rate change records its evidence. Commands report the rebuild until it finishes.
|
|
21
|
+
- A metering-rate change recorded before this upgrade reports no evidence rather than inventing any.
|
|
22
|
+
- A weekly credit is recorded at the exact second Anthropic issued it rather than the top of that hour, so a reading captured earlier in the same hour stays in the cycle it belongs to.
|
|
23
|
+
- A pre-credit reading captured before a second weekly credit now survives and is shown inside the earlier cycle, because each credit's cleanup is scoped to its own instant instead of reaching back past the previous credit.
|
|
24
|
+
- `cctally doctor` now reads Codex's own hook trust record and FAILs when the cctally handler is disabled or was never trusted in Codex `/hooks`, instead of reporting it as installed.
|
|
25
|
+
- `cctally doctor` warns when the Codex handler changed after Codex last recorded a trust decision about it, and when that record cannot be read at all.
|
|
26
|
+
- `cctally setup` refuses at exit 1 when reconciling the Codex handler would land it on a trust decision Codex recorded about a different handler. The message names the file, the slots and the fix.
|
|
27
|
+
- When Codex's trust record changes mid-run, the `cctally setup` refusal names each Codex hooks file it had already rewritten and the dated backup of its previous contents. `--json` adds `changed_hooks_backups`.
|
|
28
|
+
- `cctally doctor` states how many Codex roots are installed beside how many are enabled, so two installed-but-untrusted handlers no longer read as `0/2 root(s) enabled` alone.
|
|
29
|
+
- `cctally setup` recognizes a Codex handler written by a different install channel and collapses it into one canonical handler, instead of adding a second one beside it.
|
|
30
|
+
- `cctally setup --json` moves to `schema_version: 2`: the `installed_review_required` state is retired, and per-root `changes` becomes a per-event object with separate added, removed and unchanged counts.
|
|
31
|
+
- `cctally setup --dry-run` and the applied run now report the same added, removed and unchanged Codex handler counts for the same input.
|
|
32
|
+
- The dashboard and the TUI stop treating a Codex handler as trusted merely because it is present, and re-check that trust when `config.toml` changes, so a hook disabled while either was running no longer certifies stale conversation cost.
|
|
33
|
+
- A freshly installed Codex handler reports as untrusted, and `cctally doctor` FAILs, until you approve it in Codex `/hooks`. Until then the dashboard and the TUI walk every Codex session on each refresh.
|
|
34
|
+
- Two `cctally setup` messages read as ordinary English again: the orphaned `[hooks.state]` remedy says to remove the entry from the file by hand, and a moved trust slot says the handler would move from one key to the other.
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
- A percent milestone crossed on a week that holds two recorded resets is listed again. It was filed against one cycle while `cctally percent-breakdown` listed another, so the crossing appeared under neither.
|
|
38
|
+
- Every view of a credited week picks the cycle the week is currently in, not the reset written last. `cctally diff`, the TUI's per-percent modal and the dashboard's milestone list could each name an earlier cycle.
|
|
39
|
+
- A weekly threshold notification names the billing cycle that crossed. On a week credited more than once it read `Week starting Jun 05` for every cycle, naming all of them at once; an uncredited week reads exactly as before.
|
|
40
|
+
- A second Anthropic usage reset inside one subscription week now registers. Every reset in a week shared one slot, so the first held it and later ones were discarded, leaving the reported percentage at the pre-reset high-water mark.
|
|
41
|
+
- A weekly reset observed while cctally is interrupted is no longer lost, and one zero reading can no longer confirm itself into a credit. The pending-reset state moved into the database and commits or rolls back with the reset it fired.
|
|
42
|
+
- A stale pre-credit reading that a credit removed stays removed after `cctally db rebuild --db stats`. The removal is recorded, so a rebuild no longer restores the reading and holds the reported percentage at the pre-credit high.
|
|
43
|
+
- An idle session no longer holds the reported weekly percentage at a stale value. It re-renders its status line from a cached rate-limit block, and that block is now ignored in full once its five-hour window has closed.
|
|
44
|
+
- A severe metering-rate drop now raises a red alert toast instead of the amber that made it look like a smaller drop, and the toast's rate chip matches the colour Recent Alerts already showed for the same event.
|
|
45
|
+
- A metering-rate change detected while the quota calibration was withheld now records which calibration was withheld and how many baseline days were missing, instead of pointing you at a fitted budget that may never have been produced.
|
|
46
|
+
- A metering-rate change whose notification failed and is retried later now carries that same disclosure, because it is read back from the recorded change rather than re-derived from a later run.
|
|
47
|
+
- A metering-rate toast now names the calibration that was withheld and why, instead of pointing you at a fitted budget that may not exist. An ordinary rate change reads exactly as it did before.
|
|
48
|
+
- Recent Alerts shows the evidence behind a metering-rate change: the withheld calibration, the detector's inputs, the composition provenance, and how many baseline days were withheld.
|
|
49
|
+
- An alert toast on a multi-account install states how to dismiss it again, on its own line below the alert's own text rather than above it, so a screen reader reads the alert before it reads how to close it.
|
|
50
|
+
- Alert toasts are usable from the keyboard: press Enter or Space to dismiss one, or to activate its own button and open the window it names. A focused toast states the alert before how to close it, and dismissing one restores focus.
|
|
51
|
+
- The dashboard's Daily panel reads `loading` while it hydrates, instead of reporting the 30-day window as `withheld` over a loading skeleton.
|
|
52
|
+
- The dashboard's Trend panel states its title in full on a phone. The week and cycle counts moved to the wrapping sub-line the other cards use, so the title is no longer cut off mid-word.
|
|
53
|
+
- The dashboard's Daily panel prints its total and peak-day amounts in full on a narrow window. Below about 400px the two summary columns stack instead of cutting the dollar figures off.
|
|
54
|
+
- An install upgrading from a version that predates the append-only journal no longer records a second copy of a reset it already had, so a credited week keeps its own cycles instead of gaining an extra one.
|
|
55
|
+
|
|
56
|
+
## [1.107.0] - 2026-09-04
|
|
57
|
+
|
|
58
|
+
### Changed
|
|
59
|
+
- The same-window weekly credit model introduced in 1.106.0 is reverted. A week is anchored to its billing cycle again, and a recorded credit no longer rewrites the week in place or withholds the `$/1%` figure.
|
|
60
|
+
- Upgrading from 1.106.0 rebuilds the disposable stats index once. On a large store that took about a minute here, and commands report that a rebuild is running until it finishes.
|
|
61
|
+
- `claude-fable-5-1` and `claude-mythos-5-1` each count as their own model family in the weekly quota model, instead of being pooled with the model they follow. Your quota calibration refits once on the next `cctally quota` run.
|
|
62
|
+
- `cctally doctor` separates the one refused-pricing-write state that restarting cctally cannot clear — a recorded pricing date it cannot read — from the ordinary one, and names the step that does clear it.
|
|
63
|
+
- A shared `cctally weekly` artifact labels each row by the date its cycle began, matching the terminal table and the dashboard. On a week Anthropic reset early, that label and the artifact's stated period both move forward.
|
|
64
|
+
- The dashboard's Projects panel counts a credited week as the two billing cycles it is, so its week grid covers fewer calendar days for the same week count.
|
|
65
|
+
- The `cctally weekly` and `cctally report` artifacts, the dashboard's Weekly panel footer and the Weekly modal title all count billing cycles rather than weeks, because a credited week supplies two rows. `Avg %/wk` is now `Avg %/cycle`.
|
|
66
|
+
|
|
67
|
+
### Fixed
|
|
68
|
+
- `cctally weekly` renders a week credited in place as two rows, one per billing cycle, matching `cctally report`. The cycle before the credit had no row at all, and its spend was missing from the table and from `--json` totals.
|
|
69
|
+
- The dashboard's Weekly panel and the TUI show both cycles of a credited week from one source, so a week's two rows can no longer disagree with the panel total beside them.
|
|
70
|
+
- `cctally project` counts a credited week as the two billing cycles it is, so `--weeks N` spans a shorter calendar range there. `attributedUsedPercent` rises, because the cycle before the credit matches its usage snapshot again.
|
|
71
|
+
- A `cctally dashboard` left running across a pricing update can no longer overwrite corrected conversation cost from the table it started with. It keeps ingesting, your conversations stay listed, and `cctally doctor` says to restart it.
|
|
72
|
+
- `cctally doctor` reports a refused conversation-cost write from any process that hit it, including short-lived ones like the status line and `cctally daily` that never open the conversation store.
|
|
73
|
+
- `cctally cache-sync --rebuild` says it did nothing when it may not rewrite conversation cost, instead of reporting `0 processed` and exiting 0. It names both reasons: a stored pricing table newer than this one, or one that is not a date.
|
|
74
|
+
- `cctally doctor`'s refused-pricing-write warning clears once cctally is running current with the store, including after a refused `cctally cache-sync --rebuild`. That refusal previously left a warning the documented remedy could not clear.
|
|
75
|
+
|
|
8
76
|
## [1.106.0] - 2026-09-03
|
|
9
77
|
|
|
10
78
|
### Changed
|
package/bin/_cctally_alerts.py
CHANGED
|
@@ -252,9 +252,16 @@ def _alert_label_prefix(axis: str, account_key: "str | None",
|
|
|
252
252
|
install's alert text stays byte-identical to today.
|
|
253
253
|
|
|
254
254
|
Delegates the R8 gate + label precedence to the single-definition helpers
|
|
255
|
-
``_cctally_account.
|
|
256
|
-
">1 real account" trigger and the key->label map have exactly one home;
|
|
257
|
-
wrapper only opens the RO connection and stays best-effort/never-raise.
|
|
255
|
+
``_cctally_account.provider_is_decorated`` / ``account_label`` (P2-CQ1) so
|
|
256
|
+
the ">1 real account" trigger and the key->label map have exactly one home;
|
|
257
|
+
this wrapper only opens the RO connection and stays best-effort/never-raise.
|
|
258
|
+
|
|
259
|
+
#702: the gate is the canonical predicate itself, negated at the call site.
|
|
260
|
+
This site previously inverted the underlying counter by hand
|
|
261
|
+
(``real_account_count(conn, vendor) <= 1``), which is a second written form
|
|
262
|
+
of the R8 threshold — the arrangement that lets the two spellings disagree
|
|
263
|
+
if the threshold ever moves. ``provider_is_decorated`` is ``> 1``, so the
|
|
264
|
+
suppressed branch is ``not provider_is_decorated(...)``.
|
|
258
265
|
|
|
259
266
|
#697: the VENDOR comes from the payload's own ``provider`` when it carries
|
|
260
267
|
one, and from ``_AXIS_VENDOR`` otherwise. The metering-rate-change family
|
|
@@ -288,7 +295,7 @@ def _alert_label_prefix(axis: str, account_key: "str | None",
|
|
|
288
295
|
connect=lambda p: _sq.connect(f"file:{p}?mode=ro", uri=True),
|
|
289
296
|
)
|
|
290
297
|
try:
|
|
291
|
-
if _cctally_account.
|
|
298
|
+
if not _cctally_account.provider_is_decorated(conn, vendor):
|
|
292
299
|
return ""
|
|
293
300
|
# #416 §6: population-aware, so two accounts that auto-label to
|
|
294
301
|
# one email do not print the same alert prefix.
|
|
@@ -400,8 +407,27 @@ def _dispatch_alert_notification(
|
|
|
400
407
|
# rate transition has no percentage threshold for `severity_for` to
|
|
401
408
|
# map. Reading it verbatim is what keeps the three tiers meaningful
|
|
402
409
|
# here; falling through would floor every transition at `info`.
|
|
410
|
+
#
|
|
411
|
+
# #750 S2: the vocabulary is RESOLVED from the kernel's own tuple
|
|
412
|
+
# rather than restated as a literal. A restated literal makes this the
|
|
413
|
+
# second written form of the same rule, which is exactly the
|
|
414
|
+
# arrangement #747 closed on the envelope: a fourth member added to
|
|
415
|
+
# `RATE_CHANGE_SEVERITIES`, to the TypeScript tuple and to the CSS
|
|
416
|
+
# passes every parity test and is still clamped to `info` here, so the
|
|
417
|
+
# OS notification's urgency and the trailing `alerts.log` column are
|
|
418
|
+
# wrong while the suite is green.
|
|
419
|
+
#
|
|
420
|
+
# Guarded the same way `_cctally_dashboard_envelope` guards its copy,
|
|
421
|
+
# and an unresolvable kernel degrades to NO clamp. This function's
|
|
422
|
+
# contract is that it never raises, and clamping against an empty
|
|
423
|
+
# vocabulary would send every correct severity to `info`.
|
|
403
424
|
severity = str(payload.get("severity") or "info")
|
|
404
|
-
|
|
425
|
+
try:
|
|
426
|
+
_severities = frozenset(
|
|
427
|
+
_load_lib("_lib_meter_rate_change").RATE_CHANGE_SEVERITIES)
|
|
428
|
+
except Exception: # noqa: BLE001
|
|
429
|
+
_severities = None
|
|
430
|
+
if _severities is not None and severity not in _severities:
|
|
405
431
|
severity = "info"
|
|
406
432
|
else:
|
|
407
433
|
try:
|
|
@@ -484,6 +510,54 @@ def _dispatch_alert_notification(
|
|
|
484
510
|
return status
|
|
485
511
|
|
|
486
512
|
|
|
513
|
+
def _synthetic_instant_days_ago(days: int) -> str:
|
|
514
|
+
"""A timezone-aware ISO instant `days` before now (#699).
|
|
515
|
+
|
|
516
|
+
Negative goes forward, which is what a quota reset needs. Both non-registry
|
|
517
|
+
synthetics require real instants rather than placeholders: the rate-change
|
|
518
|
+
body renders an effective date, and the quota body renders a reset.
|
|
519
|
+
"""
|
|
520
|
+
moment = dt.datetime.now(dt.timezone.utc) - dt.timedelta(days=days)
|
|
521
|
+
return moment.isoformat().replace("+00:00", "Z")
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def _test_alert_account_key(vendor: str) -> str:
|
|
525
|
+
"""A REAL account key for `vendor`, else the vendor-wide sentinel (#699).
|
|
526
|
+
|
|
527
|
+
This is what makes the R8 `[<label>]` prefix observable from
|
|
528
|
+
`alerts test`: on a decorated install the synthetic carries a key the
|
|
529
|
+
registry can label, and on a single-account install it falls back to `*`,
|
|
530
|
+
which `_alert_label_prefix` suppresses — so that install's output stays
|
|
531
|
+
byte-identical, exactly as R8 requires.
|
|
532
|
+
|
|
533
|
+
Best-effort and never-raise, following `_alert_label_prefix`: a missing
|
|
534
|
+
database, an unreadable registry or an empty one all degrade to the
|
|
535
|
+
sentinel rather than turning a rehearsal into an error path.
|
|
536
|
+
"""
|
|
537
|
+
try:
|
|
538
|
+
import sqlite3 as _sq
|
|
539
|
+
import _cctally_account
|
|
540
|
+
import _cctally_store
|
|
541
|
+
db_path = _cctally_core.DB_PATH
|
|
542
|
+
if not db_path.exists():
|
|
543
|
+
return _lib_accounts.VENDOR_WIDE
|
|
544
|
+
conn = _cctally_store.stats_open_guarded(
|
|
545
|
+
db_path,
|
|
546
|
+
connect=lambda p: _sq.connect(f"file:{p}?mode=ro", uri=True),
|
|
547
|
+
)
|
|
548
|
+
try:
|
|
549
|
+
for row in _cctally_account.load_accounts(conn, vendor):
|
|
550
|
+
key = str(row.get("account_key") or "")
|
|
551
|
+
if key and key not in (_lib_accounts.UNATTRIBUTED,
|
|
552
|
+
_lib_accounts.VENDOR_WIDE):
|
|
553
|
+
return key
|
|
554
|
+
finally:
|
|
555
|
+
conn.close()
|
|
556
|
+
except Exception: # noqa: BLE001
|
|
557
|
+
return _lib_accounts.VENDOR_WIDE
|
|
558
|
+
return _lib_accounts.VENDOR_WIDE
|
|
559
|
+
|
|
560
|
+
|
|
487
561
|
def cmd_alerts_test(args: argparse.Namespace) -> int:
|
|
488
562
|
"""Send a synthetic test alert through the dispatch pipeline.
|
|
489
563
|
|
|
@@ -510,9 +584,28 @@ def cmd_alerts_test(args: argparse.Namespace) -> int:
|
|
|
510
584
|
axis = "codex_budget"
|
|
511
585
|
elif args.axis == "projected":
|
|
512
586
|
axis = "projected"
|
|
587
|
+
elif args.axis == "quota":
|
|
588
|
+
axis = "quota"
|
|
589
|
+
elif args.axis == "meter-rate-change":
|
|
590
|
+
axis = "meter_rate_change"
|
|
513
591
|
else:
|
|
514
592
|
axis = "five_hour"
|
|
515
|
-
threshold
|
|
593
|
+
# #699: `--threshold` now defaults to None so that "supplied" is
|
|
594
|
+
# distinguishable from "defaulted". Every axis that takes one still gets 90
|
|
595
|
+
# when it is omitted, which keeps the existing surface byte-identical.
|
|
596
|
+
raw_threshold = getattr(args, "threshold", None)
|
|
597
|
+
if axis == "meter_rate_change" and raw_threshold is not None:
|
|
598
|
+
# Exit 2 is native-usage per docs/cli-contract.md. The message names
|
|
599
|
+
# BOTH the axis and the flag: a refusal that does not say what failed
|
|
600
|
+
# costs the reader more than one that gives no reason at all.
|
|
601
|
+
print(
|
|
602
|
+
"cctally: --axis meter-rate-change does not accept --threshold; "
|
|
603
|
+
"a metering-rate change has no percentage threshold and carries "
|
|
604
|
+
"an explicit severity instead",
|
|
605
|
+
file=sys.stderr,
|
|
606
|
+
)
|
|
607
|
+
return 2
|
|
608
|
+
threshold = 90 if raw_threshold is None else int(raw_threshold)
|
|
516
609
|
# --threshold range stays [1, 100] (F5): the cap is axis-uniform with the
|
|
517
610
|
# existing weekly/5h thresholds. Over-budget tiers (>100%) are a v2
|
|
518
611
|
# deferral, not an oversight — see spec §2 (F5).
|
|
@@ -603,6 +696,54 @@ def cmd_alerts_test(args: argparse.Namespace) -> int:
|
|
|
603
696
|
denominator=denominator,
|
|
604
697
|
week_start_at=dt.date.today().isoformat(),
|
|
605
698
|
)
|
|
699
|
+
elif axis == "meter_rate_change":
|
|
700
|
+
# #699: the family is deliberately NOT in `AXIS_REGISTRY`, so there is
|
|
701
|
+
# no `_build_alert_payload_*` helper for it. The synthetic descriptor
|
|
702
|
+
# is built HERE and passed through the real
|
|
703
|
+
# `_lib_meter_rate_change.alert_payload`, which is what keeps that
|
|
704
|
+
# kernel clock-, database- and policy-free while still rehearsing the
|
|
705
|
+
# exact payload production dispatches.
|
|
706
|
+
import _lib_meter_rate_change as _mrc
|
|
707
|
+
previous, new = 2_442_620.0, 1_665_096.0
|
|
708
|
+
effective = _synthetic_instant_days_ago(7)
|
|
709
|
+
payload = _mrc.alert_payload(_mrc.RateChangeTransition(
|
|
710
|
+
provider="claude",
|
|
711
|
+
account_key=_test_alert_account_key("claude"),
|
|
712
|
+
effective_from=effective,
|
|
713
|
+
previous_units_per_point=previous,
|
|
714
|
+
new_units_per_point=new,
|
|
715
|
+
# Computed by the real kernel rather than hardcoded, so a change
|
|
716
|
+
# to the severity boundaries is rehearsed here too. These rates
|
|
717
|
+
# are a 31.8% drop — the maintainer's own observed transition —
|
|
718
|
+
# which lands on `alarm`.
|
|
719
|
+
severity=_mrc.transition_severity(previous, new),
|
|
720
|
+
detected_at=now_utc_iso(),
|
|
721
|
+
# The #690 disclosure, so the rehearsal exercises the withheld
|
|
722
|
+
# copy rather than only the ordinary path.
|
|
723
|
+
withholding_status="unsupported-model-mix",
|
|
724
|
+
detector_input_causes='["unsupported-composition"]',
|
|
725
|
+
composition_provenance='["forecast-aggregate"]',
|
|
726
|
+
baseline_withheld_days=3,
|
|
727
|
+
))
|
|
728
|
+
elif axis == "quota":
|
|
729
|
+
# #699: also outside `AXIS_REGISTRY`, but this one HAS a payload
|
|
730
|
+
# builder, so the synthetic goes through it unchanged. `source` is
|
|
731
|
+
# `codex` because that is the only vendor this family observes, and
|
|
732
|
+
# `_AXIS_VENDOR` maps the axis to the same vendor for the R8 lookup.
|
|
733
|
+
payload = _build_alert_payload_quota(
|
|
734
|
+
source="codex",
|
|
735
|
+
source_root_key="default",
|
|
736
|
+
logical_limit_key="weekly",
|
|
737
|
+
observed_slot=_synthetic_instant_days_ago(2),
|
|
738
|
+
window_minutes=10080,
|
|
739
|
+
resets_at_utc=_synthetic_instant_days_ago(-5),
|
|
740
|
+
threshold=threshold,
|
|
741
|
+
kind="weekly",
|
|
742
|
+
crossed_at_utc=now_utc_iso(),
|
|
743
|
+
qualifying_percent=float(threshold),
|
|
744
|
+
projected_percent=min(100.0, float(threshold) + 5.0),
|
|
745
|
+
account_key=_test_alert_account_key("codex"),
|
|
746
|
+
)
|
|
606
747
|
else:
|
|
607
748
|
payload = _build_alert_payload_five_hour(
|
|
608
749
|
threshold=threshold,
|