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.
- package/CHANGELOG.md +11 -0
- package/bin/_cctally_cache.py +245 -50
- package/bin/_cctally_core.py +20 -40
- package/bin/_cctally_dashboard_conversation.py +731 -94
- package/bin/_cctally_doctor.py +59 -0
- package/bin/_cctally_parser.py +17 -4
- package/bin/_cctally_record.py +162 -33
- package/bin/_cctally_refresh.py +58 -36
- package/bin/_cctally_statusline.py +811 -82
- package/bin/_cctally_transcript.py +223 -7
- package/bin/_lib_codex_conversation_export.py +124 -0
- package/bin/_lib_codex_conversation_query.py +500 -28
- package/bin/_lib_codex_conversation_watch.py +323 -0
- package/bin/_lib_conversation_dispatch.py +269 -7
- package/bin/_lib_conversation_query.py +88 -0
- package/bin/_lib_doctor.py +62 -0
- package/bin/_lib_statusline_candidates.py +734 -0
- package/bin/cctally +31 -2
- package/package.json +4 -1
|
@@ -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
|
|
package/bin/_lib_doctor.py
CHANGED
|
@@ -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"),
|