cctally 1.118.1 → 1.118.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 +8 -0
- package/bin/_cctally_cache.py +465 -7
- package/bin/_cctally_dashboard.py +73 -7
- package/bin/_cctally_dashboard_sources.py +7 -0
- package/bin/_cctally_db.py +304 -0
- package/bin/_cctally_source_analytics.py +14 -3
- package/bin/_cctally_tui.py +189 -1
- package/bin/_lib_quota.py +31 -0
- package/bin/_lib_snapshot_cache.py +480 -1
- package/package.json +1 -1
package/bin/_cctally_tui.py
CHANGED
|
@@ -4774,6 +4774,10 @@ def _tui_build_source_bundle(
|
|
|
4774
4774
|
codex_version,
|
|
4775
4775
|
prior_codex.combined_accounting if prior_codex is not None else None,
|
|
4776
4776
|
)
|
|
4777
|
+
# #1059 ack (b): set only where this build constructs the Claude
|
|
4778
|
+
# state or reuses it at its own version; a degraded, retained or
|
|
4779
|
+
# unavailable state never qualifies.
|
|
4780
|
+
claude_published_current = False
|
|
4777
4781
|
if claude_ingest_failed:
|
|
4778
4782
|
warning = SourceDashboardWarning(
|
|
4779
4783
|
"source_ingest_failed", "Source ingest failed.", "ingest",
|
|
@@ -4796,6 +4800,9 @@ def _tui_build_source_bundle(
|
|
|
4796
4800
|
claude = reuse_coherent_source_state(
|
|
4797
4801
|
prior_claude, data_version=claude_reuse_version,
|
|
4798
4802
|
)
|
|
4803
|
+
# #1059 ack (b): an exact-version reuse at this build's own
|
|
4804
|
+
# version is current; a retained refused partial is not.
|
|
4805
|
+
claude_published_current = claude is not None
|
|
4799
4806
|
# #834 S2 (#830): exact-version reuse now refuses every `partial`
|
|
4800
4807
|
# prior. The kernel decides whether a REPEATED retainable cause
|
|
4801
4808
|
# may nonetheless be retained for this tick, so refusing reuse
|
|
@@ -4808,6 +4815,7 @@ def _tui_build_source_bundle(
|
|
|
4808
4815
|
data_version=claude_reuse_version,
|
|
4809
4816
|
):
|
|
4810
4817
|
claude = prior_claude
|
|
4818
|
+
claude_published_current = False
|
|
4811
4819
|
# #556 S2 §3.6, gate 2 of 2. The version fragment above already
|
|
4812
4820
|
# rejects a failed generation, but this gate is stated explicitly
|
|
4813
4821
|
# rather than left implicit in string arithmetic: exact-version
|
|
@@ -5001,6 +5009,7 @@ def _tui_build_source_bundle(
|
|
|
5001
5009
|
cache_conn.rollback()
|
|
5002
5010
|
cache_read_tx = False
|
|
5003
5011
|
if claude is None:
|
|
5012
|
+
claude_published_current = True
|
|
5004
5013
|
claude_available = "ok" if (claude_cost_usd or claude_total_tokens) else "empty"
|
|
5005
5014
|
# #341 Task 4 (Ruling C): the conditional per-account Claude wire,
|
|
5006
5015
|
# symmetric with Codex. Built ONLY when the Claude provider has >1
|
|
@@ -5331,6 +5340,10 @@ def _tui_build_source_bundle(
|
|
|
5331
5340
|
return _tui_withdraw_codex_quota_dependency(prior_bundle)
|
|
5332
5341
|
return prior_bundle
|
|
5333
5342
|
raise RuntimeError("source read generation moved during build")
|
|
5343
|
+
if attempt is not None:
|
|
5344
|
+
attempt["claude_published_current"] = bool(
|
|
5345
|
+
claude_published_current
|
|
5346
|
+
and not aggregate_scope_failed(claude))
|
|
5334
5347
|
return bundle
|
|
5335
5348
|
finally:
|
|
5336
5349
|
_record_cache_pin()
|
|
@@ -5642,6 +5655,134 @@ def _tui_build_snapshot_impl(
|
|
|
5642
5655
|
)
|
|
5643
5656
|
|
|
5644
5657
|
|
|
5658
|
+
def _tui_project_identity_now() -> float:
|
|
5659
|
+
"""The monotonic clock of the project-identity sweep (a test seam)."""
|
|
5660
|
+
return time.monotonic()
|
|
5661
|
+
|
|
5662
|
+
|
|
5663
|
+
def _tui_codex_regroup_selector(buckets: "frozenset"):
|
|
5664
|
+
"""Select retained Codex entries whose opaque key names a bucket in ``B``.
|
|
5665
|
+
|
|
5666
|
+
#1059 §6.2 step 4. The opaque key is per root, so the stale keys are
|
|
5667
|
+
computed once per root an entry carries. An entry whose key came from
|
|
5668
|
+
``git_json`` never matches, because it was not built from a bucket path.
|
|
5669
|
+
Over-selection costs a reload, never correctness.
|
|
5670
|
+
"""
|
|
5671
|
+
opaque_project_key = _cctally()._load_sibling(
|
|
5672
|
+
"_lib_source_analytics").opaque_project_key
|
|
5673
|
+
stale_by_root: "dict[str, frozenset]" = {}
|
|
5674
|
+
|
|
5675
|
+
def select(entry) -> bool:
|
|
5676
|
+
if not buckets:
|
|
5677
|
+
return False
|
|
5678
|
+
root = str(entry.source_root_key)
|
|
5679
|
+
stale = stale_by_root.get(root)
|
|
5680
|
+
if stale is None:
|
|
5681
|
+
stale = frozenset(
|
|
5682
|
+
opaque_project_key("codex", root, bucket) for bucket in buckets)
|
|
5683
|
+
stale_by_root[root] = stale
|
|
5684
|
+
return entry.project_key in stale
|
|
5685
|
+
|
|
5686
|
+
return select
|
|
5687
|
+
|
|
5688
|
+
|
|
5689
|
+
def _tui_revalidate_project_identities(
|
|
5690
|
+
*, now_monotonic: "float | None" = None,
|
|
5691
|
+
) -> int:
|
|
5692
|
+
"""Run the #1059 project-identity sweep when due; apply its flips.
|
|
5693
|
+
|
|
5694
|
+
Called once per snapshot build, after the single top-of-build ingest and
|
|
5695
|
+
before the dispatch read, for every build (the dashboard's periodic,
|
|
5696
|
+
``/api/sync`` and settings builds, and the TUI's). On a successful sweep
|
|
5697
|
+
with flips, with ``B`` its stale buckets, in this order: evict every cached
|
|
5698
|
+
closed week holding a bucket in ``B`` (totals included); reset the
|
|
5699
|
+
current-week slot iff it holds one; reset the whole-envelope memo; force
|
|
5700
|
+
the retained Codex entries keyed to a bucket in ``B`` to be re-qualified;
|
|
5701
|
+
bump the generation exactly once and record it as the pending regroup
|
|
5702
|
+
generation ``G``. The bump leaves the idle path in the same build and
|
|
5703
|
+
mismatches the range memo's ``base``, which refolds it cold. Nothing else
|
|
5704
|
+
is cleared.
|
|
5705
|
+
|
|
5706
|
+
Cadence retry: a successful due sweep without flips bumps once (and
|
|
5707
|
+
records the new ``G``) while a regroup is still pending or Codex
|
|
5708
|
+
re-qualification is outstanding, so a regroup whose rebuild failed is
|
|
5709
|
+
retried at most once per interval. A failed sweep never bumps; its
|
|
5710
|
+
deadline stays due and it retries itself.
|
|
5711
|
+
|
|
5712
|
+
Returns the build generation, read after these steps; the acknowledgement
|
|
5713
|
+
at the end of the build compares it with ``G``.
|
|
5714
|
+
"""
|
|
5715
|
+
c = _cctally()
|
|
5716
|
+
sc = c._load_sibling("_lib_snapshot_cache")
|
|
5717
|
+
now = _tui_project_identity_now() if now_monotonic is None else now_monotonic
|
|
5718
|
+
sweep = sc.revalidate_project_identities(
|
|
5719
|
+
resolve=c._resolve_project_key, now_monotonic=now)
|
|
5720
|
+
if sweep is not None:
|
|
5721
|
+
if sweep.flips:
|
|
5722
|
+
buckets = sweep.old_buckets
|
|
5723
|
+
try:
|
|
5724
|
+
sc.evict_projects_env_weeks_for_buckets(buckets)
|
|
5725
|
+
sc.reset_projects_env_current_if_holds(buckets)
|
|
5726
|
+
c._projects_reset_memo()
|
|
5727
|
+
sc.force_codex_accounting_paths(
|
|
5728
|
+
_tui_codex_regroup_selector(buckets))
|
|
5729
|
+
sc.bump_generation()
|
|
5730
|
+
sc.mark_project_regroup_pending(sc.current_generation())
|
|
5731
|
+
except BaseException:
|
|
5732
|
+
# The sweep already committed `{fresh}`; without the stale
|
|
5733
|
+
# identities back, no later sweep would see this flip and
|
|
5734
|
+
# the stale grouping would last until restart. The deadline
|
|
5735
|
+
# stays, so the next due sweep retries the whole flip.
|
|
5736
|
+
sc.restore_project_identity_flips(sweep)
|
|
5737
|
+
raise
|
|
5738
|
+
elif (
|
|
5739
|
+
sc.project_regroup_pending() is not None
|
|
5740
|
+
or sc.codex_accounting_work_outstanding()
|
|
5741
|
+
):
|
|
5742
|
+
sc.bump_generation()
|
|
5743
|
+
sc.note_project_identity_retry_bump()
|
|
5744
|
+
sc.mark_project_regroup_pending(sc.current_generation())
|
|
5745
|
+
return sc.current_generation()
|
|
5746
|
+
|
|
5747
|
+
|
|
5748
|
+
def _tui_acknowledge_project_regroup(
|
|
5749
|
+
*,
|
|
5750
|
+
build_generation: int,
|
|
5751
|
+
envelope_built: bool,
|
|
5752
|
+
published_claude=None,
|
|
5753
|
+
claude_published_current: bool = True,
|
|
5754
|
+
dashboard: bool,
|
|
5755
|
+
) -> None:
|
|
5756
|
+
"""Clear the pending regroup generation ``G`` once this build published it.
|
|
5757
|
+
|
|
5758
|
+
(a) Every path: this build constructed the projects envelope without an
|
|
5759
|
+
exception, and its build generation (read right after the sweep) is at
|
|
5760
|
+
least ``G``. (b) The dashboard path also requires that the published
|
|
5761
|
+
Claude source is the one this build constructed or reused at its own
|
|
5762
|
+
input version (``claude_published_current``), that its aggregate scope
|
|
5763
|
+
did not fail, and that the range memo committed under a generation at
|
|
5764
|
+
least ``G`` (its ``base[3]``), which needs both of its folds to succeed.
|
|
5765
|
+
A build that fails any condition leaves ``G`` set for the cadence retry.
|
|
5766
|
+
"""
|
|
5767
|
+
sc = _cctally()._load_sibling("_lib_snapshot_cache")
|
|
5768
|
+
pending = sc.project_regroup_pending()
|
|
5769
|
+
if pending is None:
|
|
5770
|
+
return
|
|
5771
|
+
if not envelope_built or build_generation < pending:
|
|
5772
|
+
return
|
|
5773
|
+
if dashboard:
|
|
5774
|
+
if not claude_published_current or published_claude is None:
|
|
5775
|
+
return
|
|
5776
|
+
if aggregate_scope_failed(published_claude):
|
|
5777
|
+
return
|
|
5778
|
+
state = sys.modules["_cctally_dashboard"]._CLAUDE_RANGE_AGGREGATE_MEMO.get(
|
|
5779
|
+
"state")
|
|
5780
|
+
base = state.get("base") if isinstance(state, dict) else None
|
|
5781
|
+
if not isinstance(base, tuple) or len(base) < 4 or int(base[3]) < pending:
|
|
5782
|
+
return
|
|
5783
|
+
sc.acknowledge_project_regroup()
|
|
5784
|
+
|
|
5785
|
+
|
|
5645
5786
|
def _tui_build_snapshot_once(
|
|
5646
5787
|
*,
|
|
5647
5788
|
now_utc: dt.datetime | None = None,
|
|
@@ -5834,6 +5975,29 @@ def _tui_build_snapshot_once(
|
|
|
5834
5975
|
# Force pure reads for every view builder below, independent of the
|
|
5835
5976
|
# caller's flag: the single ingest above is the only glob per tick.
|
|
5836
5977
|
skip_sync = True
|
|
5978
|
+
# ── #1059: project-identity sweep (cadence-gated, every build) ──
|
|
5979
|
+
# After the ingest and BEFORE the dispatch read, so a filesystem-only
|
|
5980
|
+
# flip bumps the generation this build's dispatch key reads and the
|
|
5981
|
+
# regroup is published by this same build. Outside the
|
|
5982
|
+
# `precompute_envelope` branch and not gated on the caller's
|
|
5983
|
+
# `skip_sync`: an `/api/sync`, settings or TUI build sweeps when due.
|
|
5984
|
+
# `regroup_build_generation` is read once here, before any envelope
|
|
5985
|
+
# construction, and is what the acknowledgement compares with `G`.
|
|
5986
|
+
# Its own phase: a due sweep re-resolves every registered spelling
|
|
5987
|
+
# through the git-root resolver, which is filesystem work that grows
|
|
5988
|
+
# with the project corpus and would otherwise land in the root's
|
|
5989
|
+
# unattributed remainder.
|
|
5990
|
+
with _perf.phase("project_identity"):
|
|
5991
|
+
try:
|
|
5992
|
+
regroup_build_generation = _tui_revalidate_project_identities()
|
|
5993
|
+
except Exception as exc:
|
|
5994
|
+
capture_failure("project-identity", "other", exc)
|
|
5995
|
+
regroup_build_generation = _cctally()._load_sibling(
|
|
5996
|
+
"_lib_snapshot_cache").current_generation()
|
|
5997
|
+
projects_envelope_built = False
|
|
5998
|
+
# The out-parameter of `_tui_build_source_bundle`: whether the Claude
|
|
5999
|
+
# source it published is current for this build (#1059 ack (b)).
|
|
6000
|
+
source_attempt: dict[str, bool] = {}
|
|
5837
6001
|
# ── #269 M3.1: shared per-weekref immutable-cost cache (spec §4/§6) ──
|
|
5838
6002
|
# Gated OFF by default; flipped ON only on the dashboard sync-thread
|
|
5839
6003
|
# NON-IDLE path below, AFTER the once-per-rebuild `reconcile_weekref_cache`
|
|
@@ -6459,7 +6623,12 @@ def _tui_build_snapshot_once(
|
|
|
6459
6623
|
content_revision_leg=(
|
|
6460
6624
|
claude_input_identity[0].claude_content_revision
|
|
6461
6625
|
if claude_input_identity is not None else None),
|
|
6626
|
+
# #1059: this owner build retains the result through the
|
|
6627
|
+
# envelope memo, so its resolutions feed the identity sweep.
|
|
6628
|
+
project_resolver_cache=c._load_sibling(
|
|
6629
|
+
"_lib_snapshot_cache").observing_project_memo(),
|
|
6462
6630
|
)
|
|
6631
|
+
projects_envelope_built = True
|
|
6463
6632
|
except Exception as exc:
|
|
6464
6633
|
capture_failure("projects-envelope", "stats_or_cache", exc)
|
|
6465
6634
|
finally:
|
|
@@ -6504,7 +6673,10 @@ def _tui_build_snapshot_once(
|
|
|
6504
6673
|
if bp and k and bp not in key_by_bucket_path:
|
|
6505
6674
|
key_by_bucket_path[bp] = k
|
|
6506
6675
|
_resolve = _cctally()._resolve_project_key
|
|
6507
|
-
|
|
6676
|
+
# #1059: an observing memo, so the identity sweep covers the
|
|
6677
|
+
# spellings this binding resolves on the owner.
|
|
6678
|
+
resolver_cache: dict = _cctally()._load_sibling(
|
|
6679
|
+
"_lib_snapshot_cache").observing_project_memo()
|
|
6508
6680
|
annotated: list[TuiSessionRow] = []
|
|
6509
6681
|
for srow in sessions:
|
|
6510
6682
|
pkey = None
|
|
@@ -6702,6 +6874,7 @@ def _tui_build_snapshot_once(
|
|
|
6702
6874
|
evidence_eligible_at=evidence_eligible_at,
|
|
6703
6875
|
claude_input_identity=claude_input_identity,
|
|
6704
6876
|
codex_speed=codex_speed,
|
|
6877
|
+
attempt=source_attempt,
|
|
6705
6878
|
)
|
|
6706
6879
|
if source_bundle is None:
|
|
6707
6880
|
raise RuntimeError("source bundle builder returned no bundle")
|
|
@@ -6741,6 +6914,21 @@ def _tui_build_snapshot_once(
|
|
|
6741
6914
|
_cctally()._load_sibling("_lib_snapshot_cache").store_dispatch_state(
|
|
6742
6915
|
dispatch_key, snap,
|
|
6743
6916
|
)
|
|
6917
|
+
# #1059: acknowledge a pending regroup only once this build published
|
|
6918
|
+
# it — on the dashboard after the snapshot is stored above, and only
|
|
6919
|
+
# when a dispatch key let it be stored at all.
|
|
6920
|
+
if not precompute_envelope or dispatch_key is not None:
|
|
6921
|
+
published_bundle = snap.source_bundle
|
|
6922
|
+
_tui_acknowledge_project_regroup(
|
|
6923
|
+
build_generation=regroup_build_generation,
|
|
6924
|
+
envelope_built=projects_envelope_built,
|
|
6925
|
+
published_claude=(
|
|
6926
|
+
published_bundle.sources.get("claude")
|
|
6927
|
+
if published_bundle is not None else None),
|
|
6928
|
+
claude_published_current=bool(
|
|
6929
|
+
source_attempt.get("claude_published_current")),
|
|
6930
|
+
dashboard=precompute_envelope,
|
|
6931
|
+
)
|
|
6744
6932
|
# #276 perf: close the "snapshot" root and freeze the completed tree
|
|
6745
6933
|
# into the process-global slot for the loopback /api/debug/backend
|
|
6746
6934
|
# endpoint. Whole-dict atomic assignment; never mutated after. No-op
|
package/bin/_lib_quota.py
CHANGED
|
@@ -252,6 +252,37 @@ class ResetAnchorComponents:
|
|
|
252
252
|
for raw in raws:
|
|
253
253
|
self.add(raw)
|
|
254
254
|
|
|
255
|
+
@classmethod
|
|
256
|
+
def from_evidence(
|
|
257
|
+
cls, entries: "Iterable[tuple[dt.datetime, tuple[str, int, int], int]]",
|
|
258
|
+
*, tolerance_seconds: int = CODEX_RESET_ANCHOR_TOLERANCE_SECONDS,
|
|
259
|
+
) -> "ResetAnchorComponents":
|
|
260
|
+
"""Rebuild a group's components from its per-raw evidence (#1033).
|
|
261
|
+
|
|
262
|
+
Each entry is ``(instant, order_key, row_count)``: one raw reset
|
|
263
|
+
spelling's parsed instant, the exact minimum order key of its rows and
|
|
264
|
+
how many rows carry it. Adding the entries in ascending ``order_key``
|
|
265
|
+
order reproduces the one-``add``-per-row seed exactly: a member enters
|
|
266
|
+
when its minimum key is reached, so members, member orders, anchors and
|
|
267
|
+
iteration order match, and a later row of an existing instant never
|
|
268
|
+
changes anything the minimum did not already set. ``_next_order``
|
|
269
|
+
becomes the total row count, the value the per-row seed leaves, so an
|
|
270
|
+
order-key-less ``add`` afterwards draws the same order.
|
|
271
|
+
"""
|
|
272
|
+
materialized = list(entries)
|
|
273
|
+
total = 0
|
|
274
|
+
for _instant, _order_key, row_count in materialized:
|
|
275
|
+
if (not isinstance(row_count, int) or isinstance(row_count, bool)
|
|
276
|
+
or row_count < 1):
|
|
277
|
+
raise ValueError("row_count must be a positive int")
|
|
278
|
+
total += row_count
|
|
279
|
+
components = cls(tolerance_seconds=tolerance_seconds)
|
|
280
|
+
for instant, order_key, _row_count in sorted(
|
|
281
|
+
materialized, key=lambda entry: entry[1]):
|
|
282
|
+
components.add(instant, order_key=order_key)
|
|
283
|
+
components._next_order = total
|
|
284
|
+
return components
|
|
285
|
+
|
|
255
286
|
@property
|
|
256
287
|
def tolerance_seconds(self) -> int:
|
|
257
288
|
return self._tolerance
|