cctally 1.52.1 → 1.54.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.
@@ -148,6 +148,22 @@ class DoctorState:
148
148
  # existing constructors stay valid and a loopback bind is byte-identical
149
149
  # whether or not expose is set.
150
150
  expose_transcripts: bool = False
151
+ # Conversation-sessions rollup consistency (#217 S1 / U9). The browse-rail
152
+ # rollup table `conversation_sessions` should carry one row per distinct
153
+ # `conversation_messages.session_id`; a quiescent mismatch indicates the
154
+ # rollup drifted from its source. `conv_sessions_rollup_count` =
155
+ # COUNT(*) conversation_sessions; `conv_messages_distinct_sessions` =
156
+ # COUNT(DISTINCT session_id) conversation_messages WHERE session_id IS NOT
157
+ # NULL. Either is None when cache.db can't be opened / the table is absent
158
+ # (pre-rollup) — the check degrades to OK. `conv_rollup_sync_in_progress`
159
+ # is True when a writer holds the cache.db.lock (non-blocking probe) OR any
160
+ # pending reingest/split/backfill `cache_meta` flag is present — sync_cache
161
+ # commits `conversation_messages` per file BEFORE the rollup recompute, so a
162
+ # mid-sync read transiently mismatches and must NOT WARN (Codex P2). All
163
+ # defaulted (placed last) so existing constructors stay valid.
164
+ conv_sessions_rollup_count: Optional[int] = None
165
+ conv_messages_distinct_sessions: Optional[int] = None
166
+ conv_rollup_sync_in_progress: bool = False
151
167
 
152
168
 
153
169
  @dataclasses.dataclass(frozen=True)
@@ -818,6 +834,73 @@ def _check_data_post_credit_milestones(s: DoctorState) -> CheckResult:
818
834
  )
819
835
 
820
836
 
837
+ def _check_data_conversation_sessions_rollup(s: DoctorState) -> CheckResult:
838
+ """Invariant: the browse-rail rollup ``conversation_sessions`` carries one
839
+ row per distinct ``conversation_messages.session_id`` (#217 S1 / U9).
840
+
841
+ OK when the two counts are equal, when either is ``None`` (pre-rollup or an
842
+ unreadable cache.db — consistent with the kernel's graceful-degrade posture),
843
+ OR when a sync/reingest/backfill is in progress. WARN ONLY on a mismatch
844
+ observed in a QUIESCENT cache.
845
+
846
+ False-WARN avoidance (Codex P2): ``sync_cache`` commits
847
+ ``conversation_messages`` per file *before* the ``conversation_sessions``
848
+ recompute, and resumable reingest commits per file before its rollup
849
+ completes — so an unsynchronized read can transiently mismatch. The
850
+ in-progress signal (``conv_rollup_sync_in_progress``) is set by
851
+ ``doctor_gather_state`` from a NON-BLOCKING ``cache.db.lock`` flock probe
852
+ (lock held ⇒ a writer is mid-flight) AND the presence of any pending
853
+ ``cache_meta`` reingest/split/backfill flag; if either says in-progress, this
854
+ stays OK. Doctor remains read-only and never blocks on the lock.
855
+
856
+ Informational only (no remediation): the next full ``sync_cache`` re-derives
857
+ the rollup via its incremental DELETE+INSERT.
858
+ """
859
+ rollup = s.conv_sessions_rollup_count
860
+ distinct = s.conv_messages_distinct_sessions
861
+ if rollup is None or distinct is None:
862
+ return CheckResult(
863
+ id="data.conversation_sessions_rollup",
864
+ title="Conversation rollup",
865
+ severity="ok",
866
+ summary="no data",
867
+ remediation=None,
868
+ details={"rollup_count": rollup,
869
+ "messages_distinct_sessions": distinct},
870
+ )
871
+ if s.conv_rollup_sync_in_progress or rollup == distinct:
872
+ return CheckResult(
873
+ id="data.conversation_sessions_rollup",
874
+ title="Conversation rollup",
875
+ severity="ok",
876
+ summary=(
877
+ "sync in progress"
878
+ if (s.conv_rollup_sync_in_progress and rollup != distinct)
879
+ else f"{rollup} session(s) tracked"
880
+ ),
881
+ remediation=None,
882
+ details={"rollup_count": rollup,
883
+ "messages_distinct_sessions": distinct,
884
+ "sync_in_progress": s.conv_rollup_sync_in_progress},
885
+ )
886
+ return CheckResult(
887
+ id="data.conversation_sessions_rollup",
888
+ title="Conversation rollup",
889
+ severity="warn",
890
+ summary=(
891
+ f"rollup has {rollup} session(s); messages span {distinct} "
892
+ f"(quiescent mismatch)"
893
+ ),
894
+ remediation=(
895
+ "Run any cctally command (or `cctally cache-sync --rebuild`) to "
896
+ "re-derive the conversation_sessions rollup."
897
+ ),
898
+ details={"rollup_count": rollup,
899
+ "messages_distinct_sessions": distinct,
900
+ "sync_in_progress": False},
901
+ )
902
+
903
+
821
904
  def _check_pricing_coverage(s: DoctorState) -> CheckResult:
822
905
  """WARN when recent (30-day) session data contains a model cctally cannot
823
906
  price exactly (spec §5.1).
@@ -1120,6 +1203,8 @@ _CATEGORY_DEFINITIONS: tuple[tuple[str, str, tuple[tuple[str, str], ...]], ...]
1120
1203
  ("data.codex_cache", "_check_data_codex_cache"),
1121
1204
  ("data.forked_buckets", "_check_data_forked_buckets"),
1122
1205
  ("data.post_credit_milestones", "_check_data_post_credit_milestones"),
1206
+ ("data.conversation_sessions_rollup",
1207
+ "_check_data_conversation_sessions_rollup"),
1123
1208
  )),
1124
1209
  ("pricing", "Pricing", (
1125
1210
  ("pricing.coverage", "_check_pricing_coverage"),