cctally 1.67.1 → 1.69.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 +54 -2
- package/bin/_cctally_cache.py +754 -109
- package/bin/_cctally_cache_report.py +41 -17
- package/bin/_cctally_codex.py +109 -4
- package/bin/_cctally_config.py +368 -2
- package/bin/_cctally_core.py +187 -15
- package/bin/_cctally_dashboard.py +679 -5
- package/bin/_cctally_dashboard_conversation.py +9 -4
- package/bin/_cctally_dashboard_envelope.py +110 -1
- package/bin/_cctally_dashboard_share.py +557 -79
- package/bin/_cctally_db.py +366 -17
- package/bin/_cctally_diff.py +49 -16
- package/bin/_cctally_doctor.py +131 -0
- package/bin/_cctally_forecast.py +12 -4
- package/bin/_cctally_parser.py +321 -92
- package/bin/_cctally_project.py +24 -8
- package/bin/_cctally_quota.py +1381 -0
- package/bin/_cctally_record.py +319 -14
- package/bin/_cctally_refresh.py +105 -3
- package/bin/_cctally_reporting.py +7 -1
- package/bin/_cctally_setup.py +343 -113
- package/bin/_cctally_statusline.py +228 -0
- package/bin/_cctally_tui.py +787 -7
- package/bin/_lib_alert_axes.py +11 -0
- package/bin/_lib_codex_hooks.py +367 -0
- package/bin/_lib_conversation_query.py +156 -67
- package/bin/_lib_doctor.py +238 -0
- package/bin/_lib_jsonl.py +529 -162
- package/bin/_lib_quota.py +566 -0
- package/bin/_lib_share.py +152 -34
- package/bin/_lib_snapshot_cache.py +26 -0
- package/bin/_lib_source_identity.py +74 -0
- package/bin/cctally +325 -10
- package/dashboard/static/assets/index-CBbErI-P.js +80 -0
- package/dashboard/static/assets/index-kDDVOLa_.css +1 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +5 -1
- package/dashboard/static/assets/index-BybNp_Di.js +0 -80
- package/dashboard/static/assets/index-DgAmLK65.css +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.69.0] - 2026-07-17
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Dashboard source-aware backend read model (#294 S4): `/api/data` now appends
|
|
12
|
+
immutable Claude, Codex, and labelled all-source states; provider-owned
|
|
13
|
+
session/project/quota details use opaque qualified routes backed by bounded
|
|
14
|
+
relational native reads; render/composer/preset/history requests use
|
|
15
|
+
source-bearing canonical provider share snapshots and preserve identity
|
|
16
|
+
without blending independent quota windows; and loopback-only diagnostics
|
|
17
|
+
expose safe aggregate source counts. The existing Claude dashboard UI is
|
|
18
|
+
unchanged; S5 owns the visible source selector and sharing controls.
|
|
19
|
+
- Provider-aware CLI analytics for `project`, `diff`, `range-cost`,
|
|
20
|
+
`cache-report`, and `report`: choose Claude, Codex, or separate all-source
|
|
21
|
+
sections with `--source`, or use fixed `cctally claude|codex` subgroup forms.
|
|
22
|
+
Codex projects use privacy-safe qualified identities; its cache surface is
|
|
23
|
+
truthful token reuse rather than a fabricated cache-hit rate; and report
|
|
24
|
+
keeps overlapping native logical-limit series unblended. The same
|
|
25
|
+
source-labelled CLI views now support share artifacts, while the established
|
|
26
|
+
Codex daily/monthly/weekly/session reports gain read-only `--config PATH` and
|
|
27
|
+
share flags (#294 S3).
|
|
28
|
+
- Published the Claude/Codex product-parity contract for #294 S0, backed by a deterministic synthetic rollout corpus and executable acceptance matrix that pin source-qualified identity, generic quota windows, schema-drift degradation, and safe mixed-source arithmetic without claiming the later parity sessions are implemented.
|
|
29
|
+
- Codex local-rollout ingest now retains complete physical records, provider-root-qualified native quota observations, and source thread metadata alongside the existing reports. Migration 024 rederives this local cache foundation safely; S2 interprets the observations through native quota commands, while dashboard panels and the Codex conversation UI remain future work. (#294)
|
|
30
|
+
- Native `cctally codex quota` history, statusline, forecast, blocks, and breakdown commands now interpret locally retained Codex quota windows without combining roots or limits. Optional setup-managed Codex hooks refresh local observations on trusted Codex activity and deliver opt-in quota alerts; `doctor` reports local freshness, hook configuration, and recent lifecycle activity. (#294)
|
|
31
|
+
- The status line is now the primary automatic writer of weekly/5h usage: it persists the `rate_limits` Claude Code already hands it on every render, so usage stays fresh from live sessions even while Anthropic's `/api/oauth/usage` endpoint is rate-limiting the background poll. The write is throttled, guarded against concurrent sessions by a cross-process lock, and runs detached so the render stays fast.
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
- The background OAuth usage poll is now a backfill behind the status line: it only fetches when the status line has not fed recently and no rate-limit cooldown is pending, honoring the `Retry-After` header and backing off exponentially on `429` — so a transient rate-limit can no longer snowball into a persistent ban.
|
|
35
|
+
- Dashboard Sessions table is now keyboard-navigable as an ARIA grid: the ~100-row body is a single Tab stop, with Up/Down/Home/End moving the active row, Enter opening its detail, and Left/Right reaching the per-row controls — replacing ~400 sequential tab stops (#299).
|
|
36
|
+
- Dashboard board is now responsive across three width modes: below 900px it stacks, 900–1199px shows Sessions full-width with Trend and Projects paired (no more crushed columns), and 1200px+ keeps the dense three-across bento. (#293)
|
|
37
|
+
- Touch targets on the dashboard board (topbar actions, sort headers, expand/collapse, model chips, drag grip) now meet the 44px floor on any coarse-pointer device, including tablets. (#293)
|
|
38
|
+
- The unscrolled phone hero is tighter so the start of the board is visible above the fold on short phones. (#293)
|
|
39
|
+
- Dashboard Sessions: each session row is now openable by keyboard and touch via its title, not mouse only (#293 S2, A11Y-2).
|
|
40
|
+
- Dashboard: below 900px the Weekly and Monthly cards now preview the 3 most-recent periods with a "+N more" button to the full-table modal, and the Blocks card scrolls within a bounded height — so a phone dashboard is a glanceable summary instead of a 5–6k-pixel scroll (#293 S3, MOBILE-1). Every row stays reachable in the detail modal / by in-card scroll; ≥900px layouts are unchanged.
|
|
41
|
+
- Dashboard cards now describe rather than impersonate buttons: activating a card's Share or Expand control no longer also opens the card's own modal, and Tab skips the card body straight to its actions — the Expand button is the keyboard/screen-reader way into each card's detail. (#293 S4, A11Y-1/ACTION-1)
|
|
42
|
+
- The 44px touch-target floor now extends off the board — to modal close buttons and modal sort headers, and to every Share and Composer control (preset menu, manage rows, checkboxes, the drag handle) — on any coarse-pointer device, phones and tablets alike. (#293 S4)
|
|
43
|
+
- The phone Share form no longer zooms on focus (16px inputs) and shows a live preview above the options so edits are visible as you make them; the Composer's controls are touch-sized. (#293 S4, SHARE-1/COMPOSER-1)
|
|
44
|
+
- A non-empty Composer basket now stays reachable in the condensed mobile header after you scroll, and the topbar no longer overflows sideways on a 320px-wide phone (the doctor chip compacts to its status glyph). (#293 S4, BASKET-1)
|
|
45
|
+
- **Conversation viewer:** the browse rail renders far faster on large caches — per-session enrichment (git branch, models, title, cost) is now materialized onto the session rollup at ingest (cache migration 023) instead of re-read from the message table per request; the AI title stays live and the rollup cost auto-refreshes on a pricing change. (#302)
|
|
46
|
+
- Dashboard detail surfaces (the session-detail modal, the projects drill, and the conversation reader's outline) no longer re-fetch their detail endpoint on every 5-second refresh tick — they revalidate only when the underlying data actually changes, so a finished/static session, project, or conversation is fetched once instead of every ~5s while open, cutting redundant network round-trips and the cache reads they drive. (#300)
|
|
47
|
+
- The conversation reader's transcript-body growth check now follows that same change signal (#300): with the reader open and live-tail turned off, a finished/static conversation stops re-requesting its transcript tail on every ~5s refresh tick and only re-checks when its data changes — the live-tail default path is unchanged. (#303)
|
|
48
|
+
- Dashboard: the conversation browse rail (sessions list) no longer refetches on every 5s tick while idle — its page-1 revalidation is now gated on a data-version change signal, matching the #300/#303 detail-surface and reader-body gating (#305).
|
|
49
|
+
- **Conversation viewer:** the desktop reader header is regrouped into intent clusters (reading / navigation / sharing / bulk) with a quiet right-aligned status cluster, and a reader-width-governed fold reuses the compact controls when the outline column squeezes the reader below 720px — so the header reads as reading chrome instead of an eleven-control palette (#304 S3).
|
|
50
|
+
- **Conversation viewer:** rail rows split their metadata into an identity line (project · branch · time) and a stats line (model · cost · messages); model chips now render at the 11px typography floor, and a long unknown-model name is bounded so it can never push cost or message counts off the rail (#304 S3).
|
|
51
|
+
- **Conversation viewer:** all sub-floor viewer text is raised to the 11px floor (reader model chips, the export anonymize note, Codex card labels) and guarded by a static typography-floor scan; the overflow menu's completion row is now an actionable jump on compact and folded widths (#304 S3).
|
|
52
|
+
|
|
53
|
+
### Fixed
|
|
54
|
+
- Usage snapshots now record the true source of each write (`statusline` vs `api`); OAuth-fed rows were previously mislabeled `statusline`.
|
|
55
|
+
- Dashboard Sessions: the Cost column now stays visible at every width instead of hiding behind a horizontal scrollbar — the card folds Duration into the start time and sheds Cache then Project as it narrows (#293 S2, SESS-1).
|
|
56
|
+
- Dashboard board: dragging to reorder a tall card no longer stretches or squashes the displaced card (#293 S2).
|
|
57
|
+
- Removed the decorative swipe handle from the top of dashboard modals — it advertised a swipe-to-dismiss gesture that was never wired up; dismissal stays Esc, backdrop tap, or the × button. (#293 S4, MODAL-1)
|
|
58
|
+
- **Conversation viewer:** the browse filter popover and `?models=`-filtered rail no longer do a ~22 s cold full-table scan of the message `model` column on large caches — a new partial covering index (`conversation_messages(model, session_id)`, cache migration 022) makes facets and model-filtering index-only. (#301)
|
|
59
|
+
- **Conversation viewer:** fixed the 640/641px responsive cliff (#304 S1) — the workspace now stays single-pane until ~880px so the reader is never crushed beside the rail, and the compact reader header now applies across the whole ≤1100px constrained band instead of only phones.
|
|
60
|
+
- **Conversation viewer:** comparison is now fully operable on compact/single-pane widths (#304 S2) — `⋯ → Compare with…` opens the rail picker (previously a dead end at ≤880px), Cancel/Escape return to the anchor reader with focus restored, pick-mode Escape defers to the search input / filters popover, and `/` targets the pick search. Comparison accessibility: the Anonymize menu toggle is a real `menuitemcheckbox`, and Run A/Run B identity is spoken with the header titles and metric values.
|
|
61
|
+
- **Dashboard:** fixed a startup race where the first one or two transcript-gated requests (e.g. an SSE `/api/events` reconnect from an already-open tab) could error out the instant the port opened — a request thread could observe an internal module that was registered but still mid-initialization. Module loading is now serialized so every request always sees a fully-loaded module; clients previously recovered on retry, and now there is nothing to recover from. (#306)
|
|
62
|
+
- **Dashboard Blocks panel:** each model's stacked-bar segment now paints the same color as its legend dot beneath it (and as the model chip everywhere else). The gauge segments were drawing from a generic accent palette, so opus/sonnet/haiku appeared in a different hue in the bar than in the legend — for sonnet and haiku a full green↔blue swap.
|
|
63
|
+
|
|
64
|
+
## [1.68.0] - 2026-07-13
|
|
65
|
+
|
|
66
|
+
### Added
|
|
67
|
+
- `cctally db checkpoint [--db {cache,stats}] [--json]` — fast, non-destructive WAL drain (TRUNCATE checkpoint) for cache.db/stats.db; the manual escape hatch and the `doctor` remediation for a bloated write-ahead log. (#297)
|
|
68
|
+
|
|
69
|
+
### Changed
|
|
70
|
+
- `cctally-test-all`: faster full-suite runs — `reconcile` now runs inside the parallel pool instead of as a serial barrier, and a three-role worker model (`CCTALLY_TEST_JOBS` outer budget + derived/overridable `CCTALLY_INNER_JOBS` / `CCTALLY_PYTEST_JOBS`) caps the nested per-harness fan-out that made individual harnesses 2–8× slower under load. Adds a slowest-harness timing report. (#292)
|
|
71
|
+
- `doctor` gains a read-only `cache.db WAL size` check that warns when the WAL exceeds 256 MB and points to `cctally db checkpoint`. (#297)
|
|
72
|
+
|
|
73
|
+
### Fixed
|
|
74
|
+
- Prevent the cache.db write-ahead log from bloating to multi-GB under heavy concurrent multi-agent syncs, which surfaced as `Error: database is locked` on every `cctally` command — cap the WAL file (`PRAGMA journal_size_limit`), force a best-effort TRUNCATE checkpoint at end-of-sync, and raise `busy_timeout` from 5s to 15s. Existing bloated caches self-heal on the next sync (or run `cctally db checkpoint`). (#297)
|
|
75
|
+
|
|
8
76
|
## [1.67.1] - 2026-07-12
|
|
9
77
|
|
|
10
78
|
### Fixed
|
package/bin/_cctally_alerts.py
CHANGED
|
@@ -17,12 +17,14 @@ effects for the threshold-actions feature, plus the test-entry command:
|
|
|
17
17
|
`_dispatch_alert_notification` with `mode="test"`, and reports the
|
|
18
18
|
outcome via stdout / exit code.
|
|
19
19
|
|
|
20
|
-
The pure payload primitives (`_alert_text_weekly`,
|
|
20
|
+
The established pure payload primitives (`_alert_text_weekly`,
|
|
21
21
|
`_alert_text_five_hour`, `_escape_applescript_string`,
|
|
22
22
|
`_build_alert_payload_weekly`, `_build_alert_payload_five_hour`) live
|
|
23
23
|
in `bin/_lib_alerts_payload.py` (Phase A extraction); this module
|
|
24
24
|
imports them directly via `_load_lib`, which keeps the dispatch path
|
|
25
|
-
free of an extra bounce through cctally's re-exports.
|
|
25
|
+
free of an extra bounce through cctally's re-exports. The S2 quota payload is
|
|
26
|
+
defined below beside its provider-neutral dispatch text because this stage does
|
|
27
|
+
not yet expose a dashboard payload consumer.
|
|
26
28
|
|
|
27
29
|
Kernel reads from `bin/_cctally_core` (call-time module-attribute access):
|
|
28
30
|
- `LOG_DIR` — base dir under which `alerts.log` lives. Promoted to
|
|
@@ -81,6 +83,54 @@ _build_alert_payload_project_budget = _lib_alerts_payload._build_alert_payload_p
|
|
|
81
83
|
_build_alert_payload_codex_budget = _lib_alerts_payload._build_alert_payload_codex_budget
|
|
82
84
|
_build_alert_payload_projected = _lib_alerts_payload._build_alert_payload_projected
|
|
83
85
|
|
|
86
|
+
|
|
87
|
+
def _build_alert_payload_quota(
|
|
88
|
+
*, source: str, source_root_key: str, logical_limit_key: str,
|
|
89
|
+
observed_slot: str, window_minutes: int, resets_at_utc: str,
|
|
90
|
+
threshold: int, kind: str, crossed_at_utc: str,
|
|
91
|
+
qualifying_percent: float | None, projected_percent: float | None,
|
|
92
|
+
) -> dict:
|
|
93
|
+
"""Build the provider-neutral durable quota alert payload."""
|
|
94
|
+
context = {
|
|
95
|
+
"source": str(source), "source_root_key": str(source_root_key),
|
|
96
|
+
"logical_limit_key": str(logical_limit_key), "observed_slot": str(observed_slot),
|
|
97
|
+
"window_minutes": int(window_minutes), "resets_at_utc": str(resets_at_utc),
|
|
98
|
+
"kind": str(kind),
|
|
99
|
+
"qualifying_percent": (
|
|
100
|
+
None if qualifying_percent is None else float(qualifying_percent)
|
|
101
|
+
),
|
|
102
|
+
"projected_percent": (
|
|
103
|
+
None if projected_percent is None else float(projected_percent)
|
|
104
|
+
),
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
"id": "quota:{source}:{root}:{limit}:{slot}:{minutes}:{reset}:{threshold}".format(
|
|
108
|
+
source=source, root=source_root_key, limit=logical_limit_key,
|
|
109
|
+
slot=observed_slot, minutes=int(window_minutes), reset=resets_at_utc,
|
|
110
|
+
threshold=int(threshold),
|
|
111
|
+
),
|
|
112
|
+
"axis": "quota", "threshold": int(threshold), "kind": str(kind),
|
|
113
|
+
"crossed_at": crossed_at_utc, "alerted_at": crossed_at_utc,
|
|
114
|
+
**context, "context": context,
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _alert_text_quota(payload: dict, _tz) -> tuple[str, str, str]:
|
|
119
|
+
"""Render provider-native quota wording without Claude-window aliases."""
|
|
120
|
+
context = payload.get("context") or {}
|
|
121
|
+
threshold = int(payload["threshold"])
|
|
122
|
+
source = context.get("source") or payload.get("source") or "provider"
|
|
123
|
+
slot = context.get("observed_slot") or payload.get("observed_slot") or "quota"
|
|
124
|
+
minutes = context.get("window_minutes") or payload.get("window_minutes")
|
|
125
|
+
title = f"cctally - {source} quota {threshold}% reached"
|
|
126
|
+
subtitle = f"{slot} · {minutes}m window" if minutes is not None else str(slot)
|
|
127
|
+
kind = context.get("kind") or payload.get("kind") or "actual"
|
|
128
|
+
if kind == "projected":
|
|
129
|
+
body = f"Projected {float(context.get('projected_percent') or 0):.0f}% by reset"
|
|
130
|
+
else:
|
|
131
|
+
body = f"Actual usage {float(context.get('qualifying_percent') or 0):.0f}%"
|
|
132
|
+
return title, subtitle, body
|
|
133
|
+
|
|
84
134
|
# Phase B: severity policy + the cross-platform dispatch kernel. The kernel is
|
|
85
135
|
# pure (parameterized on platform + which_on_path); this module is the I/O glue
|
|
86
136
|
# that injects the real sys.platform / shutil.which and spawns with shell=False.
|
|
@@ -181,6 +231,8 @@ def _dispatch_alert_notification(
|
|
|
181
231
|
title, subtitle, body = _alert_text_codex_budget(payload, tz)
|
|
182
232
|
elif axis == "projected":
|
|
183
233
|
title, subtitle, body = _alert_text_projected(payload, tz)
|
|
234
|
+
elif axis == "quota":
|
|
235
|
+
title, subtitle, body = _alert_text_quota(payload, tz)
|
|
184
236
|
else:
|
|
185
237
|
title, subtitle, body = (
|
|
186
238
|
"cctally - alert",
|