cctally 1.114.0 → 1.116.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 (86) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/bin/_cctally_account.py +32 -8
  3. package/bin/_cctally_cache.py +4491 -295
  4. package/bin/_cctally_completion.py +1355 -0
  5. package/bin/_cctally_config.py +37 -0
  6. package/bin/_cctally_core.py +282 -9
  7. package/bin/_cctally_dashboard.py +8 -11
  8. package/bin/_cctally_dashboard_conversation.py +292 -23
  9. package/bin/_cctally_dashboard_envelope.py +42 -35
  10. package/bin/_cctally_dashboard_sources.py +18 -9
  11. package/bin/_cctally_db.py +401 -9
  12. package/bin/_cctally_doctor.py +469 -27
  13. package/bin/_cctally_five_hour.py +166 -1049
  14. package/bin/_cctally_forecast.py +81 -9
  15. package/bin/_cctally_journal.py +5076 -177
  16. package/bin/_cctally_journal_repair.py +110 -19
  17. package/bin/_cctally_oauth_samples.py +280 -0
  18. package/bin/_cctally_parser.py +47 -0
  19. package/bin/_cctally_project.py +8 -2
  20. package/bin/_cctally_quarantine.py +662 -0
  21. package/bin/_cctally_quota.py +96 -12
  22. package/bin/_cctally_quota_model.py +391 -83
  23. package/bin/_cctally_record.py +1088 -269
  24. package/bin/_cctally_rederive.py +854 -192
  25. package/bin/_cctally_refresh.py +113 -13
  26. package/bin/_cctally_source_analytics.py +23 -2
  27. package/bin/_cctally_statusline.py +7062 -505
  28. package/bin/_cctally_store.py +1244 -91
  29. package/bin/_cctally_transcript.py +8 -7
  30. package/bin/_cctally_tui.py +119 -60
  31. package/bin/_cctally_update.py +25 -0
  32. package/bin/_lib_account_binding.py +152 -0
  33. package/bin/_lib_account_protocol.py +569 -0
  34. package/bin/_lib_aggregators.py +4 -0
  35. package/bin/_lib_cache_coverage.py +13 -1
  36. package/bin/_lib_cache_writer_lock.py +19 -3
  37. package/bin/_lib_clone_copy.py +177 -0
  38. package/bin/_lib_codex_conversation_query.py +133 -19
  39. package/bin/_lib_codex_conversation_watch.py +59 -14
  40. package/bin/_lib_codex_drain_admission.py +214 -0
  41. package/bin/_lib_codex_metadata.py +18 -1
  42. package/bin/_lib_codex_publication_barrier.py +277 -0
  43. package/bin/_lib_codex_rollout_container.py +502 -0
  44. package/bin/_lib_conversation_dispatch.py +3 -0
  45. package/bin/_lib_conversation_query.py +11 -3
  46. package/bin/_lib_dashboard_sources.py +57 -15
  47. package/bin/_lib_detector_access_registry.py +4038 -0
  48. package/bin/_lib_doctor.py +753 -1
  49. package/bin/_lib_five_hour.py +126 -0
  50. package/bin/_lib_ingest_frontier.py +414 -20
  51. package/bin/_lib_journal.py +261 -10
  52. package/bin/_lib_journal_router.py +166 -48
  53. package/bin/_lib_jsonl.py +5 -4
  54. package/bin/_lib_legacy_spool.py +126 -0
  55. package/bin/_lib_legacy_writer_evidence.py +136 -0
  56. package/bin/_lib_meter_rate_change.py +10 -0
  57. package/bin/_lib_oauth_samples.py +238 -0
  58. package/bin/_lib_outcome_quarantine.py +690 -0
  59. package/bin/_lib_pricing.py +7 -2
  60. package/bin/_lib_pricing_check.py +0 -12
  61. package/bin/_lib_provider_activity.py +770 -0
  62. package/bin/_lib_quarantine.py +155 -0
  63. package/bin/_lib_quota_calibration.py +53 -8
  64. package/bin/_lib_quota_copy.py +2 -1
  65. package/bin/_lib_quota_model.py +384 -52
  66. package/bin/_lib_rebuild_progress.py +146 -0
  67. package/bin/_lib_record.py +22 -0
  68. package/bin/_lib_rederive.py +393 -8
  69. package/bin/_lib_segment_summary.py +35 -2
  70. package/bin/_lib_snapshot_cache.py +43 -1
  71. package/bin/_lib_stale_proof.py +708 -0
  72. package/bin/_lib_statusline_candidates.py +356 -27
  73. package/bin/_lib_worker_launches.py +315 -0
  74. package/bin/cctally +29 -12
  75. package/dashboard/static/assets/{ConversationsView-Bc8LCOAZ.js → ConversationsView-Dflb_myA.js} +1 -1
  76. package/dashboard/static/assets/DoctorModal-DFX5CfIs.js +1 -0
  77. package/dashboard/static/assets/{ModalRoot-DLPgyAM_.js → ModalRoot-HlxoagIa.js} +1 -1
  78. package/dashboard/static/assets/{ProjectsDrillPanel-z53a4v4M.js → ProjectsDrillPanel-C3PJy1xx.js} +1 -1
  79. package/dashboard/static/assets/{SourceDetailModal-Bw0T8W-W.js → SourceDetailModal-BczsWKhD.js} +1 -1
  80. package/dashboard/static/assets/{UpdateModal-D5AKx4Yw.js → UpdateModal-Os-Lo5cs.js} +1 -1
  81. package/dashboard/static/assets/{index-BXDOzSng.js → index-CYZ3NLqF.js} +3 -3
  82. package/dashboard/static/assets/{outlineNavigation-JObY7cUY.js → outlineNavigation-k-zIif6W.js} +1 -1
  83. package/dashboard/static/dashboard.html +1 -1
  84. package/package.json +20 -2
  85. package/bin/_lib_merge_gate.py +0 -725
  86. package/dashboard/static/assets/DoctorModal-D--qVBwe.js +0 -1
@@ -507,6 +507,31 @@ def _fetch_dashboard_write_io(descriptor: dict, *, timeout_s: float) -> dict:
507
507
  return {**base, "probe": "ok", "reason": None, "write_io": write_io}
508
508
 
509
509
 
510
+ _REBUILD_WORKERS = ("epoch", "heal")
511
+
512
+
513
+ def _rebuild_worker_gather_failed(worker: str, exc: BaseException) -> dict:
514
+ """A snapshot that could not be gathered reads as an unreadable state
515
+ (the existing row), with the error in `details`, never as "has run"."""
516
+ return {"worker": worker, "state": None, "stateUnreadable": True,
517
+ "ledger": None, "ledgerUnreadable": False, "identities": {},
518
+ "pending": {}, "reread": False,
519
+ "gatherError": f"{type(exc).__name__}: {exc}"}
520
+
521
+
522
+ def _gather_rebuild_workers() -> dict:
523
+ """#997 D10: each detached rebuild worker's read-only snapshot; a worker
524
+ whose snapshot fails is reported unreadable, the other still gathered."""
525
+ out: dict = {}
526
+ for worker in _REBUILD_WORKERS:
527
+ try:
528
+ import _cctally_store
529
+ out[worker] = _cctally_store.rebuild_worker_snapshot(worker)
530
+ except Exception as exc: # noqa: BLE001 — reported, never fatal
531
+ out[worker] = _rebuild_worker_gather_failed(worker, exc)
532
+ return out
533
+
534
+
510
535
  def _gather_dashboard_disk_writes(
511
536
  *, fetch=None, timeout_s: float = DASHBOARD_WRITE_IO_TIMEOUT_SECONDS,
512
537
  discover: bool = True,
@@ -542,6 +567,189 @@ def _gather_dashboard_disk_writes(
542
567
  return {"mode": "discovery", "instances": instances}
543
568
 
544
569
 
570
+ def _gather_legacy_writer(deep_excluded, deep_boundary,
571
+ deep_error=None) -> dict:
572
+ """The `journal.legacy_writer` evidence (#825/#832 spec section 8).
573
+
574
+ Reads the admission summary on a validated current snapshot (an
575
+ unvalidated or missing summary is unavailable, never zero — the durable
576
+ evidence sidecar is still returned beside it, for the leg to report from)
577
+ and the durable evidence sidecar. A deep run substitutes its own whole-journal
578
+ recomputation of the excluded population; ``deep_error`` names why that
579
+ recomputation could not complete (fix round K11, K9K10-R6/R11), and then
580
+ the count is unavailable — never zero.
581
+
582
+ Spec revision 5: the leg also reports an incomplete legacy spool
583
+ inventory (``legacySpoolInventory``), independently of the count, and
584
+ says the detection pause exists only when the validated index holds the
585
+ durable `inventory-incomplete` marker. Read-only: the sidecar is never
586
+ written here, the spool is only read, and nothing is journaled."""
587
+ import _cctally_journal
588
+ import _cctally_store
589
+ import _lib_account_protocol
590
+
591
+ out: dict = {"available": False}
592
+ row = None
593
+ inventory_marker = False
594
+ try:
595
+ if _cctally_core.DB_PATH.exists():
596
+ pc = _stats_ro_guarded()
597
+ try:
598
+ with _cctally_store.current_stats_snapshot(pc):
599
+ row = pc.execute(
600
+ "SELECT cutover_id, excluded_total, coordinate_digest "
601
+ "FROM journal_admission_summary WHERE id = 1"
602
+ ).fetchone()
603
+ inventory_marker = _durable_inventory_marker(pc)
604
+ finally:
605
+ pc.close()
606
+ except Exception: # noqa: BLE001 — a diagnostic read degrades to unavailable
607
+ row = None
608
+ # #797/#823/#840 review RV-5: the v2 sidecar, else the previous
609
+ # release's v1 one (read only) as its seed.
610
+ evidence, _seeded = _cctally_journal.read_legacy_writer_evidence_document()
611
+ inventory = _legacy_spool_inventory_probe(inventory_marker)
612
+ if inventory is not None:
613
+ out["legacySpoolInventory"] = inventory
614
+ if deep_error is not None:
615
+ return {**out, "unavailableCause": str(deep_error),
616
+ "evidence": evidence}
617
+ if deep_excluded is not None:
618
+ cutover_id = (None if deep_boundary is None
619
+ else _lib_account_protocol.CUTOVER_OP_ID)
620
+ count = len(deep_excluded)
621
+ digest = (_lib_account_protocol.coordinate_population_digest(
622
+ item.coordinate for item in deep_excluded) if count else None)
623
+ elif row is not None:
624
+ cutover_id, count, digest = row[0], int(row[1] or 0), row[2]
625
+ else:
626
+ # No current summary — exactly what an older process's epoch
627
+ # republish leaves behind. The durable sidecar still carries the
628
+ # excluded population and when it was first observed (#825/#832 fix
629
+ # round B, P2-2), so the leg reports from it rather than "unavailable".
630
+ return {**out, "evidence": evidence}
631
+ return {**out, "available": True, "cutoverId": cutover_id,
632
+ "excludedCount": count, "coordinateDigest": digest,
633
+ "evidence": evidence}
634
+
635
+
636
+ def _scan_cause(exc: BaseException) -> str:
637
+ """A journal read failure as a doctor cause (K9K10-R6): the message and,
638
+ for an OS error, its errno name — never an empty string."""
639
+ import errno as _errno
640
+ name = _errno.errorcode.get(getattr(exc, "errno", None) or 0)
641
+ text = getattr(exc, "strerror", None) or str(exc) or type(exc).__name__
642
+ filename = getattr(exc, "filename", None)
643
+ if filename:
644
+ text = f"{text}: {filename}"
645
+ return f"{text} [{name}]" if name else text
646
+
647
+
648
+ def _durable_inventory_marker(conn) -> bool:
649
+ """Whether the validated index holds an unresolved spool marker left by
650
+ an incomplete inventory (spec revision 5, section 8). Read inside the
651
+ caller's validated snapshot; a missing quarantine table is no marker."""
652
+ import _cctally_quarantine
653
+ import _lib_account_protocol
654
+ try:
655
+ state = _cctally_quarantine.load_state(conn)
656
+ except Exception: # noqa: BLE001 — no readable marker claims no pause
657
+ return False
658
+ return any(marker.get("reason")
659
+ == _lib_account_protocol.INVENTORY_INCOMPLETE_REASON
660
+ for marker in state.spool_markers)
661
+
662
+
663
+ def _legacy_spool_inventory_probe(paused: bool):
664
+ """The legacy spool inventory doctor itself takes (spec revision 5,
665
+ section 8): None when it completes, else ``{"available": False, "path",
666
+ "operation", "error", "paused"}``. Doctor can be the first process to
667
+ meet the error, so it probes rather than reading what the reducer saw.
668
+ Read-only: the spool is listed and read to digest it, never changed.
669
+
670
+ Fix round K12 (K11R-13): any other failure leaves the inventory
671
+ unavailable with its cause (``{"available": False, "unavailableCause",
672
+ "paused"}``), never complete."""
673
+ import errno as _errno
674
+ import _cctally_journal
675
+ try:
676
+ _cctally_journal.capture_legacy_spool_state()
677
+ except _cctally_journal.LegacySpoolInventoryIncomplete as exc:
678
+ code = exc.errno or _errno.EIO
679
+ return {"available": False,
680
+ "path": exc.failed_path or exc.rel,
681
+ "operation": exc.operation,
682
+ "error": f"{os.strerror(code)} [{exc.error}]",
683
+ "paused": bool(paused)}
684
+ except Exception as exc: # noqa: BLE001 — a diagnostic probe never crashes doctor
685
+ return {"available": False, "unavailableCause": _scan_cause(exc),
686
+ "paused": bool(paused)}
687
+ return None
688
+
689
+
690
+ def _statusline_authority_commits() -> dict:
691
+ """Each account partition's reconciled `authorityCommit` (#825/#832 spec
692
+ 5.6 item 7, fix-round-1 Q6): control recovery is complete only when the
693
+ account's statusline control transition records the commit of the
694
+ observation witnessing its control certificate. Read-only; a missing or
695
+ unreadable transition contributes nothing, so its account stays paused."""
696
+ import _cctally_statusline
697
+
698
+ now_epoch = int(dt.datetime.now(dt.timezone.utc).timestamp())
699
+ commits = {}
700
+ for account_key in _cctally_statusline._account_keys_on_disk():
701
+ try:
702
+ transition = _cctally_statusline._read_transition(
703
+ account_key, now_epoch=now_epoch)
704
+ except Exception: # noqa: BLE001 — unreadable state stays paused
705
+ continue
706
+ quarantine = None if transition is None else transition.quarantine
707
+ commit = None if quarantine is None else quarantine.get(
708
+ "authorityCommit")
709
+ if commit is not None:
710
+ commits[account_key] = commit
711
+ return commits
712
+
713
+
714
+ def _gather_outcome_quarantine() -> dict:
715
+ """The `accounting.outcome_quarantine` evidence (#825/#832 spec 8, 5.6).
716
+
717
+ Read from ONE snapshot proven current (epoch and protocol); an absent,
718
+ unreadable or unvalidated index is unavailable, never "no quarantine".
719
+ Read-only."""
720
+ import _cctally_quarantine
721
+ import _cctally_store
722
+
723
+ out: dict = {"available": False, "accounts": []}
724
+ try:
725
+ if not _cctally_core.DB_PATH.exists():
726
+ return out
727
+ commits = _statusline_authority_commits()
728
+ pc = _stats_ro_guarded()
729
+ try:
730
+ with _cctally_store.current_stats_snapshot(pc):
731
+ report = _cctally_quarantine.report(
732
+ pc, authority_commits=commits)
733
+ labels = {}
734
+ for row in pc.execute(
735
+ "SELECT account_key, label, email FROM accounts"):
736
+ labels[row[0]] = row[1] or row[2] or row[0]
737
+ finally:
738
+ pc.close()
739
+ except Exception: # noqa: BLE001 — a diagnostic read degrades to unavailable
740
+ return out
741
+ accounts = []
742
+ for row in report["accounts"]:
743
+ accounts.append({**row, "accountLabel": labels.get(
744
+ row["account"], row["account"])})
745
+ out = {"available": True, "triggered": report["triggered"],
746
+ "accounts": accounts}
747
+ # Spec revision 3: the unresolved spool markers, only while one exists.
748
+ if report.get("unresolvedSpoolMarkers"):
749
+ out["unresolvedSpoolMarkers"] = report["unresolvedSpoolMarkers"]
750
+ return out
751
+
752
+
545
753
  def _gather_statusline_pipeline(c, *, now_utc: dt.datetime) -> dict:
546
754
  """Read the #318 statusline pipeline without creating or pruning files."""
547
755
  now_epoch = int(now_utc.timestamp())
@@ -586,21 +794,12 @@ def _gather_statusline_pipeline(c, *, now_utc: dt.datetime) -> dict:
586
794
  )
587
795
  except Exception:
588
796
  pass
589
- for axis, path in (
590
- ("fiveHour", _cctally_core.STATUSLINE_AUTHORITATIVE_5H_PATH),
591
- ("sevenDay", _cctally_core.STATUSLINE_AUTHORITATIVE_7D_PATH),
592
- ):
593
- try:
594
- if not path.exists():
595
- continue
596
- tombstone = c._read_tombstone(
597
- axis, now_epoch=now_epoch, fail_closed=False
598
- )
599
- result["tombstones"][axis] = (
600
- tombstone.state if tombstone is not None else "invalid"
601
- )
602
- except Exception:
603
- result["tombstones"][axis] = "invalid"
797
+ # #825/#832 section 7: the current protocol's fences are per exact
798
+ # account and window under the account-bound root; only live windows count.
799
+ try:
800
+ result["tombstones"] = c._statusline_tombstone_states(now_epoch=now_epoch)
801
+ except Exception:
802
+ result["tombstones"] = {"fiveHour": "invalid", "sevenDay": "invalid"}
604
803
  return result
605
804
 
606
805
 
@@ -1436,6 +1635,132 @@ def doctor_gather_state(
1436
1635
  cache_lock.close()
1437
1636
 
1438
1637
 
1638
+ def _gather_codex_container_state(c, *, now_utc, cache_probe_allowed):
1639
+ """Live, read-only #968 census; never opens/migrates a writable store.
1640
+
1641
+ Presence is independent of decoder availability. Missing pre-migration
1642
+ tables mean no retained evidence; an unreadable probe means unknown, not
1643
+ an invented zero population. No decoding or detached work runs here.
1644
+ """
1645
+ import _cctally_cache as cache
1646
+ import _lib_codex_rollout_container as rc
1647
+
1648
+ def table_exists(conn, name):
1649
+ return conn.execute(
1650
+ "SELECT 1 FROM sqlite_master WHERE type='table' AND name=?",
1651
+ (name,)).fetchone() is not None
1652
+
1653
+ def deletion_debt(conn, table):
1654
+ if not table_exists(conn, table):
1655
+ return 0, None
1656
+ row = conn.execute(
1657
+ f"SELECT COUNT(*), MIN(recorded_at_utc) FROM {table}").fetchone()
1658
+ return int(row[0]), row[1]
1659
+
1660
+ try:
1661
+ decoder = rc.decoder_capability()
1662
+ selected = [item for item in cache._iter_codex_logical_containers(
1663
+ c._codex_session_roots()) if item.kind == rc.ZSTD]
1664
+ census = dict(compressedOnly=len(selected), retainedComplete=0,
1665
+ retainedIncomplete=0, neverIngested=0, unverified=0)
1666
+ status = None
1667
+ verifications, deletions = 0, 0
1668
+ deletion_starts = []
1669
+ path = _cctally_core.CACHE_DB_PATH
1670
+ if path.exists():
1671
+ if not cache_probe_allowed:
1672
+ return None, decoder, None, None
1673
+ conn = sqlite3.connect(f"file:{path}?mode=ro", uri=True, timeout=0.0)
1674
+ try:
1675
+ conn.row_factory = sqlite3.Row
1676
+ cursor_exists = table_exists(conn, "codex_session_files")
1677
+ columns = ({row[1] for row in conn.execute(
1678
+ "PRAGMA table_info(codex_session_files)")} if cursor_exists else set())
1679
+ if "container_state" in columns:
1680
+ verifications = int(conn.execute(
1681
+ "SELECT COUNT(*) FROM codex_session_files "
1682
+ "WHERE container_state='pending'").fetchone()[0])
1683
+ for item in selected:
1684
+ row = (conn.execute("SELECT * FROM codex_session_files WHERE path=?",
1685
+ (str(item.logical),)).fetchone()
1686
+ if cursor_exists else None)
1687
+ if row is None:
1688
+ census["neverIngested"] += 1
1689
+ continue
1690
+ row = dict(row)
1691
+ state = row.get("container_state")
1692
+ if state == "unverified":
1693
+ census["unverified"] += 1
1694
+ elif state == "pending":
1695
+ continue # verification work, never read debt (D3)
1696
+ elif (row.get("container_kind") == rc.ZSTD
1697
+ and state == "accepted"
1698
+ and row.get("ingest_complete") == 1
1699
+ and row.get("size_bytes") == row.get("decoded_size_bytes")
1700
+ and all(row.get(key) == value for key, value in (
1701
+ ("container_size_bytes", item.fingerprint.size),
1702
+ ("mtime_ns", item.fingerprint.mtime_ns),
1703
+ ("device_id", item.fingerprint.device),
1704
+ ("inode", item.fingerprint.inode)))):
1705
+ census["retainedComplete"] += 1
1706
+ else:
1707
+ census["retainedIncomplete"] += 1
1708
+ if table_exists(conn, "cache_meta"):
1709
+ row = conn.execute("SELECT value FROM cache_meta WHERE key=?",
1710
+ (cache.CODEX_CONTAINER_STATUS_KEY,)).fetchone()
1711
+ if row:
1712
+ parsed = json.loads(row[0])
1713
+ status = parsed if isinstance(parsed, dict) else None
1714
+ count, since = deletion_debt(conn, "codex_prune_debt")
1715
+ deletions += count
1716
+ if count:
1717
+ deletion_starts.append(since)
1718
+ finally:
1719
+ conn.close()
1720
+ else:
1721
+ census["neverIngested"] = len(selected)
1722
+ maintenance_coverage = "complete"
1723
+ if _cctally_core.CONVERSATIONS_DB_PATH.exists():
1724
+ # An unavailable second store cannot erase the first store's known
1725
+ # counts or ages. Its missing contribution remains explicitly unknown.
1726
+ try:
1727
+ with _conversation_ro_guarded(timeout=0.0) as conn:
1728
+ if conn is None:
1729
+ maintenance_coverage = "partial"
1730
+ else:
1731
+ count, since = deletion_debt(conn, "codex_conversation_prune_debt")
1732
+ deletions += count
1733
+ if count:
1734
+ deletion_starts.append(since)
1735
+ except Exception:
1736
+ maintenance_coverage = "partial"
1737
+ record = status or {}
1738
+ starts = list(deletion_starts)
1739
+ if verifications:
1740
+ starts.append(record.get("verificationPendingSince"))
1741
+ reads = (census["neverIngested"] + census["retainedIncomplete"]
1742
+ if decoder != "unavailable" else 0)
1743
+ if reads:
1744
+ starts.append(record.get("readsPendingSince"))
1745
+ old = False
1746
+ for stamp in starts:
1747
+ if not stamp:
1748
+ continue
1749
+ try:
1750
+ age = (now_utc - parse_iso_datetime(
1751
+ stamp, "codex maintenance start")).total_seconds()
1752
+ old = old or age > 86400
1753
+ except (TypeError, ValueError):
1754
+ pass
1755
+ debt = dict(verifications=verifications, deletions=deletions,
1756
+ olderThan24h=old)
1757
+ if maintenance_coverage != "complete":
1758
+ debt["coverage"] = maintenance_coverage
1759
+ return census, decoder, status, debt
1760
+ except Exception:
1761
+ return None, None, None, None
1762
+
1763
+
1439
1764
  def _doctor_gather_state_impl(
1440
1765
  *,
1441
1766
  now_utc: "dt.datetime | None" = None,
@@ -2032,12 +2357,18 @@ def _doctor_gather_state_impl(
2032
2357
  codex_jsonl_present = False
2033
2358
  try:
2034
2359
  for codex_dir in c._codex_session_roots():
2035
- if next(codex_dir.glob("**/*.jsonl"), None) is not None:
2360
+ if (next(codex_dir.glob("**/*.jsonl"), None) is not None
2361
+ or next(codex_dir.glob("**/*.jsonl.zst"), None) is not None):
2036
2362
  codex_jsonl_present = True
2037
2363
  break
2038
2364
  except Exception:
2039
2365
  pass
2040
2366
 
2367
+ with _lib_perf.phase("doctor.codex_containers"):
2368
+ (codex_compressed_census, codex_decoder_capability,
2369
+ codex_container_status, codex_maintenance_debt) = _gather_codex_container_state(
2370
+ c, now_utc=now_utc, cache_probe_allowed=_cache_probe_allowed)
2371
+
2041
2372
  with _lib_perf.phase("doctor.quota_summary"):
2042
2373
  # ── Codex quota lifecycle (#294 S2) ──────────────────────────────
2043
2374
  # All three probes are read-only and root-qualified. The physical cache
@@ -2496,10 +2827,17 @@ def _doctor_gather_state_impl(
2496
2827
  journal_cursor_lag_bytes = None
2497
2828
  journal_hw_segment = None
2498
2829
  journal_cursor_segment = None
2830
+ journal_cursor_invalid = False
2499
2831
  journal_conflicts = None
2500
2832
  journal_protocol_violations = None
2501
2833
  journal_protocol_acknowledged = None
2502
2834
  journal_protocol_error = None
2835
+ # #825/#832 spec 8: the deep scan's whole-journal excluded population.
2836
+ deep_excluded = None
2837
+ deep_admission_boundary = None
2838
+ # K9K10-R6/R11: why the listing or the deep scan could not complete.
2839
+ journal_listing_error = None
2840
+ journal_scan_error = None
2503
2841
  try:
2504
2842
  jdir = _cctally_core.JOURNAL_DIR
2505
2843
  journal_present = jdir.exists()
@@ -2511,8 +2849,14 @@ def _doctor_gather_state_impl(
2511
2849
  import _cctally_journal as _jr
2512
2850
  try:
2513
2851
  segs = _jr.list_segments() # canonical (segment) order
2514
- except Exception:
2852
+ except Exception as exc: # noqa: BLE001 — reported, never empty
2853
+ # K9K10-R6: an unlistable journal is unreadable, not an
2854
+ # empty one; the presence leg names the cause.
2515
2855
  segs = []
2856
+ journal_listing_error = _scan_cause(exc)
2857
+ if deep:
2858
+ # The deep recomputation cannot run either.
2859
+ journal_scan_error = journal_listing_error
2516
2860
  journal_segment_count = len(segs)
2517
2861
  # #402: the disposable stats index persists the most recent complete
2518
2862
  # selector result. Shallow Dashboard/TUI gathers read that bounded
@@ -2556,13 +2900,28 @@ def _doctor_gather_state_impl(
2556
2900
  torn = 0
2557
2901
  decoded_records: list = []
2558
2902
  protocol_evidence = []
2903
+ protocol_evidence_owners = []
2559
2904
  prior_high_water = None
2560
2905
  cutover_value = None
2906
+ # #825/#832 (spec 5.3, 8): the deep scan applies the same
2907
+ # admission predicate as ingest and rebuild — resolved
2908
+ # within this one pass and applied after it — and
2909
+ # recomputes the excluded population the legacy-writer leg
2910
+ # compares.
2911
+ pass_boundary = _jr._PassBoundary()
2912
+ deep_coordinates: list = []
2913
+ cutover_candidates: list = []
2914
+ deep_excluded: list = []
2561
2915
  for seg in segs:
2562
2916
  try:
2563
2917
  data = (jdir / seg).read_bytes()
2564
- except OSError:
2918
+ except FileNotFoundError:
2565
2919
  continue
2920
+ except OSError as exc:
2921
+ # K9K10-R6: a skipped segment would let every
2922
+ # check below report a partial journal as whole.
2923
+ journal_scan_error = _scan_cause(exc)
2924
+ break
2566
2925
  if not data:
2567
2926
  continue
2568
2927
  if not data.endswith(b"\n"):
@@ -2585,20 +2944,22 @@ def _doctor_gather_state_impl(
2585
2944
  )
2586
2945
  offset += len(raw) + 1
2587
2946
  continue
2947
+ pass_boundary.observe(record)
2588
2948
  _jr._capture_protocol_prefix_evidence(
2589
2949
  record,
2590
2950
  prior_high_water,
2591
2951
  protocol_evidence,
2592
2952
  )
2953
+ if (len(protocol_evidence)
2954
+ > len(protocol_evidence_owners)):
2955
+ protocol_evidence_owners.append(
2956
+ len(decoded_records))
2593
2957
  # first cutover op wins, exactly as
2594
2958
  # `find_accounts_cutover_op` scans — captured here so the
2595
2959
  # conflict scan does not decode the whole journal twice.
2596
- if (cutover_value is None
2597
- and record.get("id") == _jr.CUTOVER_OP_ID):
2598
- payload = record.get("payload")
2599
- if isinstance(payload, dict):
2600
- cutover_value = payload.get(
2601
- "claude_legacy_account")
2960
+ if record.get("id") == _jr.CUTOVER_OP_ID:
2961
+ cutover_candidates.append(
2962
+ (record, seg, offset))
2602
2963
  # RETAIN ONLY what the selector consumes. `obs` lines are
2603
2964
  # ~97% of a real journal (984k of 1.02M) and
2604
2965
  # `resolve_effective_events` ignores them entirely —
@@ -2609,13 +2970,56 @@ def _doctor_gather_state_impl(
2609
2970
  decoded_records.append(
2610
2971
  _lib_journal_router.selector_slot(record)
2611
2972
  )
2973
+ deep_coordinates.append((seg, offset))
2612
2974
  prior_high_water = (
2613
2975
  seg,
2614
2976
  offset + len(raw) + 1,
2615
2977
  )
2616
2978
  offset += len(raw) + 1
2617
- journal_malformed_count = malformed
2618
- journal_torn_tail_count = torn
2979
+ admission_boundary = None
2980
+ if journal_scan_error is None:
2981
+ try:
2982
+ admission_boundary = pass_boundary.resolve(
2983
+ (segs[-1], sizes.get(segs[-1], 0)))
2984
+ except Exception as exc: # noqa: BLE001
2985
+ # K9K10-R6: a None boundary admits everything:
2986
+ # zero excluded records and an unfiltered scan.
2987
+ journal_scan_error = _scan_cause(exc)
2988
+ if journal_scan_error is None:
2989
+ try:
2990
+ deep_excluded = _jr.excluded_population(
2991
+ admission_boundary)
2992
+ except Exception as exc: # noqa: BLE001
2993
+ # K9K10-R11: an unreadable tail is never a
2994
+ # smaller population.
2995
+ journal_scan_error = _scan_cause(exc)
2996
+ if journal_scan_error is not None:
2997
+ deep_excluded = None
2998
+ journal_conflicts = None
2999
+ journal_protocol_error = None
3000
+ else:
3001
+ journal_malformed_count = malformed
3002
+ journal_torn_tail_count = torn
3003
+ if deep and segs and journal_scan_error is None:
3004
+ _jr._apply_pass_admission(
3005
+ decoded_records, deep_coordinates, admission_boundary)
3006
+ # #825/#832 fix round C (CD2-005): an excluded
3007
+ # acknowledgement's evidence is excluded with it.
3008
+ protocol_evidence = _jr._admitted_prefix_evidence(
3009
+ protocol_evidence, protocol_evidence_owners,
3010
+ decoded_records)
3011
+ # Every excluded record counts, obs included, so the
3012
+ # population matches what ingest and rebuild record. Only
3013
+ # the post-boundary byte ranges can hold one.
3014
+ deep_admission_boundary = admission_boundary
3015
+ for candidate, cand_seg, cand_off in cutover_candidates:
3016
+ if _jr._is_admitted(candidate, cand_seg, cand_off,
3017
+ admission_boundary):
3018
+ payload = candidate.get("payload")
3019
+ if isinstance(payload, dict):
3020
+ cutover_value = payload.get(
3021
+ "claude_legacy_account")
3022
+ break
2619
3023
  # #374: same-revision quarantine, via the SHARED selector over
2620
3024
  # rebuild-equivalent input. Raw `(id, rev)` grouping would report
2621
3025
  # lower-revision groups a completed rev-1 batch legitimately
@@ -2704,7 +3108,23 @@ def _doctor_gather_state_impl(
2704
3108
  order = {s: i for i, s in enumerate(segs)}
2705
3109
  if cseg in order:
2706
3110
  ci = order[cseg]
2707
- lag = max(0, sizes.get(segs[ci], 0) - coff)
3111
+ # #946: the lag of the SAFE applied cursor. A torn
3112
+ # suffix past it stays outstanding until a repair, and
3113
+ # a cursor past its segment or inside a line is no
3114
+ # safe endpoint at all.
3115
+ csize = sizes.get(segs[ci], 0)
3116
+ if coff > csize:
3117
+ journal_cursor_invalid = True
3118
+ elif coff > 0:
3119
+ try:
3120
+ with open(_cctally_core.JOURNAL_DIR / cseg,
3121
+ "rb") as _seg_fh:
3122
+ _seg_fh.seek(coff - 1)
3123
+ if _seg_fh.read(1) != b"\n":
3124
+ journal_cursor_invalid = True
3125
+ except OSError:
3126
+ journal_cursor_invalid = True
3127
+ lag = max(0, csize - coff)
2708
3128
  for s in segs[ci + 1:]:
2709
3129
  lag += sizes.get(s, 0)
2710
3130
  journal_cursor_lag_bytes = lag
@@ -2812,6 +3232,14 @@ def _doctor_gather_state_impl(
2812
3232
  discover=_discover_dashboards)
2813
3233
  except Exception: # noqa: BLE001 — a probe failure must not fail doctor
2814
3234
  dashboard_disk_writes = {"mode": "error", "instances": []}
3235
+ # #997 D10: the detached rebuild workers, read-only (no worker or
3236
+ # admission flock is taken; a probe would make a starting worker `busy`).
3237
+ try:
3238
+ with _lib_perf.phase("doctor.rebuild_workers"):
3239
+ rebuild_workers = _gather_rebuild_workers()
3240
+ except Exception as exc: # noqa: BLE001 — a gather failure must not fail doctor
3241
+ rebuild_workers = {worker: _rebuild_worker_gather_failed(worker, exc)
3242
+ for worker in _REBUILD_WORKERS}
2815
3243
  return _lib_doctor.DoctorState(
2816
3244
  symlink_state=symlink_state,
2817
3245
  path_includes_local_bin=path_includes,
@@ -2881,6 +3309,10 @@ def _doctor_gather_state_impl(
2881
3309
  codex_replay_blocked=codex_replay_blocked,
2882
3310
  codex_replay_deferred=codex_replay_deferred,
2883
3311
  codex_prune_refusals=codex_prune_refusals or None,
3312
+ codex_compressed_census=codex_compressed_census,
3313
+ codex_decoder_capability=codex_decoder_capability,
3314
+ codex_container_status=codex_container_status,
3315
+ codex_maintenance_debt=codex_maintenance_debt,
2884
3316
  conversation_rollup_pricing_refusal=(
2885
3317
  conversation_rollup_pricing_refusal),
2886
3318
  conversation_rollup_pricing_fp=conversation_rollup_pricing_fp,
@@ -2898,9 +3330,17 @@ def _doctor_gather_state_impl(
2898
3330
  conversations_db_wal_bytes=conversations_db_wal_bytes,
2899
3331
  # #374: quarantined same-revision groups + structural protocol violation.
2900
3332
  journal_conflicts=journal_conflicts,
3333
+ # #825/#832 spec 8: the legacy-writer evidence.
3334
+ legacy_writer=_gather_legacy_writer(
3335
+ deep_excluded, deep_admission_boundary,
3336
+ deep_error=journal_scan_error),
3337
+ # #825/#832 spec 8: the per-account outcome quarantine.
3338
+ outcome_quarantine=_gather_outcome_quarantine(),
2901
3339
  journal_protocol_violations=journal_protocol_violations,
2902
3340
  journal_protocol_acknowledged=journal_protocol_acknowledged,
2903
3341
  journal_protocol_error=journal_protocol_error,
3342
+ journal_listing_error=journal_listing_error,
3343
+ journal_scan_error=journal_scan_error,
2904
3344
  # #496 S5b: the published generation's incomplete-quota-projection flag.
2905
3345
  stats_quota_projection_incomplete=stats_quota_projection_incomplete,
2906
3346
  # #315: read-only cache free-page evidence for the reclaim hint.
@@ -2910,6 +3350,7 @@ def _doctor_gather_state_impl(
2910
3350
  conversations_db_freelist_count=conversations_db_freelist_count,
2911
3351
  conversations_db_page_size=conversations_db_page_size,
2912
3352
  dashboard_disk_writes=dashboard_disk_writes,
3353
+ rebuild_workers=rebuild_workers,
2913
3354
  conversations_reclaim_pending=conversations_reclaim_pending,
2914
3355
  codex_quota_windows=codex_quota_windows,
2915
3356
  codex_hook_roots=codex_hook_roots,
@@ -2927,6 +3368,7 @@ def _doctor_gather_state_impl(
2927
3368
  journal_malformed_count=journal_malformed_count,
2928
3369
  journal_torn_tail_count=journal_torn_tail_count,
2929
3370
  journal_cursor_lag_bytes=journal_cursor_lag_bytes,
3371
+ journal_cursor_invalid=journal_cursor_invalid,
2930
3372
  journal_hw_segment=journal_hw_segment,
2931
3373
  journal_cursor_segment=journal_cursor_segment,
2932
3374
  journal_heal_incidents=journal_heal_incidents,