cctally 1.104.0 → 1.106.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.
Files changed (71) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/README.md +6 -6
  3. package/bin/_cctally_alerts.py +46 -13
  4. package/bin/_cctally_cache.py +193 -71
  5. package/bin/_cctally_core.py +151 -20
  6. package/bin/_cctally_dashboard.py +703 -297
  7. package/bin/_cctally_dashboard_conversation.py +438 -6
  8. package/bin/_cctally_dashboard_envelope.py +96 -10
  9. package/bin/_cctally_dashboard_perf.py +93 -0
  10. package/bin/_cctally_dashboard_share.py +7 -1
  11. package/bin/_cctally_dashboard_sources.py +988 -94
  12. package/bin/_cctally_db.py +297 -8
  13. package/bin/_cctally_diagnosis_sources.py +699 -186
  14. package/bin/_cctally_doctor.py +62 -23
  15. package/bin/_cctally_five_hour.py +53 -6
  16. package/bin/_cctally_forecast.py +205 -42
  17. package/bin/_cctally_journal.py +515 -67
  18. package/bin/_cctally_milestone_history.py +141 -58
  19. package/bin/_cctally_milestones.py +72 -0
  20. package/bin/_cctally_parser.py +10 -6
  21. package/bin/_cctally_percent_breakdown.py +26 -20
  22. package/bin/_cctally_project.py +5 -3
  23. package/bin/_cctally_quota.py +27 -2
  24. package/bin/_cctally_quota_model.py +596 -62
  25. package/bin/_cctally_record.py +1373 -353
  26. package/bin/_cctally_reporting.py +19 -0
  27. package/bin/_cctally_share.py +21 -3
  28. package/bin/_cctally_statusline.py +15 -11
  29. package/bin/_cctally_tui.py +509 -112
  30. package/bin/_cctally_weekrefs.py +322 -161
  31. package/bin/_lib_codex_conversation_query.py +258 -12
  32. package/bin/_lib_codex_hooks.py +26 -0
  33. package/bin/_lib_conversation_query.py +145 -15
  34. package/bin/_lib_credit.py +9 -2
  35. package/bin/_lib_credit_identity.py +159 -0
  36. package/bin/_lib_credit_selection.py +342 -0
  37. package/bin/_lib_dashboard_json.py +105 -0
  38. package/bin/_lib_dashboard_sources.py +16 -4
  39. package/bin/_lib_diagnosis.py +21 -2
  40. package/bin/_lib_doctor.py +65 -9
  41. package/bin/_lib_ingest_frontier.py +935 -0
  42. package/bin/_lib_meter_rate_change.py +95 -29
  43. package/bin/_lib_perf.py +22 -0
  44. package/bin/_lib_pricing.py +30 -3
  45. package/bin/_lib_quota_copy.py +30 -4
  46. package/bin/_lib_quota_model.py +249 -56
  47. package/bin/_lib_rate_change_delivery.py +119 -0
  48. package/bin/_lib_record.py +82 -0
  49. package/bin/_lib_rederive.py +8 -0
  50. package/bin/_lib_render.py +72 -4
  51. package/bin/_lib_retained_size.py +187 -0
  52. package/bin/_lib_snapshot_cache.py +517 -27
  53. package/bin/_lib_statusline.py +3 -29
  54. package/bin/_lib_subscription_weeks.py +141 -54
  55. package/bin/_lib_tick_stats.py +28 -4
  56. package/bin/_lib_view_models.py +128 -22
  57. package/bin/cctally +7 -0
  58. package/dashboard/static/assets/ConversationsView-Dst33C0n.js +72 -0
  59. package/dashboard/static/assets/DoctorModal-5rvkyksy.js +1 -0
  60. package/dashboard/static/assets/ModalRoot-BJH02VzE.js +1 -0
  61. package/dashboard/static/assets/ProjectsDrillPanel-CsR1bjJu.js +1 -0
  62. package/dashboard/static/assets/SourceDetailModal-BnTIwt-R.js +1 -0
  63. package/dashboard/static/assets/UpdateModal-D_RfssnW.js +7 -0
  64. package/dashboard/static/assets/index-rzXw99Hy.js +13 -0
  65. package/dashboard/static/assets/index-u3wLfjt2.css +1 -0
  66. package/dashboard/static/assets/outlineNavigation-C7J9lhGB.js +9 -0
  67. package/dashboard/static/assets/useKeymap-B3KCc_c7.js +1 -0
  68. package/dashboard/static/dashboard.html +3 -2
  69. package/package.json +6 -1
  70. package/dashboard/static/assets/index-D19TO7Mg.js +0 -97
  71. package/dashboard/static/assets/index-klO46NcU.css +0 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,76 @@ based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [1.106.0] - 2026-09-03
9
+
10
+ ### Changed
11
+ - `cctally record-credit --force` replaces the single credit `--at` names instead of clearing the whole week, and refuses when `--at` names none. Recording a second credit in one week needs no `--force`: rerun with a different `--at`.
12
+ - A week Anthropic credited in place renders as one row on its own boundaries, instead of two rows split at the credit. A credit that also moved the week's declared end is marked, but still renders on the windows the API stated.
13
+ - The dashboard's Current Week header and modal name the week's own boundaries on a credited week, instead of the window that starts at the credit.
14
+ - A credited week's row is marked `+` in `cctally report` and `cctally weekly`, so a low `Used %` beside a week's worth of spend has a visible explanation.
15
+ - The dashboard's Weekly panel and its week detail mark a credited week, and the week detail, the Current Week card and its modal name why `$/1%` is withheld instead of the em-dash or `$0.000` you cannot tell from a week with no usage.
16
+ - The dashboard's `$/1%` tables — the Weekly modal's week list, the `$/1% Trend` panel and its modal — say `No climb` for a credited week whose ratio is withheld, instead of the em-dash you cannot tell from a week with no usage.
17
+ - `$/1%` for a credited week is measured from the credit forward, over the climb since it. With no climb yet the figure is withheld and names why. Uncredited weeks are unchanged.
18
+ - The dashboard's Current Week card, the TUI header and a shared Current Week recap report a credited week's whole spend, matching the week they name and the Weekly card beside them. Only `$/1%` is measured from the credit forward.
19
+ - `cctally milestone` history lists a credited week once instead of once per credit, and its detail separates the week's percent ladders with a row naming each credit and the level it dropped from.
20
+
21
+ ### Fixed
22
+ - A conversation open in the viewer updates its own cost as its turns stream, for both Claude and Codex. The live tail advanced the transcript but not the accounting the cost is read from.
23
+ - A Codex conversation's cost no longer reads `$0.00` long after its turns finish. The dashboard now re-scans each provider at least every two minutes, so accounting stays current even when that provider's activity hooks never fire.
24
+ - Upgrading an existing installation to v1.105.0 left every background sync failing and the dashboard stuck on `server sync error`. The two columns that release added now reach an existing database, not only a freshly created one.
25
+ - After Anthropic zeroes your weekly counter, every 7d surface lowers on the next status-line tick. A reading captured in the same hour no longer holds the week at its pre-credit percentage, and no manual command is needed.
26
+ - A goodwill credit to a non-zero level records that level's own percent milestone. Before, the ladder for the new period opened one percent late, and `cctally percent-breakdown` was missing its first row.
27
+ - `cctally percent-breakdown`, the TUI's milestone panel and the milestone writer agree on which credit a percent belongs to, including after a database rebuild and for a credit recorded with `cctally record-credit`.
28
+ - A percent milestone crossed after a credit reports the cost spent since that credit. After a credit recorded with `cctally record-credit` it reported the whole week's spend, which made the first post-credit `$/1%` figure far too high.
29
+ - `cctally record-credit` no longer deletes genuine usage recorded between the moment you assert with `--at` and the moment you run the command.
30
+ - On an install with more than one Claude account, a reset detected for one account no longer records a credit for another, and no longer discards the first account's pending detection.
31
+ - `cctally forecast` measures its rate from the latest credit rather than across it, including one you recorded yourself. `cctally budget` keeps summing the whole week, because the money was spent inside one unchanged window.
32
+ - `cctally doctor`'s post-credit milestone check now sees a week credited with `cctally record-credit`, which it silently skipped before.
33
+ - `cctally record-credit` accepts a second credit inside the same hour as the first. Only a credit at the identical instant is still refused, because that is indistinguishable from running the command twice.
34
+ - An install upgrading from a version that predates the append-only journal now gets the current weekly-credit table shape, instead of reporting the new database version while missing the columns a credit needs.
35
+ - `cctally record-credit --force` no longer leaves a replaced credit's percent milestones behind after a database rebuild, and `cctally record-usage` no longer leaves a removed stale reading's five-hour milestones pointing at it.
36
+ - `cctally` no longer records a second, automatically-shaped credit for a large credit you already recorded with `cctally record-credit`.
37
+ - A stale pre-credit reading replayed by the status line after a credit is removed on the next tick that contradicts it, instead of holding every 7d surface at the old percentage and provoking a second, phantom credit.
38
+ - On an install with more than one Claude account, `cctally record-credit` no longer names another account's credit when refusing, and no longer finishes another account's interrupted credit as though it were yours.
39
+
40
+ ## [1.105.0] - 2026-09-02
41
+
42
+ ### Added
43
+ - `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.
44
+
45
+ ### Changed
46
+ - `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.
47
+ - `cctally quota` refits your stored calibration once after this upgrade, because the set of model names it recognises has changed.
48
+ - `cctally quota` excludes fast-mode requests that Anthropic bills to usage credits, and widens its family-composition support floor to the corrected empirical bound.
49
+ - 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.
50
+ - 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.
51
+ - `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.
52
+
53
+ ### Fixed
54
+ - `cctally dashboard` shares a progressive outline's first-chunk build, cancels abandoned transfers, and rejects an oversized outline before allocating its encoded body.
55
+ - `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.
56
+ - `cctally dashboard` keeps production-size cache admission checks inside the whole-process memory ceiling without shrinking or evicting the retained caches.
57
+ - `cctally dashboard` stops loading a previous conversation's outline after you switch sessions, so the new reader stays current and avoids duplicate outline requests.
58
+ - 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.
59
+ - `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.
60
+ - `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.
61
+ - `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.
62
+ - `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.
63
+ - 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.
64
+ - `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.
65
+ - `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.
66
+ - `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.
67
+ - The metering-rate notification says the calibration was withheld, and names why, instead of promising a fitted budget it cannot supply.
68
+ - `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.
69
+ - The dashboard's rate-change toast points at the evidence for a metering-rate change rather than promising a fitted budget.
70
+ - `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.
71
+ - `cctally dashboard` reuses unchanged Codex accounting and account-card populations across quota and one-file updates, shortening active refreshes without changing cadence or freshness.
72
+ - `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`.
73
+ - 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.
74
+ - 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.
75
+ - The dashboard's Forecast modal keeps an extreme projection label inside the card instead of clipping it at the 110% track edge.
76
+ - `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.
77
+
8
78
  ## [1.104.0] - 2026-08-29
9
79
 
10
80
  ### 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.101.0** (2026-08-19)
33
+ **Latest stable: v1.104.0** (2026-08-29)
34
34
 
35
- Highlights from the `v1.95.5` to `v1.101.0` stable upgrade:
35
+ Highlights from the `v1.101.0` to `v1.104.0` stable upgrade:
36
36
 
37
- - Dashboard refreshes release their read lock on `cache.db` sooner, cutting the measured median hold from 3.40 seconds to 1.04. The published data is unchanged.
38
- - Every dashboard warning carries a button that opens the surface explaining it, the week, the five-hour block, the month, the project, or the forecast. A warning whose window has closed says so and opens nothing.
39
- - The dashboard's Projects table writes out what its two percentages mean under the week selector, where a phone can read it. `Used pp` is relabelled `Used pp (sum)`, and the caption names the denominator of `Cost share`.
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.101.0)
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
@@ -149,8 +149,9 @@ def _alert_text_meter_rate_change(payload: dict, tz) -> tuple[str, str, str]:
149
149
  is no threshold to name, which is the whole reason this family is not an
150
150
  `AXIS_REGISTRY` member.
151
151
 
152
- Two DELIBERATE divergences from the threshold builders, both recorded in
153
- `docs/alerts-gotchas.md` (#661 S2 Stage C review, F7b):
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):
154
155
 
155
156
  * The body does not end with `_with_next_step`. That helper derives its
156
157
  affordance through `alert_next_step_command`, which branches on
@@ -159,7 +160,20 @@ def _alert_text_meter_rate_change(payload: dict, tz) -> tuple[str, str, str]:
159
160
  directly instead.
160
161
  * This builder and `_alert_text_quota` above it live in this glue module
161
162
  rather than in the pure `_lib_alerts_payload` kernel, which holds the
162
- other five. That is the placement both non-registry families use.
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.
163
177
 
164
178
  `effective_from` IS routed through `format_display_dt`. It is a clock
165
179
  instant, not a calendar day, so the carve-out `_alert_text_weekly`
@@ -183,10 +197,13 @@ def _alert_text_meter_rate_change(payload: dict, tz) -> tuple[str, str, str]:
183
197
  raw, tz, fmt="%Y-%m-%d", suffix=False) if raw else ""
184
198
  except ValueError:
185
199
  effective = ""
186
- body = (
187
- f"Effective {effective}. Run `cctally quota` for the fitted budget "
188
- f"and its evidence." if effective else
189
- "Run `cctally quota` for the fitted budget and its evidence.")
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
190
207
  return f"cctally - {provider} metering rate changed", subtitle, body
191
208
 
192
209
 
@@ -227,7 +244,8 @@ _AXIS_VENDOR = {
227
244
  }
228
245
 
229
246
 
230
- def _alert_label_prefix(axis: str, account_key: "str | None") -> str:
247
+ def _alert_label_prefix(axis: str, account_key: "str | None",
248
+ provider: "object | None" = None) -> str:
231
249
  """Return ``"[<label>] "`` for an account-specific crossing when the vendor
232
250
  has >1 real account (R8), else ``""``. Best-effort + never-raise: a missing
233
251
  DB / read error / vendor-wide (`*`) row yields no prefix, so a ≤1-real-account
@@ -236,13 +254,26 @@ def _alert_label_prefix(axis: str, account_key: "str | None") -> str:
236
254
  Delegates the R8 gate + label precedence to the single-definition helpers
237
255
  ``_cctally_account.real_account_count`` / ``account_label`` (P2-CQ1) so the
238
256
  ">1 real account" trigger and the key->label map have exactly one home; this
239
- 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
+ """
240
269
  if not account_key or account_key == _lib_accounts.VENDOR_WIDE:
241
270
  return ""
242
- vendor = _AXIS_VENDOR.get(axis)
243
- if vendor is None:
244
- return ""
245
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 ""
246
277
  import sqlite3 as _sq
247
278
  import _cctally_account
248
279
  db_path = _cctally_core.DB_PATH
@@ -355,8 +386,10 @@ def _dispatch_alert_notification(
355
386
  # R8 label prefix (#341): an account-specific crossing gains a `[<label>] `
356
387
  # title prefix ONLY when the vendor has >1 real account. Byte-identical at
357
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.
358
391
  account_key = payload.get("account_key")
359
- title = _alert_label_prefix(axis, account_key) + title
392
+ title = _alert_label_prefix(axis, account_key, payload.get("provider")) + title
360
393
 
361
394
  # Severity (3-tier) drives both the notify-send urgency token and the
362
395
  # trailing log column. A missing threshold (defensive — shouldn't happen for