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
package/bin/_lib_write_io.py
CHANGED
|
@@ -21,6 +21,7 @@ from __future__ import annotations
|
|
|
21
21
|
import array
|
|
22
22
|
import dataclasses
|
|
23
23
|
import datetime as dt
|
|
24
|
+
import hashlib
|
|
24
25
|
import ipaddress
|
|
25
26
|
import json
|
|
26
27
|
import os
|
|
@@ -238,6 +239,17 @@ class _State:
|
|
|
238
239
|
# #901 Q9: the latest reclaim attempt the planner refused, in memory
|
|
239
240
|
# only (a refusal writes nothing durable); None after a chunk runs.
|
|
240
241
|
self.reclaim_refusal = None
|
|
242
|
+
# #939 D5: the startup admission hold. ``drain`` is the evaluator,
|
|
243
|
+
# kept only while the hold is placed (its obligations scale with the
|
|
244
|
+
# backlog, so it is dropped at release and never part of the fixed
|
|
245
|
+
# state budget); the rest is the fixed-size published state.
|
|
246
|
+
self.hold_placed = False
|
|
247
|
+
self.released_by = None
|
|
248
|
+
self.first_non_cold_ns = None
|
|
249
|
+
self.accounting_caught_up = False
|
|
250
|
+
self.residual_counts = None
|
|
251
|
+
self.last_pass = None
|
|
252
|
+
self.drain = None
|
|
241
253
|
|
|
242
254
|
|
|
243
255
|
_LOCK = threading.Lock()
|
|
@@ -412,14 +424,21 @@ def _steady_locked(now_ns):
|
|
|
412
424
|
|
|
413
425
|
|
|
414
426
|
def note_publication(*, cold: bool) -> None:
|
|
415
|
-
"""One completed main publication: count it, sample, admit, evaluate.
|
|
427
|
+
"""One completed main publication: count it, sample, admit, evaluate.
|
|
428
|
+
|
|
429
|
+
#939 D5: while the startup admission hold is placed, a non-cold
|
|
430
|
+
publication is recorded but does not admit warm; `release_admission`
|
|
431
|
+
admits at release when one was already seen."""
|
|
416
432
|
state = _STATE
|
|
417
433
|
with _LOCK:
|
|
418
434
|
state.publications += 1
|
|
419
435
|
now, reading = _sample_locked(state)
|
|
420
436
|
if state.warm_admitted_ns is None and not cold:
|
|
421
|
-
state.
|
|
422
|
-
|
|
437
|
+
if state.first_non_cold_ns is None:
|
|
438
|
+
state.first_non_cold_ns = now
|
|
439
|
+
if not state.hold_placed or state.released_by is not None:
|
|
440
|
+
state.warm_admitted_ns = now
|
|
441
|
+
state.cold_startup_bytes = reading.value
|
|
423
442
|
if (state.last_hysteresis_ns is None
|
|
424
443
|
or now - state.last_hysteresis_ns
|
|
425
444
|
>= HYSTERESIS_EVALUATION_SPACING_NS):
|
|
@@ -480,11 +499,12 @@ def local_instance() -> "dict | None":
|
|
|
480
499
|
|
|
481
500
|
|
|
482
501
|
def state_for_budget() -> tuple:
|
|
483
|
-
"""Every object this module retains, for the
|
|
502
|
+
"""Every object this module retains, for the tick-stats state budget."""
|
|
484
503
|
state = _STATE
|
|
485
504
|
return (state.t, state.b, state.p, state.ledger, state.ex[0],
|
|
486
505
|
state.ex[1:], state.open, state.hysteresis, state.local_instance,
|
|
487
|
-
state.cold_startup_bytes, state.last_status, state.reclaim_refusal
|
|
506
|
+
state.cold_startup_bytes, state.last_status, state.reclaim_refusal,
|
|
507
|
+
state.released_by, state.residual_counts, state.last_pass)
|
|
488
508
|
|
|
489
509
|
|
|
490
510
|
#: The reason strings a reclaim refusal may publish; anything else is
|
|
@@ -644,6 +664,384 @@ def loopback_target(bind_host) -> "str | None":
|
|
|
644
664
|
return host if address.is_loopback else None
|
|
645
665
|
|
|
646
666
|
|
|
667
|
+
# ── #939 D5: the startup admission hold and its evaluator ──────────────────
|
|
668
|
+
#
|
|
669
|
+
# `cmd_dashboard` places the hold before the main sync thread starts (not
|
|
670
|
+
# under --no-sync). While it is placed, `note_publication` does not admit warm
|
|
671
|
+
# samples, so the trailing statistic stays `warming_up` and doctor and
|
|
672
|
+
# dashboard-perf keep printing "no samples yet (needs 5 minutes)". There is no
|
|
673
|
+
# deadline: a time bound would judge a long catch-up as steady state. Only
|
|
674
|
+
# `StartupDrain`'s evidence ("drained", "settled") or the end of the
|
|
675
|
+
# conversation loop ("loopExit") releases it.
|
|
676
|
+
|
|
677
|
+
RELEASE_REASONS = ("drained", "settled", "loopExit")
|
|
678
|
+
RESIDUAL_COUNT_KEYS = ("failed", "unpublishedDeferred", "obligations",
|
|
679
|
+
"containerPending", "keysDeferred")
|
|
680
|
+
#: The Codex conversation counts that are part of a pass's residual.
|
|
681
|
+
_RESIDUAL_COUNT_FIELDS = ("container_pending", "container_reads_held",
|
|
682
|
+
"barrier_keys_deferred", "files_deferred_torn",
|
|
683
|
+
"prune_refused")
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
def conversation_residual(stats) -> tuple:
|
|
687
|
+
"""One provider pass's residual as canonical ``(name, value)`` pairs.
|
|
688
|
+
|
|
689
|
+
The failed paths, the deferrals no unit of the same pass published (with
|
|
690
|
+
the keys each named), the causes of ``barrier_retry``, the existing
|
|
691
|
+
leftover counts, and the pass's deferred reason and maintenance failure
|
|
692
|
+
(a refusal that repeats identically settles; one that changes restarts the
|
|
693
|
+
chain). ``()`` when the pass left nothing unfinished."""
|
|
694
|
+
if stats is None:
|
|
695
|
+
return ()
|
|
696
|
+
reason = getattr(stats, "deferred_reason", None)
|
|
697
|
+
failed = tuple(sorted(str(path) for path in
|
|
698
|
+
(getattr(stats, "failed_paths", None) or ())))
|
|
699
|
+
deferred = getattr(stats, "barrier_deferred_keys", None) or {}
|
|
700
|
+
published = {str(path) for path in
|
|
701
|
+
(getattr(stats, "barrier_published_paths", None) or ())}
|
|
702
|
+
unpublished = tuple(sorted(
|
|
703
|
+
(str(path), tuple(sorted(keys))) for path, keys in deferred.items()
|
|
704
|
+
if str(path) not in published))
|
|
705
|
+
causes = tuple(sorted(getattr(stats, "barrier_retry_causes", None) or ()))
|
|
706
|
+
items = [("failed_paths", failed), ("unpublished_deferred", unpublished),
|
|
707
|
+
("barrier_retry_causes", causes)]
|
|
708
|
+
items += [(name, getattr(stats, name, 0) or 0)
|
|
709
|
+
for name in _RESIDUAL_COUNT_FIELDS]
|
|
710
|
+
items += [("deferred_reason", str(reason) if reason else None),
|
|
711
|
+
("maintenance_failed",
|
|
712
|
+
bool(getattr(stats, "maintenance_failed", False)))]
|
|
713
|
+
return tuple((name, value) for name, value in items if value)
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
def unpublished_deferrals(stats) -> dict:
|
|
717
|
+
"""``{path: (file_identity, covered, keys)}`` for every Codex deferral of
|
|
718
|
+
the pass that no unit of the same pass published."""
|
|
719
|
+
if stats is None:
|
|
720
|
+
return {}
|
|
721
|
+
deferred = getattr(stats, "barrier_deferred_keys", None) or {}
|
|
722
|
+
targets = getattr(stats, "barrier_deferred_targets", None) or {}
|
|
723
|
+
published = {str(path) for path in
|
|
724
|
+
(getattr(stats, "barrier_published_paths", None) or ())}
|
|
725
|
+
out = {}
|
|
726
|
+
for path, keys in deferred.items():
|
|
727
|
+
if str(path) in published:
|
|
728
|
+
continue
|
|
729
|
+
identity, covered = targets.get(path, (None, False))
|
|
730
|
+
out[str(path)] = (identity, bool(covered), frozenset(keys))
|
|
731
|
+
return out
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
def residual_digest(provider_results: dict, deferral_facts: dict) -> tuple:
|
|
735
|
+
"""Canonical, hashable: equal iff every residual component is equal.
|
|
736
|
+
|
|
737
|
+
``provider_results`` maps a component name (a provider, or "accounting")
|
|
738
|
+
to its ``(name, value)`` pairs; ``deferral_facts`` maps each unpublished
|
|
739
|
+
deferral's path to its clipped facts. Never carries the global seal
|
|
740
|
+
token. ``()`` means empty."""
|
|
741
|
+
parts = tuple((name, tuple(value))
|
|
742
|
+
for name, value in sorted(provider_results.items()) if value)
|
|
743
|
+
facts = tuple(sorted(deferral_facts.items()))
|
|
744
|
+
if not parts and not facts:
|
|
745
|
+
return ()
|
|
746
|
+
return (parts, facts)
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
def digest_hash(digest) -> "str | None":
|
|
750
|
+
"""A short stable hash of a residual digest; ``None`` for the empty one."""
|
|
751
|
+
if not digest:
|
|
752
|
+
return None
|
|
753
|
+
return hashlib.sha256(repr(digest).encode("utf-8")).hexdigest()[:16]
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
@dataclasses.dataclass(frozen=True)
|
|
757
|
+
class _Anchor:
|
|
758
|
+
"""A deferral's startup work, bound when its anchor resolved."""
|
|
759
|
+
generation: object
|
|
760
|
+
end: int
|
|
761
|
+
required: tuple # sorted ((key, required_revision), ...)
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
def _resolve_anchor(keys, raw) -> "_Anchor | None":
|
|
765
|
+
"""Resolved only with a complete line end at the target's generation,
|
|
766
|
+
membership proven there and a population row for every key."""
|
|
767
|
+
target = (raw or {}).get("target")
|
|
768
|
+
if target is None:
|
|
769
|
+
return None
|
|
770
|
+
generation = target.get("generation")
|
|
771
|
+
end = target.get("complete_line_end")
|
|
772
|
+
if end is None or target.get("accepted_generation") != generation:
|
|
773
|
+
return None
|
|
774
|
+
facts = (raw or {}).get("keys") or {}
|
|
775
|
+
required = []
|
|
776
|
+
for key in sorted(set(keys) | set((raw or {}).get("membership") or ())):
|
|
777
|
+
revision = (facts.get(key) or {}).get("required")
|
|
778
|
+
if revision is None:
|
|
779
|
+
return None
|
|
780
|
+
required.append((key, int(revision)))
|
|
781
|
+
return _Anchor(generation, int(end), tuple(required))
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
def _cap(value, limit):
|
|
785
|
+
return None if value is None else min(int(value), int(limit))
|
|
786
|
+
|
|
787
|
+
|
|
788
|
+
def _clipped_facts(keys, anchor, raw) -> tuple:
|
|
789
|
+
"""A deferral's facts, scoped to its startup work: progress on its own
|
|
790
|
+
keys and the resolution of its anchor change them, input beyond the
|
|
791
|
+
anchored end and other conversations' seals do not."""
|
|
792
|
+
target = (raw or {}).get("target")
|
|
793
|
+
if anchor is not None:
|
|
794
|
+
same = (target is not None
|
|
795
|
+
and target.get("generation") == anchor.generation)
|
|
796
|
+
accepted = None
|
|
797
|
+
if (same and target.get("accepted_generation") == anchor.generation
|
|
798
|
+
and target.get("accepted_end") is not None):
|
|
799
|
+
accepted = min(int(target["accepted_end"]), anchor.end)
|
|
800
|
+
facts = (raw or {}).get("keys") or {}
|
|
801
|
+
per_key = tuple(
|
|
802
|
+
(key, _cap((facts.get(key) or {}).get("certificate"), required),
|
|
803
|
+
_cap((facts.get(key) or {}).get("published"), required),
|
|
804
|
+
_cap((facts.get(key) or {}).get("repair"), required))
|
|
805
|
+
for key, required in anchor.required)
|
|
806
|
+
return ("resolved", same,
|
|
807
|
+
None if target is None else target.get("state"),
|
|
808
|
+
None if target is None else target.get("reason"),
|
|
809
|
+
accepted, per_key)
|
|
810
|
+
if target is None:
|
|
811
|
+
return ("unresolved", None, None, None, False, None,
|
|
812
|
+
tuple(sorted(keys)))
|
|
813
|
+
return ("unresolved", target.get("generation"), target.get("state"),
|
|
814
|
+
target.get("reason"), target.get("complete_line_end") is not None,
|
|
815
|
+
target.get("accepted_generation"),
|
|
816
|
+
tuple(sorted(set(keys) | set((raw or {}).get("membership") or ()))))
|
|
817
|
+
|
|
818
|
+
|
|
819
|
+
def _discharged(anchor: "_Anchor | None", raw) -> bool:
|
|
820
|
+
"""Every anchored key's publication reached its anchored requirement.
|
|
821
|
+
Accounting acceptance never counts: a contributor can be fully accepted
|
|
822
|
+
while its publication group still waits."""
|
|
823
|
+
if anchor is None or not anchor.required or raw is None:
|
|
824
|
+
return False
|
|
825
|
+
facts = raw.get("keys") or {}
|
|
826
|
+
for key, required in anchor.required:
|
|
827
|
+
published = (facts.get(key) or {}).get("published")
|
|
828
|
+
if published is None or int(published) < required:
|
|
829
|
+
return False
|
|
830
|
+
return True
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
class StartupDrain:
|
|
834
|
+
"""The pure evaluator behind writeIo's admission hold (#939 D5).
|
|
835
|
+
|
|
836
|
+
``observe_accounting`` records each accounting tick; ``observe`` receives
|
|
837
|
+
every completed conversation pass and returns ``"drained"``,
|
|
838
|
+
``"settled"`` or ``None``. ``pass_info`` carries ``counted`` (the pass
|
|
839
|
+
started after the first accounting-census-complete tick ended; only a
|
|
840
|
+
counted pass resolves anchors or discharges obligations),
|
|
841
|
+
``census_complete`` (every provider's census complete, or a pass that
|
|
842
|
+
ended in an error or an unavailable store, and no detached Codex worker
|
|
843
|
+
in progress at the pass's end), ``certifiable`` (both providers; never an
|
|
844
|
+
error pass), per-provider ``mode`` and ``residual`` (``None`` or a
|
|
845
|
+
``caught_up`` mode carries the previous one forward; the ``"pass"`` entry
|
|
846
|
+
is the pass's own status, empty for ``ok`` and ``((status, exception
|
|
847
|
+
class),)`` otherwise, so identical error passes settle and a recovery
|
|
848
|
+
restarts the chain), ``deferrals``
|
|
849
|
+
(``{path: (identity, covered, keys)}`` of the unpublished Codex deferrals,
|
|
850
|
+
``None`` when Codex did not run) and ``facts`` (the stored facts of every
|
|
851
|
+
tracked deferral, read at the pass's end).
|
|
852
|
+
"""
|
|
853
|
+
|
|
854
|
+
def __init__(self):
|
|
855
|
+
self.first_complete_end_ns = None
|
|
856
|
+
self.accounting: dict = {}
|
|
857
|
+
self._accounting_seen = False
|
|
858
|
+
self._chain_broken = False
|
|
859
|
+
self.components: dict = {}
|
|
860
|
+
self.deferrals: dict = {}
|
|
861
|
+
self.tracked: dict = {}
|
|
862
|
+
self.anchors: dict = {}
|
|
863
|
+
self.obligations: set = set()
|
|
864
|
+
self.previous_digest = None
|
|
865
|
+
self.digest: tuple = ()
|
|
866
|
+
self.counts = None
|
|
867
|
+
|
|
868
|
+
def observe_accounting(self, *, ended_ns, census_complete, residuals):
|
|
869
|
+
merged = dict(self.accounting)
|
|
870
|
+
for provider, residual in (residuals or {}).items():
|
|
871
|
+
if residual is not None: # caught_up carries forward
|
|
872
|
+
merged[provider] = tuple(residual)
|
|
873
|
+
if self._accounting_seen and (
|
|
874
|
+
not census_complete or merged != self.accounting):
|
|
875
|
+
self._chain_broken = True
|
|
876
|
+
self._accounting_seen = True
|
|
877
|
+
self.accounting = merged
|
|
878
|
+
if census_complete and self.first_complete_end_ns is None:
|
|
879
|
+
self.first_complete_end_ns = int(ended_ns)
|
|
880
|
+
|
|
881
|
+
def watch(self) -> dict:
|
|
882
|
+
out = {}
|
|
883
|
+
for path, (identity, keys) in self.tracked.items():
|
|
884
|
+
anchor = self.anchors.get(path)
|
|
885
|
+
anchored = (frozenset(key for key, _ in anchor.required)
|
|
886
|
+
if anchor is not None else frozenset())
|
|
887
|
+
out[path] = (identity, frozenset(keys) | anchored)
|
|
888
|
+
return out
|
|
889
|
+
|
|
890
|
+
def _update_deferrals(self, deferrals) -> None:
|
|
891
|
+
self.deferrals = {str(path): (identity, bool(covered), frozenset(keys))
|
|
892
|
+
for path, (identity, covered, keys)
|
|
893
|
+
in deferrals.items()}
|
|
894
|
+
for path, (identity, covered, keys) in self.deferrals.items():
|
|
895
|
+
prior = self.tracked.get(path)
|
|
896
|
+
self.tracked[path] = (
|
|
897
|
+
identity, keys | (prior[1] if prior else frozenset()))
|
|
898
|
+
if covered and keys:
|
|
899
|
+
self.obligations.add(path)
|
|
900
|
+
for path in list(self.tracked):
|
|
901
|
+
if path not in self.deferrals and path not in self.obligations:
|
|
902
|
+
self.tracked.pop(path)
|
|
903
|
+
self.anchors.pop(path, None)
|
|
904
|
+
|
|
905
|
+
def _residual_counts(self, unpublished) -> tuple:
|
|
906
|
+
failed = sum(len(dict(component).get("failed_paths", ()))
|
|
907
|
+
for component in self.components.values())
|
|
908
|
+
codex = dict(self.components.get("codex", ()))
|
|
909
|
+
return (failed, len(unpublished), len(self.obligations),
|
|
910
|
+
int(codex.get("container_pending", 0) or 0),
|
|
911
|
+
int(codex.get("barrier_keys_deferred", 0) or 0))
|
|
912
|
+
|
|
913
|
+
def observe(self, pass_info) -> "str | None":
|
|
914
|
+
info = pass_info or {}
|
|
915
|
+
for name, entry in (info.get("providers") or {}).items():
|
|
916
|
+
residual = entry.get("residual")
|
|
917
|
+
if entry.get("mode") == "caught_up" or residual is None:
|
|
918
|
+
continue
|
|
919
|
+
self.components[name] = tuple(residual)
|
|
920
|
+
if info.get("deferrals") is not None:
|
|
921
|
+
self._update_deferrals(info["deferrals"])
|
|
922
|
+
facts = info.get("facts") or {}
|
|
923
|
+
if info.get("counted"):
|
|
924
|
+
# Only a counted pass anchors or discharges: one that started
|
|
925
|
+
# before accounting caught up could freeze an anchor at
|
|
926
|
+
# accounting's partial progress.
|
|
927
|
+
for path, (_identity, keys) in self.tracked.items():
|
|
928
|
+
if path not in self.anchors and path in facts:
|
|
929
|
+
anchor = _resolve_anchor(keys, facts[path])
|
|
930
|
+
if anchor is not None:
|
|
931
|
+
self.anchors[path] = anchor
|
|
932
|
+
for path in list(self.obligations):
|
|
933
|
+
if _discharged(self.anchors.get(path), facts.get(path)):
|
|
934
|
+
self.obligations.discard(path)
|
|
935
|
+
unpublished = set(self.deferrals) | self.obligations
|
|
936
|
+
clipped = {
|
|
937
|
+
path: _clipped_facts(self.tracked.get(path, (None, frozenset()))[1],
|
|
938
|
+
self.anchors.get(path), facts.get(path))
|
|
939
|
+
for path in unpublished}
|
|
940
|
+
results = dict(self.components)
|
|
941
|
+
results["accounting"] = tuple(sorted(
|
|
942
|
+
(provider, residual)
|
|
943
|
+
for provider, residual in self.accounting.items() if residual))
|
|
944
|
+
self.digest = residual_digest(results, clipped)
|
|
945
|
+
self.counts = self._residual_counts(unpublished)
|
|
946
|
+
if self._chain_broken:
|
|
947
|
+
self.previous_digest = None
|
|
948
|
+
self._chain_broken = False
|
|
949
|
+
if not info.get("counted") or not info.get("census_complete"):
|
|
950
|
+
self.previous_digest = None
|
|
951
|
+
return None
|
|
952
|
+
if (info.get("certifiable") and not self.obligations
|
|
953
|
+
and not any(self.accounting.values())):
|
|
954
|
+
return "drained"
|
|
955
|
+
if self.digest and self.digest == self.previous_digest:
|
|
956
|
+
return "settled"
|
|
957
|
+
self.previous_digest = self.digest
|
|
958
|
+
return None
|
|
959
|
+
|
|
960
|
+
|
|
961
|
+
def hold_admission() -> None:
|
|
962
|
+
"""Place the startup admission hold (before the main sync thread)."""
|
|
963
|
+
state = _STATE
|
|
964
|
+
with _LOCK:
|
|
965
|
+
state.hold_placed = True
|
|
966
|
+
state.released_by = None
|
|
967
|
+
state.drain = StartupDrain()
|
|
968
|
+
|
|
969
|
+
|
|
970
|
+
def admission_held() -> bool:
|
|
971
|
+
state = _STATE
|
|
972
|
+
return state.hold_placed and state.released_by is None
|
|
973
|
+
|
|
974
|
+
|
|
975
|
+
def release_admission(reason: str) -> bool:
|
|
976
|
+
"""Release the hold once; admits warm at once when a non-cold publication
|
|
977
|
+
was already seen, otherwise at the next one. False when no hold was
|
|
978
|
+
placed or it was already released."""
|
|
979
|
+
if reason not in RELEASE_REASONS:
|
|
980
|
+
raise ValueError(f"unknown release reason: {reason!r}")
|
|
981
|
+
state = _STATE
|
|
982
|
+
with _LOCK:
|
|
983
|
+
if not state.hold_placed or state.released_by is not None:
|
|
984
|
+
return False
|
|
985
|
+
state.released_by = reason
|
|
986
|
+
state.drain = None
|
|
987
|
+
if (state.warm_admitted_ns is None
|
|
988
|
+
and state.first_non_cold_ns is not None):
|
|
989
|
+
now, reading = _sample_locked(state)
|
|
990
|
+
state.warm_admitted_ns = now
|
|
991
|
+
state.cold_startup_bytes = reading.value
|
|
992
|
+
return True
|
|
993
|
+
|
|
994
|
+
|
|
995
|
+
def observe_accounting_tick(*, ended_ns, census_complete, residuals) -> None:
|
|
996
|
+
"""One accounting tick of the main sync thread (any process may call
|
|
997
|
+
this; only a placed hold evaluates it)."""
|
|
998
|
+
state = _STATE
|
|
999
|
+
with _LOCK:
|
|
1000
|
+
if census_complete:
|
|
1001
|
+
state.accounting_caught_up = True
|
|
1002
|
+
if state.drain is not None:
|
|
1003
|
+
state.drain.observe_accounting(
|
|
1004
|
+
ended_ns=ended_ns, census_complete=bool(census_complete),
|
|
1005
|
+
residuals=residuals)
|
|
1006
|
+
|
|
1007
|
+
|
|
1008
|
+
def observe_conversation_pass(pass_info, *, started_ns, seq) -> "str | None":
|
|
1009
|
+
"""Feed one completed conversation pass to the evaluator; returns its
|
|
1010
|
+
release verdict. A pass counts only if it started after the end of the
|
|
1011
|
+
first accounting-census-complete tick (same monotonic clock)."""
|
|
1012
|
+
state = _STATE
|
|
1013
|
+
with _LOCK:
|
|
1014
|
+
drain = state.drain
|
|
1015
|
+
if drain is None or state.released_by is not None:
|
|
1016
|
+
return None
|
|
1017
|
+
first = drain.first_complete_end_ns
|
|
1018
|
+
counted = first is not None and int(started_ns) > first
|
|
1019
|
+
census = bool(pass_info and pass_info.get("census_complete"))
|
|
1020
|
+
info = dict(pass_info or {})
|
|
1021
|
+
info["counted"] = counted
|
|
1022
|
+
info["census_complete"] = census
|
|
1023
|
+
try:
|
|
1024
|
+
verdict = drain.observe(info)
|
|
1025
|
+
except Exception:
|
|
1026
|
+
# A pass the evaluator could not judge breaks the comparison
|
|
1027
|
+
# chain: the next pass must not settle against the one before
|
|
1028
|
+
# it, across an observation that never completed.
|
|
1029
|
+
drain.previous_digest = None
|
|
1030
|
+
raise
|
|
1031
|
+
state.last_pass = (int(seq), census, counted, digest_hash(drain.digest))
|
|
1032
|
+
if counted:
|
|
1033
|
+
state.residual_counts = drain.counts
|
|
1034
|
+
return verdict
|
|
1035
|
+
|
|
1036
|
+
|
|
1037
|
+
def startup_watch() -> dict:
|
|
1038
|
+
"""``{path: (file_identity, keys)}`` of every deferral the evaluator
|
|
1039
|
+
tracks; empty once released or when no hold was placed."""
|
|
1040
|
+
with _LOCK:
|
|
1041
|
+
drain = _STATE.drain
|
|
1042
|
+
return {} if drain is None else drain.watch()
|
|
1043
|
+
|
|
1044
|
+
|
|
647
1045
|
# ── the `/api/debug/backend` `writeIo` object (spec §5.5) ─────────────────
|
|
648
1046
|
|
|
649
1047
|
def write_io_payload(*, now_ns=None, now_utc=None, limits=None) -> dict:
|
|
@@ -664,6 +1062,11 @@ def write_io_payload(*, now_ns=None, now_utc=None, limits=None) -> dict:
|
|
|
664
1062
|
last_status = state.last_status
|
|
665
1063
|
cold = state.cold_startup_bytes
|
|
666
1064
|
admitted = state.warm_admitted_ns is not None
|
|
1065
|
+
released_by = state.released_by
|
|
1066
|
+
held = state.hold_placed and released_by is None
|
|
1067
|
+
caught_up = state.accounting_caught_up
|
|
1068
|
+
counts = state.residual_counts
|
|
1069
|
+
last_pass = state.last_pass
|
|
667
1070
|
local = None if state.local_instance is None else dict(state.local_instance)
|
|
668
1071
|
observed = state.ledger_observed
|
|
669
1072
|
verdict, reasons = _budget.steady_verdict(stat, hysteresis, limits)
|
|
@@ -690,7 +1093,17 @@ def write_io_payload(*, now_ns=None, now_utc=None, limits=None) -> dict:
|
|
|
690
1093
|
"lastEndedAt": (None if last_wall is None else _budget.iso_z(
|
|
691
1094
|
dt.datetime.fromtimestamp(last_wall, UTC))),
|
|
692
1095
|
},
|
|
693
|
-
"coldStartup": {
|
|
1096
|
+
"coldStartup": {
|
|
1097
|
+
"bytes": cold, "admitted": admitted,
|
|
1098
|
+
# #939 D5: the startup admission hold (additive keys).
|
|
1099
|
+
"releasedBy": released_by, "held": held,
|
|
1100
|
+
"accountingCaughtUp": caught_up,
|
|
1101
|
+
"residual": (None if counts is None
|
|
1102
|
+
else dict(zip(RESIDUAL_COUNT_KEYS, counts))),
|
|
1103
|
+
"lastPass": (None if last_pass is None else {
|
|
1104
|
+
"seq": last_pass[0], "censusComplete": last_pass[1],
|
|
1105
|
+
"counted": last_pass[2], "residualDigest": last_pass[3]}),
|
|
1106
|
+
},
|
|
694
1107
|
"maintenance": maintenance,
|
|
695
1108
|
"reclaimRefusal": reclaim_refusal(),
|
|
696
1109
|
"budget": {
|
package/bin/cctally
CHANGED
|
@@ -1145,6 +1145,10 @@ cmd_db_vacuum = _cctally_db.cmd_db_vacuum
|
|
|
1145
1145
|
# _CACHE_MIGRATIONS) at its own module-load time — both are no-ops
|
|
1146
1146
|
# when bin/cctally already imported them above, but they make the
|
|
1147
1147
|
# sibling self-contained for isolated test loads.
|
|
1148
|
+
# #944: write-time Claude cost attribution. The pure rule/identity kernel and
|
|
1149
|
+
# its cache.db + journal glue, loaded before the cache sibling that uses them.
|
|
1150
|
+
_lib_claude_attribution = _load_sibling("_lib_claude_attribution")
|
|
1151
|
+
_cctally_claude_attribution = _load_sibling("_cctally_claude_attribution")
|
|
1148
1152
|
_cctally_cache = _load_sibling("_cctally_cache")
|
|
1149
1153
|
ProjectKey = _cctally_cache.ProjectKey
|
|
1150
1154
|
_resolve_project_key = _cctally_cache._resolve_project_key
|
|
@@ -1193,6 +1197,12 @@ _reset_orphan_warning_throttle = _cctally_cache._reset_orphan_warning_throttle
|
|
|
1193
1197
|
cmd_cache_sync = _cctally_cache.cmd_cache_sync
|
|
1194
1198
|
cmd_codex_replay_drain_internal = _cctally_cache.cmd_codex_replay_drain_internal
|
|
1195
1199
|
cmd_codex_prune_drain_internal = _cctally_cache.cmd_codex_prune_drain_internal
|
|
1200
|
+
# #927 B2: the detached retained-identity inventory bootstrap worker.
|
|
1201
|
+
cmd_codex_inventory_bootstrap_internal = (
|
|
1202
|
+
_cctally_cache.cmd_codex_inventory_bootstrap_internal)
|
|
1203
|
+
# #977 C7: the detached quota natural-key index rebuild worker.
|
|
1204
|
+
cmd_quota_index_rebuild_internal = _load_sibling(
|
|
1205
|
+
"_cctally_journal").cmd_quota_index_rebuild_internal
|
|
1196
1206
|
|
|
1197
1207
|
|
|
1198
1208
|
# Record-usage / hook-tick hot-path subsystem — the runtime path that
|
|
@@ -1762,6 +1772,10 @@ BudgetRow = _cctally_forecast.BudgetRow
|
|
|
1762
1772
|
_resolve_forecast_now = _cctally_forecast._resolve_forecast_now
|
|
1763
1773
|
_fetch_current_week_snapshots = _cctally_forecast._fetch_current_week_snapshots
|
|
1764
1774
|
_apply_midweek_reset_override = _cctally_forecast._apply_midweek_reset_override
|
|
1775
|
+
ClaudeCycleScope = _cctally_forecast.ClaudeCycleScope
|
|
1776
|
+
_claude_select_current_owner = _cctally_forecast._claude_select_current_owner
|
|
1777
|
+
_claude_current_cycle_scope = _cctally_forecast._claude_current_cycle_scope
|
|
1778
|
+
_claude_cycle_scope_samples = _cctally_forecast._claude_cycle_scope_samples
|
|
1765
1779
|
_current_week_five_hour_percent = _cctally_forecast._current_week_five_hour_percent
|
|
1766
1780
|
_current_week_five_hour_reading = _cctally_forecast._current_week_five_hour_reading
|
|
1767
1781
|
_resolve_current_budget_window = _cctally_forecast._resolve_current_budget_window
|
|
@@ -1891,24 +1905,26 @@ def _codex_home_roots() -> list[pathlib.Path]:
|
|
|
1891
1905
|
def _codex_session_roots() -> list[pathlib.Path]:
|
|
1892
1906
|
"""Directories to walk for *.jsonl, applying the sessions/-subdir rule.
|
|
1893
1907
|
|
|
1894
|
-
For each home root r (in $CODEX_HOME order)
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1908
|
+
For each home root r (in $CODEX_HOME order), by the container leaf's
|
|
1909
|
+
native-home recognition (#970 spec 5.1):
|
|
1910
|
+
r has sessions/ or archived_sessions/ -> r / "sessions" (Codex home;
|
|
1911
|
+
reported even when only the
|
|
1912
|
+
archive exists yet)
|
|
1913
|
+
elif r.is_dir() -> r (direct JSONL dir)
|
|
1914
|
+
else -> skipped
|
|
1898
1915
|
Exact-duplicate roots are de-duped (first occurrence kept). File-level
|
|
1899
1916
|
de-dup for overlapping/prefix roots happens in the discovery walkers
|
|
1900
1917
|
(set of absolute paths); this function only collapses identical roots.
|
|
1901
1918
|
"""
|
|
1919
|
+
import _lib_codex_rollout_container as _rc_leaf
|
|
1920
|
+
|
|
1902
1921
|
out: list[pathlib.Path] = []
|
|
1903
1922
|
seen: set[pathlib.Path] = set()
|
|
1904
1923
|
for r in _codex_home_roots():
|
|
1905
|
-
|
|
1906
|
-
if
|
|
1907
|
-
cand = sess
|
|
1908
|
-
elif r.is_dir():
|
|
1909
|
-
cand = r
|
|
1910
|
-
else:
|
|
1924
|
+
locations = _rc_leaf.home_locations(r)
|
|
1925
|
+
if locations is None:
|
|
1911
1926
|
continue
|
|
1927
|
+
cand = locations.walk_root
|
|
1912
1928
|
if cand in seen:
|
|
1913
1929
|
continue
|
|
1914
1930
|
seen.add(cand)
|
|
@@ -3830,7 +3846,8 @@ def _post_command_update_hooks(command: str | None, args) -> None:
|
|
|
3830
3846
|
if command in ("_update-check", "_telemetry-beat", "_codex-quota-verify",
|
|
3831
3847
|
"_stats-epoch-rebuild", "_stats-corruption-heal",
|
|
3832
3848
|
"_codex-replay-drain", "_codex-prune-drain", "_artifact-retention",
|
|
3833
|
-
"_statusline-continue"
|
|
3849
|
+
"_statusline-continue", "_quota-index-rebuild",
|
|
3850
|
+
"_codex-inventory-bootstrap"):
|
|
3834
3851
|
# All six hidden workers are detached and have already done their one job
|
|
3835
3852
|
# in their own command handler; none must re-enter this hook.
|
|
3836
3853
|
# Without the guard the ``_update-check`` worker would fall through to
|