cctally 1.77.0 → 1.79.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.
@@ -38,7 +38,6 @@ edits — the forwarders hit ``sys.modules["_cctally_dashboard"]`` at call time
38
38
  """
39
39
  from __future__ import annotations
40
40
 
41
- import json
42
41
  import re
43
42
  import socket
44
43
  import sqlite3
@@ -51,6 +50,7 @@ from _cctally_cache import (
51
50
  sync_codex_conversations,
52
51
  _codex_provider_roots,
53
52
  )
53
+ from _lib_dashboard_json import encode_dashboard_json
54
54
 
55
55
  # Live-tail watch-loop tuning — used ONLY by _handle_get_conversation_events_impl
56
56
  # below, so moved here with the events handler (spec §4.1 / §6).
@@ -709,7 +709,8 @@ def _run_conversation_events_stream(
709
709
  import time as _time
710
710
  watch = sys.modules["cctally"]._load_sibling("_lib_conversation_watch")
711
711
  tail_frame = (
712
- "event: tail\ndata: " + json.dumps(tail_data) + "\n\n").encode("utf-8")
712
+ "event: tail\ndata: " + encode_dashboard_json(tail_data) + "\n\n"
713
+ ).encode("utf-8")
713
714
 
714
715
  if passive:
715
716
  # Frozen-data contract: no ingest, no emit. Keep-alive only.
@@ -1056,7 +1057,7 @@ def _handle_get_conversation_payload_impl(handler, path: str) -> None:
1056
1057
  q = _u.parse_qs(handler.path.partition("?")[2])
1057
1058
  if session_id.startswith("v1."):
1058
1059
  # Qualified payload readback (§3.4): Codex uses block_key + which={call,
1059
- # output}; a v1.claude key keeps the tool_use_id + which={input,result}
1060
+ # output,event}; a v1.claude key keeps the tool_use_id + which={input,result}
1060
1061
  # selector. neutral_payload picks per the resolved source; gone → 410.
1061
1062
  which_q = _qs_str(q, "which", "")
1062
1063
  tool_use_id_q = _qs_str(q, "tool_use_id", "") or None
@@ -1287,6 +1287,12 @@ def snapshot_to_envelope(snap: "DataSnapshot", *,
1287
1287
  update_envelope = {
1288
1288
  "state": _update_state_envelope,
1289
1289
  "suppress": _update_suppress_envelope,
1290
+ # Beta-channel (spec 2026-07-21 §3): the configured release channel,
1291
+ # derived DIRECTLY from config (never from cached state — a cached
1292
+ # producer field can't seed the settings toggle after a failed fetch
1293
+ # or on brew). The resolved-target fields (latest_version /
1294
+ # resolved_dist_tag / latest_version_channel) ride inside `state`.
1295
+ "configured_channel": sys.modules["cctally"].resolve_update_channel(config),
1290
1296
  }
1291
1297
 
1292
1298
  # Doctor aggregate block (spec §5.5). Only the small severity tree
@@ -1427,6 +1433,12 @@ def snapshot_to_envelope(snap: "DataSnapshot", *,
1427
1433
  # bound or the data source crashed during sync
1428
1434
  # (recorded on ``last_sync_error``).
1429
1435
  "five_hour_milestones": getattr(snap, "five_hour_milestones", []) or [],
1436
+ # Hero-modal historical-milestone navigation index (spec §3).
1437
+ # Additive/optional: newest-first per-week entries built on the
1438
+ # non-idle rebuild and stored on the snapshot; ``getattr``
1439
+ # default keeps legacy/positional fixture snapshots serializing
1440
+ # (they emit ``[]``). Historical rows never ride the envelope.
1441
+ "week_index": getattr(snap, "week_index", []) or [],
1430
1442
  },
1431
1443
 
1432
1444
  "forecast":
@@ -49,7 +49,10 @@ from _lib_jsonl import CodexEntry, codex_model_scoped_quota_pool
49
49
  from _lib_fmt import stable_sum
50
50
  from _lib_aggregators import _aggregate_codex_buckets
51
51
  from _lib_five_hour import _FIVE_HOUR_JITTER_FLOOR_SECONDS
52
- from _lib_source_analytics import build_codex_project_result
52
+ from _lib_source_analytics import (
53
+ build_codex_project_result,
54
+ collision_safe_project_label_map,
55
+ )
53
56
  from _lib_view_models import (
54
57
  CodexWeeklyView,
55
58
  build_codex_daily_view,
@@ -1783,12 +1786,17 @@ def _partial_projects_wire(
1783
1786
  model_totals[field] += value
1784
1787
  session_totals[field] += value
1785
1788
 
1789
+ label_map = collision_safe_project_label_map(
1790
+ (f"{root_key}\0{project_key}", str(group["label"]))
1791
+ for (root_key, project_key), group in groups.items()
1792
+ )
1786
1793
  rows = []
1787
1794
  for (root_key, _project_key), group in groups.items():
1795
+ internal_identity = f"{root_key}\0{group['project_key']}"
1788
1796
  rows.append({
1789
1797
  "key": dashboard_resource_key("project", "codex", root_key, group["project_key"]),
1790
1798
  "source": "codex",
1791
- "label": group["label"],
1799
+ "label": label_map[internal_identity],
1792
1800
  "session_count": len(group["sessions"]),
1793
1801
  "first_seen": group["first_seen"].astimezone(UTC).isoformat(),
1794
1802
  "last_seen": group["last_seen"].astimezone(UTC).isoformat(),
@@ -2072,7 +2080,21 @@ def build_codex_source_state(
2072
2080
  now_utc=context.now_utc,
2073
2081
  display_tz_name=context.display_tz_name,
2074
2082
  )
2075
- quota = {**quota, "blocks": quota_blocks}
2083
+ # Hero-modal historical-milestone navigation index (spec §1c, §3). Built
2084
+ # here on the non-idle codex source rebuild (idle ticks reuse the stored
2085
+ # bundle) over the durable projection — a pure serializer never touches it.
2086
+ # Guarded: an index failure must never fail the codex source build.
2087
+ cycle_index: tuple = ()
2088
+ if cycle is not None and not hero_failure:
2089
+ try:
2090
+ cycle_index = tuple(
2091
+ sys.modules["cctally"].build_codex_cycle_index(
2092
+ context.stats_conn, identity=cycle, now_utc=context.now_utc,
2093
+ )
2094
+ )
2095
+ except sqlite3.Error:
2096
+ cycle_index = ()
2097
+ quota = {**quota, "blocks": quota_blocks, "cycle_index": cycle_index}
2076
2098
  budget_rows = _budget_wire(context.stats_conn)
2077
2099
  projected_budget_rows = _projected_budget_wire(context.stats_conn)
2078
2100
  budget_cost_events = _codex_budget_cost_events(context, budget_entries)
@@ -27,6 +27,7 @@ import argparse
27
27
  import datetime as dt
28
28
  import fcntl
29
29
  import json
30
+ import math
30
31
  import pathlib
31
32
  import shutil
32
33
  import sqlite3
@@ -35,6 +36,7 @@ import sys
35
36
  import _cctally_core
36
37
  import _lib_changelog
37
38
  from _cctally_core import _now_utc, eprint, now_utc_iso, parse_iso_datetime
39
+ from _lib_dashboard_json import encode_dashboard_json
38
40
 
39
41
 
40
42
  def _cctally():
@@ -53,11 +55,25 @@ def _gather_statusline_pipeline(c, *, now_utc: dt.datetime) -> dict:
53
55
  "tombstones": {"fiveHour": "absent", "sevenDay": "absent"},
54
56
  }
55
57
  try:
56
- result["transport_age_seconds"] = c._statusline_transport_age_seconds()
58
+ age = c._statusline_transport_age_seconds()
59
+ result["transport_age_seconds"] = (
60
+ age
61
+ if isinstance(age, (int, float))
62
+ and not isinstance(age, bool)
63
+ and math.isfinite(float(age))
64
+ else None
65
+ )
57
66
  except Exception:
58
67
  pass
59
68
  try:
60
- result["selected_age_seconds"] = c._statusline_observe_age_seconds()
69
+ age = c._statusline_observe_age_seconds()
70
+ result["selected_age_seconds"] = (
71
+ age
72
+ if isinstance(age, (int, float))
73
+ and not isinstance(age, bool)
74
+ and math.isfinite(float(age))
75
+ else None
76
+ )
61
77
  except Exception:
62
78
  pass
63
79
  try:
@@ -784,14 +800,28 @@ def doctor_gather_state(
784
800
  # on corruption — both behaviors would hide diagnostic state
785
801
  # (codex H1).
786
802
  config_json_error = None
803
+ config_parsed: dict = {}
787
804
  try:
788
805
  if _cctally_core.CONFIG_PATH.exists():
789
- json.loads(_cctally_core.CONFIG_PATH.read_text(encoding="utf-8"))
806
+ config_parsed = json.loads(
807
+ _cctally_core.CONFIG_PATH.read_text(encoding="utf-8")
808
+ )
790
809
  except json.JSONDecodeError as exc:
791
810
  config_json_error = f"{type(exc).__name__}: {exc}"
792
811
  except OSError as exc:
793
812
  config_json_error = f"OSError: {exc}"
794
813
 
814
+ # Configured update (release) channel (beta-channel, spec 2026-07-21 §3):
815
+ # derived from the SAME raw read (never load_config, which auto-creates on
816
+ # first run). Fail-soft to "stable" — resolve_update_channel already
817
+ # tolerates a non-dict block / junk value.
818
+ try:
819
+ update_channel = c.resolve_update_channel(
820
+ config_parsed if isinstance(config_parsed, dict) else {}
821
+ )
822
+ except Exception:
823
+ update_channel = "stable"
824
+
795
825
  update_state = None
796
826
  update_state_error = None
797
827
  try:
@@ -864,6 +894,7 @@ def doctor_gather_state(
864
894
  codex_jsonl_present=codex_jsonl_present,
865
895
  codex_project_metadata_health=codex_project_metadata_health,
866
896
  codex_project_metadata_error=codex_project_metadata_error,
897
+ update_channel=update_channel,
867
898
  dashboard_bind_stored=dashboard_bind_stored,
868
899
  runtime_bind=runtime_bind,
869
900
  # Conversation viewer (Plan 2, spec §5): only consulted on a LAN bind.
@@ -943,7 +974,7 @@ def cmd_doctor(args: argparse.Namespace) -> int:
943
974
  state = c.doctor_gather_state(deep=True)
944
975
  report = _lib_doctor.run_checks(state)
945
976
  if getattr(args, "json", False):
946
- print(json.dumps(
977
+ print(encode_dashboard_json(
947
978
  _lib_doctor.serialize_json(report), indent=2, sort_keys=True,
948
979
  ))
949
980
  else: