cctally 1.78.0 → 1.79.1

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.
@@ -787,13 +787,13 @@ def neutral_payload(
787
787
 
788
788
  Provider-specific selectors: Claude keeps its existing contract (``tool_use_id``
789
789
  + ``which ∈ {input, result}``), Codex uses ``block_key`` + ``which ∈ {call,
790
- output}``. Statuses: ``ok`` | ``not_found`` (404) | ``gone`` (410 — the physical
790
+ output, event}``. Statuses: ``ok`` | ``not_found`` (404) | ``gone`` (410 — the physical
791
791
  record moved/mutated)."""
792
792
  cref = resolve_conversation_ref(ref)
793
793
  if cref is None:
794
794
  return {"status": "not_found", "conversation_key": ref}
795
795
  if cref.source == "codex":
796
- if not block_key or which not in ("call", "output"):
796
+ if not block_key or which not in ("call", "output", "event"):
797
797
  return {"status": "not_found", "block_key": block_key, "which": which}
798
798
  return q.read_codex_payload(conn, cref.conversation_key, block_key, which)
799
799
  # Claude: tool_use_id + which={input,result}, contract unchanged.
@@ -234,6 +234,13 @@ class DoctorState:
234
234
  # retained corpus from unreadable metadata.
235
235
  codex_project_metadata_health: Optional[dict] = None
236
236
  codex_project_metadata_error: Optional[str] = None
237
+ # Beta-channel (spec 2026-07-21 §3): the configured RELEASE channel
238
+ # (stable|beta) that `cctally update` tracks — DISTINCT from the preview
239
+ # `channel` (prod|preview) above. Populated by doctor_gather_state from a
240
+ # raw config read (fail-soft to "stable"). Drives the `install.update_channel`
241
+ # check (WARN on beta+brew, else OK). Defaulted (placed last) so existing
242
+ # constructors stay valid and default to the stable posture.
243
+ update_channel: str = "stable"
237
244
 
238
245
 
239
246
  @dataclasses.dataclass(frozen=True)
@@ -445,6 +452,36 @@ def _check_install_dev_mode(s: DoctorState) -> CheckResult:
445
452
  )
446
453
 
447
454
 
455
+ def _check_install_update_channel(s: DoctorState) -> CheckResult:
456
+ """Report the configured update (release) channel alongside the install
457
+ method (beta-channel, spec 2026-07-21 §3).
458
+
459
+ DISTINCT from the preview `channel` in install.mode: this reports the
460
+ `update.channel` config leaf (stable|beta) that `cctally update` tracks.
461
+ WARN on the brew+beta mismatch — Homebrew IS the stable channel (Q2), so
462
+ a beta opt-in on brew silently resolves stable; the WARN makes that
463
+ explicit and actionable. Otherwise always OK (a diagnostic surface, never
464
+ a hard failure — doctor exits 2 only on FAIL)."""
465
+ channel = s.update_channel or "stable"
466
+ if channel == "beta" and s.install_is_brew:
467
+ return CheckResult(
468
+ id="install.update_channel", title="Update channel",
469
+ severity="warn",
470
+ summary="beta (unavailable on Homebrew — installs track stable)",
471
+ remediation=(
472
+ "Homebrew tracks stable only. Install via npm or source to "
473
+ "receive beta releases, or run "
474
+ "`cctally config set update.channel stable`."
475
+ ),
476
+ details={"channel": channel, "method": "brew"},
477
+ )
478
+ return CheckResult(
479
+ id="install.update_channel", title="Update channel",
480
+ severity="ok", summary=channel, remediation=None,
481
+ details={"channel": channel},
482
+ )
483
+
484
+
448
485
  _REQUIRED_HOOK_EVENTS = ("PostToolBatch", "Stop", "SubagentStop")
449
486
 
450
487
 
@@ -1830,6 +1867,7 @@ def _check_db_conversations_reclaimable(s: DoctorState) -> CheckResult:
1830
1867
  _CATEGORY_DEFINITIONS: tuple[tuple[str, str, tuple[tuple[str, str], ...]], ...] = (
1831
1868
  ("install", "Install", (
1832
1869
  ("install.mode", "_check_install_dev_mode"),
1870
+ ("install.update_channel", "_check_install_update_channel"),
1833
1871
  ("install.symlinks", "_check_install_symlinks"),
1834
1872
  ("install.path", "_check_install_path"),
1835
1873
  ("install.legacy_snippet", "_check_install_legacy_snippet"),
@@ -0,0 +1,81 @@
1
+ """Pure kernel for the hero-modal milestone-history feature (stdlib only).
2
+
3
+ Small, dependency-free helpers shared by the Claude + Codex history glue
4
+ in ``_cctally_milestone_history.py``:
5
+
6
+ * ``compute_detail_stamp`` — content digest used both on the SSE index
7
+ entry and the detail response so the client can cache by
8
+ ``(source, key, detail_stamp)`` and revalidate when a week's underlying
9
+ rows move (spec §2 caching/invalidation).
10
+ * ``intersects`` — half-open interval intersection on epoch seconds
11
+ (a block appears in every week it intersects, spec §1b dual membership).
12
+ No imports of the cctally namespace or ``_cctally_core`` — these are pure
13
+ functions.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import hashlib
18
+
19
+
20
+ # Codex ``quota_window_blocks`` rows carry second-level capture jitter in their
21
+ # ``resets_at``: one physical reset (weekly *or* 5h) surfaces as several rows
22
+ # whose resets differ by a few seconds. This is the Codex-scoped analogue of
23
+ # the Claude 5h jitter floor (``_FIVE_HOUR_JITTER_FLOOR_SECONDS``) — same
24
+ # concept, different provider. Deliberately NOT ``_canonical_5h_window_key``,
25
+ # which is Claude-scoped per spec §1c. 600s (10 min) sits far above the
26
+ # observed jitter yet far below any genuine early re-anchor (hours apart) or a
27
+ # real weekly reset (~7 days apart), so only jitter is collapsed.
28
+ CODEX_CYCLE_JITTER_FLOOR_SECONDS = 600
29
+
30
+
31
+ def cluster_by_reset_jitter(items, *, reset_key, floor_seconds=CODEX_CYCLE_JITTER_FLOOR_SECONDS):
32
+ """Group items that share one physical reset, collapsing capture jitter.
33
+
34
+ ``items`` is any iterable; ``reset_key(item)`` returns that item's reset as
35
+ an epoch ``int``/``float``. Items are sorted by reset ascending, and each
36
+ run of consecutive items whose neighbour-to-neighbour reset gap is
37
+ ``<= floor_seconds`` forms one cluster. A gap greater than the floor starts
38
+ a new cluster, so genuine early re-anchors (hours apart) and real weekly
39
+ resets (~7 days apart) stay in distinct clusters — only sub-floor jitter is
40
+ merged.
41
+
42
+ Returns a list of clusters (each a non-empty list of the original items),
43
+ ordered by ascending cluster reset; within a cluster items follow the reset
44
+ sort. An empty ``items`` yields ``[]``.
45
+ """
46
+ ordered = sorted(items, key=reset_key)
47
+ clusters: list = []
48
+ current: list = []
49
+ prev = None
50
+ for item in ordered:
51
+ r = reset_key(item)
52
+ if prev is None or (r - prev) <= floor_seconds:
53
+ current.append(item)
54
+ else:
55
+ clusters.append(current)
56
+ current = [item]
57
+ prev = r
58
+ if current:
59
+ clusters.append(current)
60
+ return clusters
61
+
62
+
63
+ def compute_detail_stamp(*parts: object) -> str:
64
+ """Return a 16-hex-char sha256 digest over ``"|"``-joined ``str(p)``.
65
+
66
+ ``None`` parts serialize as the empty string so a nullable count/timestamp
67
+ contributes deterministically. Truncating to 16 chars keeps the wire small
68
+ while staying collision-safe for the small per-week input space.
69
+ """
70
+ joined = "|".join("" if p is None else str(p) for p in parts)
71
+ return hashlib.sha256(joined.encode("utf-8")).hexdigest()[:16]
72
+
73
+
74
+ def intersects(start_a: int, end_a: int, start_b: int, end_b: int) -> bool:
75
+ """Half-open interval intersection on epoch seconds.
76
+
77
+ ``[start_a, end_a)`` overlaps ``[start_b, end_b)`` iff each interval
78
+ starts before the other ends. Used for block/week dual membership so a
79
+ block straddling a week boundary is reported in every week it intersects.
80
+ """
81
+ return start_a < end_b and start_b < end_a
package/bin/cctally CHANGED
@@ -1131,6 +1131,10 @@ _UPDATE_CHECK_TTL_HOURS_MIN = _cctally_update._UPDATE_CHECK_TTL_HOURS_MIN
1131
1131
  _UPDATE_CHECK_TTL_HOURS_MAX = _cctally_update._UPDATE_CHECK_TTL_HOURS_MAX
1132
1132
  _normalize_update_check_enabled_value = _cctally_update._normalize_update_check_enabled_value
1133
1133
  _validate_update_check_ttl_hours_value = _cctally_update._validate_update_check_ttl_hours_value
1134
+ UPDATE_CHANNELS = _cctally_update.UPDATE_CHANNELS
1135
+ UPDATE_CHANNEL_DEFAULT = _cctally_update.UPDATE_CHANNEL_DEFAULT
1136
+ _validate_update_channel_value = _cctally_update._validate_update_channel_value
1137
+ resolve_update_channel = _cctally_update.resolve_update_channel
1134
1138
  _UPDATE_STATE_SCHEMA_MAX = _cctally_update._UPDATE_STATE_SCHEMA_MAX
1135
1139
  _UPDATE_SUPPRESS_SCHEMA_MAX = _cctally_update._UPDATE_SUPPRESS_SCHEMA_MAX
1136
1140
  _load_update_state = _cctally_update._load_update_state
@@ -1153,6 +1157,9 @@ _BREW_VERSION_RE_LIST = _cctally_update._BREW_VERSION_RE_LIST
1153
1157
  _update_user_agent = _cctally_update._update_user_agent
1154
1158
  _fetch_url = _cctally_update._fetch_url
1155
1159
  _check_npm_latest_version = _cctally_update._check_npm_latest_version
1160
+ _fetch_npm_dist_tag_version = _cctally_update._fetch_npm_dist_tag_version
1161
+ _resolve_npm_channel_target = _cctally_update._resolve_npm_channel_target
1162
+ _semver_or_parse_error = _cctally_update._semver_or_parse_error
1156
1163
  _check_brew_latest_version = _cctally_update._check_brew_latest_version
1157
1164
  _is_update_check_due = _cctally_update._is_update_check_due
1158
1165
  _do_update_check = _cctally_update._do_update_check
@@ -1162,6 +1169,9 @@ _spawn_background_telemetry_beat = _cctally_update._spawn_background_telemetry_b
1162
1169
  cmd_telemetry_beat_internal = _cctally_update.cmd_telemetry_beat_internal
1163
1170
  SKIP_USE_STATE_LATEST = _cctally_update.SKIP_USE_STATE_LATEST
1164
1171
  _format_update_command = _cctally_update._format_update_command
1172
+ BREW_BETA_ADVISORY = _cctally_update.BREW_BETA_ADVISORY
1173
+ _resolved_install_target = _cctally_update._resolved_install_target
1174
+ _resolved_update_command = _cctally_update._resolved_update_command
1165
1175
  _prerelease_note = _cctally_update._prerelease_note
1166
1176
  _format_update_check_json = _cctally_update._format_update_check_json
1167
1177
  _UPDATE_METHOD_HUMAN_LABEL = _cctally_update._UPDATE_METHOD_HUMAN_LABEL
@@ -1631,6 +1641,14 @@ UPDATE_NPM_REGISTRY_URL = os.environ.get(
1631
1641
  "CCTALLY_TEST_UPDATE_NPM_URL",
1632
1642
  "https://registry.npmjs.org/cctally/latest",
1633
1643
  )
1644
+ # Beta dist-tag endpoint (beta-channel, spec 2026-07-21 §3). The client
1645
+ # resolves the beta target as SemVer-max(dist-tags.beta, dist-tags.latest);
1646
+ # an exact 404 here means the `beta` dist-tag is not yet published
1647
+ # (transition window → fall back to latest). Env-overridable for fixtures.
1648
+ UPDATE_NPM_BETA_REGISTRY_URL = os.environ.get(
1649
+ "CCTALLY_TEST_UPDATE_NPM_BETA_URL",
1650
+ "https://registry.npmjs.org/cctally/beta",
1651
+ )
1634
1652
  UPDATE_BREW_FORMULA_URL = os.environ.get(
1635
1653
  "CCTALLY_TEST_UPDATE_BREW_URL",
1636
1654
  "https://raw.githubusercontent.com/omrikais/homebrew-cctally/main/Formula/cctally.rb",
@@ -2936,6 +2954,18 @@ _RESET_ZERO_MIN_DROP_PCT = _cctally_weekrefs._RESET_ZERO_MIN_DROP_PCT
2936
2954
  _is_reset_drop = _cctally_weekrefs._is_reset_drop
2937
2955
 
2938
2956
 
2957
+ # Eager re-export of bin/_cctally_milestone_history.py — the hero-modal
2958
+ # historical milestone navigation glue (week/cycle index + week/cycle detail).
2959
+ # Reached via `c.` from the envelope build, the source build, and the
2960
+ # /api/milestones route handler. Refs get their impure cctally symbols via the
2961
+ # module's own call-time _cctally() accessor, so load order is irrelevant.
2962
+ _cctally_milestone_history = _load_sibling("_cctally_milestone_history")
2963
+ build_claude_week_index = _cctally_milestone_history.build_claude_week_index
2964
+ build_claude_week_detail = _cctally_milestone_history.build_claude_week_detail
2965
+ build_codex_cycle_index = _cctally_milestone_history.build_codex_cycle_index
2966
+ build_codex_cycle_detail = _cctally_milestone_history.build_codex_cycle_detail
2967
+
2968
+
2939
2969
  # Eager re-export of bin/_cctally_percent_breakdown.py — the percent-breakdown
2940
2970
  # handler (C8, #125 Batch B). Parser dispatch reaches c.cmd_percent_breakdown.
2941
2971
  _cctally_percent_breakdown = _load_sibling("_cctally_percent_breakdown")
@@ -3080,6 +3110,7 @@ _tui_sparkline_big = _cctally_tui._tui_sparkline_big
3080
3110
  _tui_width_bucket = _cctally_tui._tui_width_bucket
3081
3111
  # Snapshot builders (per-panel + orchestrator + empty)
3082
3112
  _tui_build_percent_milestones = _cctally_tui._tui_build_percent_milestones
3113
+ _tui_build_five_hour_milestones = _cctally_tui._tui_build_five_hour_milestones # milestone-history week detail c.
3083
3114
  _tui_build_current_week = _cctally_tui._tui_build_current_week
3084
3115
  _tui_build_forecast = _cctally_tui._tui_build_forecast
3085
3116
  _tui_build_trend = _cctally_tui._tui_build_trend