cctally 1.108.0 → 1.109.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 (78) hide show
  1. package/CHANGELOG.md +101 -0
  2. package/bin/_cctally_account.py +4 -0
  3. package/bin/_cctally_alerts.py +6 -2
  4. package/bin/_cctally_cache.py +1590 -153
  5. package/bin/_cctally_config.py +3 -2
  6. package/bin/_cctally_core.py +589 -32
  7. package/bin/_cctally_dashboard.py +1223 -160
  8. package/bin/_cctally_dashboard_conversation.py +263 -8
  9. package/bin/_cctally_dashboard_envelope.py +144 -4
  10. package/bin/_cctally_dashboard_share.py +154 -36
  11. package/bin/_cctally_dashboard_sources.py +2146 -347
  12. package/bin/_cctally_db.py +634 -5
  13. package/bin/_cctally_diagnosis_sources.py +57 -11
  14. package/bin/_cctally_doctor.py +84 -15
  15. package/bin/_cctally_five_hour.py +1295 -40
  16. package/bin/_cctally_forecast.py +109 -26
  17. package/bin/_cctally_journal.py +380 -37
  18. package/bin/_cctally_milestone_history.py +86 -6
  19. package/bin/_cctally_percent_breakdown.py +6 -1
  20. package/bin/_cctally_project.py +48 -76
  21. package/bin/_cctally_quota.py +39 -7
  22. package/bin/_cctally_quota_model.py +17 -2
  23. package/bin/_cctally_record.py +1603 -437
  24. package/bin/_cctally_refresh.py +2 -1
  25. package/bin/_cctally_share.py +24 -5
  26. package/bin/_cctally_source_analytics.py +553 -30
  27. package/bin/_cctally_statusline.py +83 -19
  28. package/bin/_cctally_store.py +61 -4
  29. package/bin/_cctally_tui.py +229 -74
  30. package/bin/_cctally_weekrefs.py +28 -5
  31. package/bin/_lib_blocks.py +127 -20
  32. package/bin/_lib_cache_report.py +20 -5
  33. package/bin/_lib_codex_conversation_query.py +11 -1
  34. package/bin/_lib_codex_hooks.py +102 -0
  35. package/bin/_lib_conversation.py +119 -0
  36. package/bin/_lib_conversation_dispatch.py +31 -1
  37. package/bin/_lib_conversation_query.py +61 -14
  38. package/bin/_lib_conversation_retention.py +413 -25
  39. package/bin/_lib_dashboard_sources.py +267 -7
  40. package/bin/_lib_diagnosis.py +10 -0
  41. package/bin/_lib_diff_kernel.py +12 -1
  42. package/bin/_lib_doctor.py +214 -6
  43. package/bin/_lib_ingest_frontier.py +1366 -49
  44. package/bin/_lib_journal.py +19 -2
  45. package/bin/_lib_merge_gate.py +307 -0
  46. package/bin/_lib_pricing.py +522 -18
  47. package/bin/_lib_record.py +130 -12
  48. package/bin/_lib_render.py +9 -3
  49. package/bin/_lib_retained_size.py +27 -2
  50. package/bin/_lib_segment_summary.py +66 -8
  51. package/bin/_lib_share_templates.py +28 -18
  52. package/bin/_lib_snapshot_cache.py +115 -1
  53. package/bin/_lib_source_retry.py +320 -0
  54. package/bin/_lib_statusline_candidates.py +400 -36
  55. package/bin/_lib_tick_stats.py +119 -0
  56. package/bin/_lib_view_models.py +251 -76
  57. package/bin/cctally +69 -0
  58. package/dashboard/static/assets/ConversationsView-TO5bpIyc.js +72 -0
  59. package/dashboard/static/assets/{DoctorModal-CWn3U4Wl.js → DoctorModal-BZGXOlc2.js} +1 -1
  60. package/dashboard/static/assets/ModalRoot-C_fvugTl.js +1 -0
  61. package/dashboard/static/assets/{ProjectsDrillPanel-QLI9i5mZ.js → ProjectsDrillPanel-KbO_KWa7.js} +1 -1
  62. package/dashboard/static/assets/SourceDetailModal-BrOPLVRL.js +1 -0
  63. package/dashboard/static/assets/{UpdateModal-D3m8GG6V.js → UpdateModal-CYgEFAi4.js} +2 -2
  64. package/dashboard/static/assets/dashboardStream.shared-worker-DbG6Ef0T.js +1 -0
  65. package/dashboard/static/assets/index-Cj_l7T7u.css +1 -0
  66. package/dashboard/static/assets/index-DLVwACUM.js +13 -0
  67. package/dashboard/static/assets/outlineNavigation-DXX-RjKl.js +9 -0
  68. package/dashboard/static/assets/useKeymap-DQx_A1MB.js +1 -0
  69. package/dashboard/static/dashboard.html +3 -3
  70. package/package.json +3 -1
  71. package/dashboard/static/assets/ConversationsView-BOSaBRtu.js +0 -72
  72. package/dashboard/static/assets/ModalRoot-SR070V6c.js +0 -1
  73. package/dashboard/static/assets/SourceDetailModal-pv0wlFxl.js +0 -1
  74. package/dashboard/static/assets/dashboardStream.shared-worker-1XTMV3nr.js +0 -1
  75. package/dashboard/static/assets/index-BHu4mxd8.css +0 -1
  76. package/dashboard/static/assets/index-CIWsbux3.js +0 -13
  77. package/dashboard/static/assets/outlineNavigation-CJvKqmLV.js +0 -9
  78. package/dashboard/static/assets/useKeymap-ffqsS5G0.js +0 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,107 @@ based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [1.109.0] - 2026-09-15
9
+
10
+ ### Added
11
+ - The dashboard picks up a new pricing revision without a restart. It re-reads the deployed pricing file every 60 seconds, adopts a complete newer revision, and keeps serving; no connected client is dropped.
12
+ - The conversation filter popover says `Projects appear once indexing finishes.` in place of `No projects.` while the transcript index is being rebuilt. Model counts stay live on the Claude tab.
13
+ - The dashboard's Projects panel gives every billing cycle of a credited week its own percentage. Both cycles shared one calendar date, and only the later reading survived, so the second cycle could never carry one.
14
+ - The milestone modal names an observation gap where it showed a bare em dash, and states each back-filled run once above the table, as `cctally percent-breakdown` already did. A gap in the current cycle is disclosed too.
15
+
16
+ ### Changed
17
+ - The dashboard's conversation routes open the transcript store read-only. Every browse used to run two write-capable pragmas first, so a read competed with maintenance for the SQLite write lock and lost.
18
+ - A conversation route answers with a typed degraded envelope instead of HTTP 500 while a rebuild, a reclaim pass or a schema upgrade holds the store. Sessions reappear on their own; no request returns a server error.
19
+ - Transcript reclaim runs under a two-second budget and continues on the next pass, so it no longer holds the maintenance and both provider locks until the freelist drains. It is reported as its own timed phase.
20
+ - `cctally project` reports the same per-cycle percentages as `cctally weekly` on the same store. A credited week now contributes one percentage per cycle to `totals.usedPercent`, so a week credited once reports both figures instead of one.
21
+ - A cycle with no observation of its own is reported as missing rather than given the previous cycle's reading, so a week credited `n` times can report up to `n + 1` independently missing observations.
22
+ - An observation captured at the exact instant of a weekly credit belongs to the cycle that starts there, matching where the spend recorded at that instant already goes.
23
+ - The Trend panel, the Trend modal, the Projects modal and the period modal count in billing cycles for both providers, and share artifacts follow.
24
+ - The Projects window pills render bare numbers under a `cycles` label, the Projects grid columns drop the `wk` prefix, and the period modal's `Subscription window` field is now `Reset cycle`.
25
+ - Two cycles of a credited week that fall on one calendar day now render distinct labels, with a time suffix on each. An uncredited week and an ordinarily credited week are unchanged.
26
+ - The dashboard's Projects share artifact states the period its data actually covers. It multiplied a cycle count by seven days, which overstates the span once a credited week is inside it.
27
+ - An All-tab project drill widens to a window that reaches the ranking the row came from, instead of assuming every cycle spans seven days.
28
+ - `cctally project`'s `Used %` cell counts billing cycles: `(3wk)` now reads `(3cy)`, and the trend share artifacts' `Δ` rows follow. A credited week contributes two cycles, so the week abbreviation named the wrong unit.
29
+ - Screen readers announce the Weekly modal's cost chart and the Current Week navigator in billing cycles on the Claude tab. They announced "week" and "period" while every visible label beside them said cycle.
30
+ - `--no-sync` no longer runs the two transcript derivations at startup, so a frozen dashboard starts promptly. While either is owed, conversation reads report that state rather than an empty list, until you run `cctally cache-sync`.
31
+ - A Codex project or quota block reports itself partial when any accounting record inside the past year is unreadable, and states that project metadata is withheld for that item, so a cost is no longer missing with nothing saying so.
32
+ - A momentary database error marks that refresh's Codex project and quota block views partial, and the next refresh recovers on its own once the store is healthy.
33
+ - When the dashboard cannot read the account registry, the Codex source says so: it reports the refresh as partial and states that per-account detail is withheld, instead of presenting the install as if it held one account.
34
+ - The source status chip names the withheld account detail instead of reading `Source degraded`, so a registry the dashboard could not read is distinguishable from a source that could not be built at all.
35
+ - The first journal rebuild after upgrading re-reads every segment once, because the stored segment-summary format changed. Later rebuilds skip unchanged segments as before.
36
+
37
+ ### Fixed
38
+ - `cctally record-credit` plans a credit from the confirmed account's own snapshots, floors and five-hour readings, and its preview names the account it will write to. An install with one account is unchanged.
39
+ - `cctally record-usage`, `cctally report` and `cctally percent-breakdown` read each account's own week boundary, so one account's earliest capture no longer sets another account's week.
40
+ - `cctally explain` decides Codex prompt evidence per conversation. A conversation whose transcript is no longer retained leaves the evaluated population and lowers reported coverage, instead of counting as a confident non-contributor.
41
+ - A journal segment summary survives an ordinary volume remount, after proving the file's bytes are unchanged, so a rebuild no longer re-reads the whole journal once an external or network volume is remounted.
42
+ - The dashboard rebuilds a Codex view whose project metadata was incomplete instead of serving it again for the life of the process, and retries on a bounded schedule rather than once.
43
+ - The dashboard tells a temporary read failure apart from a record it cannot interpret, so only the second advises `cctally cache-sync --source codex --rebuild`.
44
+ - The Projects drill issues one request at the width where two panels are mounted, so resizing the window no longer fetches the same project twice.
45
+ - `cctally five-hour-breakdown` and the current-usage modal report the weekly percentage a reader saw at each five-hour crossing, not the raw reading a held tick recorded. Where that reading is no longer retained the column reads `—`.
46
+ - A five-hour block shows only its own account's crossings. One window owns one block per account, and both the current-usage modal and the historical cycle view listed every account's crossings under each.
47
+ - `cctally five-hour-blocks` and `cctally five-hour-breakdown` withhold a weekly percentage a weekly credit retired, at a block's start and end, closed and reset-crossing blocks included. A block observed only before the credit is unchanged.
48
+ - A held status-line tick keeps its five-hour reading through an in-place weekly credit. The credit's cleanup of stale pre-credit replays removed that tick's row, the only record of the reading it carried.
49
+ - A status-line tick whose weekly percentage is held at its high-water mark no longer discards that tick's five-hour usage, so `cctally five-hour-blocks` and the dashboard's five-hour panel stay current.
50
+ - A status-line tick held at the weekly high-water mark does not advance the weekly percentage or its freshness stamp. `cctally forecast`, `cctally report`, the status line, the TUI and the dashboard keep the last observed reading.
51
+ - The Codex source recovers on the next refresh once the account registry is readable again. A refresh that could not read it republished that result for the life of the process, so the per-account panels stayed missing until a restart.
52
+ - A Codex quota block whose retained metadata is incomplete opens and states that, instead of replacing the whole panel with a message saying the dashboard had updated. Nothing had updated.
53
+ - Above one Codex account, clicking a quota block row in the Blocks panel opens that row's own account while the account selector is on All accounts. One of two rows sharing a window opened the other account's block.
54
+ - Opening a Codex project or quota block in the dashboard is immediate. Each click read a year of Codex accounting first, which on a large store is every row it has, and now reads only that project's or that window's own rows.
55
+ - A Codex quota block opens while one unreadable Codex accounting record sits elsewhere in the store. The view converted a year of records it never used, so a single bad record failed the whole request.
56
+ - A Codex quota block renders every observation of its own window. The detail read a thousand recent rows, so a busy window could lose members, or arrive empty and report the block as missing.
57
+ - Above one Codex account, opening a quota block returns that account's block. Two accounts whose blocks share a root, a limit, a slot, a window length and a reset produced one link, and whichever sorted first was shown.
58
+ - A Codex quota block older than the two hundred and fiftieth most recent now opens instead of reporting itself missing.
59
+ - A Claude transcript keeps its account after its volume is remounted. A remount renumbers the device each transcript records, and the next time one grew, its whole history took the account signed in then. Existing damage is not repaired.
60
+ - The conversation list names the transcript-import state and the command that clears it, instead of reporting a load failure beside a Retry that could never succeed. The server reported that state; the page discarded it.
61
+ - The current-usage modal describes a reconciling Codex quota projection the way the hero does, rather than calling it unavailable directly above the figures it publishes. The Codex-only view carries that qualification too.
62
+ - A Codex hero that is showing its last coherent figure says so in text and to assistive technology. The explanation was in a tooltip, which a touch user cannot reach.
63
+ - Opening a Codex row in Recent Sessions is immediate. The detail rebuilt every Codex session of the past year on each click, and now uses the row the dashboard already published, so it reads no accounting data at all.
64
+ - The dashboard's Codex hero keeps its last known spend, with a quiet updating marker, while the quota projection reconciles. It blanked the figure for that moment, and the account card beneath it went on showing a number.
65
+ - A Codex hero says it is pending, rather than rendering an empty value that reads as no spend, both before its first coherent reading and whenever its weekly cycle stops resolving afterwards.
66
+ - The dashboard publishes a recorded metering-rate change on the next refresh. Nothing the dashboard compared between refreshes changed when one was written, so an idle dashboard kept serving the alert list from before it.
67
+ - The dashboard evicts its least recently used Codex caches at their retained-memory budget instead of discarding all of them. A store permanently above the budget rebuilt every cached view on every refresh. Published figures are unchanged.
68
+ - The visible-population memo is still evicted whole, not in part, because it is one indivisible population. Two other cases take every Codex cache cold: a total eviction cannot reduce, and a refresh whose generation moved mid-build.
69
+ - The dashboard no longer discards its most expensive Codex caches first. Two reported no age and no size to the eviction order, so the visible-population memo and the quota memo always went first, however recently they had been used.
70
+ - `cctally dashboard-perf` reports what the Codex source caches retain. The figure came from a background measurement that never finished on a large store, so it read as zero.
71
+ - Codex spend and transcripts reach the dashboard within one tick of a rollout being appended to. An append made mid-turn left no evidence any freshness check examined, so only the two-minute safety walk ever found it.
72
+ - A Codex rollout cctally has read since this release is re-read from the start when it is replaced in place, even at an unchanged size and modification time. An older read position carries no file identity to compare.
73
+ - Adding or removing a `$CODEX_HOME` entry is noticed immediately. A configured root could be added or removed with nothing on disk changing, and the dashboard would keep reporting itself up to date.
74
+ - The conversation viewer keeps every session title and the browse list's cost, project and date columns while `cctally cache-sync --rebuild` replays. The rebuild cleared those tables first, so the list showed no titles throughout.
75
+ - A transcript sync keeps reading a file that the running session appended to mid-read. Any size or modification-time change read as a replacement, so an ordinary append discarded that pass's records and left the whole rebuild to run again.
76
+ - A transcript rebuild drops session titles whose sessions no longer exist. Every rebuild kept the previous titles in full, so titles for deleted sessions stayed forever and title search returned them.
77
+ - A transcript rebuild keeps each message's recorded account instead of re-attributing it to whichever account is active. The attribution was held in memory, so a rebuild resumed in a fresh process lost it.
78
+ - A transcript file replaced in place is recognised as a new file even when its size and modification time are unchanged, so records that no longer exist cannot lend their identity to whatever replaced them.
79
+ - `cctally cache-sync --rebuild` refuses, before deleting anything, when the volume holding `conversations.db` is nearly full, instead of failing partway through and leaving the store half-rebuilt.
80
+ - `cctally doctor` warns when it cannot read a store's rollup pricing fingerprint, instead of reporting no refused write. A locked store read as one that recorded nothing, and every writer of conversation cost authorized itself over it.
81
+ - `cctally cache-sync --prune-orphans` exits 3 and names the affected files when the conversation rollup re-derive is refused. It reported success, and the dashboard's automatic prune said nothing at all.
82
+ - The conversation browse rail falls back to live aggregation when the rollup's state cannot be read, instead of reporting it authoritative over a failed read. Sessions stay visible; cost sorting and project filtering degrade.
83
+ - `cctally doctor` reports a transcript reclaim backlog that is not draining, and fails above 16 GiB. A rebuild is refused at that point, because it adds churn to a store already failing to return the space it freed.
84
+ - `cctally cache-sync --rebuild` measures the free space it needs against the store instead of a fixed 64 MiB. A rebuild replays the whole corpus before the pages it frees come back, so the fixed figure admitted rebuilds that ran out of room.
85
+ - A transcript rebuild that finds no files on disk no longer deletes every message's recorded account. An unmounted volume produced that walk, and the account is the one thing in the store that cannot be derived again.
86
+ - A `cctally dashboard --no-sync` run advances the transcript schema once at startup. Nothing else does in that mode, so a store one migration behind stayed behind and every conversation route stayed degraded.
87
+ - `cctally five-hour-blocks`, `five-hour-breakdown` and the dashboard's Blocks panel record a five-hour credit only when one reporting source both observes the drop and confirms it. A stale status-line sample below an API reading minted one.
88
+ - `cctally statusline` records a weekly reset within 180 seconds even while another session still reports the pre-reset percentage. Agreement from every session was required, so one session that never agreed held the reset back forever.
89
+ - `cctally record-credit` keeps its journalled cleanup when the operation is retried. The list of rows to remove followed database row order, so a retry could describe the same removal differently and have the second description withheld.
90
+ - The stats-database writer guard `cctally doctor` reports now checks every write, not only the first of its kind on a connection. A repeated write that reused an earlier statement went unchecked, so it was neither refused nor recorded.
91
+ - `cctally five-hour-blocks` totals, their breakdowns and the dashboard's Blocks panel count an entry once when a reset shift makes two windows overlap. The later window also priced the earlier one's overlapping entries.
92
+ - The dashboard's Blocks panel and its block modal report a closed five-hour block's recorded totals and model split. They recomputed from the cache, so a session ingested after the block closed changed numbers that are meant to be final.
93
+ - The Blocks share artifact counts a project's cost in one five-hour block only. It read the recorded rollup with a key that never matched, then swept each block's raw interval, so two overlapping windows both claimed the shared entries.
94
+ - The dashboard's block modal says when a closed five-hour block's headline comes from the record kept at its close while the chart below is drawn from the local cache. The two can differ, and nothing said so.
95
+ - After `cctally cache-sync --rebuild`, `cctally doctor` warns that `conversations.db` has space to reclaim until the passes drain it. The dashboard drains it; without one, run `cctally db vacuum --db conversations`.
96
+ - `cctally cache-sync --rebuild` still runs when the free space on its volume cannot be measured. A failed measurement is now unknown and the check is skipped; it used to read as too little space and defer every rebuild.
97
+ - The conversation viewer's live-tail stream answers immediately while a rebuild holds the transcript store, instead of holding the connection open for the whole rebuild. Two other conversation-route paths answer the same way.
98
+ - A dashboard request, panel rebuild or transcript sync that is under way when a new pricing revision is adopted finishes on the revision it started with, so one response is never priced from two revisions.
99
+ - `cctally cache-report`'s per-call tier threshold follows an adopted pricing revision. It was a fixed figure that a reload could not replace.
100
+ - A transcript schema upgrade is started once when several conversation requests find the store behind head at the same moment, instead of once per request.
101
+ - The dashboard's Codex cache report keeps each row on the day it belongs to when a row moves to another day. Its former day kept a copy, so the report either refused to build or published the row under a date it had left.
102
+
103
+ ### Maintenance
104
+ - The test-evidence scrub no longer redacts a diagnostic line for containing the ordinary word "token". A token credential now needs a real separator, so a line such as `unexpected token ')' at line 4` is published rather than replaced.
105
+ - A failing envelope-oracle verification keeps the envelope that disagreed with the baseline, and the sanitized failure extract names it, so a mismatch can be diagnosed from the retained evidence instead of by reproducing it.
106
+ - The sanitized failure extract is withheld whole, and says which check withheld it, when the scrub's transformer fails a health probe before publication. A per-line refusal could otherwise publish beside a secret the backstop cannot see.
107
+ - The envelope-oracle harness emits its results in the one form the failure-extract reader parses, bringing it under the harness scrub contract so its output survives sanitization.
108
+
8
109
  ## [1.108.0] - 2026-09-05
9
110
 
10
111
  ### Added
@@ -446,6 +446,10 @@ def _cmd_account_show(args: argparse.Namespace) -> int:
446
446
  ).fetchone()
447
447
  a = ({col: row[i] for i, col in enumerate(_ACCOUNT_COLUMNS)}
448
448
  if row is not None else None)
449
+ # HELD-INCLUSIVE, decided rather than overlooked (#769 S11,
450
+ # #824). This is an inventory of the rows the account actually
451
+ # wrote, not a reading of its weekly level, and a held row is a
452
+ # real write by a real tick.
449
453
  snap_count = _count_scoped(conn, "weekly_usage_snapshots", key)
450
454
  milestone_count = _count_scoped(conn, "percent_milestones", key)
451
455
  # Resolved while the connection is still open — the map is
@@ -292,7 +292,9 @@ def _alert_label_prefix(axis: str, account_key: "str | None",
292
292
  import _cctally_store
293
293
  conn = _cctally_store.stats_open_guarded(
294
294
  db_path,
295
- connect=lambda p: _sq.connect(f"file:{p}?mode=ro", uri=True),
295
+ # #778: forward the opener's `cached_statements=0`.
296
+ connect=lambda p, **kw: _sq.connect(
297
+ f"file:{p}?mode=ro", uri=True, **kw),
296
298
  )
297
299
  try:
298
300
  if not _cctally_account.provider_is_decorated(conn, vendor):
@@ -543,7 +545,9 @@ def _test_alert_account_key(vendor: str) -> str:
543
545
  return _lib_accounts.VENDOR_WIDE
544
546
  conn = _cctally_store.stats_open_guarded(
545
547
  db_path,
546
- connect=lambda p: _sq.connect(f"file:{p}?mode=ro", uri=True),
548
+ # #778: forward the opener's `cached_statements=0`.
549
+ connect=lambda p, **kw: _sq.connect(
550
+ f"file:{p}?mode=ro", uri=True, **kw),
547
551
  )
548
552
  try:
549
553
  for row in _cctally_account.load_accounts(conn, vendor):