cctally 1.109.0 → 1.110.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 (44) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +6 -6
  3. package/bin/_cctally_cache.py +62 -13
  4. package/bin/_cctally_core.py +5 -1
  5. package/bin/_cctally_dashboard.py +362 -41
  6. package/bin/_cctally_dashboard_conversation.py +231 -40
  7. package/bin/_cctally_dashboard_envelope.py +48 -32
  8. package/bin/_cctally_dashboard_sources.py +503 -109
  9. package/bin/_cctally_db.py +44 -0
  10. package/bin/_cctally_diagnosis_sources.py +181 -15
  11. package/bin/_cctally_doctor.py +15 -3
  12. package/bin/_cctally_forecast.py +25 -12
  13. package/bin/_cctally_journal.py +59 -32
  14. package/bin/_cctally_milestone_history.py +65 -9
  15. package/bin/_cctally_parser.py +5 -0
  16. package/bin/_cctally_record.py +50 -12
  17. package/bin/_cctally_rederive.py +899 -30
  18. package/bin/_cctally_source_analytics.py +567 -133
  19. package/bin/_cctally_transcript.py +27 -2
  20. package/bin/_cctally_tui.py +39 -6
  21. package/bin/_lib_codex_conversation_query.py +286 -63
  22. package/bin/_lib_codex_metadata.py +207 -0
  23. package/bin/_lib_conversation_query.py +254 -37
  24. package/bin/_lib_doctor.py +16 -2
  25. package/bin/_lib_pricing.py +69 -14
  26. package/bin/_lib_rederive.py +903 -2
  27. package/bin/_lib_view_models.py +5 -0
  28. package/dashboard/static/assets/ConversationsView-BB9vUiL6.js +72 -0
  29. package/dashboard/static/assets/{DoctorModal-BZGXOlc2.js → DoctorModal-DFY2iPDf.js} +1 -1
  30. package/dashboard/static/assets/ModalRoot-DvRKHbQE.js +1 -0
  31. package/dashboard/static/assets/{ProjectsDrillPanel-KbO_KWa7.js → ProjectsDrillPanel-DAv39uSo.js} +1 -1
  32. package/dashboard/static/assets/{SourceDetailModal-BrOPLVRL.js → SourceDetailModal-DSAWo2Fp.js} +1 -1
  33. package/dashboard/static/assets/{UpdateModal-CYgEFAi4.js → UpdateModal-R_9dOVnh.js} +3 -3
  34. package/dashboard/static/assets/index-COhxe3vU.css +1 -0
  35. package/dashboard/static/assets/index-DdU62sov.js +13 -0
  36. package/dashboard/static/assets/outlineNavigation-BOCLcRa6.js +9 -0
  37. package/dashboard/static/assets/{useKeymap-DQx_A1MB.js → useKeymap-CW7Up0Ay.js} +1 -1
  38. package/dashboard/static/dashboard.html +3 -3
  39. package/package.json +2 -1
  40. package/dashboard/static/assets/ConversationsView-TO5bpIyc.js +0 -72
  41. package/dashboard/static/assets/ModalRoot-C_fvugTl.js +0 -1
  42. package/dashboard/static/assets/index-Cj_l7T7u.css +0 -1
  43. package/dashboard/static/assets/index-DLVwACUM.js +0 -13
  44. package/dashboard/static/assets/outlineNavigation-DXX-RjKl.js +0 -9
package/CHANGELOG.md CHANGED
@@ -5,6 +5,52 @@ based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [1.110.0] - 2026-09-22
9
+
10
+ ### Added
11
+ - Claude cost reports price `claude-opus-5-5` at its published input, output, cache and Fast-mode rates instead of showing $0 for new usage.
12
+ - Codex cost reports price `gpt-6-luna` at its published Standard, long-context and Fast-mode rates, and resolve `gpt-daybreak-red-latest` to the current Cyber card.
13
+ - Historical `gpt-5.5-cyber` Codex turns use their retained LiteLLM OpenAI-provider rate card instead of the legacy `gpt-5` fallback.
14
+ - Codex reporting prices `gpt-6-sol` from OpenAI's published Standard, cached-input, long-context and Fast-mode rates instead of the legacy `gpt-5` fallback.
15
+ - The dashboard's `Blocks` panel marks Claude rows whose cost and model figures come from the facts retained when the block closed, without recalculating those figures from the current cache.
16
+ - Maintainers can turn latest full-backlog triage into a dependency-safe Codex execution plan and run automatically in two Sol/High worktree slots, with refresh-safe splitting, one resumable monitor, and no worker-created orchestration chain.
17
+
18
+ ### Fixed
19
+ - Codex long-context requests above 272K input tokens now use OpenAI's higher input, cached-input and output rates for the entire request, including Fast-mode costs and dashboard cache savings.
20
+ - The dashboard promptly releases a Conversation Viewer live-tail thread when its reader closes, instead of retaining each one until the next 15-second keep-alive.
21
+ - The dashboard's Current Week modal reliably returns keyboard focus to the hero after Escape, even when a source-selector click is still pending under load.
22
+ - The `Blocks` detail modal connects its retained-facts explanation to the cost and token figures for screen readers.
23
+ - The `dashboard` fully hides its onboarding hint while an alert toast is active on a phone, so the alert action stays visible while the original onboarding timer continues.
24
+ - `cctally db rederive` retains a weekly cost snapshot when replay names the same range instants with different UTC-offset spelling, so reviewed corrections do not rewrite unchanged cost facts.
25
+ - `cctally db rederive` can apply an operator-reviewed weekly correction without carrying unrelated baseline drift. Version 2 manifests can preserve or release an accepted snapshot identity and pin both plan hashes before append.
26
+ - `cctally db rederive` preserves closed five-hour costs and accepted snapshot references when current replay rules revisit the same retained evidence.
27
+ - A Conversation Viewer link that waits for its outline now limits a failed page load to three requests, then shows the connection error with a Retry button for the linked message.
28
+ - `cctally db rederive` now refuses a correction that would add a burst of matching weekly reset events, and its JSON preview breaks action counts out by event kind for inspection.
29
+ - Dashboard diagnosis and conversation outlines now bound request surges and stalled reads. A timed-out diagnosis process is retired so later requests recover, and unchanged dashboard files revalidate without rereading or recompression.
30
+ - The dashboard's `Current Week` five-hour block now follows the account that supplied the displayed weekly percentage. Its credits and crossings stay with that account when two accounts share a window, including while browsing older blocks.
31
+ - The dashboard's Claude Session detail now shows cache rebuilds and cache savings over plain HTTP LAN access, including the healthy zero-rebuild state. Opening the same session over loopback and LAN shows the same verified outline.
32
+ - Dashboard panel headings now shorten before their action buttons escape narrow cards, including when Projects data is unavailable.
33
+ - The dashboard calls Claude's credited window a cycle in the hero and milestone modal. Its cost comparison names a comparable cycle, multi-account spend no longer implies one common cycle, and week-scoped panels keep their week labels.
34
+ - The dashboard's phone-width account cards now show a swipe cue while more cards remain, and long snapshot ages stay readable inside the hero.
35
+ - At phone widths where the dashboard source-status chip is shown, warning labels now name both the affected area and its state, such as `Accounts unavailable` or `Projects partial`.
36
+ - The dashboard's `Send test alert` preview now shows the same account chip and dismiss hint as a real alert on single-account and multi-account installs.
37
+ - Closing a dashboard modal now returns keyboard focus to its opener, and explanatory card text no longer opens a modal when clicked.
38
+ - The dashboard's All-source Daily detail shows a single-provider day's model costs once, without an empty second provider row.
39
+ - The dashboard's Projects modal presents its cycle window and chart scale as separate radio groups for screen readers.
40
+ - Corrupt Codex token or timestamp values no longer empty the provider. Valid costs and projects remain visible while affected rows are skipped and counted, including rows in a budget period older than the detail view.
41
+ - Long identifiers in Doctor remediation text now wrap inside the modal at narrow screen widths instead of being clipped.
42
+ - One Codex conversation whose stored project path cannot be read no longer empties the whole Codex provider. Every other project is published from current data, and the affected rows are reported as withheld.
43
+ - A Codex project path the store cannot read is counted and named in `cctally doctor`, under a third reason beside the missing key and missing thread join it already reports.
44
+ - A refresh that could not read project metadata is replaced on the next refresh instead of being kept for up to two minutes.
45
+ - A refresh whose project metadata could not be read states so in the Projects panel body, so an empty panel is no longer silent about why.
46
+ - The Conversation Viewer opens a conversation whose stored project path cannot be read, showing no project instead of failing, and its spawned-agent links, raw export and live tail work again.
47
+ - No conversation list, filter, search hit or outline shows a project for a conversation whose stored project path can no longer be read.
48
+ - An anonymized transcript export refuses while any Codex project path cannot be read, rather than producing bytes that only look scrubbed. `cctally transcript export` exits 3 and the viewer says so; `--raw` is unaffected.
49
+ - An account-scoped anonymized Codex transcript now scrubs project paths tied to that account. If path ownership is uncertain, export and the viewer's Anon copy refuse instead of claiming the copy is anonymous.
50
+ - In the Conversation Viewer, a refused anonymized per-turn copy now shows a red X on the control, so a tap visibly reports that nothing was copied.
51
+ - The dashboard's Session detail no longer logs a failed outline request when a transcript was not kept. The Conversation Viewer names a temporarily unavailable transcript store on deep links and while search results load.
52
+ - `cctally explain` and the dashboard's Explain view retry a brief database change before reporting that the read could not complete.
53
+
8
54
  ## [1.109.0] - 2026-09-15
9
55
 
10
56
  ### 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.104.0** (2026-08-29)
33
+ **Latest stable: v1.109.0** (2026-09-15)
34
34
 
35
- Highlights from the `v1.101.0` to `v1.104.0` stable upgrade:
35
+ Highlights from the `v1.104.0` to `v1.109.0` stable upgrade:
36
36
 
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.
37
+ - 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.
38
+ - 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.
39
+ - 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.
40
40
 
41
- [See every change in this stable upgrade](https://github.com/omrikais/cctally/releases/tag/v1.104.0)
41
+ [See every change in this stable upgrade](https://github.com/omrikais/cctally/releases/tag/v1.109.0)
42
42
  <!-- cctally:latest-stable:end -->
43
43
 
44
44
  ## Quick start
@@ -136,6 +136,9 @@ import _lib_quota
136
136
  # #496 S5b §4: the journal-to-cache coverage certificate kernel. Stdlib-only,
137
137
  # so it is circular-safe for the same reason.
138
138
  import _lib_cache_coverage
139
+ # #845 §4.1: the decode helper is a stdlib-only leaf that imports nothing from
140
+ # this package, so binding it here is circular-safe for the same reason.
141
+ import _lib_codex_metadata
139
142
 
140
143
 
141
144
  # Module-level back-ref shims for the three out-of-scope JSONL/project
@@ -2766,18 +2769,45 @@ def _recompute_codex_rollups(
2766
2769
  models = sorted({r.model for r in rows if r.model})
2767
2770
  models_json = json.dumps(models) if models else None
2768
2771
  source_root_key = rows[0].source_root_key
2772
+ # #845 §4.7. The two columns are read as BLOBs and decoded field by
2773
+ # field, so one undecodable byte no longer raises here and no longer
2774
+ # fails the whole recompute. The table name stays UNQUALIFIED, because
2775
+ # this writer must honor account scoping: under
2776
+ # `scope_conversations_db_to_account` it sees the empty TEMP view on
2777
+ # purpose.
2769
2778
  thread = conn.execute(
2770
- "SELECT cwd, git_json, parent_thread_id, source_root_key "
2779
+ "SELECT CAST(cwd AS BLOB) AS cwd_blob, "
2780
+ "CAST(git_json AS BLOB) AS git_json_blob, parent_thread_id, "
2781
+ "source_root_key "
2771
2782
  "FROM codex_conversation_threads WHERE conversation_key = ?",
2772
2783
  (conversation_key,),
2773
2784
  ).fetchone()
2774
2785
  cwd = git_json = parent_thread_id = None
2786
+ thread_malformed = False
2775
2787
  if thread is not None:
2776
- cwd, git_json, parent_thread_id, thread_root = thread
2788
+ cwd_blob, git_json_blob, parent_thread_id, thread_root = thread
2789
+ decoded = _lib_codex_metadata.decode_codex_project_metadata(
2790
+ cwd_blob, git_json_blob)
2791
+ cwd, git_json = decoded.cwd, decoded.git_json
2792
+ # The DIRECT-ONLY predicate: this writer resolves the thread's own
2793
+ # fields through `_codex_conversation_project_attribution`, which
2794
+ # stops at a usable `cwd`, so a valid `cwd` beside an undecodable
2795
+ # `git_json` is still attributed.
2796
+ thread_malformed = _lib_codex_metadata.codex_metadata_is_malformed(
2797
+ decoded, None)
2777
2798
  if thread_root:
2778
2799
  source_root_key = thread_root
2779
- project_key, project_label = _codex_conversation_project_attribution(
2780
- source_root_key, cwd, git_json)
2800
+ if thread_malformed:
2801
+ # `(unassigned)` is a real answer for a thread with no metadata.
2802
+ # Persisting it for a thread whose metadata could not be READ would
2803
+ # stamp a wrong answer that every later read prefers, which is the
2804
+ # failure class `docs/codex-gotchas.md` records for the materialized
2805
+ # `"(unassigned)"`. NULL is the absence every reader already
2806
+ # handles.
2807
+ project_key = project_label = None
2808
+ else:
2809
+ project_key, project_label = _codex_conversation_project_attribution(
2810
+ source_root_key, cwd, git_json)
2781
2811
  conn.execute(
2782
2812
  "INSERT INTO codex_conversation_rollups "
2783
2813
  "(conversation_key, source_root_key, parent_thread_id, item_count, "
@@ -11012,23 +11042,42 @@ def scope_conversations_db_to_account(
11012
11042
  }
11013
11043
  safe_project_attribution: dict[str, tuple[str | None, str | None]] = {}
11014
11044
  for conversation_key in codex_keys:
11045
+ # #845 §4.7. The thread is re-tested EVEN WHEN a rollup is persisted:
11046
+ # a rollup materialized before the thread was corrupted carries a
11047
+ # pre-corruption attribution, and preferring it would keep publishing
11048
+ # an answer the store can no longer support. The raw table is named
11049
+ # explicitly because the unqualified name is an empty TEMP view here.
11050
+ thread = conn.execute(
11051
+ "SELECT source_root_key, CAST(cwd AS BLOB) AS cwd_blob, "
11052
+ "CAST(git_json AS BLOB) AS git_json_blob "
11053
+ "FROM cache_db.codex_conversation_threads WHERE conversation_key=?",
11054
+ (conversation_key,),
11055
+ ).fetchone()
11056
+ decoded = None
11057
+ thread_malformed = False
11058
+ if thread is not None:
11059
+ decoded = _lib_codex_metadata.decode_codex_project_metadata(
11060
+ thread[1], thread[2])
11061
+ thread_malformed = _lib_codex_metadata.codex_metadata_is_malformed(
11062
+ decoded, None)
11015
11063
  persisted = conn.execute(
11016
11064
  "SELECT project_key,project_label "
11017
11065
  "FROM main.codex_conversation_rollups WHERE conversation_key=?",
11018
11066
  (conversation_key,),
11019
11067
  ).fetchone()
11020
11068
  if persisted is not None:
11021
- safe_project_attribution[conversation_key] = persisted
11022
- continue
11023
- thread = conn.execute(
11024
- "SELECT source_root_key,cwd,git_json "
11025
- "FROM cache_db.codex_conversation_threads WHERE conversation_key=?",
11026
- (conversation_key,),
11027
- ).fetchone()
11028
- if thread is not None:
11029
11069
  safe_project_attribution[conversation_key] = (
11030
- _codex_conversation_project_attribution(*thread)
11070
+ (None, None) if thread_malformed else persisted
11031
11071
  )
11072
+ continue
11073
+ if thread is not None:
11074
+ if thread_malformed:
11075
+ safe_project_attribution[conversation_key] = (None, None)
11076
+ else:
11077
+ safe_project_attribution[conversation_key] = (
11078
+ _codex_conversation_project_attribution(
11079
+ thread[0], decoded.cwd, decoded.git_json)
11080
+ )
11032
11081
  _recompute_codex_rollups(
11033
11082
  conn, codex_keys, advance_render_revision=False,
11034
11083
  )
@@ -1923,7 +1923,11 @@ def open_db(*, _target_path=None) -> sqlite3.Connection:
1923
1923
  import importlib
1924
1924
  _cctally_store = importlib.import_module("_cctally_store")
1925
1925
 
1926
- ensure_dirs()
1926
+ # Alternate targets are private scratch indexes. A rederive preview may
1927
+ # open one while the live data directory has never had a logs/ directory;
1928
+ # creating that directory would violate the preview's zero-write contract.
1929
+ if _target_path is None:
1930
+ ensure_dirs()
1927
1931
  # #453: probe the live index before the maintenance-shared opener. During
1928
1932
  # a large detached rebuild that opener can wait for its bounded 5-second
1929
1933
  # guard; a statusline arriving mid-replay would otherwise pay that delay