cctally 1.101.0 → 1.102.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 +29 -0
- package/README.md +8 -2
- package/bin/_cctally_dashboard.py +433 -122
- package/bin/_cctally_dashboard_conversation.py +12 -0
- package/bin/_cctally_dashboard_envelope.py +25 -23
- package/bin/_cctally_diagnosis.py +1172 -0
- package/bin/_cctally_diagnosis_sources.py +4054 -0
- package/bin/_cctally_forecast.py +25 -15
- package/bin/_cctally_parser.py +84 -0
- package/bin/_cctally_quota.py +14 -0
- package/bin/_cctally_record.py +144 -70
- package/bin/_cctally_refresh.py +105 -93
- package/bin/_cctally_statusline.py +8 -1
- package/bin/_lib_blocks.py +12 -0
- package/bin/_lib_codex_conversation.py +14 -0
- package/bin/_lib_codex_conversation_query.py +22 -8
- package/bin/_lib_conversation.py +6 -3
- package/bin/_lib_conversation_query.py +256 -69
- package/bin/_lib_diagnosis.py +1261 -0
- package/bin/_lib_forecast.py +50 -0
- package/bin/_lib_perf.py +12 -0
- package/bin/_lib_readme_refresh.py +26 -5
- package/bin/cctally +93 -16
- package/bin/cctally-explain +5 -0
- package/dashboard/static/assets/index-Di2hljvB.css +1 -0
- package/dashboard/static/assets/index-XYCIWjVG.js +97 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +5 -1
- package/dashboard/static/assets/index-D6Eb9KDn.js +0 -97
- package/dashboard/static/assets/index-i3g7g8zo.css +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,34 @@ based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [1.102.0] - 2026-08-22
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- `cctally explain` answers one question about a window: which subjects account for the money it spent. It reports four contributor classes — an expensive model mix, one project dominating, one session dominating, and concentrated 5-hour bursts — ranked by observed cost against a denominator it names, with one `-> Run cctally …` next step per contributor that takes you to the evidence. Each next step reproduces this command's own accounting where the subcommand it names allows it, so the dollars you see after following it are the dollars that sent you there; `docs/commands/explain.md` names the two targets that cannot take that flag. It gives no behavioural advice and never projects a quota percentage onto a slice of spend. Every figure states the population it was measured over, and a figure that could not be measured is withheld with a typed cause rather than printed as a zero: a window nothing could be priced in says `pricing_unavailable` where the denominator would go, not `$0.00`. A class that finds no contributor is reported as such, stating the same population a contributor row states and closing with its confidence, so a healthy answer is distinguishable from an unmeasured one; a class that could not be measured states which minimum it missed and claims no confidence, because confidence is a statement about a measurement it never made. A `contributor_detected` verdict states how many applicable classes were withheld beside it, so it can never be read as a complete account of the window. `--source all` gives each provider its own section, its own denominator and its own verdict, and a provider whose store cannot be read withholds itself rather than ending the report. A store that could not be read is the one withheld cause that also sets the exit code: when no requested provider answered, the report is still printed — you need to see the typed cause — and the command exits 3, so a script sees the failure. Every other withheld cause is a correct answer about what the store holds and exits 0. Projects are anonymized by default; sessions stay opaque in every mode. `--json` emits a stamped `schemaVersion` 1 envelope. Flags: `--source`, `--account`, `--window`, `--speed`, `--tz`, `--json`, `--reveal-projects`. See `docs/commands/explain.md` (#620).
|
|
12
|
+
- `cctally explain` now reports three more contributor classes, drawn from your locally retained conversations rather than from accounting rows alone: **prompt-cache churn** (turns whose cached prefix collapsed and was re-created in the same turn), **short conversations carrying large context** (at most three human turns where one request used at least 80% of that request's own model context window), and **subagent fan-out** (cost attributable to delegated work, grouped by parent). Each contributes one aggregate row for the qualifying set, ranked on the same denominator as the other four, with its members published beneath it as evidence — how many turns were flagged, how many conversations they touched, the estimated wasted cost, the median human-turn count, the largest single request as a share of its context window, how many subagents were identified, the largest subagent's share, and any cost that could not be attributed to exactly one parent. Every class now also prints the rule that produced its verdict and the definition of a human turn, so a verdict can be reproduced rather than trusted; that definition matters because the message count beside a session includes subagent sidechains and is not a human-turn count.
|
|
13
|
+
- Every authoritative test run now records how long each test took. A standalone pytest plugin, enabled only on an authoritative run that has an evidence directory, writes one record per test phase — setup, call and teardown — with the node id, the duration, the outcome and which pytest leg produced it, and the run publishes them as `timings/pytest-tests.jsonl.gz` beside the rest of its evidence. Nobody could previously name the slowest test in the repository. The plugin is loaded with `-p` rather than from `pytest.ini` or a `conftest.py`, so running pytest directly is unaffected, and it refuses to write in an xdist worker, so only the controller — which receives every worker's reports — produces records. The main and benchmark legs are separate processes and write separate files, which the run then merges in a fixed order and publishes atomically. The merged artifact states its own completeness, and the run says so too. A leg whose process was killed leaves a truncated file, which the merge reads as far as it decodes and then publishes marked incomplete — it no longer abandons the artifact entirely on the one failure the completeness contract exists for. A leg that reached the end of pytest but did not run what it collected — an internal error, an interruption, a usage error — is also marked incomplete, because reaching the end is not the same as finishing the work; a run that merely had failing tests is a normal, complete run. An incomplete artifact is reported as a contract note on the run itself, so the state is visible without opening the file, and a consumer cannot read a truncated run as one in which the missing tests were simply fast. Only runs on the maintainer's LAN runners retain this history, because hosted CI destroys its evidence directory when the job ends (#630).
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- The public-mirror preview now recognizes the recorded README-refresh overlay, matching the release publisher's existing safety check instead of reporting an expected post-promotion refresh as unrelated drift.
|
|
17
|
+
- The README's compact Latest stable block now names the actual previous-stable-to-target upgrade range and links its complete release notes instead of making three target-release highlights look like the whole stable upgrade (#639).
|
|
18
|
+
- An authoritative red is now far more likely to mean the change is broken rather than that the runner was busy. One hundred and ninety-nine test budgets that a contended runner could blow — every one of them a wait for something that should arrive, including a hundred and eighteen loopback HTTP requests and connects at two to five seconds — now use the estate's one shared thirty-second backstop, derived from the slowest node ever measured under real contention. One further site was kept instead of raised, because it is one attempt's observation window inside a four-attempt retry and raising it would have made the failing path longer rather than the healthy one safer; it states that reason at the site.
|
|
19
|
+
- A failing authoritative test run no longer loses its whole failure extract to one over-eager sanitizer check. A refused line is now replaced with a placeholder naming the check that refused it, and the rest of the extract is published with a trailing notice stating how many lines went and why; the count and the reasons also reach stderr and the run manifest, so the removal is never silent. The offending line's own bytes still never reach the file. That per-line treatment applies to the three checks that judge the shape of a line the sanitizer deliberately keeps; a check that finds actual content the sanitizer should have removed — a machine path, an address, a credential, a sentence of prose — still refuses the whole export and writes no file, because that means the sanitizer itself leaked and the lines it did not flag cannot be trusted either. Refusing the entire file for any single violation was the previous rule, and it turned a sanitizer false positive into the operator receiving zero bytes of diagnostic evidence — over-redaction reappearing at the file level. Deciding it by how many lines were flagged instead was tried and withdrawn: a sanitizer fault confined to its vocabulary stage publishes production text verbatim while flagging about one line in fourteen, so a proportional threshold published exactly the extract the strict rule withheld. The console path degrades the same way, per line, instead of dropping a whole harness's block, and the run manifest now records a refused export as a refusal with its cause rather than as an absence (#630).
|
|
20
|
+
- Three of the export validator's structural checks were wrong and are corrected. The doubled-`E `-gutter refusal is withdrawn: pytest prefixes every line of a multi-line assertion message with its gutter, so the sanitizer legitimately emits `E E assert …`, and refusing it destroyed the extract for exactly the failure class the extract exists to explain. It closed nothing either, because the validator has no gutter awareness in any other check, the single-gutter form was already accepted, and the payload is judged identically either way — so the check is not replaced and the code records that plainly, together with the measurements behind it. The counters check no longer refuses a counters line that arrives behind that same gutter, which is how pytest reports a nested test session and is the single most diagnostic line such a failure produces; it accepts the gutter at the front of the line only, so foreign text anywhere else on a counters line is still refused. The counters check's trigger no longer depends on the sanitizer's own leading anchor: it now also fires on a counter pair plus pytest's `in <duration>` tail anywhere on the line, so a summary line carrying foreign text before its counts is judged rather than skipped. A pytest node name whose bracket closes before it opens is now normalized and refused as well, closing the last member of the retained-parameter-fragment class (#630).
|
|
21
|
+
- `bin/cctally-test-remote --status` no longer states a measured zero over an evidence ledger it never read. When the ledger cannot be opened, the eviction block reads `unmeasured — the ledger could not be read` and every count in `evidenceEviction` is null, which is a different answer from a window in which no retention pass ran. `docs/remote-testing.md` now documents the eviction block on both surfaces and the `evidenceEviction` and `evidenceEvictionWindow` JSON keys (#630).
|
|
22
|
+
- The retained-evidence budget on each test runner is now twelve days and 2 GiB, up from seven days and 1 GiB, and both figures are derived from measurement rather than chosen. The horizon rises because the run ledger's own window measured 11.06 days while the age cutoff counts whole days, so eleven does not cover it — at seven days the store was deleting evidence the ledger still reported on, and every coverage gap ever recorded came from age rather than from the size cap, which has never bound. The cap rises to hold the new per-test durations artifact at its uncompressed size, because a compression ratio is not a correctness guarantee: one measured run's artifact is 531,404 compressed bytes and 7,902,147 uncompressed, and projecting the uncompressed figure across the runs the twelve-day horizon holds does not fit under 1 GiB. Raising the horizon does not restore evidence already deleted, and historical gaps never heal, so a store can read degraded permanently; `docs/remote-testing.md` states the warm-up and what the acceptance actually is (#630).
|
|
23
|
+
- `bin/cctally-test-remote --report` now measures how often the evidence store actually loses runs, and the retention record it reads stops overstating what was reclaimed. The report gains an eviction block that states every total beside its denominator — how many retention passes ran in the window, how many of them evicted anything, how many runs and bytes went, and the split by reason. Passes are counted after deduplication, because every completed run re-imports the current retention record whether or not a pass occurred, so the raw event count runs about eight times the real one; and deduplication happens before the window filter, so a pass from outside the window that was merely re-imported inside it does not count. A rate over no passes reads as unmeasured rather than as zero. On the producer side, the record is now rebuilt from what is actually on disk after every deletion attempt rather than from the planned set: a deletion that failed previously left the record claiming the bytes were reclaimed and a coverage hole where the evidence still sat, and the byte cap is enforced against exactly that number. A partially deleted run whose manifest is gone is counted as an orphan — its bytes are included in the total and in the cap comparison, and the store reads degraded — instead of being invisible to both, and a pass that degrades only because of an orphan now prints the operator notice rather than degrading silently. A run directory that exists but has not written its first manifest yet is given a short grace window instead of being reported as an orphan, so a concurrent run no longer flips another run's coverage to degraded; its bytes are still counted, as are those of a manifest carrying a state this binary does not recognise, which previously belonged to no total at all and left the retained figure understating what is on disk. The record also now carries the exact bytes evicted and a per-reason run and byte split, and both text surfaces state the bytes retained and the eviction totals with their denominators — `--status` names the window its denominator covers, because it has no window flag of its own (#630).
|
|
24
|
+
- `bin/cctally-test-remote --status` and `--report` no longer crash on the live evidence store. Both surfaces read each retention coverage gap by position, while the producer has written keyed objects since the merge helper was introduced; the only fixtures covering them hand-built the positional shape, so both surfaces raised `KeyError: 0` on every real record and the fixtures agreed with each other rather than with the producer. Gaps are now normalized once, where the ledger record is decoded, and both renderers read named keys. An open-ended gap — the shape the retention planner emits when the newest evidence is what was lost — renders as `(ongoing)` rather than being dropped, which a key-only fix would have turned into a store with a permanent hole reported as complete. A record that cannot be read as the producer writes it is named on stderr and exits 3 with the rest of the report still rendered, rather than raising a traceback on the instrument you run because something is broken (#630).
|
|
25
|
+
- A failing authoritative test run now leaves a retained failure extract that says what failed. The sanitizer keeps the failing pytest node id together with its exception class instead of discarding the tail; it keeps an indented summary line on the node rule rather than dropping it to an unclassified marker, which is the shape the aggregator actually prints; it strips pytest's `E ` gutter and classifies what the gutter wraps, so a bare exception line survives as its class; and it admits pytest's own counters line without needing a repository vocabulary, including the `(H:MM:SS)` form pytest appends once a session runs a minute or longer. The exception message is still never retained, and a parameter id is still normalized to `[<param>]`. A parameter id containing a space — 309 of this repository's parametrize constants carry one — is normalized whole rather than truncated part-way through and published as a fragment of the value. The independent export validator gained three structural refusals written separately from the sanitizer's own rules: a parameter id that is not the placeholder, closed or unterminated; any text after the exception-message placeholder; and a foreign token on pytest's counters line, judged now by requiring every token on the line to come from a closed set rather than by restating the shape the sanitizer matches, which left the check blind to anything the sanitizer got wrong in that dimension. The parameter refusal is also scoped to a real pytest node identifier: it used to fire on any line containing a `::name[value]` span, and at the time a refusal deleted the whole export file, so one false positive turned a partially useful extract into zero bytes (#630).
|
|
26
|
+
- Dashboard modals now recover orphaned Tab and Shift+Tab focus into the topmost open surface, and five-hour blocks, block details, and recent alerts state their bracketed IANA display zone instead of showing only an ambiguous abbreviation or offset (#635, #636).
|
|
27
|
+
- Scheduled issue intake no longer mistakes a valid tracker ledger for malformed JSON when a retained mutation error contains an older tracker body.
|
|
28
|
+
- Conversation-derived diagnosis now keeps enough Codex event history to evaluate production-sized windows without letting unrelated payload rows consume the turn-inference budget. The reproducible benchmark now materializes Claude sidechains and Codex conversations, reports those populations, and can interleave a reference build with the candidate on one pinned runner; retained text normalization also follows one shared malformed-block convention across parsing and search (#631, #632, #633).
|
|
29
|
+
- Public Linux release validation no longer lets a cache-local JSON read trap affect unrelated stdlib callers, and dashboard settings tests now report the exact socket phase on timeout while closing every temporary server socket and thread. The hosted workflow now selects its Linux profile instead of running the macOS remote-runner harness, and retention unit fixtures no longer inherit the production free-disk emergency floor from a loaded Actions runner (#628).
|
|
30
|
+
- The dashboard can open the current five-hour block when Anthropic shifts a reset far enough that two recorded block windows overlap. The block grouping already assigned every entry in the overlap to exactly one window, but the detail endpoint selected entries from the raw time interval again, counted the overlap a second time, and failed its cost-reconciliation guard with HTTP 500. Block samples and model costs now consume the grouping pass's exact entry ownership, so the modal and the block summary describe the same calls and the guard remains load-bearing (#620).
|
|
31
|
+
- Fully decorated desktop headers now keep every account chip and global action on one row, while condensed 480px touch headers preserve the account selector and 44px action targets without horizontal page overflow.
|
|
32
|
+
- The dashboard's Projects envelope now reads only the latest usage snapshot for each rendered week instead of transferring the entire snapshot history into Python on every build, while preserving later lower percentages from in-place credits (#627).
|
|
33
|
+
- Dashboard refresh state now distinguishes queued requests from batches that have started, clears every rebuilding claim structurally, keeps OAuth network waits outside the shared state lock, prefers stale-maintenance recovery over an incidental busy remedy, updates settled sync text and accessibility state together, and renders over-budget remainders with the sign before the currency symbol (#605).
|
|
34
|
+
- Remote test runs where pytest exits non-zero without naming any failing test now always offer the exact private-log retrieval command, even when harmless pass counters survive the sanitized extract and would otherwise hide the missing cause. Test-loaded CLI generations also retain the exact sibling modules they completed, so an older request thread cannot adopt a newer generation's half-initialized module during suite teardown (#629).
|
|
35
|
+
|
|
8
36
|
## [1.101.0] - 2026-08-19
|
|
9
37
|
|
|
10
38
|
### Changed
|
|
@@ -20,6 +48,7 @@ based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
20
48
|
- Project drill-downs now render the exact provider-native interval queried by the server instead of reconstructing Claude's interval in the browser (#571).
|
|
21
49
|
|
|
22
50
|
### Fixed
|
|
51
|
+
- The dashboard Projects table and its drill-down now measure the same subscription-week buckets for every `1w` / `4w` / `8w` / `12w` selection. Reset shifts can leave time gaps between those buckets; the drill previously included activity from the gaps while the table did not, and the table counted a session twice when it crossed a bucket boundary. Both cost and session totals now reconcile, and the drill states when its displayed span contains reset gaps (#634).
|
|
23
52
|
- Public-test dependency closure now preserves the real directory depth of `Path(__file__)` parent chains, so a public test cannot hide a mirror-private fixture by addressing it relative to `tests/` (#626).
|
|
24
53
|
- Conversation and dashboard verification now fail on missing goldens unless regeneration is explicit, and a failed regeneration write can no longer print success or let the scenario pass (#625).
|
|
25
54
|
- The committed dashboard envelope oracle now verifies identically on macOS and every supported Linux/Python release lane. Its generated store uses resolved filesystem paths for reads and privacy containment while hashing one explicit logical corpus root for opaque identities, and Claude fixture ingestion now has a deterministic lexical file order instead of inheriting APFS or ext4 enumeration order. The oracle therefore keeps asserting the full Codex session metadata and aggregate payload across platforms rather than publishing two different identity universes from the same corpus (#622).
|
package/README.md
CHANGED
|
@@ -30,9 +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.101.0** (2026-08-19)
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
Highlights from the `v1.95.5` to `v1.101.0` stable upgrade:
|
|
36
|
+
|
|
37
|
+
- Dashboard refreshes now release their coherent `cache.db` snapshot before constructing provider views, cutting the measured median read hold from 3.40 seconds to 1.04 seconds while preserving the exact published payload (#617).
|
|
38
|
+
- Every dashboard warning state now offers the explanation for its own window. A row in Recent alerts, an alert toast, a warn or over budget block, and a capped forecast each carry a button that opens the surface explaining that warning: a weekly crossing opens the current week, a five-hour crossing opens that block, a calendar-month budget opens that month, a project-budget crossing opens that project's drill-down, and a projected-pace crossing opens the forecast. The window is checked before anything opens, a warning about a week the dashboard is no longer showing states that its window has closed and opens nothing at all, rather than quietly showing you the current week instead. A five-hour block still opens however old it is, as long as the alert recorded which block it was; an alert that recorded no block start says so and opens nothing. On Codex the button says `Open this cycle` rather than `Open this week`, because a Codex quota cycle is what it opens. A project-budget crossing on a machine with more than one account is recorded across all of them, so it states that it cannot be narrowed to one account's projects rather than picking one. Every button is an ordinary control reachable by Tab and activated by Enter or Space, and no keyboard shortcut is claimed (#620).
|
|
39
|
+
- The dashboard's Projects table now writes out what its two percentages mean, under the week selector rather than only in a tooltip, a tooltip never appears on a phone. The `Used pp` column is relabelled `Used pp (sum)` and the caption states that it adds each week's attributed usage percentage across the selected window, so it is a sum of percentage points rather than a share of any one week; the caption also names the denominator of the `Cost share` column as total project spend in that same window. No column is added or removed. On an installation with more than one Claude account, both the Projects panel and the Projects table now also state that accounts are folded into one ranking and that their weekly quota percentages are never added together. That sentence appears on the Claude and All views only, because the Codex view ranks Codex projects and folds no Claude accounts at all (#620).
|
|
40
|
+
|
|
41
|
+
[See every change in this stable upgrade](https://github.com/omrikais/cctally/releases/tag/v1.101.0)
|
|
36
42
|
<!-- cctally:latest-stable:end -->
|
|
37
43
|
|
|
38
44
|
## Quick start
|