cctally 1.116.1 → 1.117.2
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 +57 -3
- package/bin/_cctally_cache.py +5720 -885
- package/bin/_cctally_claude_attribution.py +997 -0
- package/bin/_cctally_completion.py +101 -28
- package/bin/_cctally_core.py +235 -12
- package/bin/_cctally_dashboard.py +263 -17
- package/bin/_cctally_dashboard_conversation.py +107 -21
- package/bin/_cctally_dashboard_envelope.py +44 -0
- package/bin/_cctally_db.py +928 -80
- package/bin/_cctally_diagnosis_sources.py +78 -13
- package/bin/_cctally_doctor.py +211 -86
- package/bin/_cctally_five_hour.py +446 -22
- package/bin/_cctally_forecast.py +302 -6
- package/bin/_cctally_journal.py +2418 -158
- package/bin/_cctally_milestone_history.py +379 -91
- package/bin/_cctally_oauth_samples.py +185 -7
- package/bin/_cctally_parser.py +38 -0
- package/bin/_cctally_project.py +34 -8
- package/bin/_cctally_quarantine.py +821 -343
- package/bin/_cctally_quota.py +69 -4
- package/bin/_cctally_quota_calibration.py +677 -3
- package/bin/_cctally_quota_model.py +927 -127
- package/bin/_cctally_record.py +922 -445
- package/bin/_cctally_rederive.py +119 -15
- package/bin/_cctally_statusline.py +1048 -176
- package/bin/_cctally_store.py +544 -110
- package/bin/_cctally_tui.py +455 -42
- package/bin/_cctally_update.py +77 -14
- package/bin/_lib_account_protocol.py +227 -19
- package/bin/_lib_claude_attribution.py +421 -0
- package/bin/_lib_clone_copy.py +30 -0
- package/bin/_lib_codex_conversation_query.py +45 -9
- package/bin/_lib_codex_conversation_watch.py +72 -13
- package/bin/_lib_codex_rollout_container.py +234 -19
- package/bin/_lib_codex_worker_lifecycle.py +374 -0
- package/bin/_lib_credit.py +22 -13
- package/bin/_lib_dashboard_sources.py +257 -18
- package/bin/_lib_detector_access_registry.py +1335 -166
- package/bin/_lib_doctor.py +276 -92
- package/bin/_lib_ingest_frontier.py +269 -36
- package/bin/_lib_journal.py +273 -22
- package/bin/_lib_jsonl.py +12 -0
- package/bin/_lib_meter_rate_change.py +72 -0
- package/bin/_lib_outcome_quarantine.py +917 -554
- package/bin/_lib_provider_activity.py +24 -6
- package/bin/_lib_quota_calibration.py +57 -3
- package/bin/_lib_quota_copy.py +7 -2
- package/bin/_lib_quota_lineage.py +613 -0
- package/bin/_lib_quota_model.py +464 -33
- package/bin/_lib_rate_change_delivery.py +16 -0
- package/bin/_lib_reclaim_planner.py +6 -4
- package/bin/_lib_rederive.py +2046 -21
- package/bin/_lib_selector_state.py +131 -3
- package/bin/_lib_snapshot_cache.py +8 -0
- package/bin/_lib_stale_proof.py +113 -4
- package/bin/_lib_statusline.py +72 -3
- package/bin/_lib_statusline_candidates.py +246 -42
- package/bin/_lib_tick_stats.py +10 -5
- package/bin/_lib_write_io.py +419 -6
- package/bin/cctally +28 -11
- package/dashboard/static/assets/{ConversationsView-Dflb_myA.js → ConversationsView-oqNsWzWV.js} +1 -1
- package/dashboard/static/assets/{DoctorModal-DFX5CfIs.js → DoctorModal-B0DnU9d9.js} +1 -1
- package/dashboard/static/assets/ModalRoot-CYua2wP_.js +1 -0
- package/dashboard/static/assets/{ProjectsDrillPanel-C3PJy1xx.js → ProjectsDrillPanel-DSxXQSa-.js} +1 -1
- package/dashboard/static/assets/{SourceDetailModal-BczsWKhD.js → SourceDetailModal-BleDpQqG.js} +1 -1
- package/dashboard/static/assets/{UpdateModal-Os-Lo5cs.js → UpdateModal-CMqaAkSK.js} +1 -1
- package/dashboard/static/assets/{index-CYZ3NLqF.js → index-BjQV-0-D.js} +3 -3
- package/dashboard/static/assets/index-D34eYhAc.css +1 -0
- package/dashboard/static/assets/{outlineNavigation-k-zIif6W.js → outlineNavigation-CO-VWBm7.js} +1 -1
- package/dashboard/static/assets/{useKeymap-CW7Up0Ay.js → useKeymap-CUgEnnua.js} +1 -1
- package/dashboard/static/dashboard.html +3 -3
- package/package.json +5 -1
- package/dashboard/static/assets/ModalRoot-HlxoagIa.js +0 -1
- package/dashboard/static/assets/index-BKITk1CT.css +0 -1
|
@@ -435,6 +435,8 @@ class ObservedFile(NamedTuple):
|
|
|
435
435
|
container_known: bool = False
|
|
436
436
|
kind: str = "plain"
|
|
437
437
|
mtime_ns: "int | None" = None
|
|
438
|
+
#: #970: ``"dated"`` or ``"archive"`` — where the selected container is.
|
|
439
|
+
location: str = _rc.DATED
|
|
438
440
|
|
|
439
441
|
|
|
440
442
|
class ContainerEvidence(NamedTuple):
|
|
@@ -448,6 +450,30 @@ class ContainerEvidence(NamedTuple):
|
|
|
448
450
|
inode: "int | None"
|
|
449
451
|
state: str
|
|
450
452
|
pending_fingerprint: "str | None"
|
|
453
|
+
#: #970 spec 5.5: the stored ``container_path`` — None means the accepted
|
|
454
|
+
#: container is the logical path; otherwise the container's path.
|
|
455
|
+
location: "str | None" = None
|
|
456
|
+
|
|
457
|
+
def accepted_location(self, logical, archive_dir) -> str:
|
|
458
|
+
"""``"archive"`` when the accepted container sits in ``archive_dir``.
|
|
459
|
+
|
|
460
|
+
The stored path's DIRECTORY is compared, canonically when the
|
|
461
|
+
spellings differ, so a symlinked file in the archive is still an
|
|
462
|
+
archive container — exactly as selection names its location. No
|
|
463
|
+
archive directory, or an uncontained stored path, is ``"dated"``.
|
|
464
|
+
"""
|
|
465
|
+
if self.location is None or archive_dir is None:
|
|
466
|
+
return _rc.DATED
|
|
467
|
+
parent = pathlib.Path(self.location).parent
|
|
468
|
+
if parent == pathlib.Path(logical).parent:
|
|
469
|
+
return _rc.DATED
|
|
470
|
+
if parent == pathlib.Path(archive_dir):
|
|
471
|
+
return _rc.ARCHIVE
|
|
472
|
+
try:
|
|
473
|
+
same = os.path.realpath(parent) == os.path.realpath(archive_dir)
|
|
474
|
+
except (OSError, ValueError):
|
|
475
|
+
return _rc.DATED
|
|
476
|
+
return _rc.ARCHIVE if same else _rc.DATED
|
|
451
477
|
|
|
452
478
|
def _same_container(self, kind: str, st) -> bool:
|
|
453
479
|
if kind != self.kind or source_identity_replaced(
|
|
@@ -479,7 +505,8 @@ class ContainerEvidence(NamedTuple):
|
|
|
479
505
|
|
|
480
506
|
_CONTAINER_EVIDENCE_COLUMNS = (
|
|
481
507
|
"path, container_kind, container_size_bytes, decoded_size_bytes, "
|
|
482
|
-
"mtime_ns, device_id, inode, container_state, pending_container_fingerprint"
|
|
508
|
+
"mtime_ns, device_id, inode, container_state, pending_container_fingerprint, "
|
|
509
|
+
"container_path")
|
|
483
510
|
|
|
484
511
|
|
|
485
512
|
def _container_evidence_for(conn, table: str, paths) -> "dict[str, ContainerEvidence]":
|
|
@@ -502,43 +529,61 @@ def _container_evidence_for(conn, table: str, paths) -> "dict[str, ContainerEvid
|
|
|
502
529
|
f"WHERE path IN ({placeholders})", chunk):
|
|
503
530
|
out[str(row[0])] = ContainerEvidence(
|
|
504
531
|
row[1] or _rc.PLAIN, row[2], row[3], row[4], row[5],
|
|
505
|
-
row[6], row[7] or "accepted", row[8])
|
|
532
|
+
row[6], row[7] or "accepted", row[8], row[9])
|
|
506
533
|
except sqlite3.OperationalError:
|
|
507
534
|
return {}
|
|
508
535
|
return out
|
|
509
536
|
|
|
510
537
|
|
|
511
|
-
def _observe_logical(path: str, evidence: "ContainerEvidence | None"
|
|
538
|
+
def _observe_logical(path: str, evidence: "ContainerEvidence | None", *,
|
|
539
|
+
archive_dir=None):
|
|
512
540
|
"""Restat one LOGICAL rollout through its selected container (5.2, 5.9).
|
|
513
541
|
|
|
514
542
|
Plain wins and keeps today's single ``pathlib`` stat, so a plain estate
|
|
515
543
|
is observed exactly as before; the ``.zst`` twin is statted only when the
|
|
516
|
-
plain file is absent
|
|
544
|
+
plain file is absent, and — #970 — the archive candidates only when neither dated
|
|
545
|
+
container exists and the caller names the archive directory, in the
|
|
546
|
+
leaf's selection order.
|
|
547
|
+
|
|
548
|
+
#970 A8: location is observed separately from content. A container at a
|
|
549
|
+
different location than the cursor accepted is pending location
|
|
550
|
+
adoption — never a cursor gap or a vanished file. When the stat evidence
|
|
551
|
+
otherwise accepts it, the walk adopts the location on its next pass, so
|
|
552
|
+
the hold is ``container_known`` (the move itself changes certified
|
|
553
|
+
directory evidence, which is what schedules that pass); any other
|
|
554
|
+
relocation is a new container transition and is requested once.
|
|
517
555
|
"""
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
except OSError:
|
|
556
|
+
st = None
|
|
557
|
+
for kind, location, candidate in _rc.container_candidates(
|
|
558
|
+
path, archive_dir=archive_dir):
|
|
522
559
|
try:
|
|
523
|
-
st =
|
|
560
|
+
st = candidate.stat()
|
|
524
561
|
except OSError:
|
|
525
|
-
|
|
526
|
-
|
|
562
|
+
continue
|
|
563
|
+
break
|
|
564
|
+
if st is None:
|
|
565
|
+
return None
|
|
566
|
+
if (evidence is not None and evidence.state == "accepted"
|
|
567
|
+
and evidence.accepted_location(path, archive_dir) != location):
|
|
568
|
+
return ObservedFile(
|
|
569
|
+
0, st.st_dev, st.st_ino, container_pending=True,
|
|
570
|
+
container_known=evidence.accepts(kind, st),
|
|
571
|
+
kind=kind, mtime_ns=st.st_mtime_ns, location=location)
|
|
527
572
|
if evidence is not None and evidence.accepts(kind, st):
|
|
528
573
|
size = (st.st_size if kind == _rc.PLAIN
|
|
529
574
|
else int(evidence.decoded_size))
|
|
530
575
|
return ObservedFile(size, st.st_dev, st.st_ino, kind=kind,
|
|
531
|
-
mtime_ns=st.st_mtime_ns)
|
|
576
|
+
mtime_ns=st.st_mtime_ns, location=location)
|
|
532
577
|
if kind == _rc.PLAIN and (evidence is None or (
|
|
533
578
|
evidence.state == "accepted" and evidence.kind == _rc.PLAIN)):
|
|
534
579
|
# Today's plain evidence: append, shrink and replacement are decided
|
|
535
580
|
# by the existing classifier from size and identity.
|
|
536
581
|
return ObservedFile(st.st_size, st.st_dev, st.st_ino, kind=kind,
|
|
537
|
-
mtime_ns=st.st_mtime_ns)
|
|
582
|
+
mtime_ns=st.st_mtime_ns, location=location)
|
|
538
583
|
return ObservedFile(
|
|
539
584
|
0, st.st_dev, st.st_ino, container_pending=True,
|
|
540
585
|
container_known=evidence is not None and evidence.holds(kind, st),
|
|
541
|
-
kind=kind, mtime_ns=st.st_mtime_ns)
|
|
586
|
+
kind=kind, mtime_ns=st.st_mtime_ns, location=location)
|
|
542
587
|
|
|
543
588
|
|
|
544
589
|
def source_identity_replaced(
|
|
@@ -781,9 +826,10 @@ def _bump_counter(name: str, amount: int = 1) -> None:
|
|
|
781
826
|
|
|
782
827
|
|
|
783
828
|
def _observe_file(path: str, table: str,
|
|
784
|
-
evidence: "ContainerEvidence | None" = None
|
|
829
|
+
evidence: "ContainerEvidence | None" = None, *,
|
|
830
|
+
archive_dir=None) -> "ObservedFile | None":
|
|
785
831
|
_bump_counter(f"retained_file_visits.{table}")
|
|
786
|
-
return _observe_logical(path, evidence)
|
|
832
|
+
return _observe_logical(path, evidence, archive_dir=archive_dir)
|
|
787
833
|
|
|
788
834
|
|
|
789
835
|
#: The two Codex cursor tables the restat may read, and whether each carries
|
|
@@ -820,7 +866,7 @@ def _ingest_source_table(provider: str) -> str:
|
|
|
820
866
|
|
|
821
867
|
|
|
822
868
|
def plan_recent_active_restat(
|
|
823
|
-
conn, *, table: str = "codex_session_files", extra_paths=(),
|
|
869
|
+
conn, *, table: str = "codex_session_files", extra_paths=(), roots=(),
|
|
824
870
|
):
|
|
825
871
|
"""Restat the bounded recent set and return ``(targets, escalation)``.
|
|
826
872
|
|
|
@@ -829,7 +875,9 @@ def plan_recent_active_restat(
|
|
|
829
875
|
targeted work at all, in which case the caller must walk exhaustively.
|
|
830
876
|
|
|
831
877
|
An empty recent set performs ZERO stats, which is the whole shape of an
|
|
832
|
-
idle caught-up tick on the production store.
|
|
878
|
+
idle caught-up tick on the production store. ``roots`` are the plan's
|
|
879
|
+
frontier roots; their archive directories are the candidates an archived
|
|
880
|
+
rollout is observed in (#970).
|
|
833
881
|
"""
|
|
834
882
|
try:
|
|
835
883
|
candidates = select_recently_active_codex_paths(
|
|
@@ -847,7 +895,8 @@ def plan_recent_active_restat(
|
|
|
847
895
|
for path, (size, offset, complete, device, inode) in sorted(
|
|
848
896
|
candidates.items()
|
|
849
897
|
):
|
|
850
|
-
observed = _observe_file(path, table, evidence.get(path)
|
|
898
|
+
observed = _observe_file(path, table, evidence.get(path),
|
|
899
|
+
archive_dir=_archive_dir_for(path, roots))
|
|
851
900
|
verdict = classify_recent_active_path(
|
|
852
901
|
committed_size=size,
|
|
853
902
|
committed_offset=offset,
|
|
@@ -968,12 +1017,27 @@ def _configuration_generation_reason(
|
|
|
968
1017
|
return "configuration_generation_changed"
|
|
969
1018
|
|
|
970
1019
|
|
|
1020
|
+
def _codex_ticket_logical(path, roots) -> str:
|
|
1021
|
+
"""One Codex ticket's logical target (#968 5.9, #970 5.8).
|
|
1022
|
+
|
|
1023
|
+
Either container spelling names one logical rollout; an archive spelling
|
|
1024
|
+
of a native home names its dated logical path. Anything else keeps its
|
|
1025
|
+
own logical spelling, which containment then judges.
|
|
1026
|
+
"""
|
|
1027
|
+
dated = _rc.dated_logical_for_archive(path, _codex_locations_of(roots))
|
|
1028
|
+
if dated is not None:
|
|
1029
|
+
return os.fspath(dated)
|
|
1030
|
+
return os.fspath(_rc.logical_path(path))
|
|
1031
|
+
|
|
1032
|
+
|
|
971
1033
|
def _ticket_target_paths(provider: str, records, roots):
|
|
972
1034
|
"""The ticketed paths this plan may target, or a refusal reason.
|
|
973
1035
|
|
|
974
1036
|
#968 spec 5.9: a Codex ticket may name either spelling; it is
|
|
975
1037
|
normalized to ONE logical target, and containment is enforced on the
|
|
976
|
-
selected physical container.
|
|
1038
|
+
selected physical container. #970 spec 5.8: an archive spelling is
|
|
1039
|
+
normalized to its dated logical path BEFORE de-duplication, so the
|
|
1040
|
+
archive pathname never becomes a second target.
|
|
977
1041
|
"""
|
|
978
1042
|
provider_paths = tuple(
|
|
979
1043
|
path for kind, path, _e, _s, _c in records if kind == provider)
|
|
@@ -981,7 +1045,7 @@ def _ticket_target_paths(provider: str, records, roots):
|
|
|
981
1045
|
return None, "ambiguous_activity"
|
|
982
1046
|
if provider == "codex":
|
|
983
1047
|
provider_paths = tuple(
|
|
984
|
-
|
|
1048
|
+
_codex_ticket_logical(path, roots) for path in provider_paths)
|
|
985
1049
|
paths = frozenset(provider_paths)
|
|
986
1050
|
if any(not _target_in_roots(path, roots,
|
|
987
1051
|
allow_compressed=provider == "codex")
|
|
@@ -992,7 +1056,7 @@ def _ticket_target_paths(provider: str, records, roots):
|
|
|
992
1056
|
|
|
993
1057
|
def _restat_and_return(
|
|
994
1058
|
provider: str, conn, *, cursor_table: str, paths, marker_end: int,
|
|
995
|
-
ledger_epoch, ledger_sequence: int,
|
|
1059
|
+
ledger_epoch, ledger_sequence: int, roots=(),
|
|
996
1060
|
):
|
|
997
1061
|
"""#724's bounded restat, then the caught-up or targeted plan it implies.
|
|
998
1062
|
|
|
@@ -1012,7 +1076,7 @@ def _restat_and_return(
|
|
|
1012
1076
|
restat_paths = frozenset()
|
|
1013
1077
|
if provider == "codex":
|
|
1014
1078
|
restat_paths, escalation = plan_recent_active_restat(
|
|
1015
|
-
conn, table=cursor_table, extra_paths=paths)
|
|
1079
|
+
conn, table=cursor_table, extra_paths=paths, roots=roots)
|
|
1016
1080
|
if escalation is not None:
|
|
1017
1081
|
return FrontierPlan(provider, "full", reason=escalation)
|
|
1018
1082
|
combined = paths | restat_paths
|
|
@@ -1687,6 +1751,70 @@ def _write_generation_stamp(conn, provider: str, generation: str) -> None:
|
|
|
1687
1751
|
pass
|
|
1688
1752
|
|
|
1689
1753
|
|
|
1754
|
+
def codex_frontier_roots(configured_homes) -> "tuple[_rc.CodexLocations, ...]":
|
|
1755
|
+
"""The Codex frontier roots for the configured homes (#970 spec 5.1, 5.8).
|
|
1756
|
+
|
|
1757
|
+
One ``CodexLocations`` per configured entry, by the leaf's native-home
|
|
1758
|
+
recognition: a home with ``sessions/`` or ``archived_sessions/`` walks
|
|
1759
|
+
``<home>/sessions`` and owns ``<home>/archived_sessions``; any other
|
|
1760
|
+
existing directory is walked directly with no archive; anything else is
|
|
1761
|
+
not a root. De-duplicated by walk root in configured order, exactly as the
|
|
1762
|
+
walk's own roots are. Both the dashboard and the TUI build their frontier
|
|
1763
|
+
roots here, so the two planners and the walk agree on the scope.
|
|
1764
|
+
"""
|
|
1765
|
+
out: "list[_rc.CodexLocations]" = []
|
|
1766
|
+
seen: set = set()
|
|
1767
|
+
for configured in configured_homes:
|
|
1768
|
+
locations = _rc.home_locations(configured)
|
|
1769
|
+
if locations is None or locations.walk_root in seen:
|
|
1770
|
+
continue
|
|
1771
|
+
seen.add(locations.walk_root)
|
|
1772
|
+
out.append(locations)
|
|
1773
|
+
# #970 N4: an archive another configured root walks belongs to that root,
|
|
1774
|
+
# exactly as in the walk's own roots.
|
|
1775
|
+
return tuple(_rc.release_walked_archives(out))
|
|
1776
|
+
|
|
1777
|
+
|
|
1778
|
+
def _codex_locations_of(roots) -> "tuple[_rc.CodexLocations, ...]":
|
|
1779
|
+
"""The ``CodexLocations`` among frontier roots (a bare path has none)."""
|
|
1780
|
+
return tuple(root for root in roots if isinstance(root, _rc.CodexLocations))
|
|
1781
|
+
|
|
1782
|
+
|
|
1783
|
+
def _flatten_frontier_roots(roots) -> tuple:
|
|
1784
|
+
"""The directory roots the certificate and containment are stated over.
|
|
1785
|
+
|
|
1786
|
+
A ``CodexLocations`` contributes its walk root and, when it exists, its
|
|
1787
|
+
archive directory (#970 spec 5.8: an archive directory is certified
|
|
1788
|
+
directory evidence and a permitted container location). A bare path — the
|
|
1789
|
+
Claude roots, and a caller that predates #970 — contributes itself. A home
|
|
1790
|
+
without an archive therefore flattens to exactly the pre-#970 root, so its
|
|
1791
|
+
certificate identity is unchanged; an archive appearing later changes the
|
|
1792
|
+
root set, which is itself a reason to walk.
|
|
1793
|
+
"""
|
|
1794
|
+
flat: list = []
|
|
1795
|
+
for root in roots:
|
|
1796
|
+
if isinstance(root, _rc.CodexLocations):
|
|
1797
|
+
flat.append(root.walk_root)
|
|
1798
|
+
archive = root.archive_root
|
|
1799
|
+
if archive is not None:
|
|
1800
|
+
try:
|
|
1801
|
+
if pathlib.Path(archive).is_dir():
|
|
1802
|
+
flat.append(archive)
|
|
1803
|
+
except OSError:
|
|
1804
|
+
pass
|
|
1805
|
+
else:
|
|
1806
|
+
flat.append(root)
|
|
1807
|
+
return tuple(flat)
|
|
1808
|
+
|
|
1809
|
+
|
|
1810
|
+
def _archive_dir_for(path, roots):
|
|
1811
|
+
"""The archive directory holding ``path``'s archive candidates, or None."""
|
|
1812
|
+
locations = _codex_locations_of(roots)
|
|
1813
|
+
if not locations:
|
|
1814
|
+
return None
|
|
1815
|
+
return _rc.archive_dir_for(path, locations)
|
|
1816
|
+
|
|
1817
|
+
|
|
1690
1818
|
def normalized_root_membership(roots) -> tuple[str, ...]:
|
|
1691
1819
|
"""The configured root SET, as certificate identity.
|
|
1692
1820
|
|
|
@@ -1694,10 +1822,12 @@ def normalized_root_membership(roots) -> tuple[str, ...]:
|
|
|
1694
1822
|
while adding or removing an entry is. Without this a configured root could
|
|
1695
1823
|
be added or removed with no filesystem change anywhere and an old
|
|
1696
1824
|
certificate would still look caught up, because plan-time roots were used
|
|
1697
|
-
only to validate ticket targets.
|
|
1825
|
+
only to validate ticket targets. Codex roots are flattened first, so an
|
|
1826
|
+
existing archive directory is a member (#970).
|
|
1698
1827
|
"""
|
|
1699
1828
|
return tuple(sorted({
|
|
1700
|
-
str(pathlib.Path(root).absolute())
|
|
1829
|
+
str(pathlib.Path(root).absolute())
|
|
1830
|
+
for root in _flatten_frontier_roots(roots)
|
|
1701
1831
|
}))
|
|
1702
1832
|
|
|
1703
1833
|
|
|
@@ -1831,7 +1961,8 @@ def _source_directory_closure(read_sources, roots) -> tuple[pathlib.Path, ...]:
|
|
|
1831
1961
|
the seed path treats as a non-certifiable walk rather than behind a store
|
|
1832
1962
|
error.
|
|
1833
1963
|
"""
|
|
1834
|
-
normalized_roots = tuple(
|
|
1964
|
+
normalized_roots = tuple(
|
|
1965
|
+
pathlib.Path(root).absolute() for root in _flatten_frontier_roots(roots))
|
|
1835
1966
|
root_strings = tuple(str(root) for root in normalized_roots)
|
|
1836
1967
|
# A canonical parent strictly below root `r` starts with `r + "/"`; the
|
|
1837
1968
|
# filesystem root is already its own prefix. A root anchored at `//` can
|
|
@@ -1984,7 +2115,8 @@ def _validate_marker_prefix(marker: pathlib.Path, cutoff: "MarkerCutoff") -> Non
|
|
|
1984
2115
|
_decode_marker_records(raw)
|
|
1985
2116
|
|
|
1986
2117
|
|
|
1987
|
-
def _target_has_cursor_gap(conn, provider: str, source_path: str
|
|
2118
|
+
def _target_has_cursor_gap(conn, provider: str, source_path: str, *,
|
|
2119
|
+
roots=()) -> bool:
|
|
1988
2120
|
table = "session_files" if provider == "claude" else "codex_session_files"
|
|
1989
2121
|
row = conn.execute(
|
|
1990
2122
|
f"SELECT size_bytes, last_byte_offset FROM {table} WHERE path=?",
|
|
@@ -1996,7 +2128,8 @@ def _target_has_cursor_gap(conn, provider: str, source_path: str) -> bool:
|
|
|
1996
2128
|
if provider == "codex":
|
|
1997
2129
|
observed = _observe_logical(
|
|
1998
2130
|
source_path,
|
|
1999
|
-
_container_evidence_for(conn, table, [source_path]).get(source_path)
|
|
2131
|
+
_container_evidence_for(conn, table, [source_path]).get(source_path),
|
|
2132
|
+
archive_dir=_archive_dir_for(source_path, roots))
|
|
2000
2133
|
if observed is None:
|
|
2001
2134
|
return True
|
|
2002
2135
|
if observed.container_pending:
|
|
@@ -2011,13 +2144,26 @@ def _target_has_cursor_gap(conn, provider: str, source_path: str) -> bool:
|
|
|
2011
2144
|
|
|
2012
2145
|
def _target_in_roots(source_path: str, roots, *,
|
|
2013
2146
|
allow_compressed: bool = False) -> bool:
|
|
2147
|
+
"""Whether a ticketed path's selected container lies inside the roots.
|
|
2148
|
+
|
|
2149
|
+
#970 spec 5.8 (A7): a Codex logical path is selected through its archive
|
|
2150
|
+
candidates too, and the container is bounded by the union of the walk
|
|
2151
|
+
roots and the existing archive directories. A logical path spelled
|
|
2152
|
+
INSIDE an archive directory has no dated identity (an unmappable or
|
|
2153
|
+
nested archive name) and is refused, because discovery never ingests it.
|
|
2154
|
+
"""
|
|
2014
2155
|
path = pathlib.Path(source_path)
|
|
2015
2156
|
if not path.is_absolute():
|
|
2016
2157
|
return False
|
|
2017
2158
|
if allow_compressed:
|
|
2018
2159
|
if not _rc.is_container_name(path.name):
|
|
2019
2160
|
return False
|
|
2020
|
-
|
|
2161
|
+
for location in _codex_locations_of(roots):
|
|
2162
|
+
archive = location.archive_root
|
|
2163
|
+
if archive is not None and path.is_relative_to(archive):
|
|
2164
|
+
return False
|
|
2165
|
+
selected = _rc.select_container(
|
|
2166
|
+
path, archive_dir=_archive_dir_for(path, roots))
|
|
2021
2167
|
if selected is None:
|
|
2022
2168
|
return False
|
|
2023
2169
|
physical = selected.path
|
|
@@ -2031,7 +2177,7 @@ def _target_in_roots(source_path: str, roots, *,
|
|
|
2031
2177
|
return False
|
|
2032
2178
|
if not resolved.is_file():
|
|
2033
2179
|
return False
|
|
2034
|
-
for root in roots:
|
|
2180
|
+
for root in _flatten_frontier_roots(roots):
|
|
2035
2181
|
try:
|
|
2036
2182
|
resolved.relative_to(pathlib.Path(root).resolve(strict=True))
|
|
2037
2183
|
except (OSError, ValueError):
|
|
@@ -2086,6 +2232,57 @@ def provider_sync_certifiable(mode: str, stats: object) -> bool:
|
|
|
2086
2232
|
return False
|
|
2087
2233
|
|
|
2088
2234
|
|
|
2235
|
+
|
|
2236
|
+
#: #939 D5: what an accounting tick could not finish, per provider. A refusal
|
|
2237
|
+
#: or a per-file leftover keeps ``drained`` from releasing writeIo's admission
|
|
2238
|
+
#: hold, and a change in it between two counted conversation passes keeps the
|
|
2239
|
+
#: second from settling.
|
|
2240
|
+
ACCOUNTING_RESIDUAL_FIELDS = (
|
|
2241
|
+
"deferred_reason", "rebuild_refused", "prune_refused",
|
|
2242
|
+
"finalization_deferred", "maintenance_failed", "files_failed",
|
|
2243
|
+
"files_deferred_torn", "container_pending",
|
|
2244
|
+
)
|
|
2245
|
+
|
|
2246
|
+
|
|
2247
|
+
def accounting_census_complete(
|
|
2248
|
+
mode: str, stats: object, *, expected_paths: int = 0,
|
|
2249
|
+
) -> bool:
|
|
2250
|
+
"""Whether one accounting result visited its whole census (#939 D5).
|
|
2251
|
+
|
|
2252
|
+
A frontier-certified ``caught_up`` plan covers its census; a missing result
|
|
2253
|
+
in any other mode does not. Otherwise only what cut the walk short fails
|
|
2254
|
+
it (lock contention, an exhausted budget, a drain yield, Codex backlog
|
|
2255
|
+
files), and a targeted census must be exactly its plan. It deliberately
|
|
2256
|
+
reads neither ``full_walk_complete`` nor ``targeted_clean``: one
|
|
2257
|
+
persistently unreadable file clears both, and a refusal or a leftover is
|
|
2258
|
+
the tick's residual (``accounting_residual``), not an unvisited census.
|
|
2259
|
+
"""
|
|
2260
|
+
if mode == "caught_up":
|
|
2261
|
+
return True
|
|
2262
|
+
if stats is None:
|
|
2263
|
+
return False
|
|
2264
|
+
if any((
|
|
2265
|
+
getattr(stats, "lock_contended", False),
|
|
2266
|
+
getattr(stats, "budget_exhausted", False),
|
|
2267
|
+
getattr(stats, "drain_yielded", False),
|
|
2268
|
+
getattr(stats, "backlog_files", 0),
|
|
2269
|
+
)):
|
|
2270
|
+
return False
|
|
2271
|
+
if mode == "targeted":
|
|
2272
|
+
return int(getattr(stats, "files_total", 0) or 0) == expected_paths
|
|
2273
|
+
return mode == "full"
|
|
2274
|
+
|
|
2275
|
+
|
|
2276
|
+
def accounting_residual(stats: object) -> tuple:
|
|
2277
|
+
"""The ``(name, value)`` pairs of what one accounting result left
|
|
2278
|
+
unfinished (#939 D5); ``()`` when nothing."""
|
|
2279
|
+
if stats is None:
|
|
2280
|
+
return ()
|
|
2281
|
+
return tuple(
|
|
2282
|
+
(name, value) for name in ACCOUNTING_RESIDUAL_FIELDS
|
|
2283
|
+
if (value := getattr(stats, name, None)))
|
|
2284
|
+
|
|
2285
|
+
|
|
2089
2286
|
class DashboardIngestFrontier:
|
|
2090
2287
|
"""One process-local certificate per provider, rebuilt after full sync."""
|
|
2091
2288
|
|
|
@@ -2295,12 +2492,13 @@ class DashboardIngestFrontier:
|
|
|
2295
2492
|
if reason is not None:
|
|
2296
2493
|
return FrontierPlan(
|
|
2297
2494
|
provider, "full", marker_end=marker_end, reason=reason)
|
|
2298
|
-
if any(_target_has_cursor_gap(conn, provider, path
|
|
2495
|
+
if any(_target_has_cursor_gap(conn, provider, path, roots=roots)
|
|
2496
|
+
for path in paths):
|
|
2299
2497
|
return FrontierPlan(provider, "full", reason="cursor_gap")
|
|
2300
2498
|
return _restat_and_return(
|
|
2301
2499
|
provider, conn, cursor_table=_ingest_source_table(provider),
|
|
2302
2500
|
paths=paths, marker_end=marker_end, ledger_epoch=ledger_epoch,
|
|
2303
|
-
ledger_sequence=ledger_sequence,
|
|
2501
|
+
ledger_sequence=ledger_sequence, roots=roots,
|
|
2304
2502
|
)
|
|
2305
2503
|
|
|
2306
2504
|
def commit_provider(
|
|
@@ -2392,7 +2590,8 @@ def _conversation_directory_identity(conn, provider: str, roots):
|
|
|
2392
2590
|
}
|
|
2393
2591
|
|
|
2394
2592
|
|
|
2395
|
-
def _conversation_target_risk(conn, provider: str, source_path: str
|
|
2593
|
+
def _conversation_target_risk(conn, provider: str, source_path: str, *,
|
|
2594
|
+
roots=()) -> str | None:
|
|
2396
2595
|
"""Classify a ticketed path that cannot use ordinary append ingest."""
|
|
2397
2596
|
table = _conversation_source_table(provider)
|
|
2398
2597
|
row = conn.execute(
|
|
@@ -2408,7 +2607,8 @@ def _conversation_target_risk(conn, provider: str, source_path: str) -> str | No
|
|
|
2408
2607
|
# planner, so the two planners converge.
|
|
2409
2608
|
observed = _observe_logical(
|
|
2410
2609
|
source_path,
|
|
2411
|
-
_container_evidence_for(conn, table, [source_path]).get(source_path)
|
|
2610
|
+
_container_evidence_for(conn, table, [source_path]).get(source_path),
|
|
2611
|
+
archive_dir=_archive_dir_for(source_path, roots))
|
|
2412
2612
|
if observed is None:
|
|
2413
2613
|
return "cursor_gap"
|
|
2414
2614
|
if observed.container_pending:
|
|
@@ -2470,6 +2670,39 @@ def conversation_sync_certifiable(
|
|
|
2470
2670
|
return False
|
|
2471
2671
|
|
|
2472
2672
|
|
|
2673
|
+
|
|
2674
|
+
def conversation_census_complete(
|
|
2675
|
+
mode: str, stats: object, *, expected_paths: int = 0,
|
|
2676
|
+
) -> bool:
|
|
2677
|
+
"""Whether one transcript pass visited every file of its census (#939 D5).
|
|
2678
|
+
|
|
2679
|
+
`conversation_sync_certifiable` without the predicates and the
|
|
2680
|
+
completed-count equality that describe per-file leftovers (failed, torn,
|
|
2681
|
+
pending, held and deferred files, refused prunes and prune debt,
|
|
2682
|
+
``barrier_retry``): those are the pass's residual, which writeIo's
|
|
2683
|
+
admission hold compares between passes. Only what cut the walk short and
|
|
2684
|
+
finishes on its own fails it (lock contention, an exhausted budget), and a
|
|
2685
|
+
targeted census must be exactly its plan. A deferred reason (a refused
|
|
2686
|
+
pricing write, a pending rebuild, an unavailable barrier, too little free
|
|
2687
|
+
space) and a maintenance failure can repeat for the dashboard's whole
|
|
2688
|
+
life, so they join the residual instead
|
|
2689
|
+
(`_lib_write_io.conversation_residual`): a changed one restarts the
|
|
2690
|
+
settled chain and an identical one settles.
|
|
2691
|
+
"""
|
|
2692
|
+
if mode == "caught_up":
|
|
2693
|
+
return True
|
|
2694
|
+
if stats is None:
|
|
2695
|
+
return False
|
|
2696
|
+
if any((
|
|
2697
|
+
getattr(stats, "lock_contended", False),
|
|
2698
|
+
getattr(stats, "budget_exhausted", False),
|
|
2699
|
+
)):
|
|
2700
|
+
return False
|
|
2701
|
+
if mode == "targeted":
|
|
2702
|
+
return int(getattr(stats, "files_total", 0) or 0) == expected_paths
|
|
2703
|
+
return mode == "full"
|
|
2704
|
+
|
|
2705
|
+
|
|
2473
2706
|
def _codex_publication_backlog(conn) -> "tuple[bool, frozenset[str]]":
|
|
2474
2707
|
"""Certificates newer than their publication (#968 spec 5.11).
|
|
2475
2708
|
|
|
@@ -2692,7 +2925,7 @@ class ConversationSyncFrontier:
|
|
|
2692
2925
|
return FrontierPlan(
|
|
2693
2926
|
provider, "full", marker_end=marker_end, reason=reason)
|
|
2694
2927
|
target_risks = {
|
|
2695
|
-
_conversation_target_risk(conn, provider, path)
|
|
2928
|
+
_conversation_target_risk(conn, provider, path, roots=roots)
|
|
2696
2929
|
for path in paths
|
|
2697
2930
|
}
|
|
2698
2931
|
if "source_replaced" in target_risks:
|
|
@@ -2702,7 +2935,7 @@ class ConversationSyncFrontier:
|
|
|
2702
2935
|
plan = _restat_and_return(
|
|
2703
2936
|
provider, conn, cursor_table=_conversation_source_table(provider),
|
|
2704
2937
|
paths=paths, marker_end=marker_end, ledger_epoch=ledger_epoch,
|
|
2705
|
-
ledger_sequence=ledger_sequence,
|
|
2938
|
+
ledger_sequence=ledger_sequence, roots=roots,
|
|
2706
2939
|
)
|
|
2707
2940
|
if provider == "codex" and plan.mode in ("caught_up", "targeted"):
|
|
2708
2941
|
due, backlog = _codex_publication_backlog(conn)
|