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.
Files changed (76) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +4 -6
  3. package/bin/_cctally_account.py +57 -3
  4. package/bin/_cctally_cache.py +5720 -885
  5. package/bin/_cctally_claude_attribution.py +997 -0
  6. package/bin/_cctally_completion.py +101 -28
  7. package/bin/_cctally_core.py +235 -12
  8. package/bin/_cctally_dashboard.py +263 -17
  9. package/bin/_cctally_dashboard_conversation.py +107 -21
  10. package/bin/_cctally_dashboard_envelope.py +44 -0
  11. package/bin/_cctally_db.py +928 -80
  12. package/bin/_cctally_diagnosis_sources.py +78 -13
  13. package/bin/_cctally_doctor.py +211 -86
  14. package/bin/_cctally_five_hour.py +446 -22
  15. package/bin/_cctally_forecast.py +302 -6
  16. package/bin/_cctally_journal.py +2418 -158
  17. package/bin/_cctally_milestone_history.py +379 -91
  18. package/bin/_cctally_oauth_samples.py +185 -7
  19. package/bin/_cctally_parser.py +38 -0
  20. package/bin/_cctally_project.py +34 -8
  21. package/bin/_cctally_quarantine.py +821 -343
  22. package/bin/_cctally_quota.py +69 -4
  23. package/bin/_cctally_quota_calibration.py +677 -3
  24. package/bin/_cctally_quota_model.py +927 -127
  25. package/bin/_cctally_record.py +922 -445
  26. package/bin/_cctally_rederive.py +119 -15
  27. package/bin/_cctally_statusline.py +1048 -176
  28. package/bin/_cctally_store.py +544 -110
  29. package/bin/_cctally_tui.py +455 -42
  30. package/bin/_cctally_update.py +77 -14
  31. package/bin/_lib_account_protocol.py +227 -19
  32. package/bin/_lib_claude_attribution.py +421 -0
  33. package/bin/_lib_clone_copy.py +30 -0
  34. package/bin/_lib_codex_conversation_query.py +45 -9
  35. package/bin/_lib_codex_conversation_watch.py +72 -13
  36. package/bin/_lib_codex_rollout_container.py +234 -19
  37. package/bin/_lib_codex_worker_lifecycle.py +374 -0
  38. package/bin/_lib_credit.py +22 -13
  39. package/bin/_lib_dashboard_sources.py +257 -18
  40. package/bin/_lib_detector_access_registry.py +1335 -166
  41. package/bin/_lib_doctor.py +276 -92
  42. package/bin/_lib_ingest_frontier.py +269 -36
  43. package/bin/_lib_journal.py +273 -22
  44. package/bin/_lib_jsonl.py +12 -0
  45. package/bin/_lib_meter_rate_change.py +72 -0
  46. package/bin/_lib_outcome_quarantine.py +917 -554
  47. package/bin/_lib_provider_activity.py +24 -6
  48. package/bin/_lib_quota_calibration.py +57 -3
  49. package/bin/_lib_quota_copy.py +7 -2
  50. package/bin/_lib_quota_lineage.py +613 -0
  51. package/bin/_lib_quota_model.py +464 -33
  52. package/bin/_lib_rate_change_delivery.py +16 -0
  53. package/bin/_lib_reclaim_planner.py +6 -4
  54. package/bin/_lib_rederive.py +2046 -21
  55. package/bin/_lib_selector_state.py +131 -3
  56. package/bin/_lib_snapshot_cache.py +8 -0
  57. package/bin/_lib_stale_proof.py +113 -4
  58. package/bin/_lib_statusline.py +72 -3
  59. package/bin/_lib_statusline_candidates.py +246 -42
  60. package/bin/_lib_tick_stats.py +10 -5
  61. package/bin/_lib_write_io.py +419 -6
  62. package/bin/cctally +28 -11
  63. package/dashboard/static/assets/{ConversationsView-Dflb_myA.js → ConversationsView-oqNsWzWV.js} +1 -1
  64. package/dashboard/static/assets/{DoctorModal-DFX5CfIs.js → DoctorModal-B0DnU9d9.js} +1 -1
  65. package/dashboard/static/assets/ModalRoot-CYua2wP_.js +1 -0
  66. package/dashboard/static/assets/{ProjectsDrillPanel-C3PJy1xx.js → ProjectsDrillPanel-DSxXQSa-.js} +1 -1
  67. package/dashboard/static/assets/{SourceDetailModal-BczsWKhD.js → SourceDetailModal-BleDpQqG.js} +1 -1
  68. package/dashboard/static/assets/{UpdateModal-Os-Lo5cs.js → UpdateModal-CMqaAkSK.js} +1 -1
  69. package/dashboard/static/assets/{index-CYZ3NLqF.js → index-BjQV-0-D.js} +3 -3
  70. package/dashboard/static/assets/index-D34eYhAc.css +1 -0
  71. package/dashboard/static/assets/{outlineNavigation-k-zIif6W.js → outlineNavigation-CO-VWBm7.js} +1 -1
  72. package/dashboard/static/assets/{useKeymap-CW7Up0Ay.js → useKeymap-CUgEnnua.js} +1 -1
  73. package/dashboard/static/dashboard.html +3 -3
  74. package/package.json +5 -1
  75. package/dashboard/static/assets/ModalRoot-HlxoagIa.js +0 -1
  76. package/dashboard/static/assets/index-BKITk1CT.css +0 -1
@@ -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.warm_admitted_ns = now
422
- state.cold_startup_bytes = reading.value
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 65,536-byte state test."""
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": {"bytes": cold, "admitted": admitted},
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
- (r / "sessions").is_dir() -> r / "sessions" (Codex home)
1896
- elif r.is_dir() -> r (direct JSONL dir)
1897
- else -> skipped
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
- sess = r / "sessions"
1906
- if sess.is_dir():
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