cctally 1.103.0 → 1.105.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 +94 -0
- package/README.md +6 -6
- package/bin/_cctally_alerts.py +104 -10
- package/bin/_cctally_cache.py +193 -71
- package/bin/_cctally_config.py +62 -2
- package/bin/_cctally_core.py +62 -1
- package/bin/_cctally_dashboard.py +694 -70
- package/bin/_cctally_dashboard_conversation.py +400 -6
- package/bin/_cctally_dashboard_envelope.py +336 -14
- package/bin/_cctally_dashboard_perf.py +93 -0
- package/bin/_cctally_dashboard_share.py +56 -25
- package/bin/_cctally_dashboard_sources.py +988 -94
- package/bin/_cctally_db.py +30 -3
- package/bin/_cctally_diagnosis_sources.py +699 -186
- package/bin/_cctally_doctor.py +77 -0
- package/bin/_cctally_forecast.py +932 -51
- package/bin/_cctally_journal.py +430 -24
- package/bin/_cctally_parser.py +66 -0
- package/bin/_cctally_project.py +535 -10
- package/bin/_cctally_quota.py +28 -0
- package/bin/_cctally_quota_calibration.py +146 -0
- package/bin/_cctally_quota_model.py +2142 -0
- package/bin/_cctally_record.py +204 -30
- package/bin/_cctally_share.py +16 -8
- package/bin/_cctally_statusline.py +34 -0
- package/bin/_cctally_tui.py +531 -115
- package/bin/_lib_codex_conversation_query.py +258 -12
- package/bin/_lib_codex_hooks.py +26 -0
- package/bin/_lib_conversation_query.py +145 -15
- package/bin/_lib_dashboard_json.py +105 -0
- package/bin/_lib_dashboard_settings_contract.py +2 -0
- package/bin/_lib_diagnosis.py +21 -2
- package/bin/_lib_doctor.py +215 -1
- package/bin/_lib_forecast.py +337 -43
- package/bin/_lib_ingest_frontier.py +889 -0
- package/bin/_lib_meter_rate_change.py +360 -0
- package/bin/_lib_perf.py +22 -0
- package/bin/_lib_pricing.py +30 -3
- package/bin/_lib_quota_calibration.py +311 -0
- package/bin/_lib_quota_copy.py +157 -0
- package/bin/_lib_quota_model.py +2520 -0
- package/bin/_lib_rate_change_delivery.py +119 -0
- package/bin/_lib_record.py +50 -0
- package/bin/_lib_rederive.py +10 -0
- package/bin/_lib_render.py +6 -0
- package/bin/_lib_retained_size.py +187 -0
- package/bin/_lib_share_templates.py +37 -5
- package/bin/_lib_snapshot_cache.py +504 -17
- package/bin/_lib_statusline.py +200 -2
- package/bin/_lib_tick_stats.py +28 -4
- package/bin/_lib_view_models.py +30 -12
- package/bin/cctally +32 -0
- package/dashboard/static/assets/ConversationsView-DA4j7Yso.js +72 -0
- package/dashboard/static/assets/DoctorModal-D4PbVnbE.js +1 -0
- package/dashboard/static/assets/ModalRoot-D4oSPxPp.js +1 -0
- package/dashboard/static/assets/ProjectsDrillPanel-DkPlB9JH.js +1 -0
- package/dashboard/static/assets/SourceDetailModal-BE7FONsS.js +1 -0
- package/dashboard/static/assets/UpdateModal-BJuI8glf.js +7 -0
- package/dashboard/static/assets/index-klO46NcU.css +1 -0
- package/dashboard/static/assets/index-w_bINkJ8.js +13 -0
- package/dashboard/static/assets/outlineNavigation-zDVm6Hdd.js +9 -0
- package/dashboard/static/assets/useKeymap-CJ-Pi17D.js +1 -0
- package/dashboard/static/dashboard.html +3 -2
- package/package.json +10 -1
- package/dashboard/static/assets/index-Di2hljvB.css +0 -1
- package/dashboard/static/assets/index-XYCIWjVG.js +0 -97
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,100 @@ based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [1.105.0] - 2026-09-02
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- `cctally` prices Claude Fable 5.1 and Claude Mythos 5.1, including their cache reads at $0.25 per million tokens, so usage on either model is costed instead of contributing nothing with only a warning.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- `cctally quota` counts Claude Fable 5.1 and Claude Mythos 5.1 as part of the Fable 5 and Mythos 5 families, so a week containing that usage no longer has its weekly-quota fit withheld as an unsupported model mix.
|
|
15
|
+
- `cctally quota` refits your stored calibration once after this upgrade, because the set of model names it recognises has changed.
|
|
16
|
+
- `cctally quota` excludes fast-mode requests that Anthropic bills to usage credits, and widens its family-composition support floor to the corrected empirical bound.
|
|
17
|
+
- The dashboard's Forecast basis and `cctally statusline` use one vocabulary; the status line remains a meter-only hot path, while Forecast can name its calibrated model.
|
|
18
|
+
- The dashboard's Conversation viewer opens long histories from a bounded, integrity-checked outline ticket, hydrates the exact account-scoped outline automatically, and skips unchanged transcript estates between updates.
|
|
19
|
+
- `cctally dashboard` compresses and caches content-hashed assets and loads conversations and detail views only when opened, cutting cold JavaScript transfer while warm reloads reuse unchanged code.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
- `cctally dashboard` shares a progressive outline's first-chunk build, cancels abandoned transfers, and rejects an oversized outline before allocating its encoded body.
|
|
23
|
+
- `cctally dashboard` applies failed or over-limit background memory checks as soon as they finish, including while sync is paused, and its soak gate now distinguishes measured growth from noisy flat samples.
|
|
24
|
+
- `cctally dashboard` keeps production-size cache admission checks inside the whole-process memory ceiling without shrinking or evicting the retained caches.
|
|
25
|
+
- `cctally dashboard` stops loading a previous conversation's outline after you switch sessions, so the new reader stays current and avoids duplicate outline requests.
|
|
26
|
+
- Concurrent dashboard Explain requests share identical in-flight work and admit only one diagnosis process tree at a time, so multiple tabs or LAN clients no longer multiply provider workers and transient memory.
|
|
27
|
+
- `cctally dashboard` bounds retained source, snapshot, conversation, ingest, and SSE work; invalidates conversation caches after same-path database replacement; keeps the newest slow-tab update; and releases owners without slowing refreshes.
|
|
28
|
+
- `cctally quota` now records a metering-rate change it detected earlier but could not store at the time, so a busy or rebuilding database no longer costs you that entry in your rate-change history permanently.
|
|
29
|
+
- `cctally quota` delivers a metering-rate change notification that an interrupted write recorded but never announced, and remembers which ones it has announced, so upgrading never replays your whole rate-change history at once.
|
|
30
|
+
- `cctally quota` retries a metering-rate notification that was recorded without an account attribution, including on a run narrowed with `--account`, so an install that later adds a second Claude account no longer loses that announcement.
|
|
31
|
+
- A metering-rate change notification names the account whose rate changed, so two accounts changing rate no longer produce two identical popups. An install with one account, or none, sees exactly the notification it saw before.
|
|
32
|
+
- `cctally explain` and the dashboard's Explain modal build current, baseline and conversation-derived evidence within their documented warm and cold latency budgets without caching a stale report or slowing dashboard updates.
|
|
33
|
+
- `cctally doctor` reports `not assessed` rather than `no change detected` when no quota calibration has been fitted, so a withheld fit no longer reads as a confirmed all-clear about your metering rate.
|
|
34
|
+
- `cctally quota` records the durable metering-rate history whenever its detector confirms a change, even when the fitted budget is withheld because this week's composition sits outside the calibration's support.
|
|
35
|
+
- The metering-rate notification says the calibration was withheld, and names why, instead of promising a fitted budget it cannot supply.
|
|
36
|
+
- `cctally doctor` stops telling you to run `cctally quota` for a fitted budget when the calibration behind the change has none, and names the withholding instead.
|
|
37
|
+
- The dashboard's rate-change toast points at the evidence for a metering-rate change rather than promising a fitted budget.
|
|
38
|
+
- `cctally dashboard` skips both provider file estates on caught-up ticks, falls back to a full pass when hook activity cannot be recorded, and reports the main refresh loop's measured CPU duty.
|
|
39
|
+
- `cctally dashboard` reuses unchanged Codex accounting and account-card populations across quota and one-file updates, shortening active refreshes without changing cadence or freshness.
|
|
40
|
+
- `cctally quota` withholds malformed retained timestamps without a traceback, filters offset-bearing credit times by their instant, and emits every JSON timestamp with a trailing `Z`.
|
|
41
|
+
- Historical forecast replays ignore mid-week reset events until their recorded detection instant, so a future event can no longer erase the samples that were available at the replayed time.
|
|
42
|
+
- Weekly projected-pace alerts use the same model-backed value as `cctally forecast` when a quota calibration applies, instead of silently stopping for calibrated users.
|
|
43
|
+
- The dashboard's Forecast modal keeps an extreme projection label inside the card instead of clipping it at the 110% track edge.
|
|
44
|
+
- `cctally record-usage` no longer fabricates a percent milestone from a stale pre-credit reading after Anthropic zeroes your weekly counter, so a fresh week's milestone ladder no longer opens at a percentage you never reached.
|
|
45
|
+
|
|
46
|
+
## [1.104.0] - 2026-08-29
|
|
47
|
+
|
|
48
|
+
### Added
|
|
49
|
+
- `cctally quota` reports how much of your weekly quota your usage consumes and whether Anthropic has changed the metering rate, from a budget fitted to your own history rather than a shipped constant. See `docs/commands/quota.md`.
|
|
50
|
+
- `cctally quota --json` publishes the fit, the current week's consumption and projection, the composition support radii and the detector's own bound under `schemaVersion` 1.
|
|
51
|
+
- `cctally quota --reset-calibration` discards the stored calibration for one account, and `--since` and `--watch-from` narrow or override the window the analysis uses.
|
|
52
|
+
- `bin/cctally-preflight` compiles every Python file in the repository, not only the ones beside it, so a syntax error anywhere in the tree now blocks a test run instead of surfacing during one.
|
|
53
|
+
- `cctally forecast --json` states the reading its arithmetic used and where its projection came from, through `weekly_percent_corrected`, `right_censored`, `projection_basis`, `projection_code` and `calibration_code`.
|
|
54
|
+
- `cctally forecast --json` moves to `schemaVersion` 2: its three projection fields can now be null, and they are measured from a different reading than before.
|
|
55
|
+
- `cctally project --json` moves to `schemaVersion` 2. `attributedUsedPercent` and `costPerPercent` keep their spelling and change their meaning, from a share of the window's cost to modelled weekly quota.
|
|
56
|
+
- `cctally project` reports each project's `Used %` as modelled quota units rather than as its share of the window's dollars, so a cache-heavy project no longer reads high and an output-heavy Opus one no longer reads low.
|
|
57
|
+
- `cctally doctor` reports a new Quota category: whether Anthropic changed your weekly metering rate, and whether the stored calibration is usable. Neither check can fail, so neither changes doctor's exit code.
|
|
58
|
+
- `cctally quota` records a durable event when it confirms a metering-rate change, so it and `doctor` report the change with no configuration. The desktop notification stays off until you set `alerts.rate_change_enabled`.
|
|
59
|
+
- `cctally project` states its four modelled-quota figures under the table — window total, listed-row total, filtered or unmodelled, and the meter's reading minus the modelled total — where before they were in `--json` only.
|
|
60
|
+
- The status line's 7d slot shows where the week is heading and which measurement said so, as `7d 42% (2d 3h) → 58% meter`, plus a `Δrate` marker while a metering-rate change is in force.
|
|
61
|
+
- The dashboard's Forecast panel names the basis its projection came from and shows a `Δ rate` chip while a metering-rate change is in force.
|
|
62
|
+
- The dashboard's Forecast modal adds the modelled consumption, the headroom, what the meter reading covers, and the meter's reading minus the modelled total.
|
|
63
|
+
- The dashboard's Recent Alerts modal lists metering-rate changes in their own section, with the size and direction of the change and the instant it took effect, and a rate change now raises its own toast.
|
|
64
|
+
- `alerts.rate_change_enabled` is settable with `cctally config set` and from the dashboard's Settings overlay, so turning the notification on no longer means editing `config.json` by hand.
|
|
65
|
+
- `bin/cctally-test-all` refuses to start when the recorded test estate disagrees with what this tree collects, when a recorded test disappeared without a declaration authorizing it, or when it cannot derive the estate at all.
|
|
66
|
+
- `bin/cctally-test-all --with-estate-check` runs that check on a `--harness` subset, which otherwise skips it to keep a targeted run fast.
|
|
67
|
+
|
|
68
|
+
### Changed
|
|
69
|
+
- `cctally forecast`, the TUI and the dashboard measure your pace from what you consumed rather than from the rounded-up meter reading, so rates and projections read about half a point lower and daily budgets a little larger.
|
|
70
|
+
- At a displayed 100% the forecast shows no projection, no time-to-cap and no daily budget, because a capped meter says only that you are past 99%. `cctally quota` still models consumption there, from tokens.
|
|
71
|
+
- The projected-pace weekly alert stays quiet once `cctally quota` has fitted your budget, rather than firing on a pace the forecast no longer shows you.
|
|
72
|
+
- `cctally forecast`'s trailing four-week dollar rate divides each prior week's cost by the meter movement that week realized, not by its post-credit high-water mark, which priced one measured week at $355 per point against a true $25.
|
|
73
|
+
- A prior week whose realized movement cannot be established is left out of the forecast's four-week median rather than guessed at, and so is a week whose meter reached 100%.
|
|
74
|
+
- `cctally project` withholds its meter-versus-modelled difference when the requested range slices a subscription week, instead of stating a whole-week difference beside a three-day range.
|
|
75
|
+
- `cctally forecast` names its dollar rate's source in words — `trailing 4wk median (drift-reduced confidence)` rather than the bare code — and says when the comparability test behind that rate could not run.
|
|
76
|
+
- `rates.week_average_pct_per_hour` in `cctally forecast --json` is null at a reading of 100% or more, because a capped meter supplies no rate to average.
|
|
77
|
+
- `cctally quota` refuses to predict from a metering rate it has only just detected. It says so, and the forecast, the TUI and the dashboard fall back to the meter and state the reason, rather than projecting from a three-day fit.
|
|
78
|
+
- `bin/cctally-preflight` lists the repository once instead of once per check, so widening the Python check to the whole tree costs one pass rather than two.
|
|
79
|
+
- `docs/commands/report.md` states that `$ / 1%` falls when the provider charges more points for the same tokens, so a drop can read as improved efficiency, and points at `cctally quota` as the detector. The metric itself is unchanged.
|
|
80
|
+
- `cctally project` names the absent operand when it withholds the meter-versus-modelled difference — a modelled week with no snapshot, or a window that modelled no week — instead of reporting either as a misaligned population.
|
|
81
|
+
- `bin/cctally-test-all` builds both of its pytest phases from one recorded declaration rather than from hard-coded file names, and refuses at startup when a named target no longer exists.
|
|
82
|
+
- `bin/cctally-test-all` removes `PYTEST_ADDOPTS` and `PYTEST_PLUGINS` from both pytest phases so neither can narrow a run; set `CCTALLY_PYTEST_DURATIONS=1` for per-test timings instead.
|
|
83
|
+
|
|
84
|
+
### Fixed
|
|
85
|
+
- The dashboard keeps the full account-count value visible in the `All` hero at tablet widths instead of clipping it mid-word.
|
|
86
|
+
- A shared forecast artifact writes a ceiling more than 30 days out as `>30d` instead of printing the literal figure, which on a barely-used week read `1166.8`.
|
|
87
|
+
- The terminal forecast's explain modal says why it shows no daily budgets at a capped meter, instead of printing the heading over nothing.
|
|
88
|
+
- `bin/cctally-preflight` bounds every command it runs, including the harness-ownership check, and reports a hung system bash version probe rather than silently skipping the bash 3.2 floor check.
|
|
89
|
+
- `bin/cctally-preflight` stops its shell syntax sweep and reports what went unchecked when an interpreter hangs, rather than spending its full per-file timeout on every remaining file.
|
|
90
|
+
- `cctally project` withholds modelled quota on a multi-account install whose account registry cannot be read, instead of falling back to a cost share under the `Used %` column.
|
|
91
|
+
- `cctally forecast` leaves a prior week out of its four-week median when the recorded credit or reset rows for that week cannot be read, instead of summing the week as if the credit had zeroed the meter.
|
|
92
|
+
- `cctally forecast` keeps prior weeks from before a metering-rate change out of its four-week median even when it cannot read the entry cache, instead of admitting every one of them after a single failed read.
|
|
93
|
+
- The dashboard hero shows no week-over-week `$ / 1%` comparison when it has no `$ / 1%` to compare, instead of printing a change beside a dash.
|
|
94
|
+
- The dashboard's Forecast panel keeps its pace bar visible and its content inside the card when the source reports a degradation, where the bar collapsed to nothing and the panel overflowed.
|
|
95
|
+
- The dashboard's Forecast modal says when a published `$ / 1%` rate was measured over a reduced or unverified population, where before the qualification was only visible in `--json`.
|
|
96
|
+
- A metering-rate change desktop notification shows the effective date in your own timezone rather than in UTC.
|
|
97
|
+
- Interrupting `cctally quota` with Ctrl-C while it records a metering-rate change stops the command, rather than printing a recording failure and carrying on.
|
|
98
|
+
- `cctally quota` carries on when it cannot read your stored calibration file, rather than stopping with a Python traceback.
|
|
99
|
+
- The status line's 7d slot shows no projection once the stored reset instant has passed, rather than projecting over a week that has already ended.
|
|
100
|
+
- The dashboard's Forecast panel puts its verdict, status and `Δ rate` chips on one line instead of stacking them, giving the card back a row of height. The Forecast modal's Quota model heading is now tinted like every other section heading.
|
|
101
|
+
|
|
8
102
|
## [1.103.0] - 2026-08-26
|
|
9
103
|
|
|
10
104
|
### Added
|
package/README.md
CHANGED
|
@@ -30,15 +30,15 @@ Your Claude Code plan meters you with a percentage that creeps up all week. ccta
|
|
|
30
30
|
</p>
|
|
31
31
|
|
|
32
32
|
<!-- cctally:latest-stable:begin -->
|
|
33
|
-
**Latest stable: v1.
|
|
33
|
+
**Latest stable: v1.104.0** (2026-08-29)
|
|
34
34
|
|
|
35
|
-
Highlights from the `v1.
|
|
35
|
+
Highlights from the `v1.101.0` to `v1.104.0` stable upgrade:
|
|
36
36
|
|
|
37
|
-
-
|
|
38
|
-
-
|
|
39
|
-
-
|
|
37
|
+
- `cctally quota` reports how much of your weekly quota your usage consumes and whether Anthropic has changed the metering rate, from a budget fitted to your own history rather than a shipped constant. See `docs/commands/quota.md`.
|
|
38
|
+
- `cctally quota --json` publishes the fit, the current week's consumption and projection, the composition support radii and the detector's own bound under `schemaVersion` 1.
|
|
39
|
+
- `cctally quota --reset-calibration` discards the stored calibration for one account, and `--since` and `--watch-from` narrow or override the window the analysis uses.
|
|
40
40
|
|
|
41
|
-
[See every change in this stable upgrade](https://github.com/omrikais/cctally/releases/tag/v1.
|
|
41
|
+
[See every change in this stable upgrade](https://github.com/omrikais/cctally/releases/tag/v1.104.0)
|
|
42
42
|
<!-- cctally:latest-stable:end -->
|
|
43
43
|
|
|
44
44
|
## Quick start
|
package/bin/_cctally_alerts.py
CHANGED
|
@@ -140,6 +140,73 @@ def _alert_text_quota(payload: dict, _tz) -> tuple[str, str, str]:
|
|
|
140
140
|
# dashboard alert envelope, so the CLI line is the only routing it gets.
|
|
141
141
|
return title, subtitle, _with_next_step(body, payload, _tz)
|
|
142
142
|
|
|
143
|
+
|
|
144
|
+
def _alert_text_meter_rate_change(payload: dict, tz) -> tuple[str, str, str]:
|
|
145
|
+
"""#661 S2 §6.1: the non-threshold family's own copy.
|
|
146
|
+
|
|
147
|
+
It states the direction and the size of the change, and it states them as
|
|
148
|
+
a rate the provider sets rather than as anything cctally can act on. There
|
|
149
|
+
is no threshold to name, which is the whole reason this family is not an
|
|
150
|
+
`AXIS_REGISTRY` member.
|
|
151
|
+
|
|
152
|
+
THREE deliberate divergences from the threshold builders, all recorded
|
|
153
|
+
in `docs/alerts-gotchas.md` (#661 S2 Stage C review, F7b; the third
|
|
154
|
+
added by #688):
|
|
155
|
+
|
|
156
|
+
* The body does not end with `_with_next_step`. That helper derives its
|
|
157
|
+
affordance through `alert_next_step_command`, which branches on
|
|
158
|
+
`AXIS_REGISTRY` axes; this family is deliberately not one, so it has no
|
|
159
|
+
window to scope and no branch to reach. The affordance is stated
|
|
160
|
+
directly instead.
|
|
161
|
+
* This builder and `_alert_text_quota` above it live in this glue module
|
|
162
|
+
rather than in the pure `_lib_alerts_payload` kernel, which holds the
|
|
163
|
+
other six. That is the placement both non-registry families use.
|
|
164
|
+
* The BODY varies on a payload field, which no threshold builder does.
|
|
165
|
+
#688 records a transition whenever the detector qualifies, including
|
|
166
|
+
when the command's own verdict is withheld for a reason that bears on
|
|
167
|
+
predicting forward, so a recorded transition may sit beside a verdict
|
|
168
|
+
`cctally quota` refuses. Sending that user to a fitted budget that
|
|
169
|
+
does not exist is a false instruction, so the body names the
|
|
170
|
+
withholding instead and points at the evidence.
|
|
171
|
+
|
|
172
|
+
NOT a fourth divergence (#697): the standard R8 `[<label>] ` title prefix
|
|
173
|
+
is applied DOWNSTREAM by `_dispatch_alert_notification`, which resolves the
|
|
174
|
+
account registry from this payload's own `provider` field rather than from
|
|
175
|
+
`_AXIS_VENDOR`. The prefix is shared behaviour every axis takes, so this
|
|
176
|
+
builder keeps its signature and its return and never learns about accounts.
|
|
177
|
+
|
|
178
|
+
`effective_from` IS routed through `format_display_dt`. It is a clock
|
|
179
|
+
instant, not a calendar day, so the carve-out `_alert_text_weekly`
|
|
180
|
+
documents for `week_start_date` does not apply: slicing its first ten
|
|
181
|
+
characters showed a user at UTC+13 or UTC-8 a date one day away from
|
|
182
|
+
their own.
|
|
183
|
+
"""
|
|
184
|
+
previous = float(payload.get("previous_units_per_point") or 0.0)
|
|
185
|
+
new = float(payload.get("new_units_per_point") or 0.0)
|
|
186
|
+
provider = str(payload.get("provider") or "claude").capitalize()
|
|
187
|
+
if previous > 0.0 and new > 0.0:
|
|
188
|
+
change = (new - previous) / previous * 100.0
|
|
189
|
+
direction = "less" if change < 0 else "more"
|
|
190
|
+
subtitle = (
|
|
191
|
+
f"each meter point now covers {abs(change):.0f}% {direction} usage")
|
|
192
|
+
else:
|
|
193
|
+
subtitle = "the rate this account is metered at changed"
|
|
194
|
+
raw = str(payload.get("effective_from") or "")
|
|
195
|
+
try:
|
|
196
|
+
effective = _lib_alerts_payload.format_display_dt(
|
|
197
|
+
raw, tz, fmt="%Y-%m-%d", suffix=False) if raw else ""
|
|
198
|
+
except ValueError:
|
|
199
|
+
effective = ""
|
|
200
|
+
withheld = payload.get("withholding_status")
|
|
201
|
+
if withheld:
|
|
202
|
+
tail = (f"The calibration was withheld ({withheld}), so no fitted "
|
|
203
|
+
f"budget is available. Run `cctally quota` for the evidence.")
|
|
204
|
+
else:
|
|
205
|
+
tail = "Run `cctally quota` for the fitted budget and its evidence."
|
|
206
|
+
body = f"Effective {effective}. {tail}" if effective else tail
|
|
207
|
+
return f"cctally - {provider} metering rate changed", subtitle, body
|
|
208
|
+
|
|
209
|
+
|
|
143
210
|
# Phase B: severity policy + the cross-platform dispatch kernel. The kernel is
|
|
144
211
|
# pure (parameterized on platform + which_on_path); this module is the I/O glue
|
|
145
212
|
# that injects the real sys.platform / shutil.which and spawns with shell=False.
|
|
@@ -177,7 +244,8 @@ _AXIS_VENDOR = {
|
|
|
177
244
|
}
|
|
178
245
|
|
|
179
246
|
|
|
180
|
-
def _alert_label_prefix(axis: str, account_key: "str | None"
|
|
247
|
+
def _alert_label_prefix(axis: str, account_key: "str | None",
|
|
248
|
+
provider: "object | None" = None) -> str:
|
|
181
249
|
"""Return ``"[<label>] "`` for an account-specific crossing when the vendor
|
|
182
250
|
has >1 real account (R8), else ``""``. Best-effort + never-raise: a missing
|
|
183
251
|
DB / read error / vendor-wide (`*`) row yields no prefix, so a ≤1-real-account
|
|
@@ -186,13 +254,26 @@ def _alert_label_prefix(axis: str, account_key: "str | None") -> str:
|
|
|
186
254
|
Delegates the R8 gate + label precedence to the single-definition helpers
|
|
187
255
|
``_cctally_account.real_account_count`` / ``account_label`` (P2-CQ1) so the
|
|
188
256
|
">1 real account" trigger and the key->label map have exactly one home; this
|
|
189
|
-
wrapper only opens the RO connection and stays best-effort/never-raise.
|
|
257
|
+
wrapper only opens the RO connection and stays best-effort/never-raise.
|
|
258
|
+
|
|
259
|
+
#697: the VENDOR comes from the payload's own ``provider`` when it carries
|
|
260
|
+
one, and from ``_AXIS_VENDOR`` otherwise. The metering-rate-change family
|
|
261
|
+
is the only payload that carries the key, and it is authoritative there —
|
|
262
|
+
the family's visible title is derived from that same field, so a static map
|
|
263
|
+
entry could send a Codex title to Claude's registry with nothing reporting
|
|
264
|
+
the disagreement. The lookup is normalized because ``real_account_count``
|
|
265
|
+
matches ``accounts.provider`` exactly, and an unmatched provider degrades
|
|
266
|
+
silently to no prefix. The whole resolution sits INSIDE the guard so that a
|
|
267
|
+
provider whose ``__str__`` raises cannot escape the never-raise contract.
|
|
268
|
+
"""
|
|
190
269
|
if not account_key or account_key == _lib_accounts.VENDOR_WIDE:
|
|
191
270
|
return ""
|
|
192
|
-
vendor = _AXIS_VENDOR.get(axis)
|
|
193
|
-
if vendor is None:
|
|
194
|
-
return ""
|
|
195
271
|
try:
|
|
272
|
+
vendor = str(provider).strip().lower() if provider is not None else ""
|
|
273
|
+
if not vendor:
|
|
274
|
+
vendor = _AXIS_VENDOR.get(axis)
|
|
275
|
+
if vendor is None:
|
|
276
|
+
return ""
|
|
196
277
|
import sqlite3 as _sq
|
|
197
278
|
import _cctally_account
|
|
198
279
|
db_path = _cctally_core.DB_PATH
|
|
@@ -293,6 +374,8 @@ def _dispatch_alert_notification(
|
|
|
293
374
|
title, subtitle, body = _alert_text_projected(payload, tz)
|
|
294
375
|
elif axis == "quota":
|
|
295
376
|
title, subtitle, body = _alert_text_quota(payload, tz)
|
|
377
|
+
elif axis == "meter_rate_change":
|
|
378
|
+
title, subtitle, body = _alert_text_meter_rate_change(payload, tz)
|
|
296
379
|
else:
|
|
297
380
|
title, subtitle, body = (
|
|
298
381
|
"cctally - alert",
|
|
@@ -303,17 +386,28 @@ def _dispatch_alert_notification(
|
|
|
303
386
|
# R8 label prefix (#341): an account-specific crossing gains a `[<label>] `
|
|
304
387
|
# title prefix ONLY when the vendor has >1 real account. Byte-identical at
|
|
305
388
|
# <=1 real account (empty prefix). Applied to the notification title only.
|
|
389
|
+
# #697: the vendor whose registry is counted comes from the payload's own
|
|
390
|
+
# `provider` when it carries one, and from `_AXIS_VENDOR` otherwise.
|
|
306
391
|
account_key = payload.get("account_key")
|
|
307
|
-
title = _alert_label_prefix(axis, account_key) + title
|
|
392
|
+
title = _alert_label_prefix(axis, account_key, payload.get("provider")) + title
|
|
308
393
|
|
|
309
394
|
# Severity (3-tier) drives both the notify-send urgency token and the
|
|
310
395
|
# trailing log column. A missing threshold (defensive — shouldn't happen for
|
|
311
396
|
# a real crossing) floors at "info".
|
|
312
397
|
threshold = payload.get("threshold")
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
398
|
+
if axis == "meter_rate_change":
|
|
399
|
+
# #661 S2 §6.1: this family carries an EXPLICIT severity, because a
|
|
400
|
+
# rate transition has no percentage threshold for `severity_for` to
|
|
401
|
+
# map. Reading it verbatim is what keeps the three tiers meaningful
|
|
402
|
+
# here; falling through would floor every transition at `info`.
|
|
403
|
+
severity = str(payload.get("severity") or "info")
|
|
404
|
+
if severity not in ("info", "warn", "alarm"):
|
|
405
|
+
severity = "info"
|
|
406
|
+
else:
|
|
407
|
+
try:
|
|
408
|
+
severity = severity_for(int(threshold)) if threshold is not None else "info"
|
|
409
|
+
except (TypeError, ValueError):
|
|
410
|
+
severity = "info"
|
|
317
411
|
urgency = severity_to_urgency(severity)
|
|
318
412
|
|
|
319
413
|
if platform is None:
|