cctally 1.113.0 → 1.115.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.
Files changed (97) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +4 -6
  3. package/bin/_cctally_account.py +32 -8
  4. package/bin/_cctally_cache.py +6035 -722
  5. package/bin/_cctally_completion.py +1287 -0
  6. package/bin/_cctally_config.py +37 -0
  7. package/bin/_cctally_core.py +400 -2
  8. package/bin/_cctally_dashboard.py +98 -20
  9. package/bin/_cctally_dashboard_conversation.py +292 -23
  10. package/bin/_cctally_dashboard_envelope.py +16 -3
  11. package/bin/_cctally_dashboard_perf.py +128 -9
  12. package/bin/_cctally_dashboard_sources.py +285 -213
  13. package/bin/_cctally_db.py +1177 -23
  14. package/bin/_cctally_diagnosis_sources.py +22 -4
  15. package/bin/_cctally_doctor.py +572 -45
  16. package/bin/_cctally_five_hour.py +84 -33
  17. package/bin/_cctally_forecast.py +108 -15
  18. package/bin/_cctally_journal.py +3950 -116
  19. package/bin/_cctally_journal_repair.py +101 -12
  20. package/bin/_cctally_milestone_history.py +82 -9
  21. package/bin/_cctally_parser.py +42 -0
  22. package/bin/_cctally_percent_breakdown.py +3 -2
  23. package/bin/_cctally_project.py +8 -2
  24. package/bin/_cctally_quarantine.py +652 -0
  25. package/bin/_cctally_quota.py +227 -37
  26. package/bin/_cctally_quota_model.py +360 -83
  27. package/bin/_cctally_record.py +691 -149
  28. package/bin/_cctally_rederive.py +320 -41
  29. package/bin/_cctally_refresh.py +84 -13
  30. package/bin/_cctally_source_analytics.py +83 -17
  31. package/bin/_cctally_statusline.py +6154 -517
  32. package/bin/_cctally_store.py +367 -53
  33. package/bin/_cctally_transcript.py +8 -7
  34. package/bin/_cctally_tui.py +61 -56
  35. package/bin/_cctally_weekrefs.py +128 -21
  36. package/bin/_lib_account_binding.py +152 -0
  37. package/bin/_lib_account_protocol.py +386 -0
  38. package/bin/_lib_aggregators.py +4 -0
  39. package/bin/_lib_cache_coverage.py +13 -1
  40. package/bin/_lib_cache_report.py +35 -6
  41. package/bin/_lib_cache_writer_lock.py +19 -3
  42. package/bin/_lib_clone_copy.py +142 -0
  43. package/bin/_lib_codex_conversation.py +32 -0
  44. package/bin/_lib_codex_conversation_query.py +297 -49
  45. package/bin/_lib_codex_conversation_watch.py +59 -14
  46. package/bin/_lib_codex_drain_admission.py +214 -0
  47. package/bin/_lib_codex_metadata.py +18 -1
  48. package/bin/_lib_codex_publication_barrier.py +277 -0
  49. package/bin/_lib_codex_rollout_container.py +502 -0
  50. package/bin/_lib_conversation_dispatch.py +3 -0
  51. package/bin/_lib_conversation_query.py +106 -59
  52. package/bin/_lib_conversation_retention.py +1605 -640
  53. package/bin/_lib_cost_provenance.py +53 -2
  54. package/bin/_lib_detector_access_registry.py +3817 -0
  55. package/bin/_lib_doctor.py +714 -69
  56. package/bin/_lib_five_hour.py +126 -0
  57. package/bin/_lib_fmt.py +11 -0
  58. package/bin/_lib_ingest_frontier.py +414 -20
  59. package/bin/_lib_journal.py +141 -10
  60. package/bin/_lib_jsonl.py +5 -4
  61. package/bin/_lib_legacy_spool.py +126 -0
  62. package/bin/_lib_legacy_writer_evidence.py +136 -0
  63. package/bin/_lib_merge_gate.py +231 -86
  64. package/bin/_lib_meter_rate_change.py +10 -0
  65. package/bin/_lib_outcome_quarantine.py +690 -0
  66. package/bin/_lib_perf.py +27 -1
  67. package/bin/_lib_pricing.py +199 -9
  68. package/bin/_lib_pricing_check.py +20 -4
  69. package/bin/_lib_quarantine.py +154 -0
  70. package/bin/_lib_quota_calibration.py +53 -8
  71. package/bin/_lib_quota_copy.py +2 -1
  72. package/bin/_lib_quota_model.py +288 -46
  73. package/bin/_lib_reclaim_planner.py +792 -0
  74. package/bin/_lib_rederive.py +319 -82
  75. package/bin/_lib_segment_summary.py +35 -2
  76. package/bin/_lib_snapshot_cache.py +21 -0
  77. package/bin/_lib_sqlite_close.py +81 -0
  78. package/bin/_lib_statusline_candidates.py +292 -23
  79. package/bin/_lib_subscription_weeks.py +100 -17
  80. package/bin/_lib_tick_stats.py +217 -17
  81. package/bin/_lib_wal_checkpoint.py +1077 -0
  82. package/bin/_lib_write_budget.py +483 -0
  83. package/bin/_lib_write_io.py +703 -0
  84. package/bin/cctally +31 -1
  85. package/dashboard/static/assets/{ConversationsView-CEZ-BUyi.js → ConversationsView-Bc8LCOAZ.js} +1 -1
  86. package/dashboard/static/assets/{DoctorModal-CE1oH_Zy.js → DoctorModal-D--qVBwe.js} +1 -1
  87. package/dashboard/static/assets/ModalRoot-DLPgyAM_.js +1 -0
  88. package/dashboard/static/assets/{ProjectsDrillPanel-B3DZoCeD.js → ProjectsDrillPanel-z53a4v4M.js} +1 -1
  89. package/dashboard/static/assets/{SourceDetailModal-CHp9EgLP.js → SourceDetailModal-Bw0T8W-W.js} +1 -1
  90. package/dashboard/static/assets/{UpdateModal-5iKST6jo.js → UpdateModal-D5AKx4Yw.js} +1 -1
  91. package/dashboard/static/assets/index-BKITk1CT.css +1 -0
  92. package/dashboard/static/assets/{index-Cm0VTxJC.js → index-BXDOzSng.js} +3 -3
  93. package/dashboard/static/assets/{outlineNavigation-DkXGL8J6.js → outlineNavigation-JObY7cUY.js} +1 -1
  94. package/dashboard/static/dashboard.html +2 -2
  95. package/package.json +19 -1
  96. package/dashboard/static/assets/ModalRoot-D5Y8IFUG.js +0 -1
  97. package/dashboard/static/assets/index-Dt4tqf43.css +0 -1
@@ -470,6 +470,262 @@ def _gather_writer_guard_log(now_utc: dt.datetime):
470
470
  }
471
471
 
472
472
 
473
+ #: Bounded per-instance probe of a running dashboard's write telemetry.
474
+ DASHBOARD_WRITE_IO_TIMEOUT_SECONDS = 2.0
475
+
476
+
477
+ def _fetch_dashboard_write_io(descriptor: dict, *, timeout_s: float) -> dict:
478
+ """One loopback probe of a discovered dashboard (#901 §5.6).
479
+
480
+ Reuses `dashboard-perf`'s client — IP-literal loopback target, an
481
+ `Origin` matching the `Host` — and sends NO token, so an instance that
482
+ requires one answers 401 and is reported `authentication_required`: no
483
+ authentication bypass exists. The instance id in the answer must equal
484
+ the descriptor's, or the probe is an `identity_mismatch`."""
485
+ import _lib_write_io as wio
486
+
487
+ perf = _cctally()._cctally_dashboard_perf
488
+ base = {"instance_id": descriptor["instanceId"],
489
+ "port": descriptor["port"], "pid": descriptor["pid"]}
490
+ host = wio.loopback_target(descriptor.get("host"))
491
+ if host is None:
492
+ return {**base, "probe": "failed", "reason": "not_loopback_reachable",
493
+ "write_io": None}
494
+ try:
495
+ payload = perf._request(host, descriptor["port"], perf.WRITE_IO_PATH,
496
+ token=None, timeout=timeout_s)
497
+ except perf.DashboardPerfError as exc:
498
+ return {**base, "probe": "failed", "reason": exc.reason,
499
+ "write_io": None}
500
+ write_io = payload.get("writeIo") if isinstance(payload, dict) else None
501
+ if not isinstance(write_io, dict):
502
+ return {**base, "probe": "failed", "reason": "malformed_response",
503
+ "write_io": None}
504
+ if write_io.get("instanceId") != descriptor["instanceId"]:
505
+ return {**base, "probe": "failed", "reason": "identity_mismatch",
506
+ "write_io": None}
507
+ return {**base, "probe": "ok", "reason": None, "write_io": write_io}
508
+
509
+
510
+ def _gather_dashboard_disk_writes(
511
+ *, fetch=None, timeout_s: float = DASHBOARD_WRITE_IO_TIMEOUT_SECONDS,
512
+ discover: bool = True,
513
+ ) -> dict:
514
+ """Running dashboards' write telemetry for `performance.dashboard_disk_
515
+ writes` (#901 §5.6).
516
+
517
+ In the dashboard process the local instance's own snapshot is read
518
+ directly — no self-HTTP and no recursive gather. Anywhere else (the CLI,
519
+ the TUI) instances are discovered through their owner-readable
520
+ descriptors; a stale one (dead or reused pid) is ignored and never
521
+ deleted, because doctor is read-only. Discovery runs only for the
522
+ `cctally doctor` CLI (PR-11); with ``discover`` false nothing is probed and
523
+ the mode is ``not_probed``."""
524
+ import _lib_write_io as wio
525
+
526
+ local = wio.local_instance()
527
+ if local is not None:
528
+ return {"mode": "in_process", "instances": [{
529
+ "instance_id": local["instanceId"], "port": local["port"],
530
+ "pid": local["pid"], "probe": "ok", "reason": None,
531
+ "write_io": wio.write_io_payload(),
532
+ }]}
533
+ if not discover:
534
+ return {"mode": "not_probed", "instances": []}
535
+ probe = fetch or _fetch_dashboard_write_io
536
+ instances = [
537
+ probe(descriptor, timeout_s=timeout_s)
538
+ for descriptor in wio.read_instance_descriptors(_cctally_core.APP_DIR)
539
+ if wio.descriptor_is_live(descriptor)
540
+ ]
541
+ instances.sort(key=lambda entry: (entry["port"], entry["instance_id"]))
542
+ return {"mode": "discovery", "instances": instances}
543
+
544
+
545
+ def _gather_legacy_writer(deep_excluded, deep_boundary,
546
+ deep_error=None) -> dict:
547
+ """The `journal.legacy_writer` evidence (#825/#832 spec section 8).
548
+
549
+ Reads the admission summary on a validated current snapshot (an
550
+ unvalidated or missing summary is unavailable, never zero — the durable
551
+ evidence sidecar is still returned beside it, for the leg to report from)
552
+ and the durable evidence sidecar. A deep run substitutes its own whole-journal
553
+ recomputation of the excluded population; ``deep_error`` names why that
554
+ recomputation could not complete (fix round K11, K9K10-R6/R11), and then
555
+ the count is unavailable — never zero.
556
+
557
+ Spec revision 5: the leg also reports an incomplete legacy spool
558
+ inventory (``legacySpoolInventory``), independently of the count, and
559
+ says the detection pause exists only when the validated index holds the
560
+ durable `inventory-incomplete` marker. Read-only: the sidecar is never
561
+ written here, the spool is only read, and nothing is journaled."""
562
+ import _cctally_store
563
+ import _lib_account_protocol
564
+
565
+ out: dict = {"available": False}
566
+ row = None
567
+ inventory_marker = False
568
+ try:
569
+ if _cctally_core.DB_PATH.exists():
570
+ pc = _stats_ro_guarded()
571
+ try:
572
+ with _cctally_store.current_stats_snapshot(pc):
573
+ row = pc.execute(
574
+ "SELECT cutover_id, excluded_total, coordinate_digest "
575
+ "FROM journal_admission_summary WHERE id = 1"
576
+ ).fetchone()
577
+ inventory_marker = _durable_inventory_marker(pc)
578
+ finally:
579
+ pc.close()
580
+ except Exception: # noqa: BLE001 — a diagnostic read degrades to unavailable
581
+ row = None
582
+ try:
583
+ evidence = json.loads(
584
+ _cctally_core.LEGACY_WRITER_EVIDENCE_PATH.read_bytes())
585
+ except (OSError, ValueError):
586
+ evidence = None
587
+ inventory = _legacy_spool_inventory_probe(inventory_marker)
588
+ if inventory is not None:
589
+ out["legacySpoolInventory"] = inventory
590
+ if deep_error is not None:
591
+ return {**out, "unavailableCause": str(deep_error),
592
+ "evidence": evidence}
593
+ if deep_excluded is not None:
594
+ cutover_id = (None if deep_boundary is None
595
+ else _lib_account_protocol.CUTOVER_OP_ID)
596
+ count = len(deep_excluded)
597
+ digest = (_lib_account_protocol.coordinate_population_digest(
598
+ item.coordinate for item in deep_excluded) if count else None)
599
+ elif row is not None:
600
+ cutover_id, count, digest = row[0], int(row[1] or 0), row[2]
601
+ else:
602
+ # No current summary — exactly what an older process's epoch
603
+ # republish leaves behind. The durable sidecar still carries the
604
+ # excluded population and when it was first observed (#825/#832 fix
605
+ # round B, P2-2), so the leg reports from it rather than "unavailable".
606
+ return {**out, "evidence": evidence}
607
+ return {**out, "available": True, "cutoverId": cutover_id,
608
+ "excludedCount": count, "coordinateDigest": digest,
609
+ "evidence": evidence}
610
+
611
+
612
+ def _scan_cause(exc: BaseException) -> str:
613
+ """A journal read failure as a doctor cause (K9K10-R6): the message and,
614
+ for an OS error, its errno name — never an empty string."""
615
+ import errno as _errno
616
+ name = _errno.errorcode.get(getattr(exc, "errno", None) or 0)
617
+ text = getattr(exc, "strerror", None) or str(exc) or type(exc).__name__
618
+ filename = getattr(exc, "filename", None)
619
+ if filename:
620
+ text = f"{text}: {filename}"
621
+ return f"{text} [{name}]" if name else text
622
+
623
+
624
+ def _durable_inventory_marker(conn) -> bool:
625
+ """Whether the validated index holds an unresolved spool marker left by
626
+ an incomplete inventory (spec revision 5, section 8). Read inside the
627
+ caller's validated snapshot; a missing quarantine table is no marker."""
628
+ import _cctally_quarantine
629
+ import _lib_account_protocol
630
+ try:
631
+ state = _cctally_quarantine.load_state(conn)
632
+ except Exception: # noqa: BLE001 — no readable marker claims no pause
633
+ return False
634
+ return any(marker.get("reason")
635
+ == _lib_account_protocol.INVENTORY_INCOMPLETE_REASON
636
+ for marker in state.spool_markers)
637
+
638
+
639
+ def _legacy_spool_inventory_probe(paused: bool):
640
+ """The legacy spool inventory doctor itself takes (spec revision 5,
641
+ section 8): None when it completes, else ``{"available": False, "path",
642
+ "operation", "error", "paused"}``. Doctor can be the first process to
643
+ meet the error, so it probes rather than reading what the reducer saw.
644
+ Read-only: the spool is listed and read to digest it, never changed.
645
+
646
+ Fix round K12 (K11R-13): any other failure leaves the inventory
647
+ unavailable with its cause (``{"available": False, "unavailableCause",
648
+ "paused"}``), never complete."""
649
+ import errno as _errno
650
+ import _cctally_journal
651
+ try:
652
+ _cctally_journal.capture_legacy_spool_state()
653
+ except _cctally_journal.LegacySpoolInventoryIncomplete as exc:
654
+ code = exc.errno or _errno.EIO
655
+ return {"available": False,
656
+ "path": exc.failed_path or exc.rel,
657
+ "operation": exc.operation,
658
+ "error": f"{os.strerror(code)} [{exc.error}]",
659
+ "paused": bool(paused)}
660
+ except Exception as exc: # noqa: BLE001 — a diagnostic probe never crashes doctor
661
+ return {"available": False, "unavailableCause": _scan_cause(exc),
662
+ "paused": bool(paused)}
663
+ return None
664
+
665
+
666
+ def _statusline_authority_commits() -> dict:
667
+ """Each account partition's reconciled `authorityCommit` (#825/#832 spec
668
+ 5.6 item 7, fix-round-1 Q6): control recovery is complete only when the
669
+ account's statusline control transition records the commit of the
670
+ observation witnessing its control certificate. Read-only; a missing or
671
+ unreadable transition contributes nothing, so its account stays paused."""
672
+ import _cctally_statusline
673
+
674
+ now_epoch = int(dt.datetime.now(dt.timezone.utc).timestamp())
675
+ commits = {}
676
+ for account_key in _cctally_statusline._account_keys_on_disk():
677
+ try:
678
+ transition = _cctally_statusline._read_transition(
679
+ account_key, now_epoch=now_epoch)
680
+ except Exception: # noqa: BLE001 — unreadable state stays paused
681
+ continue
682
+ quarantine = None if transition is None else transition.quarantine
683
+ commit = None if quarantine is None else quarantine.get(
684
+ "authorityCommit")
685
+ if commit is not None:
686
+ commits[account_key] = commit
687
+ return commits
688
+
689
+
690
+ def _gather_outcome_quarantine() -> dict:
691
+ """The `accounting.outcome_quarantine` evidence (#825/#832 spec 8, 5.6).
692
+
693
+ Read from ONE snapshot proven current (epoch and protocol); an absent,
694
+ unreadable or unvalidated index is unavailable, never "no quarantine".
695
+ Read-only."""
696
+ import _cctally_quarantine
697
+ import _cctally_store
698
+
699
+ out: dict = {"available": False, "accounts": []}
700
+ try:
701
+ if not _cctally_core.DB_PATH.exists():
702
+ return out
703
+ commits = _statusline_authority_commits()
704
+ pc = _stats_ro_guarded()
705
+ try:
706
+ with _cctally_store.current_stats_snapshot(pc):
707
+ report = _cctally_quarantine.report(
708
+ pc, authority_commits=commits)
709
+ labels = {}
710
+ for row in pc.execute(
711
+ "SELECT account_key, label, email FROM accounts"):
712
+ labels[row[0]] = row[1] or row[2] or row[0]
713
+ finally:
714
+ pc.close()
715
+ except Exception: # noqa: BLE001 — a diagnostic read degrades to unavailable
716
+ return out
717
+ accounts = []
718
+ for row in report["accounts"]:
719
+ accounts.append({**row, "accountLabel": labels.get(
720
+ row["account"], row["account"])})
721
+ out = {"available": True, "triggered": report["triggered"],
722
+ "accounts": accounts}
723
+ # Spec revision 3: the unresolved spool markers, only while one exists.
724
+ if report.get("unresolvedSpoolMarkers"):
725
+ out["unresolvedSpoolMarkers"] = report["unresolvedSpoolMarkers"]
726
+ return out
727
+
728
+
473
729
  def _gather_statusline_pipeline(c, *, now_utc: dt.datetime) -> dict:
474
730
  """Read the #318 statusline pipeline without creating or pruning files."""
475
731
  now_epoch = int(now_utc.timestamp())
@@ -514,21 +770,12 @@ def _gather_statusline_pipeline(c, *, now_utc: dt.datetime) -> dict:
514
770
  )
515
771
  except Exception:
516
772
  pass
517
- for axis, path in (
518
- ("fiveHour", _cctally_core.STATUSLINE_AUTHORITATIVE_5H_PATH),
519
- ("sevenDay", _cctally_core.STATUSLINE_AUTHORITATIVE_7D_PATH),
520
- ):
521
- try:
522
- if not path.exists():
523
- continue
524
- tombstone = c._read_tombstone(
525
- axis, now_epoch=now_epoch, fail_closed=False
526
- )
527
- result["tombstones"][axis] = (
528
- tombstone.state if tombstone is not None else "invalid"
529
- )
530
- except Exception:
531
- result["tombstones"][axis] = "invalid"
773
+ # #825/#832 section 7: the current protocol's fences are per exact
774
+ # account and window under the account-bound root; only live windows count.
775
+ try:
776
+ result["tombstones"] = c._statusline_tombstone_states(now_epoch=now_epoch)
777
+ except Exception:
778
+ result["tombstones"] = {"fiveHour": "invalid", "sevenDay": "invalid"}
532
779
  return result
533
780
 
534
781
 
@@ -874,6 +1121,26 @@ def _gather_codex_window_attribution_state() -> "dict | None":
874
1121
  conn.close()
875
1122
 
876
1123
 
1124
+ def _journal_protocol_violation_rows(conn: sqlite3.Connection) -> list:
1125
+ """Every stored protocol violation, parsed, in (batch, kind, fingerprint)
1126
+ order: the doctor's shallow journal leg.
1127
+
1128
+ #901 Amendment 19 T1 (spec I1): the doctor holds every violation anyway,
1129
+ so they are ordered here, in Python, where ``ORDER BY batch_id, kind,
1130
+ fingerprint`` left a partial sorter over each batch's rows — and one batch
1131
+ is not a bound. ``fingerprint`` is the primary key, so the order is total
1132
+ and identical (text compared as UTF-8 bytes, SQLite's BINARY collation).
1133
+ """
1134
+ rows = conn.execute(
1135
+ "SELECT batch_id, kind, fingerprint, violation_json "
1136
+ "FROM journal_protocol_violations"
1137
+ ).fetchall()
1138
+ rows.sort(key=lambda row: tuple(
1139
+ (value is not None, str(value or "").encode("utf-8"))
1140
+ for value in row[:3]))
1141
+ return [json.loads(str(row[3])) for row in rows]
1142
+
1143
+
877
1144
  def _gather_accounts_state(now_utc: "dt.datetime") -> dict:
878
1145
  """Best-effort account-attribution state for the doctor `accounts.*` legs
879
1146
  (#341). Never raises: identity + registry reads are read-only and each guard
@@ -936,14 +1203,21 @@ def _gather_accounts_state(now_utc: "dt.datetime") -> dict:
936
1203
  cutoff = (now_utc - dt.timedelta(days=7)).astimezone(
937
1204
  dt.timezone.utc).isoformat()
938
1205
  try:
939
- for account_key, cnt in conn.execute(
940
- "SELECT account_key, COUNT(*) FROM weekly_usage_snapshots "
941
- "WHERE captured_at_utc >= ? GROUP BY account_key", (cutoff,)
942
- ).fetchall():
1206
+ # #901 Amendment 19 T1 (spec I1): counted in Python over the
1207
+ # streamed rows, one total per class, where `GROUP BY
1208
+ # account_key` sorted the trailing week's every snapshot through a
1209
+ # temp b-tree — and a week of activity is not a bound.
1210
+ attributed = unattributed = 0
1211
+ for (account_key,) in conn.execute(
1212
+ "SELECT account_key FROM weekly_usage_snapshots "
1213
+ "WHERE captured_at_utc >= ?", (cutoff,)
1214
+ ):
943
1215
  if account_key and account_key != "unattributed":
944
- state["recent_attributed"] += int(cnt)
1216
+ attributed += 1
945
1217
  else:
946
- state["recent_unattributed"] += int(cnt)
1218
+ unattributed += 1
1219
+ state["recent_attributed"] += attributed
1220
+ state["recent_unattributed"] += unattributed
947
1221
  except sqlite3.DatabaseError:
948
1222
  pass
949
1223
  finally:
@@ -961,9 +1235,13 @@ def _gather_accounts_state(now_utc: "dt.datetime") -> dict:
961
1235
  # contains no ingest and is not a whole tick; a whole tick is ingest plus
962
1236
  # builder, and the builder is 70.5% of one measured live. Against a whole tick
963
1237
  # the same probe is therefore 14%. It is one all-history
964
- # `load_codex_quota_observations` call whose SQL runs a `MAX(...) OVER
965
- # (PARTITION BY ...)` across every retained row (277,207 on that store) to
966
- # return one row per identity (16).
1238
+ # `load_codex_quota_observations(latest_per_identity=True)` call. Its SQL used
1239
+ # to run a `MAX(...) OVER (PARTITION BY ...)` across every retained row
1240
+ # (277,207 on that store; 565K and ~1.1 GB of temp-file writes per call on the
1241
+ # #901 live store); since #901 it reads the trigger-maintained
1242
+ # `codex_quota_partition_latest` summary plus one indexed seek per raw
1243
+ # partition. The memo below therefore remains a CPU optimisation and is no
1244
+ # longer load-bearing for disk writes.
967
1245
  #
968
1246
  # Only the ROWS are retained, and only while their evidence is unmoved. Every
969
1247
  # `now`-derived interpretation over them is recomputed on every gather:
@@ -1249,6 +1527,7 @@ def doctor_gather_state(
1249
1527
  runtime_bind: "str | None" = None,
1250
1528
  deep: bool = False,
1251
1529
  force_cold_inputs: bool = False,
1530
+ discover_dashboards: bool = False,
1252
1531
  ):
1253
1532
  """Gather doctor state while excluding cache-family replacement.
1254
1533
 
@@ -1262,6 +1541,11 @@ def doctor_gather_state(
1262
1541
  caller can obtain a genuinely fresh gather at a chosen instant. It exists
1263
1542
  for the equality gate that compares a reusing gather against a fresh one at
1264
1543
  an identical ``now_utc``; production callers leave it false.
1544
+
1545
+ ``discover_dashboards`` (PR-11) lets the gather probe other running
1546
+ dashboards for their disk writes. Only the `cctally doctor` CLI passes it:
1547
+ the TUI and every snapshot build gather on each rebuild, where serial
1548
+ 2-second probes of every live descriptor do not belong.
1265
1549
  """
1266
1550
  cache_lock = None
1267
1551
  cache_probe_allowed = not _cctally_core.CACHE_DB_PATH.exists()
@@ -1317,6 +1601,7 @@ def doctor_gather_state(
1317
1601
  _cache_probe_allowed=cache_probe_allowed,
1318
1602
  _cache_repair_marker=cache_repair_marker,
1319
1603
  _force_cold_inputs=force_cold_inputs,
1604
+ _discover_dashboards=discover_dashboards,
1320
1605
  )
1321
1606
  finally:
1322
1607
  if cache_lock is not None:
@@ -1326,6 +1611,132 @@ def doctor_gather_state(
1326
1611
  cache_lock.close()
1327
1612
 
1328
1613
 
1614
+ def _gather_codex_container_state(c, *, now_utc, cache_probe_allowed):
1615
+ """Live, read-only #968 census; never opens/migrates a writable store.
1616
+
1617
+ Presence is independent of decoder availability. Missing pre-migration
1618
+ tables mean no retained evidence; an unreadable probe means unknown, not
1619
+ an invented zero population. No decoding or detached work runs here.
1620
+ """
1621
+ import _cctally_cache as cache
1622
+ import _lib_codex_rollout_container as rc
1623
+
1624
+ def table_exists(conn, name):
1625
+ return conn.execute(
1626
+ "SELECT 1 FROM sqlite_master WHERE type='table' AND name=?",
1627
+ (name,)).fetchone() is not None
1628
+
1629
+ def deletion_debt(conn, table):
1630
+ if not table_exists(conn, table):
1631
+ return 0, None
1632
+ row = conn.execute(
1633
+ f"SELECT COUNT(*), MIN(recorded_at_utc) FROM {table}").fetchone()
1634
+ return int(row[0]), row[1]
1635
+
1636
+ try:
1637
+ decoder = rc.decoder_capability()
1638
+ selected = [item for item in cache._iter_codex_logical_containers(
1639
+ c._codex_session_roots()) if item.kind == rc.ZSTD]
1640
+ census = dict(compressedOnly=len(selected), retainedComplete=0,
1641
+ retainedIncomplete=0, neverIngested=0, unverified=0)
1642
+ status = None
1643
+ verifications, deletions = 0, 0
1644
+ deletion_starts = []
1645
+ path = _cctally_core.CACHE_DB_PATH
1646
+ if path.exists():
1647
+ if not cache_probe_allowed:
1648
+ return None, decoder, None, None
1649
+ conn = sqlite3.connect(f"file:{path}?mode=ro", uri=True, timeout=0.0)
1650
+ try:
1651
+ conn.row_factory = sqlite3.Row
1652
+ cursor_exists = table_exists(conn, "codex_session_files")
1653
+ columns = ({row[1] for row in conn.execute(
1654
+ "PRAGMA table_info(codex_session_files)")} if cursor_exists else set())
1655
+ if "container_state" in columns:
1656
+ verifications = int(conn.execute(
1657
+ "SELECT COUNT(*) FROM codex_session_files "
1658
+ "WHERE container_state='pending'").fetchone()[0])
1659
+ for item in selected:
1660
+ row = (conn.execute("SELECT * FROM codex_session_files WHERE path=?",
1661
+ (str(item.logical),)).fetchone()
1662
+ if cursor_exists else None)
1663
+ if row is None:
1664
+ census["neverIngested"] += 1
1665
+ continue
1666
+ row = dict(row)
1667
+ state = row.get("container_state")
1668
+ if state == "unverified":
1669
+ census["unverified"] += 1
1670
+ elif state == "pending":
1671
+ continue # verification work, never read debt (D3)
1672
+ elif (row.get("container_kind") == rc.ZSTD
1673
+ and state == "accepted"
1674
+ and row.get("ingest_complete") == 1
1675
+ and row.get("size_bytes") == row.get("decoded_size_bytes")
1676
+ and all(row.get(key) == value for key, value in (
1677
+ ("container_size_bytes", item.fingerprint.size),
1678
+ ("mtime_ns", item.fingerprint.mtime_ns),
1679
+ ("device_id", item.fingerprint.device),
1680
+ ("inode", item.fingerprint.inode)))):
1681
+ census["retainedComplete"] += 1
1682
+ else:
1683
+ census["retainedIncomplete"] += 1
1684
+ if table_exists(conn, "cache_meta"):
1685
+ row = conn.execute("SELECT value FROM cache_meta WHERE key=?",
1686
+ (cache.CODEX_CONTAINER_STATUS_KEY,)).fetchone()
1687
+ if row:
1688
+ parsed = json.loads(row[0])
1689
+ status = parsed if isinstance(parsed, dict) else None
1690
+ count, since = deletion_debt(conn, "codex_prune_debt")
1691
+ deletions += count
1692
+ if count:
1693
+ deletion_starts.append(since)
1694
+ finally:
1695
+ conn.close()
1696
+ else:
1697
+ census["neverIngested"] = len(selected)
1698
+ maintenance_coverage = "complete"
1699
+ if _cctally_core.CONVERSATIONS_DB_PATH.exists():
1700
+ # An unavailable second store cannot erase the first store's known
1701
+ # counts or ages. Its missing contribution remains explicitly unknown.
1702
+ try:
1703
+ with _conversation_ro_guarded(timeout=0.0) as conn:
1704
+ if conn is None:
1705
+ maintenance_coverage = "partial"
1706
+ else:
1707
+ count, since = deletion_debt(conn, "codex_conversation_prune_debt")
1708
+ deletions += count
1709
+ if count:
1710
+ deletion_starts.append(since)
1711
+ except Exception:
1712
+ maintenance_coverage = "partial"
1713
+ record = status or {}
1714
+ starts = list(deletion_starts)
1715
+ if verifications:
1716
+ starts.append(record.get("verificationPendingSince"))
1717
+ reads = (census["neverIngested"] + census["retainedIncomplete"]
1718
+ if decoder != "unavailable" else 0)
1719
+ if reads:
1720
+ starts.append(record.get("readsPendingSince"))
1721
+ old = False
1722
+ for stamp in starts:
1723
+ if not stamp:
1724
+ continue
1725
+ try:
1726
+ age = (now_utc - parse_iso_datetime(
1727
+ stamp, "codex maintenance start")).total_seconds()
1728
+ old = old or age > 86400
1729
+ except (TypeError, ValueError):
1730
+ pass
1731
+ debt = dict(verifications=verifications, deletions=deletions,
1732
+ olderThan24h=old)
1733
+ if maintenance_coverage != "complete":
1734
+ debt["coverage"] = maintenance_coverage
1735
+ return census, decoder, status, debt
1736
+ except Exception:
1737
+ return None, None, None, None
1738
+
1739
+
1329
1740
  def _doctor_gather_state_impl(
1330
1741
  *,
1331
1742
  now_utc: "dt.datetime | None" = None,
@@ -1334,6 +1745,7 @@ def _doctor_gather_state_impl(
1334
1745
  _cache_probe_allowed: bool,
1335
1746
  _cache_repair_marker: dict,
1336
1747
  _force_cold_inputs: bool = False,
1748
+ _discover_dashboards: bool = False,
1337
1749
  ):
1338
1750
  """I/O chokepoint for `cctally doctor` (spec §7.2).
1339
1751
 
@@ -1727,6 +2139,7 @@ def _doctor_gather_state_impl(
1727
2139
  conv_rollup_sync_in_progress = False
1728
2140
  conversations_db_page_count = None
1729
2141
  conversations_db_freelist_count = None
2142
+ conversations_db_page_size = None
1730
2143
  conversations_reclaim_pending = None
1731
2144
  codex_prune_refusals: list[dict] = []
1732
2145
  conversation_rollup_pricing_refusal: "dict | None" = None
@@ -1751,6 +2164,9 @@ def _doctor_gather_state_impl(
1751
2164
  row = conn.execute("PRAGMA freelist_count").fetchone()
1752
2165
  if row and row[0] is not None:
1753
2166
  conversations_db_freelist_count = int(row[0])
2167
+ row = conn.execute("PRAGMA page_size").fetchone()
2168
+ if row and row[0] is not None:
2169
+ conversations_db_page_size = int(row[0])
1754
2170
  except sqlite3.Error:
1755
2171
  pass
1756
2172
  try:
@@ -1917,12 +2333,18 @@ def _doctor_gather_state_impl(
1917
2333
  codex_jsonl_present = False
1918
2334
  try:
1919
2335
  for codex_dir in c._codex_session_roots():
1920
- if next(codex_dir.glob("**/*.jsonl"), None) is not None:
2336
+ if (next(codex_dir.glob("**/*.jsonl"), None) is not None
2337
+ or next(codex_dir.glob("**/*.jsonl.zst"), None) is not None):
1921
2338
  codex_jsonl_present = True
1922
2339
  break
1923
2340
  except Exception:
1924
2341
  pass
1925
2342
 
2343
+ with _lib_perf.phase("doctor.codex_containers"):
2344
+ (codex_compressed_census, codex_decoder_capability,
2345
+ codex_container_status, codex_maintenance_debt) = _gather_codex_container_state(
2346
+ c, now_utc=now_utc, cache_probe_allowed=_cache_probe_allowed)
2347
+
1926
2348
  with _lib_perf.phase("doctor.quota_summary"):
1927
2349
  # ── Codex quota lifecycle (#294 S2) ──────────────────────────────
1928
2350
  # All three probes are read-only and root-qualified. The physical cache
@@ -2381,10 +2803,17 @@ def _doctor_gather_state_impl(
2381
2803
  journal_cursor_lag_bytes = None
2382
2804
  journal_hw_segment = None
2383
2805
  journal_cursor_segment = None
2806
+ journal_cursor_invalid = False
2384
2807
  journal_conflicts = None
2385
2808
  journal_protocol_violations = None
2386
2809
  journal_protocol_acknowledged = None
2387
2810
  journal_protocol_error = None
2811
+ # #825/#832 spec 8: the deep scan's whole-journal excluded population.
2812
+ deep_excluded = None
2813
+ deep_admission_boundary = None
2814
+ # K9K10-R6/R11: why the listing or the deep scan could not complete.
2815
+ journal_listing_error = None
2816
+ journal_scan_error = None
2388
2817
  try:
2389
2818
  jdir = _cctally_core.JOURNAL_DIR
2390
2819
  journal_present = jdir.exists()
@@ -2396,8 +2825,14 @@ def _doctor_gather_state_impl(
2396
2825
  import _cctally_journal as _jr
2397
2826
  try:
2398
2827
  segs = _jr.list_segments() # canonical (segment) order
2399
- except Exception:
2828
+ except Exception as exc: # noqa: BLE001 — reported, never empty
2829
+ # K9K10-R6: an unlistable journal is unreadable, not an
2830
+ # empty one; the presence leg names the cause.
2400
2831
  segs = []
2832
+ journal_listing_error = _scan_cause(exc)
2833
+ if deep:
2834
+ # The deep recomputation cannot run either.
2835
+ journal_scan_error = journal_listing_error
2401
2836
  journal_segment_count = len(segs)
2402
2837
  # #402: the disposable stats index persists the most recent complete
2403
2838
  # selector result. Shallow Dashboard/TUI gathers read that bounded
@@ -2407,14 +2842,8 @@ def _doctor_gather_state_impl(
2407
2842
  if _cctally_core.DB_PATH.exists():
2408
2843
  pc = _stats_ro_guarded()
2409
2844
  try:
2410
- protocol_rows = [
2411
- json.loads(str(row[0]))
2412
- for row in pc.execute(
2413
- "SELECT violation_json "
2414
- "FROM journal_protocol_violations "
2415
- "ORDER BY batch_id, kind, fingerprint"
2416
- )
2417
- ]
2845
+ protocol_rows = (
2846
+ _journal_protocol_violation_rows(pc))
2418
2847
  journal_protocol_violations = [
2419
2848
  item for item in protocol_rows
2420
2849
  if not item.get("auditId")
@@ -2447,13 +2876,28 @@ def _doctor_gather_state_impl(
2447
2876
  torn = 0
2448
2877
  decoded_records: list = []
2449
2878
  protocol_evidence = []
2879
+ protocol_evidence_owners = []
2450
2880
  prior_high_water = None
2451
2881
  cutover_value = None
2882
+ # #825/#832 (spec 5.3, 8): the deep scan applies the same
2883
+ # admission predicate as ingest and rebuild — resolved
2884
+ # within this one pass and applied after it — and
2885
+ # recomputes the excluded population the legacy-writer leg
2886
+ # compares.
2887
+ pass_boundary = _jr._PassBoundary()
2888
+ deep_coordinates: list = []
2889
+ cutover_candidates: list = []
2890
+ deep_excluded: list = []
2452
2891
  for seg in segs:
2453
2892
  try:
2454
2893
  data = (jdir / seg).read_bytes()
2455
- except OSError:
2894
+ except FileNotFoundError:
2456
2895
  continue
2896
+ except OSError as exc:
2897
+ # K9K10-R6: a skipped segment would let every
2898
+ # check below report a partial journal as whole.
2899
+ journal_scan_error = _scan_cause(exc)
2900
+ break
2457
2901
  if not data:
2458
2902
  continue
2459
2903
  if not data.endswith(b"\n"):
@@ -2476,20 +2920,22 @@ def _doctor_gather_state_impl(
2476
2920
  )
2477
2921
  offset += len(raw) + 1
2478
2922
  continue
2923
+ pass_boundary.observe(record)
2479
2924
  _jr._capture_protocol_prefix_evidence(
2480
2925
  record,
2481
2926
  prior_high_water,
2482
2927
  protocol_evidence,
2483
2928
  )
2929
+ if (len(protocol_evidence)
2930
+ > len(protocol_evidence_owners)):
2931
+ protocol_evidence_owners.append(
2932
+ len(decoded_records))
2484
2933
  # first cutover op wins, exactly as
2485
2934
  # `find_accounts_cutover_op` scans — captured here so the
2486
2935
  # conflict scan does not decode the whole journal twice.
2487
- if (cutover_value is None
2488
- and record.get("id") == _jr.CUTOVER_OP_ID):
2489
- payload = record.get("payload")
2490
- if isinstance(payload, dict):
2491
- cutover_value = payload.get(
2492
- "claude_legacy_account")
2936
+ if record.get("id") == _jr.CUTOVER_OP_ID:
2937
+ cutover_candidates.append(
2938
+ (record, seg, offset))
2493
2939
  # RETAIN ONLY what the selector consumes. `obs` lines are
2494
2940
  # ~97% of a real journal (984k of 1.02M) and
2495
2941
  # `resolve_effective_events` ignores them entirely —
@@ -2500,13 +2946,56 @@ def _doctor_gather_state_impl(
2500
2946
  decoded_records.append(
2501
2947
  _lib_journal_router.selector_slot(record)
2502
2948
  )
2949
+ deep_coordinates.append((seg, offset))
2503
2950
  prior_high_water = (
2504
2951
  seg,
2505
2952
  offset + len(raw) + 1,
2506
2953
  )
2507
2954
  offset += len(raw) + 1
2508
- journal_malformed_count = malformed
2509
- journal_torn_tail_count = torn
2955
+ admission_boundary = None
2956
+ if journal_scan_error is None:
2957
+ try:
2958
+ admission_boundary = pass_boundary.resolve(
2959
+ (segs[-1], sizes.get(segs[-1], 0)))
2960
+ except Exception as exc: # noqa: BLE001
2961
+ # K9K10-R6: a None boundary admits everything:
2962
+ # zero excluded records and an unfiltered scan.
2963
+ journal_scan_error = _scan_cause(exc)
2964
+ if journal_scan_error is None:
2965
+ try:
2966
+ deep_excluded = _jr.excluded_population(
2967
+ admission_boundary)
2968
+ except Exception as exc: # noqa: BLE001
2969
+ # K9K10-R11: an unreadable tail is never a
2970
+ # smaller population.
2971
+ journal_scan_error = _scan_cause(exc)
2972
+ if journal_scan_error is not None:
2973
+ deep_excluded = None
2974
+ journal_conflicts = None
2975
+ journal_protocol_error = None
2976
+ else:
2977
+ journal_malformed_count = malformed
2978
+ journal_torn_tail_count = torn
2979
+ if deep and segs and journal_scan_error is None:
2980
+ _jr._apply_pass_admission(
2981
+ decoded_records, deep_coordinates, admission_boundary)
2982
+ # #825/#832 fix round C (CD2-005): an excluded
2983
+ # acknowledgement's evidence is excluded with it.
2984
+ protocol_evidence = _jr._admitted_prefix_evidence(
2985
+ protocol_evidence, protocol_evidence_owners,
2986
+ decoded_records)
2987
+ # Every excluded record counts, obs included, so the
2988
+ # population matches what ingest and rebuild record. Only
2989
+ # the post-boundary byte ranges can hold one.
2990
+ deep_admission_boundary = admission_boundary
2991
+ for candidate, cand_seg, cand_off in cutover_candidates:
2992
+ if _jr._is_admitted(candidate, cand_seg, cand_off,
2993
+ admission_boundary):
2994
+ payload = candidate.get("payload")
2995
+ if isinstance(payload, dict):
2996
+ cutover_value = payload.get(
2997
+ "claude_legacy_account")
2998
+ break
2510
2999
  # #374: same-revision quarantine, via the SHARED selector over
2511
3000
  # rebuild-equivalent input. Raw `(id, rev)` grouping would report
2512
3001
  # lower-revision groups a completed rev-1 batch legitimately
@@ -2595,7 +3084,23 @@ def _doctor_gather_state_impl(
2595
3084
  order = {s: i for i, s in enumerate(segs)}
2596
3085
  if cseg in order:
2597
3086
  ci = order[cseg]
2598
- lag = max(0, sizes.get(segs[ci], 0) - coff)
3087
+ # #946: the lag of the SAFE applied cursor. A torn
3088
+ # suffix past it stays outstanding until a repair, and
3089
+ # a cursor past its segment or inside a line is no
3090
+ # safe endpoint at all.
3091
+ csize = sizes.get(segs[ci], 0)
3092
+ if coff > csize:
3093
+ journal_cursor_invalid = True
3094
+ elif coff > 0:
3095
+ try:
3096
+ with open(_cctally_core.JOURNAL_DIR / cseg,
3097
+ "rb") as _seg_fh:
3098
+ _seg_fh.seek(coff - 1)
3099
+ if _seg_fh.read(1) != b"\n":
3100
+ journal_cursor_invalid = True
3101
+ except OSError:
3102
+ journal_cursor_invalid = True
3103
+ lag = max(0, csize - coff)
2599
3104
  for s in segs[ci + 1:]:
2600
3105
  lag += sizes.get(s, 0)
2601
3106
  journal_cursor_lag_bytes = lag
@@ -2696,6 +3201,13 @@ def _doctor_gather_state_impl(
2696
3201
  _gather_codex_window_attribution_state()
2697
3202
  if _cache_probe_allowed else None)
2698
3203
 
3204
+ # #901 §5.6: running dashboards' disk writes. Read-only; degrades.
3205
+ try:
3206
+ with _lib_perf.phase("doctor.dashboard_disk_writes"):
3207
+ dashboard_disk_writes = _gather_dashboard_disk_writes(
3208
+ discover=_discover_dashboards)
3209
+ except Exception: # noqa: BLE001 — a probe failure must not fail doctor
3210
+ dashboard_disk_writes = {"mode": "error", "instances": []}
2699
3211
  return _lib_doctor.DoctorState(
2700
3212
  symlink_state=symlink_state,
2701
3213
  path_includes_local_bin=path_includes,
@@ -2765,6 +3277,10 @@ def _doctor_gather_state_impl(
2765
3277
  codex_replay_blocked=codex_replay_blocked,
2766
3278
  codex_replay_deferred=codex_replay_deferred,
2767
3279
  codex_prune_refusals=codex_prune_refusals or None,
3280
+ codex_compressed_census=codex_compressed_census,
3281
+ codex_decoder_capability=codex_decoder_capability,
3282
+ codex_container_status=codex_container_status,
3283
+ codex_maintenance_debt=codex_maintenance_debt,
2768
3284
  conversation_rollup_pricing_refusal=(
2769
3285
  conversation_rollup_pricing_refusal),
2770
3286
  conversation_rollup_pricing_fp=conversation_rollup_pricing_fp,
@@ -2782,9 +3298,17 @@ def _doctor_gather_state_impl(
2782
3298
  conversations_db_wal_bytes=conversations_db_wal_bytes,
2783
3299
  # #374: quarantined same-revision groups + structural protocol violation.
2784
3300
  journal_conflicts=journal_conflicts,
3301
+ # #825/#832 spec 8: the legacy-writer evidence.
3302
+ legacy_writer=_gather_legacy_writer(
3303
+ deep_excluded, deep_admission_boundary,
3304
+ deep_error=journal_scan_error),
3305
+ # #825/#832 spec 8: the per-account outcome quarantine.
3306
+ outcome_quarantine=_gather_outcome_quarantine(),
2785
3307
  journal_protocol_violations=journal_protocol_violations,
2786
3308
  journal_protocol_acknowledged=journal_protocol_acknowledged,
2787
3309
  journal_protocol_error=journal_protocol_error,
3310
+ journal_listing_error=journal_listing_error,
3311
+ journal_scan_error=journal_scan_error,
2788
3312
  # #496 S5b: the published generation's incomplete-quota-projection flag.
2789
3313
  stats_quota_projection_incomplete=stats_quota_projection_incomplete,
2790
3314
  # #315: read-only cache free-page evidence for the reclaim hint.
@@ -2792,6 +3316,8 @@ def _doctor_gather_state_impl(
2792
3316
  cache_db_freelist_count=cache_db_freelist_count,
2793
3317
  conversations_db_page_count=conversations_db_page_count,
2794
3318
  conversations_db_freelist_count=conversations_db_freelist_count,
3319
+ conversations_db_page_size=conversations_db_page_size,
3320
+ dashboard_disk_writes=dashboard_disk_writes,
2795
3321
  conversations_reclaim_pending=conversations_reclaim_pending,
2796
3322
  codex_quota_windows=codex_quota_windows,
2797
3323
  codex_hook_roots=codex_hook_roots,
@@ -2809,6 +3335,7 @@ def _doctor_gather_state_impl(
2809
3335
  journal_malformed_count=journal_malformed_count,
2810
3336
  journal_torn_tail_count=journal_torn_tail_count,
2811
3337
  journal_cursor_lag_bytes=journal_cursor_lag_bytes,
3338
+ journal_cursor_invalid=journal_cursor_invalid,
2812
3339
  journal_hw_segment=journal_hw_segment,
2813
3340
  journal_cursor_segment=journal_cursor_segment,
2814
3341
  journal_heal_incidents=journal_heal_incidents,
@@ -2846,7 +3373,7 @@ def cmd_doctor(args: argparse.Namespace) -> int:
2846
3373
  # #279 S2 F5b: deep=True runs PRAGMA quick_check(1) — CLI-only (the
2847
3374
  # dashboard/TUI gather callers stay deep=False so their per-rebuild
2848
3375
  # gather never pays the multi-second cost on a large cache.db).
2849
- state = c.doctor_gather_state(deep=True)
3376
+ state = c.doctor_gather_state(deep=True, discover_dashboards=True)
2850
3377
  report = _lib_doctor.run_checks(state)
2851
3378
  if getattr(args, "json", False):
2852
3379
  print(encode_dashboard_json(