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.
- package/CHANGELOG.md +47 -0
- package/README.md +4 -6
- package/bin/_cctally_account.py +32 -8
- package/bin/_cctally_cache.py +6035 -722
- package/bin/_cctally_completion.py +1287 -0
- package/bin/_cctally_config.py +37 -0
- package/bin/_cctally_core.py +400 -2
- package/bin/_cctally_dashboard.py +98 -20
- package/bin/_cctally_dashboard_conversation.py +292 -23
- package/bin/_cctally_dashboard_envelope.py +16 -3
- package/bin/_cctally_dashboard_perf.py +128 -9
- package/bin/_cctally_dashboard_sources.py +285 -213
- package/bin/_cctally_db.py +1177 -23
- package/bin/_cctally_diagnosis_sources.py +22 -4
- package/bin/_cctally_doctor.py +572 -45
- package/bin/_cctally_five_hour.py +84 -33
- package/bin/_cctally_forecast.py +108 -15
- package/bin/_cctally_journal.py +3950 -116
- package/bin/_cctally_journal_repair.py +101 -12
- package/bin/_cctally_milestone_history.py +82 -9
- package/bin/_cctally_parser.py +42 -0
- package/bin/_cctally_percent_breakdown.py +3 -2
- package/bin/_cctally_project.py +8 -2
- package/bin/_cctally_quarantine.py +652 -0
- package/bin/_cctally_quota.py +227 -37
- package/bin/_cctally_quota_model.py +360 -83
- package/bin/_cctally_record.py +691 -149
- package/bin/_cctally_rederive.py +320 -41
- package/bin/_cctally_refresh.py +84 -13
- package/bin/_cctally_source_analytics.py +83 -17
- package/bin/_cctally_statusline.py +6154 -517
- package/bin/_cctally_store.py +367 -53
- package/bin/_cctally_transcript.py +8 -7
- package/bin/_cctally_tui.py +61 -56
- package/bin/_cctally_weekrefs.py +128 -21
- package/bin/_lib_account_binding.py +152 -0
- package/bin/_lib_account_protocol.py +386 -0
- package/bin/_lib_aggregators.py +4 -0
- package/bin/_lib_cache_coverage.py +13 -1
- package/bin/_lib_cache_report.py +35 -6
- package/bin/_lib_cache_writer_lock.py +19 -3
- package/bin/_lib_clone_copy.py +142 -0
- package/bin/_lib_codex_conversation.py +32 -0
- package/bin/_lib_codex_conversation_query.py +297 -49
- 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 +106 -59
- package/bin/_lib_conversation_retention.py +1605 -640
- package/bin/_lib_cost_provenance.py +53 -2
- package/bin/_lib_detector_access_registry.py +3817 -0
- package/bin/_lib_doctor.py +714 -69
- package/bin/_lib_five_hour.py +126 -0
- package/bin/_lib_fmt.py +11 -0
- package/bin/_lib_ingest_frontier.py +414 -20
- package/bin/_lib_journal.py +141 -10
- 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_merge_gate.py +231 -86
- package/bin/_lib_meter_rate_change.py +10 -0
- package/bin/_lib_outcome_quarantine.py +690 -0
- package/bin/_lib_perf.py +27 -1
- package/bin/_lib_pricing.py +199 -9
- package/bin/_lib_pricing_check.py +20 -4
- package/bin/_lib_quarantine.py +154 -0
- package/bin/_lib_quota_calibration.py +53 -8
- package/bin/_lib_quota_copy.py +2 -1
- package/bin/_lib_quota_model.py +288 -46
- package/bin/_lib_reclaim_planner.py +792 -0
- package/bin/_lib_rederive.py +319 -82
- package/bin/_lib_segment_summary.py +35 -2
- package/bin/_lib_snapshot_cache.py +21 -0
- package/bin/_lib_sqlite_close.py +81 -0
- package/bin/_lib_statusline_candidates.py +292 -23
- package/bin/_lib_subscription_weeks.py +100 -17
- package/bin/_lib_tick_stats.py +217 -17
- package/bin/_lib_wal_checkpoint.py +1077 -0
- package/bin/_lib_write_budget.py +483 -0
- package/bin/_lib_write_io.py +703 -0
- package/bin/cctally +31 -1
- package/dashboard/static/assets/{ConversationsView-CEZ-BUyi.js → ConversationsView-Bc8LCOAZ.js} +1 -1
- package/dashboard/static/assets/{DoctorModal-CE1oH_Zy.js → DoctorModal-D--qVBwe.js} +1 -1
- package/dashboard/static/assets/ModalRoot-DLPgyAM_.js +1 -0
- package/dashboard/static/assets/{ProjectsDrillPanel-B3DZoCeD.js → ProjectsDrillPanel-z53a4v4M.js} +1 -1
- package/dashboard/static/assets/{SourceDetailModal-CHp9EgLP.js → SourceDetailModal-Bw0T8W-W.js} +1 -1
- package/dashboard/static/assets/{UpdateModal-5iKST6jo.js → UpdateModal-D5AKx4Yw.js} +1 -1
- package/dashboard/static/assets/index-BKITk1CT.css +1 -0
- package/dashboard/static/assets/{index-Cm0VTxJC.js → index-BXDOzSng.js} +3 -3
- package/dashboard/static/assets/{outlineNavigation-DkXGL8J6.js → outlineNavigation-JObY7cUY.js} +1 -1
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +19 -1
- package/dashboard/static/assets/ModalRoot-D5Y8IFUG.js +0 -1
- package/dashboard/static/assets/index-Dt4tqf43.css +0 -1
package/bin/_cctally_doctor.py
CHANGED
|
@@ -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
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
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
|
-
|
|
1216
|
+
attributed += 1
|
|
945
1217
|
else:
|
|
946
|
-
|
|
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
|
|
965
|
-
# (PARTITION BY ...)` across every retained row
|
|
966
|
-
#
|
|
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
|
-
|
|
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
|
|
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 (
|
|
2488
|
-
|
|
2489
|
-
|
|
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
|
-
|
|
2509
|
-
|
|
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
|
|
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(
|