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.
- package/CHANGELOG.md +41 -0
- package/bin/_cctally_account.py +32 -8
- package/bin/_cctally_cache.py +4491 -295
- package/bin/_cctally_completion.py +1355 -0
- package/bin/_cctally_config.py +37 -0
- package/bin/_cctally_core.py +282 -9
- package/bin/_cctally_dashboard.py +8 -11
- package/bin/_cctally_dashboard_conversation.py +292 -23
- package/bin/_cctally_dashboard_envelope.py +42 -35
- package/bin/_cctally_dashboard_sources.py +18 -9
- package/bin/_cctally_db.py +401 -9
- package/bin/_cctally_doctor.py +469 -27
- package/bin/_cctally_five_hour.py +166 -1049
- package/bin/_cctally_forecast.py +81 -9
- package/bin/_cctally_journal.py +5076 -177
- package/bin/_cctally_journal_repair.py +110 -19
- package/bin/_cctally_oauth_samples.py +280 -0
- package/bin/_cctally_parser.py +47 -0
- package/bin/_cctally_project.py +8 -2
- package/bin/_cctally_quarantine.py +662 -0
- package/bin/_cctally_quota.py +96 -12
- package/bin/_cctally_quota_model.py +391 -83
- package/bin/_cctally_record.py +1088 -269
- package/bin/_cctally_rederive.py +854 -192
- package/bin/_cctally_refresh.py +113 -13
- package/bin/_cctally_source_analytics.py +23 -2
- package/bin/_cctally_statusline.py +7062 -505
- package/bin/_cctally_store.py +1244 -91
- package/bin/_cctally_transcript.py +8 -7
- package/bin/_cctally_tui.py +119 -60
- package/bin/_cctally_update.py +25 -0
- package/bin/_lib_account_binding.py +152 -0
- package/bin/_lib_account_protocol.py +569 -0
- package/bin/_lib_aggregators.py +4 -0
- package/bin/_lib_cache_coverage.py +13 -1
- package/bin/_lib_cache_writer_lock.py +19 -3
- package/bin/_lib_clone_copy.py +177 -0
- package/bin/_lib_codex_conversation_query.py +133 -19
- package/bin/_lib_codex_conversation_watch.py +59 -14
- package/bin/_lib_codex_drain_admission.py +214 -0
- package/bin/_lib_codex_metadata.py +18 -1
- package/bin/_lib_codex_publication_barrier.py +277 -0
- package/bin/_lib_codex_rollout_container.py +502 -0
- package/bin/_lib_conversation_dispatch.py +3 -0
- package/bin/_lib_conversation_query.py +11 -3
- package/bin/_lib_dashboard_sources.py +57 -15
- package/bin/_lib_detector_access_registry.py +4038 -0
- package/bin/_lib_doctor.py +753 -1
- package/bin/_lib_five_hour.py +126 -0
- package/bin/_lib_ingest_frontier.py +414 -20
- package/bin/_lib_journal.py +261 -10
- package/bin/_lib_journal_router.py +166 -48
- package/bin/_lib_jsonl.py +5 -4
- package/bin/_lib_legacy_spool.py +126 -0
- package/bin/_lib_legacy_writer_evidence.py +136 -0
- package/bin/_lib_meter_rate_change.py +10 -0
- package/bin/_lib_oauth_samples.py +238 -0
- package/bin/_lib_outcome_quarantine.py +690 -0
- package/bin/_lib_pricing.py +7 -2
- package/bin/_lib_pricing_check.py +0 -12
- package/bin/_lib_provider_activity.py +770 -0
- package/bin/_lib_quarantine.py +155 -0
- package/bin/_lib_quota_calibration.py +53 -8
- package/bin/_lib_quota_copy.py +2 -1
- package/bin/_lib_quota_model.py +384 -52
- package/bin/_lib_rebuild_progress.py +146 -0
- package/bin/_lib_record.py +22 -0
- package/bin/_lib_rederive.py +393 -8
- package/bin/_lib_segment_summary.py +35 -2
- package/bin/_lib_snapshot_cache.py +43 -1
- package/bin/_lib_stale_proof.py +708 -0
- package/bin/_lib_statusline_candidates.py +356 -27
- package/bin/_lib_worker_launches.py +315 -0
- package/bin/cctally +29 -12
- package/dashboard/static/assets/{ConversationsView-Bc8LCOAZ.js → ConversationsView-Dflb_myA.js} +1 -1
- package/dashboard/static/assets/DoctorModal-DFX5CfIs.js +1 -0
- package/dashboard/static/assets/{ModalRoot-DLPgyAM_.js → ModalRoot-HlxoagIa.js} +1 -1
- package/dashboard/static/assets/{ProjectsDrillPanel-z53a4v4M.js → ProjectsDrillPanel-C3PJy1xx.js} +1 -1
- package/dashboard/static/assets/{SourceDetailModal-Bw0T8W-W.js → SourceDetailModal-BczsWKhD.js} +1 -1
- package/dashboard/static/assets/{UpdateModal-D5AKx4Yw.js → UpdateModal-Os-Lo5cs.js} +1 -1
- package/dashboard/static/assets/{index-BXDOzSng.js → index-CYZ3NLqF.js} +3 -3
- package/dashboard/static/assets/{outlineNavigation-JObY7cUY.js → outlineNavigation-k-zIif6W.js} +1 -1
- package/dashboard/static/dashboard.html +1 -1
- package/package.json +20 -2
- package/bin/_lib_merge_gate.py +0 -725
- package/dashboard/static/assets/DoctorModal-D--qVBwe.js +0 -1
package/bin/_cctally_doctor.py
CHANGED
|
@@ -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
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
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
|
|
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 (
|
|
2597
|
-
|
|
2598
|
-
|
|
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
|
-
|
|
2618
|
-
|
|
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
|
|
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,
|