cctally 1.72.0 → 1.73.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.
@@ -2540,6 +2540,94 @@ def build_anon_plan_for_db(conn, *, home_dir):
2540
2540
  home_dirs=sorted(home_dirs), usernames=sorted(usernames))
2541
2541
 
2542
2542
 
2543
+ def build_anon_plan_for_sources(conn, *, home_dir, sources):
2544
+ """Provider-aware :class:`AnonPlan` for QUALIFIED conversation surfaces
2545
+ (#294 S7, §3.6). A SEPARATE builder from :func:`build_anon_plan_for_db`, which
2546
+ stays byte-untouched together with every bare-Claude call site — unioning Codex
2547
+ roots into the legacy builder would renumber its ``project-N`` tokens and change
2548
+ bare-Claude anonymized bytes. This builder is used ONLY by ``v1.``-qualified
2549
+ requests (both providers), which are new and carry no byte-compat debt.
2550
+
2551
+ ``sources`` selects the provider legs to include (e.g. ``{"codex"}`` or
2552
+ ``{"claude"}``). The Codex leg draws ONLY from the authoritative tables:
2553
+ provider root dirs from ``codex_source_roots.canonical_root_path``, observed
2554
+ project paths from validated ``codex_conversation_threads.cwd`` (absolute paths
2555
+ only), display labels from ``codex_conversation_rollups.project_label``.
2556
+ ``git_json`` is NOT a path source (its fields are branch/repository metadata,
2557
+ not filesystem roots). The existing home-dir/username machinery and
2558
+ ``SECRET_PATTERNS`` apply unchanged; coverage stays best-effort-over-known-tokens
2559
+ and the per-token replacement stays fail-closed."""
2560
+ from _lib_conversation_anon import build_anon_plan
2561
+ want = set(sources or ())
2562
+ project_roots: dict = {}
2563
+ paths: set = set()
2564
+ if "claude" in want:
2565
+ cwds: set = set()
2566
+ try:
2567
+ for (cwd,) in conn.execute(
2568
+ "SELECT DISTINCT cwd FROM conversation_messages "
2569
+ "WHERE cwd IS NOT NULL AND cwd != ''"):
2570
+ if cwd:
2571
+ cwds.add(cwd)
2572
+ except sqlite3.OperationalError:
2573
+ pass
2574
+ try:
2575
+ for (pp,) in conn.execute(
2576
+ "SELECT DISTINCT project_path FROM session_files "
2577
+ "WHERE project_path IS NOT NULL AND project_path != ''"):
2578
+ if pp:
2579
+ cwds.add(pp)
2580
+ except sqlite3.OperationalError:
2581
+ pass
2582
+ for c in cwds:
2583
+ project_roots[c] = _project_label(c)
2584
+ paths.add(c)
2585
+ if "codex" in want:
2586
+ labels: dict = {}
2587
+ try:
2588
+ for ck, pl in conn.execute(
2589
+ "SELECT conversation_key, project_label FROM codex_conversation_rollups "
2590
+ "WHERE project_label IS NOT NULL AND project_label != ''"):
2591
+ labels[ck] = pl
2592
+ except sqlite3.OperationalError:
2593
+ pass
2594
+ try:
2595
+ for ck, cwd in conn.execute(
2596
+ "SELECT conversation_key, cwd FROM codex_conversation_threads "
2597
+ "WHERE cwd IS NOT NULL AND cwd != ''"):
2598
+ if isinstance(cwd, str) and cwd and os.path.isabs(cwd):
2599
+ project_roots[cwd] = labels.get(ck) or _project_label(cwd)
2600
+ paths.add(cwd)
2601
+ except sqlite3.OperationalError:
2602
+ pass
2603
+ try:
2604
+ for (root_path,) in conn.execute(
2605
+ "SELECT canonical_root_path FROM codex_source_roots "
2606
+ "WHERE canonical_root_path IS NOT NULL AND canonical_root_path != ''"):
2607
+ if isinstance(root_path, str) and root_path and os.path.isabs(root_path):
2608
+ project_roots.setdefault(root_path, _project_label(root_path))
2609
+ paths.add(root_path)
2610
+ except sqlite3.OperationalError:
2611
+ pass
2612
+ home_dirs: set = set()
2613
+ usernames: set = set()
2614
+ if home_dir:
2615
+ home_dirs.add(home_dir)
2616
+ u = os.path.basename(home_dir.rstrip("/"))
2617
+ if u:
2618
+ usernames.add(u)
2619
+ for p in paths:
2620
+ for prefix in ("/Users/", "/home/"):
2621
+ if p.startswith(prefix):
2622
+ user = p[len(prefix):].split("/", 1)[0]
2623
+ if user:
2624
+ home_dirs.add(prefix + user)
2625
+ usernames.add(user)
2626
+ return build_anon_plan(
2627
+ project_roots=project_roots,
2628
+ home_dirs=sorted(home_dirs), usernames=sorted(usernames))
2629
+
2630
+
2543
2631
  _TASK_TRIO = ("TaskCreate", "TaskUpdate", "TaskList")
2544
2632
 
2545
2633
 
@@ -15,6 +15,7 @@ import dataclasses
15
15
  import datetime as dt
16
16
  import hashlib
17
17
  import json
18
+ import math
18
19
  import pathlib
19
20
  import sys
20
21
  from typing import Optional
@@ -213,6 +214,10 @@ class DoctorState:
213
214
  # never-WARN posture (the hooks.installed / settings warnings already
214
215
  # surface a genuinely-unreadable settings.json — no double-WARN here).
215
216
  statusline_refresh_state: str = "unavailable"
217
+ # #318: read-only evidence for the per-session statusline candidate
218
+ # arbitration pipeline. None means the gather could not inspect it;
219
+ # absent transport/tombstones are normal when Claude is closed.
220
+ statusline_pipeline: Optional[dict] = None
216
221
 
217
222
 
218
223
  @dataclasses.dataclass(frozen=True)
@@ -737,6 +742,62 @@ def _check_data_latest_snapshot_age(s: DoctorState) -> CheckResult:
737
742
  )
738
743
 
739
744
 
745
+ def _check_statusline_pipeline(s: DoctorState) -> CheckResult:
746
+ """Report transport, selected, control, and authority health separately."""
747
+ pipeline = s.statusline_pipeline if isinstance(s.statusline_pipeline, dict) else {}
748
+ tombstones = pipeline.get("tombstones")
749
+ tombstones = tombstones if isinstance(tombstones, dict) else {}
750
+ details = {
751
+ "transport_age_seconds": pipeline.get("transport_age_seconds"),
752
+ "selected_age_seconds": pipeline.get("selected_age_seconds"),
753
+ "active_candidate_count": pipeline.get("active_candidate_count", 0),
754
+ "control_db_agrees": pipeline.get("control_db_agrees"),
755
+ "tombstones": tombstones,
756
+ }
757
+ if any(value in {"invalid", "inflight"} for value in tombstones.values()):
758
+ return CheckResult(
759
+ id="data.statusline_pipeline", title="Statusline pipeline",
760
+ severity="warn", summary="authoritative state needs repair",
761
+ remediation="Run `cctally refresh-usage`", details=details,
762
+ )
763
+ if pipeline.get("control_db_agrees") is False:
764
+ return CheckResult(
765
+ id="data.statusline_pipeline", title="Statusline pipeline",
766
+ severity="warn", summary="selected control disagrees with database",
767
+ remediation="Run `cctally refresh-usage`, then inspect active sessions",
768
+ details=details,
769
+ )
770
+ transport_age = pipeline.get("transport_age_seconds")
771
+ selected_age = pipeline.get("selected_age_seconds")
772
+ transport_age = (
773
+ float(transport_age)
774
+ if isinstance(transport_age, (int, float)) and not isinstance(transport_age, bool)
775
+ else math.inf
776
+ )
777
+ selected_age = (
778
+ float(selected_age)
779
+ if isinstance(selected_age, (int, float)) and not isinstance(selected_age, bool)
780
+ else math.inf
781
+ )
782
+ if transport_age < 90 and selected_age >= 300:
783
+ return CheckResult(
784
+ id="data.statusline_pipeline", title="Statusline pipeline",
785
+ severity="warn", summary="timer active; selected usage stale",
786
+ remediation="Run `cctally refresh-usage`, then inspect active sessions",
787
+ details=details,
788
+ )
789
+ if transport_age >= 90:
790
+ summary = "no recent regular-pool timer observed"
791
+ elif selected_age < 300:
792
+ summary = "timer and selected usage fresh"
793
+ else:
794
+ summary = "selected usage awaiting next active timer"
795
+ return CheckResult(
796
+ id="data.statusline_pipeline", title="Statusline pipeline",
797
+ severity="ok", summary=summary, remediation=None, details=details,
798
+ )
799
+
800
+
740
801
  def _check_data_cache_sync_state(s: DoctorState) -> CheckResult:
741
802
  count = s.cache_entries_count or 0
742
803
  if count == 0:
@@ -1658,6 +1719,7 @@ _CATEGORY_DEFINITIONS: tuple[tuple[str, str, tuple[tuple[str, str], ...]], ...]
1658
1719
  )),
1659
1720
  ("data", "Data", (
1660
1721
  ("data.latest_snapshot_age", "_check_data_latest_snapshot_age"),
1722
+ ("data.statusline_pipeline", "_check_statusline_pipeline"),
1661
1723
  ("data.cache_sync_state", "_check_data_cache_sync_state"),
1662
1724
  ("data.codex_cache", "_check_data_codex_cache"),
1663
1725
  ("data.codex_quota", "_check_data_codex_quota"),