cctally 1.111.0 → 1.112.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/README.md +5 -6
- package/bin/_cctally_cache.py +14 -7
- package/bin/_cctally_core.py +9 -1
- package/bin/_cctally_dashboard_envelope.py +7 -6
- package/bin/_cctally_dashboard_sources.py +90 -55
- package/bin/_cctally_doctor.py +73 -27
- package/bin/_cctally_forecast.py +2 -1
- package/bin/_cctally_journal.py +147 -6
- package/bin/_cctally_milestones.py +29 -3
- package/bin/_cctally_quota.py +10 -1
- package/bin/_cctally_record.py +136 -4
- package/bin/_cctally_rederive.py +311 -8
- package/bin/_cctally_source_analytics.py +20 -8
- package/bin/_cctally_tui.py +546 -45
- package/bin/_lib_codex_conversation_query.py +40 -4
- package/bin/_lib_codex_metadata.py +57 -11
- package/bin/_lib_cost_provenance.py +303 -0
- package/bin/_lib_dashboard_sources.py +263 -28
- package/bin/_lib_doctor.py +2 -2
- package/bin/_lib_ingest_frontier.py +137 -38
- package/bin/_lib_pricing.py +13 -1
- package/bin/_lib_rederive.py +704 -36
- package/bin/_lib_snapshot_cache.py +332 -9
- package/dashboard/static/assets/{ConversationsView-BB9vUiL6.js → ConversationsView-CEZ-BUyi.js} +1 -1
- package/dashboard/static/assets/{DoctorModal-DFY2iPDf.js → DoctorModal-CE1oH_Zy.js} +1 -1
- package/dashboard/static/assets/ModalRoot-D5Y8IFUG.js +1 -0
- package/dashboard/static/assets/{ProjectsDrillPanel-DAv39uSo.js → ProjectsDrillPanel-B3DZoCeD.js} +1 -1
- package/dashboard/static/assets/{SourceDetailModal-DSAWo2Fp.js → SourceDetailModal-CHp9EgLP.js} +1 -1
- package/dashboard/static/assets/{UpdateModal-R_9dOVnh.js → UpdateModal-5iKST6jo.js} +1 -1
- package/dashboard/static/assets/{index-DdU62sov.js → index-Cm0VTxJC.js} +2 -2
- package/dashboard/static/assets/index-Dt4tqf43.css +1 -0
- package/dashboard/static/assets/{outlineNavigation-BOCLcRa6.js → outlineNavigation-DkXGL8J6.js} +1 -1
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +2 -1
- package/dashboard/static/assets/ModalRoot-DvRKHbQE.js +0 -1
- package/dashboard/static/assets/index-COhxe3vU.css +0 -1
package/bin/_cctally_tui.py
CHANGED
|
@@ -302,6 +302,7 @@ from _lib_dashboard_sources import (
|
|
|
302
302
|
dashboard_resource_key,
|
|
303
303
|
degrade_source_state,
|
|
304
304
|
reuse_coherent_source_state,
|
|
305
|
+
settled_codex_cycle_unavailable,
|
|
305
306
|
unavailable_source_state,
|
|
306
307
|
)
|
|
307
308
|
from _lib_source_retry import (
|
|
@@ -3595,6 +3596,223 @@ def _tui_retain_refused_partial(
|
|
|
3595
3596
|
return decision.action == "retain"
|
|
3596
3597
|
|
|
3597
3598
|
|
|
3599
|
+
#: The server-only ``clock_data`` key carrying the Codex quota-dependency
|
|
3600
|
+
#: identity a Codex generation was BUILT from (#857). Stamped only on a fresh
|
|
3601
|
+
#: build, from the same pinned cache read as that build's capture; never
|
|
3602
|
+
#: re-stamped on a reuse, degrade or retained-bundle path, because those publish
|
|
3603
|
+
#: a generation an earlier tick read. The three handlers that retain the prior
|
|
3604
|
+
#: bundle after an attempted rebuild and return normally withdraw it (``None``,
|
|
3605
|
+
#: `_tui_withdraw_codex_quota_dependency`). Those withdrawals are per-site and
|
|
3606
|
+
#: do NOT cover every way an attempt can end unpublished — a failure escalated
|
|
3607
|
+
#: as stats corruption leaves before any handler runs, and a tick with no
|
|
3608
|
+
#: dispatch key memoizes nothing to withdraw on. What closes that class
|
|
3609
|
+
#: generically is the SECOND stamp, the accounting provenance token
|
|
3610
|
+
#: (`_lib_snapshot_cache.CODEX_ACCOUNTING_PROVENANCE_KEY`,
|
|
3611
|
+
#: `_tui_codex_accounting_provenance_current`): any capture that consumed
|
|
3612
|
+
#: accounting moves the process's consumed token past every generation it did
|
|
3613
|
+
#: not publish.
|
|
3614
|
+
_CODEX_QUOTA_DEPENDENCY_KEY = "codex_quota_dependency"
|
|
3615
|
+
|
|
3616
|
+
|
|
3617
|
+
def _tui_cache_file_before_open() -> "tuple[int, int] | None":
|
|
3618
|
+
"""The cache file a tick is about to open, observed BEFORE it opens (#857).
|
|
3619
|
+
|
|
3620
|
+
``(st_dev, st_ino)`` of the configured cache path, or ``None`` when it
|
|
3621
|
+
cannot be stat'ed — an absent file included. Taken immediately before the
|
|
3622
|
+
tick's cache handle is opened; it is the ``opened_file`` that
|
|
3623
|
+
`_tui_read_codex_quota_dependency` binds the identity read on that handle
|
|
3624
|
+
to, and ``None`` binds nothing.
|
|
3625
|
+
"""
|
|
3626
|
+
try:
|
|
3627
|
+
return _cctally()._load_sibling(
|
|
3628
|
+
"_lib_snapshot_cache"
|
|
3629
|
+
).codex_cache_file_identity(_cctally_core.CACHE_DB_PATH)
|
|
3630
|
+
except Exception: # noqa: BLE001 — an unobserved file binds nothing
|
|
3631
|
+
return None
|
|
3632
|
+
|
|
3633
|
+
|
|
3634
|
+
def _tui_read_codex_quota_dependency(conn, *, opened_file) -> "tuple | None":
|
|
3635
|
+
"""Read the #857 identity on ``conn``; any failure is "no identity".
|
|
3636
|
+
|
|
3637
|
+
``opened_file`` is `_tui_cache_file_before_open`'s observation, taken
|
|
3638
|
+
before ``conn`` was opened, and the identity is bound to it STRICTLY
|
|
3639
|
+
(`codex_quota_dependency_identity`): it exists only when the file ``conn``
|
|
3640
|
+
reports after the identity's last read is the file observed before the
|
|
3641
|
+
open. A failed or missing pre-open observation, a replacement inside that
|
|
3642
|
+
bracket, and a handle opened on any other file all read as ``None`` —
|
|
3643
|
+
"rebuild" — never as a late bracket that begins after the open.
|
|
3644
|
+
"""
|
|
3645
|
+
try:
|
|
3646
|
+
return _cctally()._load_sibling(
|
|
3647
|
+
"_lib_snapshot_cache"
|
|
3648
|
+
).codex_quota_dependency_identity(conn, opened_file=opened_file)
|
|
3649
|
+
except Exception: # noqa: BLE001 — no identity means rebuild, never a crash
|
|
3650
|
+
return None
|
|
3651
|
+
|
|
3652
|
+
|
|
3653
|
+
def _tui_withdraw_codex_quota_dependency(bundle):
|
|
3654
|
+
"""``bundle`` with its Codex generation's identity stamp withdrawn (#857).
|
|
3655
|
+
|
|
3656
|
+
For a PRIOR bundle a tick republishes after its own rebuild attempt was
|
|
3657
|
+
abandoned. Three sites withdraw: the full path's source build raised
|
|
3658
|
+
(`_tui_retain_failed_source_bundle`, always); a build completed but lost
|
|
3659
|
+
the stats generation race and returned the prior bundle instead of its own;
|
|
3660
|
+
or the idle path's bounded source adapter raised and
|
|
3661
|
+
`_tui_build_idle_snapshot` kept ``prior.source_bundle``. The last two
|
|
3662
|
+
withdraw only when that build SCHEDULED the Codex pass — a superset of
|
|
3663
|
+
running it, since a pass that fails before consuming anything counts too,
|
|
3664
|
+
which over-withdraws safely. In every case the retained generation was not
|
|
3665
|
+
re-validated against what this tick read, and the attempt may already have
|
|
3666
|
+
consumed evidence the NEXT tick needs to see as moved: the Codex pass
|
|
3667
|
+
publishes the process accounting cache before the post-build
|
|
3668
|
+
reconciliation, which can raise as well as lose the race, so accounting
|
|
3669
|
+
that moved in place (window-spend adoption stamps ``account_key`` without
|
|
3670
|
+
moving ``MAX(id)``, the physical sequence or the quota identity) no longer
|
|
3671
|
+
reads as pending, and the Codex version cannot see it. The idle adapter is
|
|
3672
|
+
no exception for running under an unchanged key: it reads the cache on its
|
|
3673
|
+
OWN snapshot, opened after the dispatch read, so accounting that commits
|
|
3674
|
+
between the two is pending to the adapter and absent from the key. The
|
|
3675
|
+
stamp is what admits a Codex generation to the idle gate — ordinary and
|
|
3676
|
+
settled alike — and to exact-version reuse
|
|
3677
|
+
(`_tui_codex_dependency_current`), so leaving it would let the next tick
|
|
3678
|
+
republish the old generation for as long as nothing else moves. Withdrawn,
|
|
3679
|
+
the next tick rebuilds Codex once, whichever path it takes. Nothing
|
|
3680
|
+
published changes; ``clock_data`` never reaches the wire.
|
|
3681
|
+
|
|
3682
|
+
These withdrawals are belt-and-braces now, not the closure. They run only
|
|
3683
|
+
in a handler that returns normally, so they cannot reach a failure that
|
|
3684
|
+
escalates as stats corruption before the handler's withdrawal, or a tick
|
|
3685
|
+
with no dispatch key, whose retained bundle nothing memoizes. The
|
|
3686
|
+
accounting provenance token covers every such site at once
|
|
3687
|
+
(`_tui_codex_accounting_provenance_current`): the capture that consumed
|
|
3688
|
+
moved the consumed token past the retained generation's stamp.
|
|
3689
|
+
|
|
3690
|
+
A raced build or failed idle adapter that reused or degraded Codex
|
|
3691
|
+
scheduled no pass, consumed nothing, and keeps the stamp. That is safe:
|
|
3692
|
+
accounting that moved is still pending, and an accounting commit moves the
|
|
3693
|
+
dispatch key, so the next tick takes the full path and rebuilds Codex. A
|
|
3694
|
+
race moves the next key in any case, because every stats leg its check
|
|
3695
|
+
compares is also a dispatch signature leg; after a failed idle adapter with
|
|
3696
|
+
nothing committed, the retained generation is exactly as current as it was.
|
|
3697
|
+
"""
|
|
3698
|
+
if not isinstance(bundle, SourceDashboardBundle):
|
|
3699
|
+
return bundle
|
|
3700
|
+
codex = bundle.sources.get("codex")
|
|
3701
|
+
if (
|
|
3702
|
+
not isinstance(codex, SourceDashboardState)
|
|
3703
|
+
or not isinstance(codex.clock_data, Mapping)
|
|
3704
|
+
or codex.clock_data.get(_CODEX_QUOTA_DEPENDENCY_KEY) is None
|
|
3705
|
+
):
|
|
3706
|
+
return bundle
|
|
3707
|
+
withdrawn = dataclasses.replace(codex, clock_data={
|
|
3708
|
+
**codex.clock_data, _CODEX_QUOTA_DEPENDENCY_KEY: None,
|
|
3709
|
+
})
|
|
3710
|
+
return dataclasses.replace(
|
|
3711
|
+
bundle, sources={**bundle.sources, "codex": withdrawn},
|
|
3712
|
+
)
|
|
3713
|
+
|
|
3714
|
+
|
|
3715
|
+
def _tui_retain_failed_source_bundle(prior_bundle, *, dispatch_key):
|
|
3716
|
+
"""The bundle a full tick publishes when its source build FAILED (#857).
|
|
3717
|
+
|
|
3718
|
+
The prior bundle, retained whole as before. When this tick also memoizes
|
|
3719
|
+
the snapshot under its new ``dispatch_key``, the retained Codex generation
|
|
3720
|
+
loses its identity stamp (`_tui_withdraw_codex_quota_dependency`): dispatch
|
|
3721
|
+
movement guaranteed this tick an ATTEMPTED rebuild, not a successful one,
|
|
3722
|
+
and the next tick must not read an unchanged key as proof that the retained
|
|
3723
|
+
generation is current. With no key, nothing is memoized — the dispatch memo
|
|
3724
|
+
keeps the snapshot the prior bundle was validated under — so the retained
|
|
3725
|
+
object is returned untouched; if the failed build consumed accounting, that
|
|
3726
|
+
memoized generation is refused anyway, by its accounting provenance token
|
|
3727
|
+
(`_tui_codex_accounting_provenance_current`).
|
|
3728
|
+
"""
|
|
3729
|
+
if dispatch_key is None:
|
|
3730
|
+
return prior_bundle
|
|
3731
|
+
return _tui_withdraw_codex_quota_dependency(prior_bundle)
|
|
3732
|
+
|
|
3733
|
+
|
|
3734
|
+
def _tui_codex_accounting_provenance_current(state) -> bool:
|
|
3735
|
+
"""Whether ``state`` names the accounting population last consumed (#857).
|
|
3736
|
+
|
|
3737
|
+
``state``'s stamped accounting provenance token — the one its build's
|
|
3738
|
+
capture returned — must equal the token of the population the process
|
|
3739
|
+
accounting cache most recently consumed
|
|
3740
|
+
(`_lib_snapshot_cache.codex_accounting_consumed_provenance`). That closes
|
|
3741
|
+
unpublished consumption as a CLASS: a capture that consumed pending
|
|
3742
|
+
accounting (a ledger advance, a revealed row, a reconstruction) mints a new
|
|
3743
|
+
token, so a build that then failed to publish what it built — a raise, a
|
|
3744
|
+
lost race, a corruption escalation and its heal retry, a tick with no
|
|
3745
|
+
dispatch key — leaves every retained generation behind the consumed token,
|
|
3746
|
+
whatever the failure site. The consumed token lives outside the evictable
|
|
3747
|
+
accounting state, so snapshot memory enforcement alone never refuses a
|
|
3748
|
+
published generation. A missing stamp (a bypassing build, a hand-built
|
|
3749
|
+
prior) and a process that has consumed nothing yet are not current. The
|
|
3750
|
+
comparison is an in-memory read, O(1) per tick.
|
|
3751
|
+
"""
|
|
3752
|
+
if not isinstance(state, SourceDashboardState):
|
|
3753
|
+
return False
|
|
3754
|
+
# A `sys.modules` hit in every load context: `_cctally_dashboard_sources`,
|
|
3755
|
+
# imported at this module's top, imports it at ITS top, and its captures
|
|
3756
|
+
# are what write the token. Deliberately not `_cctally()._load_sibling`,
|
|
3757
|
+
# so the idle gate needs no `cctally` module to answer.
|
|
3758
|
+
import _lib_snapshot_cache as sc
|
|
3759
|
+
consumed = sc.codex_accounting_consumed_provenance()
|
|
3760
|
+
if consumed is None:
|
|
3761
|
+
return False
|
|
3762
|
+
clock_data = state.clock_data
|
|
3763
|
+
return isinstance(clock_data, Mapping) and clock_data.get(
|
|
3764
|
+
sc.CODEX_ACCOUNTING_PROVENANCE_KEY) == consumed
|
|
3765
|
+
|
|
3766
|
+
|
|
3767
|
+
def _tui_codex_dependency_current(state, *, codex_dependency) -> bool:
|
|
3768
|
+
"""Whether ``state`` was built from exactly the inputs current this tick.
|
|
3769
|
+
|
|
3770
|
+
#857. The one comparison behind every Codex admission that skips a
|
|
3771
|
+
rebuild: the idle gate, for EVERY availability (the settled exception and
|
|
3772
|
+
an ordinary ``ok``/``empty`` generation alike), the settled active-path
|
|
3773
|
+
exception and ordinary exact-version reuse. Two provenance legs, both
|
|
3774
|
+
required: the quota-dependency identity read this tick, and the accounting
|
|
3775
|
+
provenance token of the population the process last consumed
|
|
3776
|
+
(`_tui_codex_accounting_provenance_current`). An unavailable identity on
|
|
3777
|
+
either side — none read now, none stamped, or a stamp withdrawn after an
|
|
3778
|
+
abandoned rebuild — or a token behind the consumed one is not current, so
|
|
3779
|
+
a generation carrying one is rebuilt once and, once its fresh stamps
|
|
3780
|
+
match, idles again.
|
|
3781
|
+
"""
|
|
3782
|
+
if codex_dependency is None or not isinstance(state, SourceDashboardState):
|
|
3783
|
+
return False
|
|
3784
|
+
clock_data = state.clock_data
|
|
3785
|
+
return (
|
|
3786
|
+
isinstance(clock_data, Mapping)
|
|
3787
|
+
and clock_data.get(_CODEX_QUOTA_DEPENDENCY_KEY) == codex_dependency
|
|
3788
|
+
and _tui_codex_accounting_provenance_current(state)
|
|
3789
|
+
)
|
|
3790
|
+
|
|
3791
|
+
|
|
3792
|
+
def _tui_codex_settled_cycle_holds(
|
|
3793
|
+
state, *, codex_dependency, now_utc: dt.datetime,
|
|
3794
|
+
) -> bool:
|
|
3795
|
+
"""Whether a settled cycle-unavailable Codex generation is still current.
|
|
3796
|
+
|
|
3797
|
+
#857. ``settled_codex_cycle_unavailable`` states the shape; this adds the
|
|
3798
|
+
facts only the tick holds. Its provenance must be current
|
|
3799
|
+
(`_tui_codex_dependency_current`): the identity the generation was built
|
|
3800
|
+
from equals the one read now — an unavailable identity on either side
|
|
3801
|
+
fails — and its accounting provenance token is the one the process last
|
|
3802
|
+
consumed; and its decision deadline must not have passed, since cycle
|
|
3803
|
+
resolution is time-dependent on frozen evidence and the deadline is the
|
|
3804
|
+
first instant it could resolve differently. Each caller adds its own path's
|
|
3805
|
+
conditions.
|
|
3806
|
+
"""
|
|
3807
|
+
if not settled_codex_cycle_unavailable(state):
|
|
3808
|
+
return False
|
|
3809
|
+
if not _tui_codex_dependency_current(
|
|
3810
|
+
state, codex_dependency=codex_dependency,
|
|
3811
|
+
):
|
|
3812
|
+
return False
|
|
3813
|
+
return not codex_decision_deadline_passed(state, now_utc)
|
|
3814
|
+
|
|
3815
|
+
|
|
3598
3816
|
def _tui_note_codex_regime(value: str) -> None:
|
|
3599
3817
|
"""Stamp the REALISED Codex source-leg decision on the open tick (§1.5).
|
|
3600
3818
|
|
|
@@ -3633,14 +3851,25 @@ def _tui_build_source_bundle(
|
|
|
3633
3851
|
projects_envelope: dict | None = None,
|
|
3634
3852
|
prior_bundle: SourceDashboardBundle | None = None,
|
|
3635
3853
|
raw_config: dict[str, object] | None = None,
|
|
3854
|
+
attempt: dict[str, bool] | None = None,
|
|
3636
3855
|
) -> SourceDashboardBundle:
|
|
3637
3856
|
"""Build one frozen source bundle after the dashboard's coordinated ingest.
|
|
3638
3857
|
|
|
3639
3858
|
This helper is reachable only from ``precompute_envelope=True``. It opens
|
|
3640
3859
|
a fresh cache handle after the ingest handle has closed, then performs
|
|
3641
3860
|
read-only provider adaptation with no implicit sync or rollout fallback.
|
|
3861
|
+
|
|
3862
|
+
``attempt`` (#857), when given, gets ``attempt["codex_pass"] = True`` as
|
|
3863
|
+
soon as this build schedules the Codex pass, so a caller that retains the
|
|
3864
|
+
prior bundle after this build RAISED knows whether the attempt may have
|
|
3865
|
+
consumed pending accounting (`_tui_withdraw_codex_quota_dependency`).
|
|
3642
3866
|
"""
|
|
3867
|
+
global _PARTIAL_RETRY_STATE
|
|
3643
3868
|
c = _cctally()
|
|
3869
|
+
# #857: observed BEFORE the handle opens, so the Codex quota-dependency
|
|
3870
|
+
# identity below is bound to the file this handle actually reads. Strict:
|
|
3871
|
+
# an unobserved file stamps no identity (the next tick rebuilds Codex once).
|
|
3872
|
+
cache_file_before_open = _tui_cache_file_before_open()
|
|
3644
3873
|
with _perf.phase("source.store_open"):
|
|
3645
3874
|
cache_conn = c.open_cache_db()
|
|
3646
3875
|
cache_read_tx = False
|
|
@@ -3772,6 +4001,18 @@ def _tui_build_source_bundle(
|
|
|
3772
4001
|
accounts_digest=accounts_digest,
|
|
3773
4002
|
claude_stats_digest=claude_digest,
|
|
3774
4003
|
)
|
|
4004
|
+
# #857: the Codex quota-dependency identity, read on the SAME pinned
|
|
4005
|
+
# cache snapshot the Codex capture below reads, so a generation built
|
|
4006
|
+
# this tick is stamped with exactly the inputs it saw — never with a
|
|
4007
|
+
# later identity it did not read. Bound STRICTLY to the file observed
|
|
4008
|
+
# before the handle opened: an unobserved file, or a replacement since
|
|
4009
|
+
# the open, is `None`, never the new inode beside the old rows. The
|
|
4010
|
+
# capture needs no second check — it reads through the descriptor this
|
|
4011
|
+
# bracket bound, and a replacement after it is a later file the next
|
|
4012
|
+
# dispatch read names.
|
|
4013
|
+
codex_dependency = _tui_read_codex_quota_dependency(
|
|
4014
|
+
cache_conn, opened_file=cache_file_before_open,
|
|
4015
|
+
)
|
|
3775
4016
|
_acct_suffix = f":a{accounts_digest}" if accounts_digest else ""
|
|
3776
4017
|
# public #5: the hook's budgeted ingest can change what the Codex
|
|
3777
4018
|
# envelope owes without moving `codex_physical_mutation_seq` — a tick
|
|
@@ -3909,9 +4150,25 @@ def _tui_build_source_bundle(
|
|
|
3909
4150
|
# list of warning strings was a second and incomplete definition
|
|
3910
4151
|
# of non-reusability, and `reuse_coherent_source_state` now
|
|
3911
4152
|
# refuses every `partial` prior unconditionally.
|
|
4153
|
+
# #857: the quota-dependency identity is the third forcing leg.
|
|
4154
|
+
# The version string carries none of its inputs — the quota change
|
|
4155
|
+
# ledger, the attribution revision, the projection certificate,
|
|
4156
|
+
# the cache file — so exact-version reuse cannot see them move:
|
|
4157
|
+
# a certificate lost behind an `ok` generation would be reused
|
|
4158
|
+
# here, and every later idle tick would retain the obsolete
|
|
4159
|
+
# verdict. A moved, unreadable or withdrawn identity (a bundle
|
|
4160
|
+
# retained across a failed rebuild) refuses every reuse and
|
|
4161
|
+
# retention path below, exactly like pending accounting. The same
|
|
4162
|
+
# comparison requires the generation's accounting provenance token
|
|
4163
|
+
# to be the process's consumed one: pending accounting that an
|
|
4164
|
+
# earlier, unpublished attempt already consumed no longer reads as
|
|
4165
|
+
# pending here, and only the token still sees it.
|
|
3912
4166
|
_codex_forced_rebuild = prior_codex is not None and (
|
|
3913
4167
|
_codex_accounting_pending
|
|
3914
4168
|
or codex_decision_deadline_passed(prior_codex, now_utc)
|
|
4169
|
+
or not _tui_codex_dependency_current(
|
|
4170
|
+
prior_codex, codex_dependency=codex_dependency,
|
|
4171
|
+
)
|
|
3915
4172
|
)
|
|
3916
4173
|
codex = (
|
|
3917
4174
|
None if _codex_forced_rebuild
|
|
@@ -3919,7 +4176,33 @@ def _tui_build_source_bundle(
|
|
|
3919
4176
|
prior_codex, data_version=codex_reuse_version,
|
|
3920
4177
|
)
|
|
3921
4178
|
)
|
|
3922
|
-
if
|
|
4179
|
+
if (
|
|
4180
|
+
codex is None
|
|
4181
|
+
and not _codex_forced_rebuild
|
|
4182
|
+
and prior_codex is not None
|
|
4183
|
+
and prior_codex.data_version == codex_reuse_version
|
|
4184
|
+
and _tui_codex_settled_cycle_holds(
|
|
4185
|
+
prior_codex,
|
|
4186
|
+
codex_dependency=codex_dependency,
|
|
4187
|
+
now_utc=now_utc,
|
|
4188
|
+
)
|
|
4189
|
+
):
|
|
4190
|
+
# #857: the settled cycle-unavailable generation over UNCHANGED
|
|
4191
|
+
# Codex inputs — exact version, exact quota-dependency identity
|
|
4192
|
+
# and accounting provenance, no pending accounting and no
|
|
4193
|
+
# elapsed decision deadline (all but the version are
|
|
4194
|
+
# `_codex_forced_rebuild`). Rebuilding it re-derives
|
|
4195
|
+
# the same partial generation, so a tick that another source
|
|
4196
|
+
# forced onto this path must not pay a Codex build for it. Its
|
|
4197
|
+
# own exception bypasses `_tui_retain_refused_partial`, so it
|
|
4198
|
+
# clears the Codex retry key itself: a metadata key armed before
|
|
4199
|
+
# this generation is obsolete, and left armed it would retain a
|
|
4200
|
+
# later metadata recurrence instead of rebuilding it.
|
|
4201
|
+
codex = prior_codex
|
|
4202
|
+
_PARTIAL_RETRY_STATE = clear_partial_retry(
|
|
4203
|
+
_PARTIAL_RETRY_STATE, provider="codex",
|
|
4204
|
+
)
|
|
4205
|
+
elif codex is None and not _codex_forced_rebuild and (
|
|
3923
4206
|
_tui_retain_refused_partial(
|
|
3924
4207
|
prior_codex,
|
|
3925
4208
|
provider="codex",
|
|
@@ -3964,6 +4247,17 @@ def _tui_build_source_bundle(
|
|
|
3964
4247
|
codex_capture = None
|
|
3965
4248
|
codex_capture_failed = False
|
|
3966
4249
|
codex_context = None
|
|
4250
|
+
# #857: whether this build schedules the Codex pass — the one step
|
|
4251
|
+
# that can consume pending accounting (it publishes the process
|
|
4252
|
+
# accounting cache). Only such a build withdraws the prior stamp if it
|
|
4253
|
+
# loses the generation race below, or (through `attempt`) if it raises
|
|
4254
|
+
# under the idle path's adapter; a reused or degraded Codex consumed
|
|
4255
|
+
# nothing. Scheduled, not completed: a pass that fails before it
|
|
4256
|
+
# consumes anything (a `DashboardReadContext` failure, say) still
|
|
4257
|
+
# counts, which over-withdraws safely at the cost of one Codex rebuild.
|
|
4258
|
+
codex_pass_attempted = codex is None
|
|
4259
|
+
if attempt is not None and codex_pass_attempted:
|
|
4260
|
+
attempt["codex_pass"] = True
|
|
3967
4261
|
codex_split_seams_unpatched = (
|
|
3968
4262
|
build_codex_source_state
|
|
3969
4263
|
is sys.modules["_cctally_dashboard_sources"].build_codex_source_state
|
|
@@ -4181,9 +4475,15 @@ def _tui_build_source_bundle(
|
|
|
4181
4475
|
# Attached ONLY on a fresh build, never on the reuse or degrade
|
|
4182
4476
|
# paths: those carry rows this tick did not produce, and their
|
|
4183
4477
|
# own carrier already describes the range that bounds them.
|
|
4478
|
+
# #857: the quota-dependency identity follows the same rule, for
|
|
4479
|
+
# the same reason — it names the inputs THIS build read.
|
|
4184
4480
|
codex = dataclasses.replace(
|
|
4185
4481
|
codex,
|
|
4186
4482
|
aggregate_scope=build_aggregate_scope(published_range),
|
|
4483
|
+
clock_data={
|
|
4484
|
+
**(codex.clock_data or {}),
|
|
4485
|
+
_CODEX_QUOTA_DEPENDENCY_KEY: codex_dependency,
|
|
4486
|
+
},
|
|
4187
4487
|
)
|
|
4188
4488
|
codex_built_this_tick = True
|
|
4189
4489
|
except Exception:
|
|
@@ -4199,6 +4499,29 @@ def _tui_build_source_bundle(
|
|
|
4199
4499
|
if prior_codex is not None
|
|
4200
4500
|
else unavailable_source_state("codex", warning)
|
|
4201
4501
|
)
|
|
4502
|
+
codex_health = (
|
|
4503
|
+
codex.metadata_health
|
|
4504
|
+
if isinstance(codex.metadata_health, Mapping)
|
|
4505
|
+
else {}
|
|
4506
|
+
)
|
|
4507
|
+
codex_retry_entry = _PARTIAL_RETRY_STATE.get("codex")
|
|
4508
|
+
if codex_built_this_tick and (
|
|
4509
|
+
codex.availability != "partial"
|
|
4510
|
+
or bool(codex_health.get("retryable"))
|
|
4511
|
+
or (
|
|
4512
|
+
codex_health.get("state") == "healthy"
|
|
4513
|
+
and getattr(codex_retry_entry, "cause", None)
|
|
4514
|
+
== "metadata_incomplete"
|
|
4515
|
+
)
|
|
4516
|
+
):
|
|
4517
|
+
# A forced rebuild bypasses `_tui_retain_refused_partial`. Once it
|
|
4518
|
+
# publishes healthy metadata, the prior metadata retry is obsolete
|
|
4519
|
+
# even when an independent projection condition keeps the overall
|
|
4520
|
+
# provider partial. Never clear another partial cause on that
|
|
4521
|
+
# metadata fact alone.
|
|
4522
|
+
_PARTIAL_RETRY_STATE = clear_partial_retry(
|
|
4523
|
+
_PARTIAL_RETRY_STATE, provider="codex",
|
|
4524
|
+
)
|
|
4202
4525
|
# #350 spec §3.3: clock Codex UNCONDITIONALLY — after every build /
|
|
4203
4526
|
# reuse / degrade branch and before composition — so the retained
|
|
4204
4527
|
# cycle's expiry invariant holds on EVERY path, including the reuse
|
|
@@ -4244,6 +4567,14 @@ def _tui_build_source_bundle(
|
|
|
4244
4567
|
# The cache pin ended after carrier capture. These cheap post-build
|
|
4245
4568
|
# signatures intentionally reject only stats-side movement; a cache
|
|
4246
4569
|
# commit during the pure folds belongs to the next generation.
|
|
4570
|
+
#
|
|
4571
|
+
# The two digests below may be served by the warm-digest memo, and that
|
|
4572
|
+
# is safe only because its key is the stats file's own write identity:
|
|
4573
|
+
# any commit that lands mid-build moves it, whether or not it moved the
|
|
4574
|
+
# journal cursor, because an entry is stored only for a settled file
|
|
4575
|
+
# and the settle window guarantees the next commit a newer timestamp
|
|
4576
|
+
# even on a coarse-timestamp filesystem. So these reads rescan and see
|
|
4577
|
+
# the new generation (#857; `_stats_relations_digest_signal`).
|
|
4247
4578
|
with _perf.phase("source.reconciliation"):
|
|
4248
4579
|
post_stats_digest = codex_stats_digest(stats_conn)
|
|
4249
4580
|
post_accounts_digest = accounts_identity_digest(stats_conn)
|
|
@@ -4266,6 +4597,16 @@ def _tui_build_source_bundle(
|
|
|
4266
4597
|
)
|
|
4267
4598
|
if stats_generation_moved:
|
|
4268
4599
|
if prior_bundle is not None:
|
|
4600
|
+
# #857: the prior bundle, never re-validated by this build.
|
|
4601
|
+
# When this build scheduled the Codex pass, that pass may have
|
|
4602
|
+
# consumed the pending accounting — so the retained Codex
|
|
4603
|
+
# generation loses its provenance and the next tick rebuilds it
|
|
4604
|
+
# once, whichever path it takes
|
|
4605
|
+
# (`_tui_withdraw_codex_quota_dependency`). A build that reused
|
|
4606
|
+
# or degraded Codex consumed nothing, and the retained stamp is
|
|
4607
|
+
# exactly as current as before.
|
|
4608
|
+
if codex_pass_attempted:
|
|
4609
|
+
return _tui_withdraw_codex_quota_dependency(prior_bundle)
|
|
4269
4610
|
return prior_bundle
|
|
4270
4611
|
raise RuntimeError("source read generation moved during build")
|
|
4271
4612
|
return bundle
|
|
@@ -4317,7 +4658,13 @@ def _tui_hydrating_source_bundle() -> SourceDashboardBundle:
|
|
|
4317
4658
|
)
|
|
4318
4659
|
|
|
4319
4660
|
|
|
4320
|
-
def _tui_source_bundle_can_idle(
|
|
4661
|
+
def _tui_source_bundle_can_idle(
|
|
4662
|
+
bundle: SourceDashboardBundle | None,
|
|
4663
|
+
*,
|
|
4664
|
+
codex_dependency: "tuple | None",
|
|
4665
|
+
codex_ingest_degraded: bool = False,
|
|
4666
|
+
now_utc: dt.datetime | None = None,
|
|
4667
|
+
) -> bool:
|
|
4321
4668
|
"""Return whether both physical provider generations are safe to retain.
|
|
4322
4669
|
|
|
4323
4670
|
A stable dispatch key alone cannot prove that a previously unavailable or
|
|
@@ -4325,13 +4672,74 @@ def _tui_source_bundle_can_idle(bundle: SourceDashboardBundle | None) -> bool:
|
|
|
4325
4672
|
repaired without changing accounting facts. Such a source must take the
|
|
4326
4673
|
full source-bundle path again; that path still independently reuses the
|
|
4327
4674
|
healthy provider by its own data version.
|
|
4675
|
+
|
|
4676
|
+
#857: a Codex generation of ANY availability idles only on current
|
|
4677
|
+
provenance (`_tui_codex_dependency_current`): the identity it was built
|
|
4678
|
+
from equals ``codex_dependency``, the identity read this tick, and its
|
|
4679
|
+
accounting provenance token equals the token of the population the process
|
|
4680
|
+
last consumed. The key being unchanged establishes neither. A generation
|
|
4681
|
+
retained across an abandoned rebuild may be memoized under a key naming
|
|
4682
|
+
inputs it never read (its stamp is then WITHDRAWN), or may have had its
|
|
4683
|
+
accounting consumed by the attempt (its token is then behind the consumed
|
|
4684
|
+
one); one built while the identity was unreadable carries no stamp. Any of
|
|
4685
|
+
them would otherwise be retained for as long as the key stays unchanged.
|
|
4686
|
+
Refused, it takes the bounded source adapter, which rebuilds Codex once and
|
|
4687
|
+
stamps the result, after which it idles again. ``codex_dependency`` is
|
|
4688
|
+
REQUIRED, with no default: ``None`` idles no Codex generation, so a caller
|
|
4689
|
+
that could omit it would silently disable Codex idling.
|
|
4690
|
+
|
|
4691
|
+
ONE degraded Codex shape is exempt from the availability refusal — the
|
|
4692
|
+
settled cycle-unavailable generation (``settled_codex_cycle_unavailable``),
|
|
4693
|
+
whose rebuild re-derives the same partial state on every tick. It also
|
|
4694
|
+
needs ``now_utc`` with its decision deadline not passed, and no Codex
|
|
4695
|
+
ingest failed or contended this tick. Entering the exception clears
|
|
4696
|
+
Codex's retry key, never Claude's. The retryable-carrier scan still runs
|
|
4697
|
+
first, for both providers.
|
|
4328
4698
|
"""
|
|
4699
|
+
global _PARTIAL_RETRY_STATE
|
|
4329
4700
|
if not isinstance(bundle, SourceDashboardBundle):
|
|
4330
4701
|
return False
|
|
4702
|
+
physical_states: dict[str, SourceDashboardState] = {}
|
|
4331
4703
|
for source in ("claude", "codex"):
|
|
4332
4704
|
state = bundle.sources.get(source)
|
|
4333
4705
|
if not isinstance(state, SourceDashboardState):
|
|
4334
4706
|
return False
|
|
4707
|
+
physical_states[source] = state
|
|
4708
|
+
retryable_carrier = False
|
|
4709
|
+
for source, state in physical_states.items():
|
|
4710
|
+
# #846 §4.6: the retryable carrier is authoritative and must be read
|
|
4711
|
+
# for BOTH providers before any broader availability refusal. A real
|
|
4712
|
+
# transient metadata generation is partial, so checking availability
|
|
4713
|
+
# source-by-source could return on Claude without clearing Codex's
|
|
4714
|
+
# armed key. Refuse AND clear each retryable provider at the idle
|
|
4715
|
+
# boundary; provider-keyed clearing leaves unrelated deadlines intact.
|
|
4716
|
+
health = state.metadata_health
|
|
4717
|
+
if health is not None and bool(health.get("retryable")):
|
|
4718
|
+
_PARTIAL_RETRY_STATE = clear_partial_retry(
|
|
4719
|
+
_PARTIAL_RETRY_STATE, provider=source,
|
|
4720
|
+
)
|
|
4721
|
+
retryable_carrier = True
|
|
4722
|
+
if retryable_carrier:
|
|
4723
|
+
return False
|
|
4724
|
+
settled_codex = False
|
|
4725
|
+
for source, state in physical_states.items():
|
|
4726
|
+
if source == "codex" and not _tui_codex_dependency_current(
|
|
4727
|
+
state, codex_dependency=codex_dependency,
|
|
4728
|
+
):
|
|
4729
|
+
# #857: no current provenance, whatever the availability.
|
|
4730
|
+
return False
|
|
4731
|
+
if (
|
|
4732
|
+
source == "codex"
|
|
4733
|
+
and now_utc is not None
|
|
4734
|
+
and not codex_ingest_degraded
|
|
4735
|
+
and _tui_codex_settled_cycle_holds(
|
|
4736
|
+
state, codex_dependency=codex_dependency, now_utc=now_utc,
|
|
4737
|
+
)
|
|
4738
|
+
):
|
|
4739
|
+
# #857: the settled cycle-unavailable generation over unchanged
|
|
4740
|
+
# inputs. Its predicate already excludes a failed aggregate fold.
|
|
4741
|
+
settled_codex = True
|
|
4742
|
+
continue
|
|
4335
4743
|
# Idle eligibility is provider-generation coherence, deliberately not a
|
|
4336
4744
|
# hero/quota/sessions age aggregate.
|
|
4337
4745
|
if (state.availability not in ("ok", "empty")
|
|
@@ -4346,31 +4754,13 @@ def _tui_source_bundle_can_idle(bundle: SourceDashboardBundle | None) -> bool:
|
|
|
4346
4754
|
# rebuild per tick, so it creates no retry loop.
|
|
4347
4755
|
if aggregate_scope_failed(state):
|
|
4348
4756
|
return False
|
|
4349
|
-
|
|
4350
|
-
#
|
|
4351
|
-
#
|
|
4352
|
-
#
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
# 47 seconds across roughly twenty generations with no rebuild.
|
|
4357
|
-
#
|
|
4358
|
-
# `_lib_dashboard_sources._reusable_provider` refuses the same
|
|
4359
|
-
# generation, and refusing it there is NOT sufficient: this branch
|
|
4360
|
-
# republishes through the clock refreshes without ever calling
|
|
4361
|
-
# `reuse_coherent_source_state`, so the predicate is bypassed before it
|
|
4362
|
-
# is consulted. Both legs are required, and the browser gate found this
|
|
4363
|
-
# one by reading the path the first fix did not reach.
|
|
4364
|
-
health = state.metadata_health
|
|
4365
|
-
if health is not None and bool(health.get("retryable")):
|
|
4366
|
-
# #846 §4.6: refuse AND clear any armed key, so the tick after the
|
|
4367
|
-
# fault clears rebuilds rather than finding a key still inside its
|
|
4368
|
-
# deadline.
|
|
4369
|
-
global _PARTIAL_RETRY_STATE
|
|
4370
|
-
_PARTIAL_RETRY_STATE = clear_partial_retry(
|
|
4371
|
-
_PARTIAL_RETRY_STATE, provider=source,
|
|
4372
|
-
)
|
|
4373
|
-
return False
|
|
4757
|
+
if settled_codex:
|
|
4758
|
+
# #857: the exception bypasses `_tui_retain_refused_partial`, so it
|
|
4759
|
+
# retires an obsolete Codex key itself (see the active-path twin in
|
|
4760
|
+
# `_tui_build_source_bundle`).
|
|
4761
|
+
_PARTIAL_RETRY_STATE = clear_partial_retry(
|
|
4762
|
+
_PARTIAL_RETRY_STATE, provider="codex",
|
|
4763
|
+
)
|
|
4374
4764
|
return True
|
|
4375
4765
|
|
|
4376
4766
|
|
|
@@ -4786,9 +5176,16 @@ def _tui_build_snapshot_once(
|
|
|
4786
5176
|
except Exception as exc:
|
|
4787
5177
|
capture_failure("prior-source-bundle", "other", exc)
|
|
4788
5178
|
dispatch_sig = None
|
|
5179
|
+
# #857: the Codex quota-dependency identity, read on the
|
|
5180
|
+
# signature's own cache handle. An unreadable identity is `None`.
|
|
5181
|
+
codex_dependency = None
|
|
4789
5182
|
with _perf.phase("signature"):
|
|
4790
5183
|
try:
|
|
4791
|
-
dispatch_sig =
|
|
5184
|
+
dispatch_sig, codex_dependency = (
|
|
5185
|
+
_tui_compute_dispatch_signature(
|
|
5186
|
+
conn, with_codex_dependency=True,
|
|
5187
|
+
)
|
|
5188
|
+
)
|
|
4792
5189
|
except QuotaProjectionIncomplete as exc:
|
|
4793
5190
|
# #496 S5b §4.7: ahead of the generic handler on purpose.
|
|
4794
5191
|
# Below it the refusal is sanitized into a generic
|
|
@@ -4796,9 +5193,11 @@ def _tui_build_snapshot_once(
|
|
|
4796
5193
|
# remedy; the message this leg records carries both.
|
|
4797
5194
|
capture_failure("quota-projection", "other", exc)
|
|
4798
5195
|
dispatch_sig = None
|
|
5196
|
+
codex_dependency = None
|
|
4799
5197
|
except Exception as exc:
|
|
4800
5198
|
capture_failure("dispatch-signature", "stats_or_cache", exc)
|
|
4801
5199
|
dispatch_sig = None
|
|
5200
|
+
codex_dependency = None
|
|
4802
5201
|
if dispatch_sig is not None:
|
|
4803
5202
|
# The idle decision keys on the DB signature AND a render key
|
|
4804
5203
|
# that captures the config-derived inputs the composite
|
|
@@ -4814,7 +5213,15 @@ def _tui_build_snapshot_once(
|
|
|
4814
5213
|
display_tz_pref_override,
|
|
4815
5214
|
json.dumps(raw_config, sort_keys=True, default=str),
|
|
4816
5215
|
)
|
|
4817
|
-
|
|
5216
|
+
# #857: the Codex quota-dependency identity — the dedicated
|
|
5217
|
+
# quota change ledger, the attribution revision, the projection
|
|
5218
|
+
# certificate and the cache file's identity, none of which the
|
|
5219
|
+
# signature above carries. It rides the dispatch KEY rather
|
|
5220
|
+
# than `SnapshotSignature`, so the published `data_version`
|
|
5221
|
+
# bytes and the signature's own contract are unchanged. `None`
|
|
5222
|
+
# (an unreadable identity) is a value like any other here; the
|
|
5223
|
+
# idle gate is what refuses to idle any Codex generation on it.
|
|
5224
|
+
dispatch_key = (dispatch_sig, render_key, codex_dependency)
|
|
4818
5225
|
# #300: carry the change signal onto the non-idle snapshot built
|
|
4819
5226
|
# below. (The idle path returns `idle_snap`, which inherits the
|
|
4820
5227
|
# prior — equal, since idle ⇒ signature unchanged — value.)
|
|
@@ -4848,6 +5255,7 @@ def _tui_build_snapshot_once(
|
|
|
4848
5255
|
claude_ingest_contended=claude_ingest_contended,
|
|
4849
5256
|
claude_ingest_failed=claude_ingest_failed,
|
|
4850
5257
|
errors=errors,
|
|
5258
|
+
codex_dependency=codex_dependency,
|
|
4851
5259
|
)
|
|
4852
5260
|
assert _sc is not None
|
|
4853
5261
|
_sc.store_dispatch_state(dispatch_key, idle_snap)
|
|
@@ -5465,12 +5873,20 @@ def _tui_build_snapshot_once(
|
|
|
5465
5873
|
# source panel with no cause and no remedy stated. The message
|
|
5466
5874
|
# this leg records carries both.
|
|
5467
5875
|
capture_failure("quota-projection", "other", exc)
|
|
5468
|
-
source_bundle =
|
|
5876
|
+
source_bundle = _tui_retain_failed_source_bundle(
|
|
5877
|
+
prior_source_bundle, dispatch_key=dispatch_key,
|
|
5878
|
+
)
|
|
5469
5879
|
except Exception as exc:
|
|
5470
5880
|
# Public source warnings are stable/sanitized; the detailed
|
|
5471
5881
|
# exception remains only on the internal rebuild-error string.
|
|
5882
|
+
# #857: `capture_failure` can raise `_StatsSnapshotCorruption`
|
|
5883
|
+
# before the retention below; the prior generation's accounting
|
|
5884
|
+
# provenance token refuses it on the heal's retry if this
|
|
5885
|
+
# build consumed accounting.
|
|
5472
5886
|
capture_failure("source-bundle", "stats_or_cache", exc)
|
|
5473
|
-
source_bundle =
|
|
5887
|
+
source_bundle = _tui_retain_failed_source_bundle(
|
|
5888
|
+
prior_source_bundle, dispatch_key=dispatch_key,
|
|
5889
|
+
)
|
|
5474
5890
|
snap = dataclasses.replace(
|
|
5475
5891
|
snap,
|
|
5476
5892
|
last_sync_error=("; ".join(errors) if errors else None),
|
|
@@ -5587,7 +6003,35 @@ def _tui_precompute_envelope_config(raw_config: dict) -> dict:
|
|
|
5587
6003
|
}
|
|
5588
6004
|
|
|
5589
6005
|
|
|
5590
|
-
def
|
|
6006
|
+
def _tui_open_observed_cache(c):
|
|
6007
|
+
"""The dispatch read's cache handle, opened after observing its file (#857).
|
|
6008
|
+
|
|
6009
|
+
``(opened_file, conn)``: `_tui_cache_file_before_open`'s observation and
|
|
6010
|
+
the handle opened immediately after it, so the Codex quota-dependency
|
|
6011
|
+
identity read on ``conn`` is bound to ``opened_file`` strictly.
|
|
6012
|
+
|
|
6013
|
+
When that observation fails, the handle has opened anyway, and the usual
|
|
6014
|
+
cause is the absence this very open repaired: a fresh install's first
|
|
6015
|
+
dispatch read is what creates cache.db. Nothing binds an identity to that
|
|
6016
|
+
handle — a stat taken after an open cannot show which file the open got —
|
|
6017
|
+
so it is closed, the file observed again, and a second handle opened,
|
|
6018
|
+
which is bound exactly like any other. Without that, a fresh install's
|
|
6019
|
+
first dispatch key would carry no identity and its second would carry one,
|
|
6020
|
+
and the second tick would pay a full rebuild for nothing. A second failed
|
|
6021
|
+
observation is final: ``opened_file`` stays ``None`` and so does the
|
|
6022
|
+
identity (rebuild). The extra open happens only on a tick whose file could
|
|
6023
|
+
not be observed, never on an ordinary tick.
|
|
6024
|
+
"""
|
|
6025
|
+
opened_file = _tui_cache_file_before_open()
|
|
6026
|
+
conn = c.open_cache_db()
|
|
6027
|
+
if opened_file is not None:
|
|
6028
|
+
return opened_file, conn
|
|
6029
|
+
conn.close()
|
|
6030
|
+
opened_file = _tui_cache_file_before_open()
|
|
6031
|
+
return opened_file, c.open_cache_db()
|
|
6032
|
+
|
|
6033
|
+
|
|
6034
|
+
def _tui_compute_dispatch_signature(stats_conn, *, with_codex_dependency=False):
|
|
5591
6035
|
"""Composite data-version signature for the three-path dispatch (#268 M5.1).
|
|
5592
6036
|
|
|
5593
6037
|
Cheap ``MAX(id)`` b-tree descents over cache.db + stats.db, the reset-event
|
|
@@ -5597,6 +6041,15 @@ def _tui_compute_dispatch_signature(stats_conn):
|
|
|
5597
6041
|
``compute_signature`` never raises (each leg degrades to 0 on a missing
|
|
5598
6042
|
table); a cache-open failure propagates so the caller skips the idle path
|
|
5599
6043
|
and does a full rebuild.
|
|
6044
|
+
|
|
6045
|
+
``with_codex_dependency`` (#857) returns ``(signature, codex_dependency)``
|
|
6046
|
+
instead: the Codex quota-dependency identity read on the SAME throwaway
|
|
6047
|
+
handle (`_tui_open_observed_cache`), strictly bound to the file observed
|
|
6048
|
+
before that handle opened, and read in the SAME read transaction as the
|
|
6049
|
+
signature's cache legs — so a commit landing between the two cannot pair
|
|
6050
|
+
an older signature with a newer identity in one dispatch key. The tick
|
|
6051
|
+
pays no second connection for it. ``None`` when it cannot be established,
|
|
6052
|
+
which the idle gate treats as "rebuild".
|
|
5600
6053
|
"""
|
|
5601
6054
|
c = _cctally()
|
|
5602
6055
|
sc = c._load_sibling("_lib_snapshot_cache")
|
|
@@ -5605,16 +6058,29 @@ def _tui_compute_dispatch_signature(stats_conn):
|
|
|
5605
6058
|
# projection tables from a pure kernel (#496 S5b section 4.7).
|
|
5606
6059
|
from _cctally_quota import assert_projection_readable
|
|
5607
6060
|
assert_projection_readable(stats_conn)
|
|
5608
|
-
|
|
6061
|
+
if with_codex_dependency:
|
|
6062
|
+
opened_file, cache_conn = _tui_open_observed_cache(c)
|
|
6063
|
+
else:
|
|
6064
|
+
opened_file, cache_conn = None, c.open_cache_db()
|
|
5609
6065
|
try:
|
|
5610
|
-
|
|
5611
|
-
|
|
5612
|
-
|
|
5613
|
-
|
|
5614
|
-
|
|
5615
|
-
|
|
5616
|
-
|
|
5617
|
-
|
|
6066
|
+
# One deferred read transaction on the cache handle: it pins nothing
|
|
6067
|
+
# until `compute_signature`'s first cache read, and every cache read
|
|
6068
|
+
# after that — the signature's legs and the identity's — shares that
|
|
6069
|
+
# snapshot. The stats legs stay statement-scoped, as before.
|
|
6070
|
+
with sc.one_read_snapshot(cache_conn):
|
|
6071
|
+
signature = sc.compute_signature(
|
|
6072
|
+
cache_conn,
|
|
6073
|
+
stats_conn,
|
|
6074
|
+
generation=sc.current_generation(),
|
|
6075
|
+
codex_stats_digest=codex_stats_digest(stats_conn),
|
|
6076
|
+
accounts_digest=accounts_identity_digest(stats_conn),
|
|
6077
|
+
claude_stats_digest=claude_stats_digest(stats_conn),
|
|
6078
|
+
)
|
|
6079
|
+
if not with_codex_dependency:
|
|
6080
|
+
return signature
|
|
6081
|
+
return signature, _tui_read_codex_quota_dependency(
|
|
6082
|
+
cache_conn, opened_file=opened_file,
|
|
6083
|
+
)
|
|
5618
6084
|
finally:
|
|
5619
6085
|
cache_conn.close()
|
|
5620
6086
|
|
|
@@ -5681,7 +6147,8 @@ def _tui_build_idle_snapshot(prior, *, now_utc, precompute_envelope,
|
|
|
5681
6147
|
claude_ingest_contended=False,
|
|
5682
6148
|
claude_ingest_failed=False,
|
|
5683
6149
|
failures=None,
|
|
5684
|
-
stats_heal_attempted=False
|
|
6150
|
+
stats_heal_attempted=False,
|
|
6151
|
+
codex_dependency):
|
|
5685
6152
|
"""Fresh snapshot reusing ``prior``'s heavy rows, re-patching only the
|
|
5686
6153
|
time-derived fields + the doctor payload / envelope precompute on each idle
|
|
5687
6154
|
tick (spec §3 idle path).
|
|
@@ -5708,6 +6175,10 @@ def _tui_build_idle_snapshot(prior, *, now_utc, precompute_envelope,
|
|
|
5708
6175
|
the idle path (the short-circuit only runs when the render key — which bundles
|
|
5709
6176
|
the full raw config — is unchanged), so recomputing is cheap small-JSON I/O,
|
|
5710
6177
|
not CPU, and byte-matches a full rebuild's envelope for the same state.
|
|
6178
|
+
|
|
6179
|
+
``codex_dependency`` (#857) is this tick's Codex quota-dependency identity,
|
|
6180
|
+
REQUIRED with no default for the reason `_tui_source_bundle_can_idle`
|
|
6181
|
+
states: ``None`` idles no Codex generation.
|
|
5711
6182
|
"""
|
|
5712
6183
|
import time
|
|
5713
6184
|
doctor_payload = prior.doctor_payload
|
|
@@ -5723,6 +6194,8 @@ def _tui_build_idle_snapshot(prior, *, now_utc, precompute_envelope,
|
|
|
5723
6194
|
errors.append(f"envelope-precompute: {exc}")
|
|
5724
6195
|
idle_failures = failures if failures is not None else []
|
|
5725
6196
|
source_bundle = prior.source_bundle
|
|
6197
|
+
# #857: whether the bounded source adapter below scheduled the Codex pass.
|
|
6198
|
+
source_attempt: dict[str, bool] = {}
|
|
5726
6199
|
if source_bundle is not None:
|
|
5727
6200
|
try:
|
|
5728
6201
|
prior_claude = source_bundle.sources["claude"]
|
|
@@ -5731,8 +6204,21 @@ def _tui_build_idle_snapshot(prior, *, now_utc, precompute_envelope,
|
|
|
5731
6204
|
# the idle clock is no longer entitled to speak for the cycle — its
|
|
5732
6205
|
# public-history view cannot re-resolve it — so fall through to the
|
|
5733
6206
|
# bounded source-adapter path, which rebuilds Codex authoritatively.
|
|
6207
|
+
#
|
|
6208
|
+
# #857: `codex_dependency` is this tick's Codex quota-dependency
|
|
6209
|
+
# identity; a Codex generation of any availability idles only
|
|
6210
|
+
# while it equals the identity that generation was built from, and
|
|
6211
|
+
# the settled cycle-unavailable one never across a failed or
|
|
6212
|
+
# contended Codex ingest.
|
|
5734
6213
|
if (
|
|
5735
|
-
_tui_source_bundle_can_idle(
|
|
6214
|
+
_tui_source_bundle_can_idle(
|
|
6215
|
+
source_bundle,
|
|
6216
|
+
codex_dependency=codex_dependency,
|
|
6217
|
+
codex_ingest_degraded=(
|
|
6218
|
+
codex_ingest_failed or codex_ingest_contended
|
|
6219
|
+
),
|
|
6220
|
+
now_utc=now_utc,
|
|
6221
|
+
)
|
|
5736
6222
|
and not codex_decision_deadline_passed(prior_codex, now_utc)
|
|
5737
6223
|
):
|
|
5738
6224
|
claude = _refresh_claude_source_clock(
|
|
@@ -5794,6 +6280,7 @@ def _tui_build_idle_snapshot(prior, *, now_utc, precompute_envelope,
|
|
|
5794
6280
|
projects_envelope=prior.projects_envelope,
|
|
5795
6281
|
prior_bundle=source_bundle,
|
|
5796
6282
|
raw_config=raw_config,
|
|
6283
|
+
attempt=source_attempt,
|
|
5797
6284
|
)
|
|
5798
6285
|
except _StatsSnapshotCorruption:
|
|
5799
6286
|
raise
|
|
@@ -5806,7 +6293,13 @@ def _tui_build_idle_snapshot(prior, *, now_utc, precompute_envelope,
|
|
|
5806
6293
|
idle_failures.append(SyncFailureAttribution(
|
|
5807
6294
|
leg="quota-projection", database="other", corruption=False,
|
|
5808
6295
|
))
|
|
5809
|
-
|
|
6296
|
+
# #857: a failed adapter that scheduled the Codex pass may have
|
|
6297
|
+
# consumed accounting that committed after the dispatch read
|
|
6298
|
+
# (`_tui_withdraw_codex_quota_dependency`).
|
|
6299
|
+
source_bundle = (
|
|
6300
|
+
_tui_withdraw_codex_quota_dependency(prior.source_bundle)
|
|
6301
|
+
if source_attempt.get("codex_pass") else prior.source_bundle
|
|
6302
|
+
)
|
|
5810
6303
|
except Exception as exc: # noqa: BLE001 — retain prior complete bundle
|
|
5811
6304
|
# #496 S3 §8 (F16). This branch read stats through
|
|
5812
6305
|
# `source_stats_conn` and swallowed the failure into a plain
|
|
@@ -5828,7 +6321,15 @@ def _tui_build_idle_snapshot(prior, *, now_utc, precompute_envelope,
|
|
|
5828
6321
|
)
|
|
5829
6322
|
else:
|
|
5830
6323
|
errors.append(f"source-clock-refresh: {exc}")
|
|
5831
|
-
|
|
6324
|
+
# #857: as in the handler above. A failure escalated as stats
|
|
6325
|
+
# corruption raised out of `_tui_capture_sync_failure` before this
|
|
6326
|
+
# line and never reaches it; the retained generation's accounting
|
|
6327
|
+
# provenance token refuses it on the heal's retry instead
|
|
6328
|
+
# (`_tui_codex_accounting_provenance_current`).
|
|
6329
|
+
source_bundle = (
|
|
6330
|
+
_tui_withdraw_codex_quota_dependency(prior.source_bundle)
|
|
6331
|
+
if source_attempt.get("codex_pass") else prior.source_bundle
|
|
6332
|
+
)
|
|
5832
6333
|
return dataclasses.replace(
|
|
5833
6334
|
prior,
|
|
5834
6335
|
generated_at=now_utc,
|