cctally 1.116.1 → 1.117.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +47 -0
- package/README.md +4 -6
- package/bin/_cctally_account.py +57 -3
- package/bin/_cctally_cache.py +5720 -885
- package/bin/_cctally_claude_attribution.py +997 -0
- package/bin/_cctally_completion.py +101 -28
- package/bin/_cctally_core.py +235 -12
- package/bin/_cctally_dashboard.py +263 -17
- package/bin/_cctally_dashboard_conversation.py +107 -21
- package/bin/_cctally_dashboard_envelope.py +44 -0
- package/bin/_cctally_db.py +928 -80
- package/bin/_cctally_diagnosis_sources.py +78 -13
- package/bin/_cctally_doctor.py +211 -86
- package/bin/_cctally_five_hour.py +446 -22
- package/bin/_cctally_forecast.py +302 -6
- package/bin/_cctally_journal.py +2418 -158
- package/bin/_cctally_milestone_history.py +379 -91
- package/bin/_cctally_oauth_samples.py +185 -7
- package/bin/_cctally_parser.py +38 -0
- package/bin/_cctally_project.py +34 -8
- package/bin/_cctally_quarantine.py +821 -343
- package/bin/_cctally_quota.py +69 -4
- package/bin/_cctally_quota_calibration.py +677 -3
- package/bin/_cctally_quota_model.py +927 -127
- package/bin/_cctally_record.py +922 -445
- package/bin/_cctally_rederive.py +119 -15
- package/bin/_cctally_statusline.py +1048 -176
- package/bin/_cctally_store.py +544 -110
- package/bin/_cctally_tui.py +455 -42
- package/bin/_cctally_update.py +77 -14
- package/bin/_lib_account_protocol.py +227 -19
- package/bin/_lib_claude_attribution.py +421 -0
- package/bin/_lib_clone_copy.py +30 -0
- package/bin/_lib_codex_conversation_query.py +45 -9
- package/bin/_lib_codex_conversation_watch.py +72 -13
- package/bin/_lib_codex_rollout_container.py +234 -19
- package/bin/_lib_codex_worker_lifecycle.py +374 -0
- package/bin/_lib_credit.py +22 -13
- package/bin/_lib_dashboard_sources.py +257 -18
- package/bin/_lib_detector_access_registry.py +1335 -166
- package/bin/_lib_doctor.py +276 -92
- package/bin/_lib_ingest_frontier.py +269 -36
- package/bin/_lib_journal.py +273 -22
- package/bin/_lib_jsonl.py +12 -0
- package/bin/_lib_meter_rate_change.py +72 -0
- package/bin/_lib_outcome_quarantine.py +917 -554
- package/bin/_lib_provider_activity.py +24 -6
- package/bin/_lib_quota_calibration.py +57 -3
- package/bin/_lib_quota_copy.py +7 -2
- package/bin/_lib_quota_lineage.py +613 -0
- package/bin/_lib_quota_model.py +464 -33
- package/bin/_lib_rate_change_delivery.py +16 -0
- package/bin/_lib_reclaim_planner.py +6 -4
- package/bin/_lib_rederive.py +2046 -21
- package/bin/_lib_selector_state.py +131 -3
- package/bin/_lib_snapshot_cache.py +8 -0
- package/bin/_lib_stale_proof.py +113 -4
- package/bin/_lib_statusline.py +72 -3
- package/bin/_lib_statusline_candidates.py +246 -42
- package/bin/_lib_tick_stats.py +10 -5
- package/bin/_lib_write_io.py +419 -6
- package/bin/cctally +28 -11
- package/dashboard/static/assets/{ConversationsView-Dflb_myA.js → ConversationsView-oqNsWzWV.js} +1 -1
- package/dashboard/static/assets/{DoctorModal-DFX5CfIs.js → DoctorModal-B0DnU9d9.js} +1 -1
- package/dashboard/static/assets/ModalRoot-CYua2wP_.js +1 -0
- package/dashboard/static/assets/{ProjectsDrillPanel-C3PJy1xx.js → ProjectsDrillPanel-DSxXQSa-.js} +1 -1
- package/dashboard/static/assets/{SourceDetailModal-BczsWKhD.js → SourceDetailModal-BleDpQqG.js} +1 -1
- package/dashboard/static/assets/{UpdateModal-Os-Lo5cs.js → UpdateModal-CMqaAkSK.js} +1 -1
- package/dashboard/static/assets/{index-CYZ3NLqF.js → index-BjQV-0-D.js} +3 -3
- package/dashboard/static/assets/index-D34eYhAc.css +1 -0
- package/dashboard/static/assets/{outlineNavigation-k-zIif6W.js → outlineNavigation-CO-VWBm7.js} +1 -1
- package/dashboard/static/assets/{useKeymap-CW7Up0Ay.js → useKeymap-CUgEnnua.js} +1 -1
- package/dashboard/static/dashboard.html +3 -3
- package/package.json +5 -1
- package/dashboard/static/assets/ModalRoot-HlxoagIa.js +0 -1
- package/dashboard/static/assets/index-BKITk1CT.css +0 -1
package/bin/_cctally_tui.py
CHANGED
|
@@ -183,6 +183,7 @@ from __future__ import annotations
|
|
|
183
183
|
import argparse
|
|
184
184
|
import dataclasses
|
|
185
185
|
import datetime as dt
|
|
186
|
+
import hashlib
|
|
186
187
|
import io
|
|
187
188
|
import json
|
|
188
189
|
import math
|
|
@@ -223,6 +224,7 @@ from _lib_display_tz import (
|
|
|
223
224
|
_compute_display_block,
|
|
224
225
|
)
|
|
225
226
|
from _lib_aggregators import _aggregate_monthly, codex_path_scope
|
|
227
|
+
from _lib_json_envelope import _iso_z
|
|
226
228
|
from _lib_fmt import _fmt_usd_accounting, stable_sum
|
|
227
229
|
# Opt-in backend phase-instrumentation collector (issue #276, Session A). Pure
|
|
228
230
|
# stdlib leaf; near-noop when CCTALLY_PERF_TRACE is unset (phase() returns a
|
|
@@ -230,6 +232,7 @@ from _lib_fmt import _fmt_usd_accounting, stable_sum
|
|
|
230
232
|
# nothing on the default path.
|
|
231
233
|
import _lib_perf as _perf
|
|
232
234
|
import _lib_tick_stats as _tick_stats
|
|
235
|
+
import _lib_write_io
|
|
233
236
|
import _lib_log
|
|
234
237
|
|
|
235
238
|
import importlib.util as _ilu
|
|
@@ -295,7 +298,10 @@ from _lib_dashboard_sources import (
|
|
|
295
298
|
aggregate_scope_failed,
|
|
296
299
|
aggregate_scope_identity,
|
|
297
300
|
build_aggregate_scope,
|
|
301
|
+
calibration_basis_digest,
|
|
302
|
+
calibration_supersession_digest,
|
|
298
303
|
claude_stats_digest,
|
|
304
|
+
claude_weekly_retired_digest,
|
|
299
305
|
codex_stats_digest,
|
|
300
306
|
combined_accounting_version,
|
|
301
307
|
compose_all_state,
|
|
@@ -909,6 +915,18 @@ class TuiCurrentWeek:
|
|
|
909
915
|
# set. Appended last with a default so fixture modules that construct
|
|
910
916
|
# `TuiCurrentWeek` positionally stay valid.
|
|
911
917
|
total_tokens: int = 0
|
|
918
|
+
# #836 (spec 5.1): the displayed account, published as
|
|
919
|
+
# `current_week.account_key` / `account_label` ONLY when Claude is
|
|
920
|
+
# decorated (more than one real account, R8). Filled by the decorated
|
|
921
|
+
# path of `_tui_build_current_week`; the undecorated card leaves all four
|
|
922
|
+
# at their defaults. ``cycle_scope`` is that account's
|
|
923
|
+
# `ClaudeCycleScope`, the one cycle identity the weekly milestones and the
|
|
924
|
+
# scoped history index share with this card. Trailing defaults, for the
|
|
925
|
+
# positional fixture constructors.
|
|
926
|
+
account_key: "str | None" = None
|
|
927
|
+
account_decorated: bool = False
|
|
928
|
+
account_label: "str | None" = None
|
|
929
|
+
cycle_scope: "Any | None" = None
|
|
912
930
|
|
|
913
931
|
|
|
914
932
|
# ---- View-model row dataclasses moved to bin/_lib_view_models.py ----
|
|
@@ -954,10 +972,21 @@ class TuiPercentMilestone:
|
|
|
954
972
|
|
|
955
973
|
def _tui_build_percent_milestones(
|
|
956
974
|
conn: sqlite3.Connection,
|
|
975
|
+
*,
|
|
976
|
+
account_key: "str | None" = None,
|
|
977
|
+
cycle_scope: "Any | None" = None,
|
|
957
978
|
) -> list[TuiPercentMilestone]:
|
|
958
979
|
"""Return per-percent crossings for the current week's ACTIVE
|
|
959
980
|
segment, ascending by percent.
|
|
960
981
|
|
|
982
|
+
#836 (spec 5.3): with ``account_key`` (a decorated store) the week, its
|
|
983
|
+
active segment and the milestone rows all come from ``cycle_scope`` — the
|
|
984
|
+
displayed account's `ClaudeCycleScope`, the identity the card was built
|
|
985
|
+
from — and only that account's rows are read. A decorated build with no
|
|
986
|
+
resolved scope publishes ``[]``, never a merged list. ``account_key=None``
|
|
987
|
+
is today's merged read below, unchanged, including legitimate
|
|
988
|
+
`unattributed` pre-attribution rows in the same week.
|
|
989
|
+
|
|
961
990
|
Resolves `week_start_date` from the latest `weekly_usage_snapshots` row
|
|
962
991
|
— the same path `cmd_percent_breakdown` takes. The post-override
|
|
963
992
|
`TuiCurrentWeek.week_start_at` is NOT suitable here: after a mid-week
|
|
@@ -974,6 +1003,16 @@ def _tui_build_percent_milestones(
|
|
|
974
1003
|
Returns [] if no usage snapshot exists, OR if the active segment
|
|
975
1004
|
has no milestone rows yet (post-credit "fresh" state).
|
|
976
1005
|
"""
|
|
1006
|
+
if account_key is not None:
|
|
1007
|
+
if cycle_scope is None or cycle_scope.account_key != account_key:
|
|
1008
|
+
return []
|
|
1009
|
+
# The scope's segment IS the active one (no second lookup): it was
|
|
1010
|
+
# resolved through the same chokepoint, scoped and as-of bounded.
|
|
1011
|
+
return _tui_percent_milestone_rows([
|
|
1012
|
+
r for r in get_milestones_for_week(
|
|
1013
|
+
conn, cycle_scope.week_start_date, account_key=account_key)
|
|
1014
|
+
if int(r["reset_event_id"] or 0) == int(cycle_scope.reset_event_id)
|
|
1015
|
+
])
|
|
977
1016
|
latest = conn.execute(
|
|
978
1017
|
"SELECT week_start_date, week_end_at FROM weekly_usage_snapshots "
|
|
979
1018
|
"WHERE week_end_at IS NOT NULL "
|
|
@@ -1022,6 +1061,11 @@ def _tui_build_percent_milestones(
|
|
|
1022
1061
|
r for r in get_milestones_for_week(conn, latest["week_start_date"])
|
|
1023
1062
|
if int(r["reset_event_id"] or 0) == active_segment
|
|
1024
1063
|
]
|
|
1064
|
+
return _tui_percent_milestone_rows(rows)
|
|
1065
|
+
|
|
1066
|
+
|
|
1067
|
+
def _tui_percent_milestone_rows(rows) -> list[TuiPercentMilestone]:
|
|
1068
|
+
"""`percent_milestones` rows as `TuiPercentMilestone`s, in row order."""
|
|
1025
1069
|
out: list[TuiPercentMilestone] = []
|
|
1026
1070
|
for r in rows:
|
|
1027
1071
|
try:
|
|
@@ -1391,6 +1435,17 @@ class DataSnapshot:
|
|
|
1391
1435
|
# ingest/read-model work. It remains trailing/defaulted for every legacy
|
|
1392
1436
|
# positional fixture constructor.
|
|
1393
1437
|
source_bundle: SourceDashboardBundle | None = None
|
|
1438
|
+
# #836 spec 5.7 item 2: the earliest instant after this build's as-of at
|
|
1439
|
+
# which evidence the build excluded by that as-of becomes eligible — a
|
|
1440
|
+
# weekly sample captured after it, a reset taking effect (or detected)
|
|
1441
|
+
# after it — or None (`_tui_evidence_eligible_at`). The clock is frozen
|
|
1442
|
+
# before the dispatch signature is read, so such a row is counted by the
|
|
1443
|
+
# signature yet excluded by the build; the idle gate refuses once a tick's
|
|
1444
|
+
# instant reaches this, and the Claude source version carries it, so the
|
|
1445
|
+
# tick that admits the evidence rebuilds and refuses source reuse.
|
|
1446
|
+
# Never published. Idle copies carry it unchanged. LAST, with a default,
|
|
1447
|
+
# for the positional constructors.
|
|
1448
|
+
evidence_eligible_at: dt.datetime | None = None
|
|
1394
1449
|
|
|
1395
1450
|
@classmethod
|
|
1396
1451
|
def synthesize_for_marketing(cls, *, as_of_iso: str) -> "DataSnapshot":
|
|
@@ -1670,6 +1725,138 @@ class RuntimeState:
|
|
|
1670
1725
|
)
|
|
1671
1726
|
|
|
1672
1727
|
|
|
1728
|
+
def _tui_card_freshness(captured, now_utc: dt.datetime):
|
|
1729
|
+
"""``(freshness_label, freshness_age)`` of the card's displayed sample,
|
|
1730
|
+
against the configured oauth_usage thresholds; ``(None, None)`` when the
|
|
1731
|
+
capture is not a datetime. Mirrors the dashboard envelope's cw_freshness
|
|
1732
|
+
derivation in `snapshot_to_envelope`. Refs spec §3.4."""
|
|
1733
|
+
if not isinstance(captured, dt.datetime):
|
|
1734
|
+
return None, None
|
|
1735
|
+
if captured.tzinfo is None:
|
|
1736
|
+
captured = captured.replace(tzinfo=dt.timezone.utc)
|
|
1737
|
+
age_s = max(0.0, (now_utc - captured).total_seconds())
|
|
1738
|
+
try:
|
|
1739
|
+
_fresh_cfg = _get_oauth_usage_config(load_config())
|
|
1740
|
+
except Exception:
|
|
1741
|
+
_fresh_cfg = _OAUTH_USAGE_DEFAULTS
|
|
1742
|
+
return _freshness_label(age_s, _fresh_cfg), int(age_s)
|
|
1743
|
+
|
|
1744
|
+
|
|
1745
|
+
def _tui_five_hour_resets_at(five_hour, now_utc: dt.datetime):
|
|
1746
|
+
"""The five-hour reading's reset instant, or None when absent,
|
|
1747
|
+
unparseable or already elapsed (a stale reset would render as
|
|
1748
|
+
"resets 0h 00m")."""
|
|
1749
|
+
if five_hour is None or five_hour.resets_at is None:
|
|
1750
|
+
return None
|
|
1751
|
+
try:
|
|
1752
|
+
resets_at = parse_iso_datetime(
|
|
1753
|
+
five_hour.resets_at, "five_hour_resets_at")
|
|
1754
|
+
except ValueError:
|
|
1755
|
+
return None
|
|
1756
|
+
return None if resets_at <= now_utc else resets_at
|
|
1757
|
+
|
|
1758
|
+
|
|
1759
|
+
def _claude_current_cycle_scope(*args, **kwargs):
|
|
1760
|
+
return sys.modules["cctally"]._claude_current_cycle_scope(*args, **kwargs)
|
|
1761
|
+
|
|
1762
|
+
|
|
1763
|
+
def _claude_select_current_owner(*args, **kwargs):
|
|
1764
|
+
return sys.modules["cctally"]._claude_select_current_owner(*args, **kwargs)
|
|
1765
|
+
|
|
1766
|
+
|
|
1767
|
+
def _claude_cycle_scope_samples(*args, **kwargs):
|
|
1768
|
+
return sys.modules["cctally"]._claude_cycle_scope_samples(*args, **kwargs)
|
|
1769
|
+
|
|
1770
|
+
|
|
1771
|
+
def _tui_build_decorated_current_week(
|
|
1772
|
+
conn: sqlite3.Connection,
|
|
1773
|
+
now_utc: dt.datetime,
|
|
1774
|
+
*,
|
|
1775
|
+
skip_sync: bool = False,
|
|
1776
|
+
) -> TuiCurrentWeek | None:
|
|
1777
|
+
"""The Current Week card on a decorated store: ONE account (#836,
|
|
1778
|
+
spec 5.2).
|
|
1779
|
+
|
|
1780
|
+
The owner is the account of the newest non-held weekly sample at the
|
|
1781
|
+
as-of instant (`_claude_select_current_owner`), so a newer held row of
|
|
1782
|
+
another account cannot choose the window (D-C). Its cycle identity
|
|
1783
|
+
(`_claude_current_cycle_scope`) supplies the window, the active reset
|
|
1784
|
+
and the effective start, so another account's credit cannot re-anchor it
|
|
1785
|
+
(D-A). Samples, the five-hour reading, the block and the spend are all the
|
|
1786
|
+
owner's (U3, D-B): ``$/1%`` divides the owner's spend by the owner's
|
|
1787
|
+
percentage. When the owner's cycle has no eligible non-held sample (its
|
|
1788
|
+
own reset postdates the chosen one) the card is None; a decorated build
|
|
1789
|
+
never falls back to the merged population. `_apply_midweek_reset_override`
|
|
1790
|
+
is not called here.
|
|
1791
|
+
"""
|
|
1792
|
+
owner = _claude_select_current_owner(conn, now_utc)
|
|
1793
|
+
if owner is None:
|
|
1794
|
+
return None
|
|
1795
|
+
scope = _claude_current_cycle_scope(conn, now_utc, account_key=owner)
|
|
1796
|
+
if scope is None:
|
|
1797
|
+
return None
|
|
1798
|
+
fetched = _claude_cycle_scope_samples(conn, scope, now_utc)
|
|
1799
|
+
if fetched is None:
|
|
1800
|
+
return None
|
|
1801
|
+
week_start_at, week_end_at, observations, week_scope = fetched
|
|
1802
|
+
week_start_before_override = week_start_at
|
|
1803
|
+
if scope.effective_start_at is not None:
|
|
1804
|
+
# The scope's effective start may also PRECEDE the nominal start (a
|
|
1805
|
+
# same-date early re-anchor, #836 review F7): the card's cycle starts
|
|
1806
|
+
# there either way, as its current index entry does.
|
|
1807
|
+
effective = parse_iso_datetime(
|
|
1808
|
+
scope.effective_start_at, "cycle_scope.effective_start_at")
|
|
1809
|
+
if effective != week_start_at:
|
|
1810
|
+
week_start_at = effective
|
|
1811
|
+
observations = [s for s in observations if s[0] >= effective]
|
|
1812
|
+
weekly_samples = [s for s in observations if not s[3]]
|
|
1813
|
+
if not weekly_samples:
|
|
1814
|
+
return None
|
|
1815
|
+
latest = weekly_samples[-1]
|
|
1816
|
+
used_pct = float(latest[1])
|
|
1817
|
+
five_hour = _current_week_five_hour_reading(
|
|
1818
|
+
conn, now_utc, week_scope, account_key=owner,
|
|
1819
|
+
week_start_at=week_start_at,
|
|
1820
|
+
week_start_before_override=week_start_before_override)
|
|
1821
|
+
spent, total_tokens = _sum_cost_and_tokens_for_range(
|
|
1822
|
+
week_start_at, now_utc, mode="auto", skip_sync=skip_sync,
|
|
1823
|
+
account_key=owner,
|
|
1824
|
+
)
|
|
1825
|
+
# The same corrected divisor as the merged card (#661 S2 section 3.4).
|
|
1826
|
+
used_pct_corrected = corrected_percent_point(used_pct)
|
|
1827
|
+
dpp_divisor = used_pct if used_pct_corrected is None else used_pct_corrected
|
|
1828
|
+
dpp = (spent / dpp_divisor) if used_pct > 0 else None
|
|
1829
|
+
freshness_label, freshness_age = _tui_card_freshness(latest[0], now_utc)
|
|
1830
|
+
import _cctally_account
|
|
1831
|
+
return TuiCurrentWeek(
|
|
1832
|
+
week_start_at=week_start_at,
|
|
1833
|
+
week_end_at=week_end_at,
|
|
1834
|
+
used_pct=used_pct,
|
|
1835
|
+
five_hour_pct=None if five_hour is None else five_hour.percent,
|
|
1836
|
+
five_hour_resets_at=_tui_five_hour_resets_at(five_hour, now_utc),
|
|
1837
|
+
spent_usd=float(spent),
|
|
1838
|
+
dollars_per_percent=dpp,
|
|
1839
|
+
latest_snapshot_at=latest[0],
|
|
1840
|
+
freshness_label=freshness_label,
|
|
1841
|
+
freshness_age=freshness_age,
|
|
1842
|
+
five_hour_block=_select_current_block_for_envelope(
|
|
1843
|
+
conn, current_used_pct=used_pct, now_utc=now_utc,
|
|
1844
|
+
account_key=owner,
|
|
1845
|
+
),
|
|
1846
|
+
total_tokens=total_tokens,
|
|
1847
|
+
account_key=owner,
|
|
1848
|
+
account_decorated=True,
|
|
1849
|
+
account_label=_cctally_account.display_account_label(conn, owner),
|
|
1850
|
+
cycle_scope=scope,
|
|
1851
|
+
)
|
|
1852
|
+
|
|
1853
|
+
|
|
1854
|
+
def _decorated_claude(conn: sqlite3.Connection) -> bool:
|
|
1855
|
+
"""The R8 decoration gate for Claude (`provider_is_decorated`)."""
|
|
1856
|
+
import _cctally_account
|
|
1857
|
+
return _cctally_account.provider_is_decorated(conn, "claude")
|
|
1858
|
+
|
|
1859
|
+
|
|
1673
1860
|
def _tui_build_current_week(
|
|
1674
1861
|
conn: sqlite3.Connection,
|
|
1675
1862
|
now_utc: dt.datetime,
|
|
@@ -1679,7 +1866,15 @@ def _tui_build_current_week(
|
|
|
1679
1866
|
"""Build the TuiCurrentWeek from the latest snapshot + live cost.
|
|
1680
1867
|
|
|
1681
1868
|
Returns None when no current-week usage snapshot exists.
|
|
1869
|
+
|
|
1870
|
+
#836: a decorated store (more than one real Claude account, R8) takes
|
|
1871
|
+
`_tui_build_decorated_current_week`, the one-account card. Below the
|
|
1872
|
+
threshold the merged card below is unchanged.
|
|
1682
1873
|
"""
|
|
1874
|
+
import _cctally_account
|
|
1875
|
+
if _cctally_account.provider_is_decorated(conn, "claude"):
|
|
1876
|
+
return _tui_build_decorated_current_week(
|
|
1877
|
+
conn, now_utc, skip_sync=skip_sync)
|
|
1683
1878
|
# #769 S11 (#824): ONE read, held-inclusive, split per axis below.
|
|
1684
1879
|
# Retain the owner of the exact non-held weekly sample this card displays,
|
|
1685
1880
|
# including when one real account coexists with unattributed history. R8
|
|
@@ -1755,35 +1950,11 @@ def _tui_build_current_week(
|
|
|
1755
1950
|
# percent and the reset together from one row), so the countdown never
|
|
1756
1951
|
# describes another row. That reading is held-inclusive (#769 S11, #824),
|
|
1757
1952
|
# because a held row's five-hour fields are the writing tick's own.
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
)
|
|
1764
|
-
except ValueError:
|
|
1765
|
-
five_hr_resets_at = None
|
|
1766
|
-
# Suppress stale resets that have already elapsed so renderers
|
|
1767
|
-
# don't show "resets 0h 00m" or a negative duration at the boundary.
|
|
1768
|
-
if five_hr_resets_at is not None and five_hr_resets_at <= now_utc:
|
|
1769
|
-
five_hr_resets_at = None
|
|
1770
|
-
# Freshness — compute label/age from latest snapshot vs. now using the
|
|
1771
|
-
# configured oauth_usage thresholds. Mirrors the dashboard envelope's
|
|
1772
|
-
# cw_freshness derivation in `snapshot_to_envelope`. Refs spec §3.4.
|
|
1773
|
-
captured = latest[0]
|
|
1774
|
-
if isinstance(captured, dt.datetime):
|
|
1775
|
-
if captured.tzinfo is None:
|
|
1776
|
-
captured = captured.replace(tzinfo=dt.timezone.utc)
|
|
1777
|
-
age_s = max(0.0, (now_utc - captured).total_seconds())
|
|
1778
|
-
try:
|
|
1779
|
-
_fresh_cfg = _get_oauth_usage_config(load_config())
|
|
1780
|
-
except Exception:
|
|
1781
|
-
_fresh_cfg = _OAUTH_USAGE_DEFAULTS
|
|
1782
|
-
freshness_label = _freshness_label(age_s, _fresh_cfg)
|
|
1783
|
-
freshness_age = int(age_s)
|
|
1784
|
-
else:
|
|
1785
|
-
freshness_label = None
|
|
1786
|
-
freshness_age = None
|
|
1953
|
+
# Suppress stale resets that have already elapsed so renderers don't
|
|
1954
|
+
# show "resets 0h 00m" or a negative duration at the boundary.
|
|
1955
|
+
five_hr_resets_at = _tui_five_hour_resets_at(five_hour, now_utc)
|
|
1956
|
+
# Freshness — label/age from the displayed sample vs. now.
|
|
1957
|
+
freshness_label, freshness_age = _tui_card_freshness(latest[0], now_utc)
|
|
1787
1958
|
return TuiCurrentWeek(
|
|
1788
1959
|
week_start_at=week_start_at,
|
|
1789
1960
|
week_end_at=week_end_at,
|
|
@@ -2424,7 +2595,72 @@ def _snapshot_data_version(sig) -> str:
|
|
|
2424
2595
|
# moves no numeric leg above, so without this the detail endpoints' change
|
|
2425
2596
|
# signal stays flat across a tick that added an alert row.
|
|
2426
2597
|
claude_digest = getattr(sig, "claude_stats_digest", "")
|
|
2427
|
-
|
|
2598
|
+
out = out if not claude_digest else f"{out}.x{claude_digest}"
|
|
2599
|
+
# #836 spec 5.7 item 1: the Claude weekly rejection state. Empty on a store
|
|
2600
|
+
# with no retired snapshot and no unavailable five-hour join, so it is
|
|
2601
|
+
# byte-neutral there.
|
|
2602
|
+
retired = getattr(sig, "claude_weekly_retired_digest", "")
|
|
2603
|
+
return out if not retired else f"{out}.r{retired}"
|
|
2604
|
+
|
|
2605
|
+
|
|
2606
|
+
def _tui_evidence_eligible_at(conn, as_of_utc: dt.datetime) -> "dt.datetime | None":
|
|
2607
|
+
"""The earliest instant after ``as_of_utc`` at which evidence a build at
|
|
2608
|
+
that as-of excluded becomes eligible, or None (#836, spec 5.7 item 2).
|
|
2609
|
+
|
|
2610
|
+
The candidates are the rows a later instant's build could admit:
|
|
2611
|
+
|
|
2612
|
+
* weekly snapshots captured after ``as_of_utc`` whose `week_start_date` is
|
|
2613
|
+
at or after the as-of date minus eight days — every boundaryful window
|
|
2614
|
+
that can contain the as-of instant (including another account's older
|
|
2615
|
+
overlapping window), the date-only leg, a later window and the
|
|
2616
|
+
no-current-card case. That leading range is an `idx_usage_week_time`
|
|
2617
|
+
range, never a whole-table scan; held rows are included, since the
|
|
2618
|
+
five-hour reading reads them;
|
|
2619
|
+
* `week_reset_events` rows taking effect after it (the decorated cycle
|
|
2620
|
+
admits a reset by its effective instant) or detected after it (the
|
|
2621
|
+
merged override admits one by its detection instant).
|
|
2622
|
+
|
|
2623
|
+
Instants are compared with `unixepoch`, never lexically: the columns carry
|
|
2624
|
+
mixed offset spellings. A far-future row (a clock error) only postpones
|
|
2625
|
+
the one rebuild; it never disables the idle path.
|
|
2626
|
+
"""
|
|
2627
|
+
as_of = as_of_utc.astimezone(dt.timezone.utc)
|
|
2628
|
+
as_of_iso = _iso_z(as_of)
|
|
2629
|
+
floor_date = (as_of.date() - dt.timedelta(days=8)).isoformat()
|
|
2630
|
+
candidates: list[str] = [
|
|
2631
|
+
row[0] for row in conn.execute(
|
|
2632
|
+
"SELECT captured_at_utc FROM weekly_usage_snapshots "
|
|
2633
|
+
"WHERE week_start_date >= ? "
|
|
2634
|
+
"AND unixepoch(captured_at_utc) > unixepoch(?)",
|
|
2635
|
+
(floor_date, as_of_iso)).fetchall()
|
|
2636
|
+
]
|
|
2637
|
+
try:
|
|
2638
|
+
for effective, detected in conn.execute(
|
|
2639
|
+
"SELECT effective_reset_at_utc, detected_at_utc "
|
|
2640
|
+
"FROM week_reset_events "
|
|
2641
|
+
"WHERE unixepoch(effective_reset_at_utc) > unixepoch(?) "
|
|
2642
|
+
" OR unixepoch(detected_at_utc) > unixepoch(?)",
|
|
2643
|
+
(as_of_iso, as_of_iso)).fetchall():
|
|
2644
|
+
candidates.extend(v for v in (effective, detected) if v)
|
|
2645
|
+
except sqlite3.OperationalError as exc:
|
|
2646
|
+
if "no such table" not in str(exc).lower():
|
|
2647
|
+
raise
|
|
2648
|
+
eligible: "dt.datetime | None" = None
|
|
2649
|
+
for text in candidates:
|
|
2650
|
+
try:
|
|
2651
|
+
instant = parse_iso_datetime(text, "evidence_eligible_at")
|
|
2652
|
+
except (TypeError, ValueError):
|
|
2653
|
+
continue
|
|
2654
|
+
if instant > as_of and (eligible is None or instant < eligible):
|
|
2655
|
+
eligible = instant
|
|
2656
|
+
return eligible
|
|
2657
|
+
|
|
2658
|
+
|
|
2659
|
+
def _tui_evidence_matured(prior, now_utc: dt.datetime) -> bool:
|
|
2660
|
+
"""Has evidence ``prior`` excluded by its as-of become eligible at
|
|
2661
|
+
``now_utc``? (#836, spec 5.7 item 2: the idle gate refuses then.)"""
|
|
2662
|
+
eligible = getattr(prior, "evidence_eligible_at", None)
|
|
2663
|
+
return eligible is not None and now_utc >= eligible
|
|
2428
2664
|
|
|
2429
2665
|
|
|
2430
2666
|
def _tui_source_copy(value: object) -> object:
|
|
@@ -2487,6 +2723,35 @@ def alert_row_owner(
|
|
|
2487
2723
|
raise ValueError(f"no ownership rule for alert axis {axis!r}")
|
|
2488
2724
|
|
|
2489
2725
|
|
|
2726
|
+
def _tui_claude_owner_version_fragment(claude_data: object) -> str:
|
|
2727
|
+
"""The decorated Claude source-version fragment (#836 spec 5.7), or "".
|
|
2728
|
+
|
|
2729
|
+
A digest of the displayed account, its label and the published scoped
|
|
2730
|
+
history index's identity (each entry's key, detail stamp and current
|
|
2731
|
+
flag), read from the already-projected source data. Only a decorated
|
|
2732
|
+
build publishes ``current_week.account_key``, so every undecorated build
|
|
2733
|
+
returns "" and its version bytes stay as they were.
|
|
2734
|
+
"""
|
|
2735
|
+
hero = claude_data.get("hero") if isinstance(claude_data, dict) else None
|
|
2736
|
+
cw = hero.get("current_week") if isinstance(hero, dict) else None
|
|
2737
|
+
if not isinstance(cw, dict):
|
|
2738
|
+
return ""
|
|
2739
|
+
owner = cw.get("account_key")
|
|
2740
|
+
if not isinstance(owner, str) or not owner:
|
|
2741
|
+
return ""
|
|
2742
|
+
index = cw.get("week_index")
|
|
2743
|
+
entries = [
|
|
2744
|
+
(e.get("key"), e.get("detail_stamp"), bool(e.get("is_current")))
|
|
2745
|
+
for e in (index if isinstance(index, list) else ())
|
|
2746
|
+
if isinstance(e, dict)
|
|
2747
|
+
]
|
|
2748
|
+
material = json.dumps(
|
|
2749
|
+
[owner, cw.get("account_label"), entries],
|
|
2750
|
+
sort_keys=True, separators=(",", ":"), ensure_ascii=True,
|
|
2751
|
+
)
|
|
2752
|
+
return hashlib.sha256(material.encode("utf-8")).hexdigest()[:16]
|
|
2753
|
+
|
|
2754
|
+
|
|
2490
2755
|
def _tui_project_claude_source_data(legacy_envelope: object) -> dict[str, object]:
|
|
2491
2756
|
"""Project one completed Claude legacy envelope without further DB reads.
|
|
2492
2757
|
|
|
@@ -3840,9 +4105,14 @@ def _tui_build_source_bundle(
|
|
|
3840
4105
|
prior_bundle: SourceDashboardBundle | None = None,
|
|
3841
4106
|
raw_config: dict[str, object] | None = None,
|
|
3842
4107
|
attempt: dict[str, bool] | None = None,
|
|
4108
|
+
evidence_eligible_at: dt.datetime | None = None,
|
|
3843
4109
|
) -> SourceDashboardBundle:
|
|
3844
4110
|
"""Build one frozen source bundle after the dashboard's coordinated ingest.
|
|
3845
4111
|
|
|
4112
|
+
``evidence_eligible_at`` (#836, spec 5.7 item 2) is the build's
|
|
4113
|
+
`DataSnapshot.evidence_eligible_at`; it enters the Claude version only
|
|
4114
|
+
when set.
|
|
4115
|
+
|
|
3846
4116
|
This helper is reachable only from ``precompute_envelope=True``. It opens
|
|
3847
4117
|
a fresh cache handle after the ingest handle has closed, then performs
|
|
3848
4118
|
read-only provider adaptation with no implicit sync or rollout fallback.
|
|
@@ -3980,6 +4250,9 @@ def _tui_build_source_bundle(
|
|
|
3980
4250
|
# rare event and a rebuild is safe, so per-provider narrowing is not worth
|
|
3981
4251
|
# the split.
|
|
3982
4252
|
accounts_digest = accounts_identity_digest(stats_conn)
|
|
4253
|
+
# #836 spec 5.7 item 1: the Claude weekly rejection state; "" (and so
|
|
4254
|
+
# byte-neutral) on a store with nothing retired or unavailable.
|
|
4255
|
+
retired_digest = claude_weekly_retired_digest(stats_conn)
|
|
3983
4256
|
with _perf.phase("source.signature"):
|
|
3984
4257
|
signature = c.compute_signature(
|
|
3985
4258
|
cache_conn,
|
|
@@ -3988,6 +4261,7 @@ def _tui_build_source_bundle(
|
|
|
3988
4261
|
codex_stats_digest=stats_digest,
|
|
3989
4262
|
accounts_digest=accounts_digest,
|
|
3990
4263
|
claude_stats_digest=claude_digest,
|
|
4264
|
+
claude_weekly_retired_digest=retired_digest,
|
|
3991
4265
|
)
|
|
3992
4266
|
# #857: the Codex quota-dependency identity, read on the SAME pinned
|
|
3993
4267
|
# cache snapshot the Codex capture below reads, so a generation built
|
|
@@ -4055,6 +4329,23 @@ def _tui_build_source_bundle(
|
|
|
4055
4329
|
f":p{_period_identity}{_budget_suffix}{_acct_suffix}"
|
|
4056
4330
|
f"{_aggregate_suffix}:x{claude_digest}"
|
|
4057
4331
|
)
|
|
4332
|
+
# #836 spec 5.7: exact-version reuse compares this string, so a
|
|
4333
|
+
# retirement or restoration (`:r`) and a tick that admits evidence an
|
|
4334
|
+
# earlier one excluded (`:e`) both refuse the prior Claude object. Each
|
|
4335
|
+
# is appended only when set, so other stores keep their version bytes.
|
|
4336
|
+
if retired_digest:
|
|
4337
|
+
claude_version = f"{claude_version}:r{retired_digest}"
|
|
4338
|
+
if evidence_eligible_at is not None:
|
|
4339
|
+
claude_version = f"{claude_version}:e{_iso_z(evidence_eligible_at)}"
|
|
4340
|
+
# #836 spec 5.7: the decorated-only fragment. A decorated Claude source
|
|
4341
|
+
# publishes ONE account's card and that account's history index, and
|
|
4342
|
+
# exact-version reuse compares this string, so the owner, its label
|
|
4343
|
+
# and the published index identity (keys, stamps, current flag) enter
|
|
4344
|
+
# it. Undecorated envelopes carry no `account_key`, so their version
|
|
4345
|
+
# bytes are unchanged.
|
|
4346
|
+
_owner_fragment = _tui_claude_owner_version_fragment(claude_data)
|
|
4347
|
+
if _owner_fragment:
|
|
4348
|
+
claude_version = f"{claude_version}:o{_owner_fragment}"
|
|
4058
4349
|
prior_claude = (
|
|
4059
4350
|
prior_bundle.sources.get("claude")
|
|
4060
4351
|
if prior_bundle is not None else None
|
|
@@ -5167,6 +5458,9 @@ def _tui_build_snapshot_once(
|
|
|
5167
5458
|
# #857: the Codex quota-dependency identity, read on the
|
|
5168
5459
|
# signature's own cache handle. An unreadable identity is `None`.
|
|
5169
5460
|
codex_dependency = None
|
|
5461
|
+
# #1015 spec 5.4: the calibration dependency, read on this tick's
|
|
5462
|
+
# own stats connection (`_tui_read_calibration_dependency`).
|
|
5463
|
+
calibration_dependency = None
|
|
5170
5464
|
with _perf.phase("signature"):
|
|
5171
5465
|
try:
|
|
5172
5466
|
dispatch_sig, codex_dependency = (
|
|
@@ -5174,6 +5468,8 @@ def _tui_build_snapshot_once(
|
|
|
5174
5468
|
conn, with_codex_dependency=True,
|
|
5175
5469
|
)
|
|
5176
5470
|
)
|
|
5471
|
+
calibration_dependency = (
|
|
5472
|
+
_tui_read_calibration_dependency(conn))
|
|
5177
5473
|
except QuotaProjectionIncomplete as exc:
|
|
5178
5474
|
# #496 S5b §4.7: ahead of the generic handler on purpose.
|
|
5179
5475
|
# Below it the refusal is sanitized into a generic
|
|
@@ -5209,7 +5505,16 @@ def _tui_build_snapshot_once(
|
|
|
5209
5505
|
# bytes and the signature's own contract are unchanged. `None`
|
|
5210
5506
|
# (an unreadable identity) is a value like any other here; the
|
|
5211
5507
|
# idle gate is what refuses to idle any Codex generation on it.
|
|
5212
|
-
|
|
5508
|
+
# #1015 spec 5.4: the calibration dependency — the sidecar's
|
|
5509
|
+
# file identity and the correction-lineage and supersession
|
|
5510
|
+
# digests — rides the key on the same terms, so a refit, a
|
|
5511
|
+
# recorded attempt, a completed correction batch or a
|
|
5512
|
+
# supersession leaves the idle path and rebuilds the forecast
|
|
5513
|
+
# and the rate-change array on the next tick.
|
|
5514
|
+
dispatch_key = (
|
|
5515
|
+
dispatch_sig, render_key, codex_dependency,
|
|
5516
|
+
calibration_dependency,
|
|
5517
|
+
)
|
|
5213
5518
|
# #300: carry the change signal onto the non-idle snapshot built
|
|
5214
5519
|
# below. (The idle path returns `idle_snap`, which inherits the
|
|
5215
5520
|
# prior — equal, since idle ⇒ signature unchanged — value.)
|
|
@@ -5223,7 +5528,10 @@ def _tui_build_snapshot_once(
|
|
|
5223
5528
|
)
|
|
5224
5529
|
)
|
|
5225
5530
|
and not _snapshot_period_rolled_over(
|
|
5226
|
-
prior_snap, now_utc, _build_display_tz)
|
|
5531
|
+
prior_snap, now_utc, _build_display_tz)
|
|
5532
|
+
# #836 spec 5.7 item 2: evidence the prior build
|
|
5533
|
+
# excluded by its as-of is eligible now.
|
|
5534
|
+
and not _tui_evidence_matured(prior_snap, now_utc)):
|
|
5227
5535
|
with _perf.phase("idle-decision"):
|
|
5228
5536
|
idle_snap = _tui_build_idle_snapshot(
|
|
5229
5537
|
prior_snap, now_utc=now_utc,
|
|
@@ -5388,7 +5696,14 @@ def _tui_build_snapshot_once(
|
|
|
5388
5696
|
with _perf.phase("build.milestones"):
|
|
5389
5697
|
try:
|
|
5390
5698
|
if cw is not None:
|
|
5391
|
-
|
|
5699
|
+
# #836 (spec 5.3): a decorated card's milestones are its
|
|
5700
|
+
# owner's, from the card's own cycle identity.
|
|
5701
|
+
if getattr(cw, "account_decorated", False):
|
|
5702
|
+
milestones = _tui_build_percent_milestones(
|
|
5703
|
+
conn, account_key=cw.account_key,
|
|
5704
|
+
cycle_scope=cw.cycle_scope)
|
|
5705
|
+
else:
|
|
5706
|
+
milestones = _tui_build_percent_milestones(conn)
|
|
5392
5707
|
except Exception as exc:
|
|
5393
5708
|
capture_failure("milestones", "stats", exc)
|
|
5394
5709
|
history: list = []
|
|
@@ -5533,12 +5848,17 @@ def _tui_build_snapshot_once(
|
|
|
5533
5848
|
# no store at all. An unreadable file yields no marker,
|
|
5534
5849
|
# which is the same thing an absent one yields, because the
|
|
5535
5850
|
# reader cannot tell them apart without scanning sidecars.
|
|
5536
|
-
|
|
5851
|
+
#
|
|
5852
|
+
# #1015 spec 5.6: over the CLEAN regimes, on this tick's own
|
|
5853
|
+
# stats connection; a superseded transition or unverifiable
|
|
5854
|
+
# correction facts show no marker.
|
|
5855
|
+
_qcal = _cctally()._load_sibling("_cctally_quota_calibration")
|
|
5537
5856
|
_qg = _cctally()._load_sibling("_cctally_quota_model")
|
|
5538
5857
|
_state = _qg.read_stored_state_readonly()
|
|
5539
5858
|
if _state is not None:
|
|
5540
|
-
quota_rate_change =
|
|
5541
|
-
_qg.stored_regimes(_state, None)
|
|
5859
|
+
quota_rate_change = _qcal.clean_active_rate_change(
|
|
5860
|
+
None, regimes=_qg.stored_regimes(_state, None),
|
|
5861
|
+
stats_conn=conn)
|
|
5542
5862
|
except Exception as exc:
|
|
5543
5863
|
capture_failure("quota_rate_change", "calibration", exc)
|
|
5544
5864
|
# ---- 5h in-place credit (v1.7.x) ----
|
|
@@ -5575,7 +5895,22 @@ def _tui_build_snapshot_once(
|
|
|
5575
5895
|
week_index: list[dict] = []
|
|
5576
5896
|
with _perf.phase("build.week_index"):
|
|
5577
5897
|
try:
|
|
5578
|
-
|
|
5898
|
+
# #836 spec 5.5 (U1): a decorated card names its account and
|
|
5899
|
+
# its cycle, and the index is that account's history derived
|
|
5900
|
+
# with the SAME scope, so the current entry is the card's
|
|
5901
|
+
# cycle. Undecorated: today's merged index. A decorated store
|
|
5902
|
+
# with no card publishes no index (it rides `current_week`),
|
|
5903
|
+
# so the merged one is not built for it (review F9); the
|
|
5904
|
+
# extra predicate runs only when there is no card.
|
|
5905
|
+
if cw is not None and getattr(cw, "account_decorated", False):
|
|
5906
|
+
week_index = sys.modules["cctally"].build_claude_week_index(
|
|
5907
|
+
conn, account_key=cw.account_key,
|
|
5908
|
+
cycle_scope=cw.cycle_scope)
|
|
5909
|
+
elif cw is None and _decorated_claude(conn):
|
|
5910
|
+
week_index = []
|
|
5911
|
+
else:
|
|
5912
|
+
week_index = sys.modules["cctally"].build_claude_week_index(
|
|
5913
|
+
conn)
|
|
5579
5914
|
except Exception as exc:
|
|
5580
5915
|
capture_failure("week-index", "stats", exc)
|
|
5581
5916
|
# ---- Projects panel + modal envelope (spec §5.2, plan Task 1) -----
|
|
@@ -5762,6 +6097,16 @@ def _tui_build_snapshot_once(
|
|
|
5762
6097
|
daily_panel, now_utc=now_utc, display_tz=_build_display_tz,
|
|
5763
6098
|
)
|
|
5764
6099
|
|
|
6100
|
+
# #836 spec 5.7 item 2: recorded on every dashboard (non-idle) build;
|
|
6101
|
+
# the idle gate and the Claude source version read it.
|
|
6102
|
+
evidence_eligible_at: dt.datetime | None = None
|
|
6103
|
+
if precompute_envelope:
|
|
6104
|
+
with _perf.phase("build.evidence_eligibility"):
|
|
6105
|
+
try:
|
|
6106
|
+
evidence_eligible_at = _tui_evidence_eligible_at(
|
|
6107
|
+
conn, now_utc)
|
|
6108
|
+
except Exception as exc:
|
|
6109
|
+
capture_failure("evidence-eligibility", "stats", exc)
|
|
5765
6110
|
snap = DataSnapshot(
|
|
5766
6111
|
current_week=cw,
|
|
5767
6112
|
forecast=fc,
|
|
@@ -5799,6 +6144,7 @@ def _tui_build_snapshot_once(
|
|
|
5799
6144
|
envelope_precompute=envelope_precompute_block,
|
|
5800
6145
|
data_version=data_version,
|
|
5801
6146
|
source_bundle=None,
|
|
6147
|
+
evidence_eligible_at=evidence_eligible_at,
|
|
5802
6148
|
)
|
|
5803
6149
|
if precompute_envelope:
|
|
5804
6150
|
source_bundle: SourceDashboardBundle | None = None
|
|
@@ -5850,6 +6196,7 @@ def _tui_build_snapshot_once(
|
|
|
5850
6196
|
projects_envelope=projects_envelope_block,
|
|
5851
6197
|
prior_bundle=prior_source_bundle,
|
|
5852
6198
|
raw_config=raw_config,
|
|
6199
|
+
evidence_eligible_at=evidence_eligible_at,
|
|
5853
6200
|
)
|
|
5854
6201
|
if source_bundle is None:
|
|
5855
6202
|
raise RuntimeError("source bundle builder returned no bundle")
|
|
@@ -6105,6 +6452,8 @@ def _tui_compute_dispatch_signature(stats_conn, *, with_codex_dependency=False):
|
|
|
6105
6452
|
codex_stats_digest=codex_stats_digest(stats_conn),
|
|
6106
6453
|
accounts_digest=accounts_identity_digest(stats_conn),
|
|
6107
6454
|
claude_stats_digest=claude_stats_digest(stats_conn),
|
|
6455
|
+
claude_weekly_retired_digest=(
|
|
6456
|
+
claude_weekly_retired_digest(stats_conn)),
|
|
6108
6457
|
)
|
|
6109
6458
|
if not with_codex_dependency:
|
|
6110
6459
|
return signature
|
|
@@ -6115,6 +6464,41 @@ def _tui_compute_dispatch_signature(stats_conn, *, with_codex_dependency=False):
|
|
|
6115
6464
|
cache_conn.close()
|
|
6116
6465
|
|
|
6117
6466
|
|
|
6467
|
+
def _tui_read_calibration_dependency(stats_conn) -> tuple:
|
|
6468
|
+
"""The dispatch key's `calibration_dependency` component (#1015 spec 5.4).
|
|
6469
|
+
|
|
6470
|
+
A tuple of the calibration sidecar's file identity
|
|
6471
|
+
(``(st_ino, st_size, st_mtime_ns)``, or None when it is absent), the
|
|
6472
|
+
correction-lineage digest (completed batches, their footprints, and the
|
|
6473
|
+
reviewed decision rows with their op coordinates) and the supersession
|
|
6474
|
+
digest (the explicit rows and the transitions' frozen lineage).
|
|
6475
|
+
|
|
6476
|
+
What the forecast refusal and the rate-change array's supersession label
|
|
6477
|
+
depend on, and what no dispatch-signature leg reads: a sweep that only
|
|
6478
|
+
rewrites the sidecar (a refit, an annotation, a recorded attempt) moves
|
|
6479
|
+
the first leg, a correction batch moves the second. Both digests go
|
|
6480
|
+
through `_stats_relations_digest` on ``stats_conn``, the connection the
|
|
6481
|
+
tick already holds, each under its own memo namespace, so an unchanged,
|
|
6482
|
+
settled stats file costs no relation scan and no extra connection, and
|
|
6483
|
+
the sidecar leg is one ``stat()``. A sidecar that cannot be ``stat``-ed
|
|
6484
|
+
for another reason is its own stable value: the reader treats it as no
|
|
6485
|
+
calibration on every tick, so there is nothing to rebuild for.
|
|
6486
|
+
"""
|
|
6487
|
+
qmg = _cctally()._load_sibling("_cctally_quota_model")
|
|
6488
|
+
try:
|
|
6489
|
+
st = os.stat(qmg.calibration_path())
|
|
6490
|
+
sidecar = (st.st_ino, st.st_size, st.st_mtime_ns)
|
|
6491
|
+
except FileNotFoundError:
|
|
6492
|
+
sidecar = None
|
|
6493
|
+
except OSError as exc:
|
|
6494
|
+
sidecar = ("unreadable", exc.errno)
|
|
6495
|
+
return (
|
|
6496
|
+
sidecar,
|
|
6497
|
+
calibration_basis_digest(stats_conn),
|
|
6498
|
+
calibration_supersession_digest(stats_conn),
|
|
6499
|
+
)
|
|
6500
|
+
|
|
6501
|
+
|
|
6118
6502
|
def _snapshot_period_rolled_over(prior, now_utc, display_tz):
|
|
6119
6503
|
"""True if a day/week/month boundary was crossed since ``prior`` was built.
|
|
6120
6504
|
|
|
@@ -6311,6 +6695,8 @@ def _tui_build_idle_snapshot(prior, *, now_utc, precompute_envelope,
|
|
|
6311
6695
|
prior_bundle=source_bundle,
|
|
6312
6696
|
raw_config=raw_config,
|
|
6313
6697
|
attempt=source_attempt,
|
|
6698
|
+
evidence_eligible_at=getattr(
|
|
6699
|
+
prior, "evidence_eligible_at", None),
|
|
6314
6700
|
)
|
|
6315
6701
|
except _StatsSnapshotCorruption:
|
|
6316
6702
|
raise
|
|
@@ -8944,10 +9330,10 @@ def _make_run_sync_now_locked(*, ref, hub, pinned_now, display_tz_pref_override,
|
|
|
8944
9330
|
claude_roots = tuple(
|
|
8945
9331
|
_cctally_core._resolve_claude_projects_dirs()
|
|
8946
9332
|
)
|
|
8947
|
-
|
|
8948
|
-
|
|
8949
|
-
|
|
8950
|
-
|
|
9333
|
+
# #970: native-home recognition (sessions and archive
|
|
9334
|
+
# locations), the same roots the walk uses.
|
|
9335
|
+
codex_roots = frontier_mod.codex_frontier_roots(
|
|
9336
|
+
_cctally()._codex_home_roots())
|
|
8951
9337
|
claude_guards = (_cctally_core.CLAUDE_SETTINGS_PATH,)
|
|
8952
9338
|
try:
|
|
8953
9339
|
codex_hooks_mod = _cctally()._load_sibling("_lib_codex_hooks")
|
|
@@ -8991,6 +9377,7 @@ def _make_run_sync_now_locked(*, ref, hub, pinned_now, display_tz_pref_override,
|
|
|
8991
9377
|
return codex_hooks_mod.codex_hook_roots_all_enabled(hook_roots)
|
|
8992
9378
|
cache_conn = None
|
|
8993
9379
|
publish_prior = False
|
|
9380
|
+
accounting_recorded = False
|
|
8994
9381
|
initial_cache_identity = None
|
|
8995
9382
|
try:
|
|
8996
9383
|
# The standalone ingest tree is frozen before the final
|
|
@@ -9100,7 +9487,22 @@ def _make_run_sync_now_locked(*, ref, hub, pinned_now, display_tz_pref_override,
|
|
|
9100
9487
|
return result
|
|
9101
9488
|
|
|
9102
9489
|
def _finalize_frontier(active_conn):
|
|
9103
|
-
nonlocal publish_prior
|
|
9490
|
+
nonlocal publish_prior, accounting_recorded
|
|
9491
|
+
# #939 D5: this tick's accounting census and residual,
|
|
9492
|
+
# computed while each provider's mode, plan and result
|
|
9493
|
+
# are known, and reported once the frontier is final.
|
|
9494
|
+
census = {}
|
|
9495
|
+
residuals = {}
|
|
9496
|
+
for provider in ("claude", "codex"):
|
|
9497
|
+
mode = effective_modes[provider]
|
|
9498
|
+
stats = result_by_provider.get(provider)
|
|
9499
|
+
census[provider] = (
|
|
9500
|
+
frontier_mod.accounting_census_complete(
|
|
9501
|
+
mode, stats,
|
|
9502
|
+
expected_paths=len(plans[provider].paths)))
|
|
9503
|
+
residuals[provider] = (
|
|
9504
|
+
None if mode == "caught_up"
|
|
9505
|
+
else frontier_mod.accounting_residual(stats))
|
|
9104
9506
|
for provider, roots, guards in (
|
|
9105
9507
|
("claude", claude_roots, claude_guards),
|
|
9106
9508
|
("codex", codex_roots, codex_guards),
|
|
@@ -9132,6 +9534,11 @@ def _make_run_sync_now_locked(*, ref, hub, pinned_now, display_tz_pref_override,
|
|
|
9132
9534
|
trusted=trusted_results[provider],
|
|
9133
9535
|
cutoff=cutoff,
|
|
9134
9536
|
)
|
|
9537
|
+
_lib_write_io.observe_accounting_tick(
|
|
9538
|
+
ended_ns=_time.monotonic_ns(),
|
|
9539
|
+
census_complete=all(census.values()),
|
|
9540
|
+
residuals=residuals)
|
|
9541
|
+
accounting_recorded = True
|
|
9135
9542
|
return None
|
|
9136
9543
|
|
|
9137
9544
|
operations = (
|
|
@@ -9173,6 +9580,12 @@ def _make_run_sync_now_locked(*, ref, hub, pinned_now, display_tz_pref_override,
|
|
|
9173
9580
|
finally:
|
|
9174
9581
|
if cache_conn is not None:
|
|
9175
9582
|
cache_conn.close()
|
|
9583
|
+
if not accounting_recorded:
|
|
9584
|
+
# #939 D5: a tick that never finalized did not cover
|
|
9585
|
+
# its census, so no conversation pass counts after it.
|
|
9586
|
+
_lib_write_io.observe_accounting_tick(
|
|
9587
|
+
ended_ns=_time.monotonic_ns(),
|
|
9588
|
+
census_complete=False, residuals={})
|
|
9176
9589
|
_ingest.__exit__(None, None, None)
|
|
9177
9590
|
_p_ingest.__exit__(None, None, None)
|
|
9178
9591
|
_perf.stash_last_ingest(
|