cctally 1.100.0 → 1.101.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 +44 -0
- package/bin/_cctally_alerts.py +13 -2
- package/bin/_cctally_cache.py +3 -1
- package/bin/_cctally_cache_report.py +103 -6
- package/bin/_cctally_dashboard.py +713 -166
- package/bin/_cctally_dashboard_envelope.py +28 -38
- package/bin/_cctally_dashboard_share.py +101 -29
- package/bin/_cctally_dashboard_sources.py +663 -192
- package/bin/_cctally_diff.py +20 -0
- package/bin/_cctally_forecast.py +304 -96
- package/bin/_cctally_milestone_history.py +10 -2
- package/bin/_cctally_project.py +155 -47
- package/bin/_cctally_record.py +7 -1
- package/bin/_cctally_share.py +9 -2
- package/bin/_cctally_source_analytics.py +40 -4
- package/bin/_cctally_tui.py +425 -234
- package/bin/_lib_alert_scope.py +685 -0
- package/bin/_lib_alerts_payload.py +112 -7
- package/bin/_lib_cache_report.py +110 -1
- package/bin/_lib_codex_pools.py +20 -8
- package/bin/_lib_dashboard_sources.py +212 -24
- package/bin/_lib_forecast.py +12 -4
- package/bin/_lib_pricing.py +8 -7
- package/bin/_lib_render.py +31 -3
- package/bin/_lib_share_templates.py +150 -55
- package/bin/_lib_snapshot_cache.py +71 -13
- package/bin/_lib_source_identity.py +50 -2
- package/bin/_lib_subscription_weeks.py +65 -0
- package/bin/cctally +10 -0
- package/dashboard/static/assets/dashboardStream.shared-worker-1XTMV3nr.js +1 -0
- package/dashboard/static/assets/index-D6Eb9KDn.js +97 -0
- package/dashboard/static/assets/index-i3g7g8zo.css +1 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +2 -1
- package/dashboard/static/assets/index-B5YfQEtn.css +0 -1
- package/dashboard/static/assets/index-Bt59nMMO.js +0 -97
package/bin/_cctally_tui.py
CHANGED
|
@@ -221,7 +221,7 @@ from _lib_display_tz import (
|
|
|
221
221
|
normalize_display_tz_value,
|
|
222
222
|
_compute_display_block,
|
|
223
223
|
)
|
|
224
|
-
from _lib_aggregators import _aggregate_monthly
|
|
224
|
+
from _lib_aggregators import _aggregate_monthly, codex_path_scope
|
|
225
225
|
from _lib_fmt import stable_sum
|
|
226
226
|
# Opt-in backend phase-instrumentation collector (issue #276, Session A). Pure
|
|
227
227
|
# stdlib leaf; near-noop when CCTALLY_PERF_TRACE is unset (phase() returns a
|
|
@@ -268,7 +268,9 @@ from _cctally_dashboard_sources import (
|
|
|
268
268
|
_claude_accounts_wire,
|
|
269
269
|
_refresh_budget_status_clock,
|
|
270
270
|
accounts_identity_digest,
|
|
271
|
+
build_codex_source_state_from_capture,
|
|
271
272
|
build_codex_source_state,
|
|
273
|
+
capture_codex_source_state,
|
|
272
274
|
codex_decision_deadline_passed,
|
|
273
275
|
refresh_codex_source_clock,
|
|
274
276
|
resolve_dashboard_source_semantics,
|
|
@@ -290,6 +292,7 @@ from _lib_dashboard_sources import (
|
|
|
290
292
|
build_aggregate_scope,
|
|
291
293
|
claude_stats_digest,
|
|
292
294
|
codex_stats_digest,
|
|
295
|
+
combined_accounting_version,
|
|
293
296
|
compose_all_state,
|
|
294
297
|
dashboard_resource_key,
|
|
295
298
|
degrade_source_state,
|
|
@@ -2839,7 +2842,14 @@ def _tui_with_account_scope(
|
|
|
2839
2842
|
_AGGREGATE_FOLD_FAILED = {"state": "failed", "code": "claude_fold_failed"}
|
|
2840
2843
|
|
|
2841
2844
|
|
|
2842
|
-
|
|
2845
|
+
@dataclass(frozen=True)
|
|
2846
|
+
class _ClaudeAggregateCapture:
|
|
2847
|
+
optimized: object | None = None
|
|
2848
|
+
rows: tuple | None = None
|
|
2849
|
+
read_failed: bool = False
|
|
2850
|
+
|
|
2851
|
+
|
|
2852
|
+
def _tui_capture_claude_aggregates(
|
|
2843
2853
|
cache_conn,
|
|
2844
2854
|
*,
|
|
2845
2855
|
shared_start: dt.datetime,
|
|
@@ -2855,15 +2865,65 @@ def _tui_build_claude_aggregates(
|
|
|
2855
2865
|
entry_mutation_seq: "int | None" = None,
|
|
2856
2866
|
generation: int = 0,
|
|
2857
2867
|
):
|
|
2858
|
-
"""
|
|
2868
|
+
"""Capture the one-snapshot Claude inputs without folding them."""
|
|
2869
|
+
from zoneinfo import ZoneInfo
|
|
2870
|
+
|
|
2871
|
+
c = _cctally()
|
|
2872
|
+
display_tz = ZoneInfo(display_tz_name) if display_tz_name else None
|
|
2873
|
+
dashboard_module = sys.modules["_cctally_dashboard"]
|
|
2874
|
+
cache_seams_unpatched = (
|
|
2875
|
+
c.build_project_aggregate_rows
|
|
2876
|
+
is dashboard_module.build_project_aggregate_rows
|
|
2877
|
+
and c.build_daily_aggregate_rows
|
|
2878
|
+
is dashboard_module.build_daily_aggregate_rows
|
|
2879
|
+
)
|
|
2880
|
+
if legacy_labels is not None and cache_seams_unpatched:
|
|
2881
|
+
try:
|
|
2882
|
+
return _ClaudeAggregateCapture(
|
|
2883
|
+
optimized=c.capture_cached_claude_range_aggregates(
|
|
2884
|
+
cache_conn,
|
|
2885
|
+
shared_start=shared_start,
|
|
2886
|
+
shared_end_exclusive=shared_end_exclusive,
|
|
2887
|
+
display_tz=display_tz,
|
|
2888
|
+
max_entry_id=max_entry_id,
|
|
2889
|
+
entry_mutation_seq=entry_mutation_seq,
|
|
2890
|
+
generation=generation,
|
|
2891
|
+
),
|
|
2892
|
+
)
|
|
2893
|
+
except Exception:
|
|
2894
|
+
_lib_log.get_logger("dashboard").error(
|
|
2895
|
+
"claude range aggregate capture failed", exc_info=True,
|
|
2896
|
+
)
|
|
2897
|
+
try:
|
|
2898
|
+
rows = tuple(c.iter_shared_range_entries(
|
|
2899
|
+
cache_conn, start=shared_start,
|
|
2900
|
+
end_exclusive=shared_end_exclusive,
|
|
2901
|
+
))
|
|
2902
|
+
except Exception:
|
|
2903
|
+
_lib_log.get_logger("dashboard").error(
|
|
2904
|
+
"claude shared-range candidate read failed", exc_info=True,
|
|
2905
|
+
)
|
|
2906
|
+
return _ClaudeAggregateCapture(read_failed=True)
|
|
2907
|
+
return _ClaudeAggregateCapture(rows=rows)
|
|
2908
|
+
|
|
2909
|
+
|
|
2910
|
+
def _tui_build_claude_aggregates_from_capture(
|
|
2911
|
+
capture: _ClaudeAggregateCapture,
|
|
2912
|
+
*,
|
|
2913
|
+
now_utc: dt.datetime,
|
|
2914
|
+
display_tz_name: str | None,
|
|
2915
|
+
legacy_labels: "dict[str, str] | None" = None,
|
|
2916
|
+
):
|
|
2917
|
+
"""Build both All-only Claude legs after the cache pin closes.
|
|
2859
2918
|
|
|
2860
2919
|
Returns ``(payload, outcomes)`` where ``payload`` holds the published rows
|
|
2861
2920
|
for whichever legs succeeded and ``outcomes`` names each leg's state.
|
|
2862
2921
|
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2922
|
+
The capture came from the caller's pinned connection beside the Codex read;
|
|
2923
|
+
this half consumes only that immutable carrier after rollback. Both legacy
|
|
2924
|
+
paths stay untouched: the attached-cache block continues to serve
|
|
2925
|
+
``env.projects`` and the Group-A read continues to serve ``env.daily`` for
|
|
2926
|
+
the Claude tab.
|
|
2867
2927
|
|
|
2868
2928
|
Each fold has its OWN error boundary, so one failure cannot take the bundle
|
|
2869
2929
|
or the other leg down. A failure of the shared read itself fails both, since
|
|
@@ -2872,48 +2932,41 @@ def _tui_build_claude_aggregates(
|
|
|
2872
2932
|
outer handler, which publishes the prior bundle or none at all, so a
|
|
2873
2933
|
cold-start fold failure could never become the outcome §3.7 promises.
|
|
2874
2934
|
"""
|
|
2875
|
-
from zoneinfo import ZoneInfo
|
|
2876
|
-
|
|
2877
2935
|
c = _cctally()
|
|
2936
|
+
from zoneinfo import ZoneInfo
|
|
2878
2937
|
display_tz = ZoneInfo(display_tz_name) if display_tz_name else None
|
|
2879
2938
|
payload: dict[str, object] = {}
|
|
2880
2939
|
outcomes: dict[str, object] = {
|
|
2881
2940
|
"projects": {"state": "ok"}, "daily": {"state": "ok"},
|
|
2882
2941
|
}
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
)
|
|
2890
|
-
if legacy_labels is not None and cache_seams_unpatched:
|
|
2942
|
+
if capture.read_failed:
|
|
2943
|
+
return {}, {
|
|
2944
|
+
"projects": dict(_AGGREGATE_FOLD_FAILED),
|
|
2945
|
+
"daily": dict(_AGGREGATE_FOLD_FAILED),
|
|
2946
|
+
}
|
|
2947
|
+
if capture.optimized is not None:
|
|
2891
2948
|
try:
|
|
2892
|
-
return c.
|
|
2893
|
-
|
|
2894
|
-
shared_start=shared_start,
|
|
2895
|
-
shared_end_exclusive=shared_end_exclusive,
|
|
2949
|
+
return c.build_cached_claude_range_aggregates_from_capture(
|
|
2950
|
+
capture.optimized,
|
|
2896
2951
|
now_utc=now_utc,
|
|
2897
2952
|
display_tz=display_tz,
|
|
2898
2953
|
legacy_labels=legacy_labels,
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
generation=generation,
|
|
2902
|
-
), outcomes
|
|
2954
|
+
tolerate_leg_failures=True,
|
|
2955
|
+
)
|
|
2903
2956
|
except Exception:
|
|
2904
|
-
#
|
|
2905
|
-
#
|
|
2957
|
+
# No second cache generation is opened for a fallback after the
|
|
2958
|
+
# pin closes. A failed optimized fold is therefore withheld and
|
|
2959
|
+
# retried on the next authoritative build rather than rebuilt from
|
|
2960
|
+
# newer cache bytes under the old version.
|
|
2906
2961
|
_lib_log.get_logger("dashboard").error(
|
|
2907
2962
|
"claude range aggregate cache failed", exc_info=True,
|
|
2908
2963
|
)
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
"claude shared-range candidate read failed", exc_info=True,
|
|
2916
|
-
)
|
|
2964
|
+
return {}, {
|
|
2965
|
+
"projects": dict(_AGGREGATE_FOLD_FAILED),
|
|
2966
|
+
"daily": dict(_AGGREGATE_FOLD_FAILED),
|
|
2967
|
+
}
|
|
2968
|
+
rows = capture.rows or ()
|
|
2969
|
+
if capture.rows is None:
|
|
2917
2970
|
return {}, {
|
|
2918
2971
|
"projects": dict(_AGGREGATE_FOLD_FAILED),
|
|
2919
2972
|
"daily": dict(_AGGREGATE_FOLD_FAILED),
|
|
@@ -2964,6 +3017,38 @@ def _tui_build_claude_aggregates(
|
|
|
2964
3017
|
return payload, outcomes
|
|
2965
3018
|
|
|
2966
3019
|
|
|
3020
|
+
def _tui_build_claude_aggregates(
|
|
3021
|
+
cache_conn,
|
|
3022
|
+
*,
|
|
3023
|
+
shared_start: dt.datetime,
|
|
3024
|
+
shared_end_exclusive: dt.datetime,
|
|
3025
|
+
now_utc: dt.datetime,
|
|
3026
|
+
display_tz_name: str | None,
|
|
3027
|
+
legacy_labels: "dict[str, str] | None" = None,
|
|
3028
|
+
max_entry_id: "int | None" = None,
|
|
3029
|
+
entry_mutation_seq: "int | None" = None,
|
|
3030
|
+
generation: int = 0,
|
|
3031
|
+
):
|
|
3032
|
+
"""Compatibility wrapper for callers that do not split the cache pin."""
|
|
3033
|
+
capture = _tui_capture_claude_aggregates(
|
|
3034
|
+
cache_conn,
|
|
3035
|
+
shared_start=shared_start,
|
|
3036
|
+
shared_end_exclusive=shared_end_exclusive,
|
|
3037
|
+
now_utc=now_utc,
|
|
3038
|
+
display_tz_name=display_tz_name,
|
|
3039
|
+
legacy_labels=legacy_labels,
|
|
3040
|
+
max_entry_id=max_entry_id,
|
|
3041
|
+
entry_mutation_seq=entry_mutation_seq,
|
|
3042
|
+
generation=generation,
|
|
3043
|
+
)
|
|
3044
|
+
return _tui_build_claude_aggregates_from_capture(
|
|
3045
|
+
capture,
|
|
3046
|
+
now_utc=now_utc,
|
|
3047
|
+
display_tz_name=display_tz_name,
|
|
3048
|
+
legacy_labels=legacy_labels,
|
|
3049
|
+
)
|
|
3050
|
+
|
|
3051
|
+
|
|
2967
3052
|
def _tui_claude_data_with_aggregates(
|
|
2968
3053
|
claude_data: dict[str, object] | None,
|
|
2969
3054
|
payload: dict[str, object],
|
|
@@ -3167,10 +3252,19 @@ def _tui_claude_budget_cost_events(
|
|
|
3167
3252
|
the 1-hour cache-write portion correctly (#195).
|
|
3168
3253
|
"""
|
|
3169
3254
|
c = _cctally()
|
|
3170
|
-
|
|
3171
|
-
for row in c.iter_shared_range_entries(
|
|
3255
|
+
rows = tuple(c.iter_shared_range_entries(
|
|
3172
3256
|
cache_conn, start=start_at, end_exclusive=end_at,
|
|
3173
|
-
)
|
|
3257
|
+
))
|
|
3258
|
+
return _tui_claude_budget_events_from_rows(rows)
|
|
3259
|
+
|
|
3260
|
+
|
|
3261
|
+
def _tui_claude_budget_events_from_rows(
|
|
3262
|
+
rows: "Iterable[tuple]",
|
|
3263
|
+
) -> tuple[tuple[dt.datetime, float], ...]:
|
|
3264
|
+
"""Price already-captured Claude rows without touching cache.db."""
|
|
3265
|
+
c = _cctally()
|
|
3266
|
+
events: list[tuple[dt.datetime, float]] = []
|
|
3267
|
+
for row in rows:
|
|
3174
3268
|
entry = c._shared_range_row_to_usage_entry(row)
|
|
3175
3269
|
timestamp = entry.timestamp
|
|
3176
3270
|
if timestamp.tzinfo is None or timestamp.utcoffset() is None:
|
|
@@ -3184,7 +3278,17 @@ def _tui_claude_budget_cost_events(
|
|
|
3184
3278
|
return tuple(events)
|
|
3185
3279
|
|
|
3186
3280
|
|
|
3187
|
-
|
|
3281
|
+
@dataclass(frozen=True)
|
|
3282
|
+
class _ClaudeBudgetCapture:
|
|
3283
|
+
config: object
|
|
3284
|
+
target: object
|
|
3285
|
+
period: str | None
|
|
3286
|
+
window: tuple[dt.datetime, dt.datetime] | None = None
|
|
3287
|
+
rows: tuple = ()
|
|
3288
|
+
immediate_overlay: object | None = None
|
|
3289
|
+
|
|
3290
|
+
|
|
3291
|
+
def _tui_capture_claude_budget_domain(
|
|
3188
3292
|
cache_conn,
|
|
3189
3293
|
stats_conn,
|
|
3190
3294
|
*,
|
|
@@ -3192,31 +3296,21 @@ def _tui_claude_budget_domain(
|
|
|
3192
3296
|
now_utc: dt.datetime,
|
|
3193
3297
|
display_tz_name: str | None,
|
|
3194
3298
|
week_start_name: str,
|
|
3195
|
-
) ->
|
|
3196
|
-
"""
|
|
3197
|
-
|
|
3198
|
-
The status is VENDOR-WIDE (spec §3.4). A populated `budget.accounts` is NOT
|
|
3199
|
-
consumed to simulate per-account scoping: Claude publishes no
|
|
3200
|
-
`account_scopes` for such a map to describe, so an account-only
|
|
3201
|
-
configuration is its own disposition rather than a fabricated status.
|
|
3202
|
-
|
|
3203
|
-
Every failure is caught HERE. The Claude build has no error boundary of its
|
|
3204
|
-
own — unlike the Codex build at `_tui_build_source_bundle`'s
|
|
3205
|
-
`source_build_failed` handler — so an escaping budget error would take the
|
|
3206
|
-
whole bundle down, not merely the provider.
|
|
3207
|
-
"""
|
|
3299
|
+
) -> _ClaudeBudgetCapture:
|
|
3300
|
+
"""Capture the configured budget window's raw rows under the cache pin."""
|
|
3208
3301
|
config = claude_budget if isinstance(claude_budget, Mapping) else {}
|
|
3209
3302
|
target = config.get("weekly_usd")
|
|
3210
3303
|
if target is None:
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3304
|
+
overlay = (
|
|
3305
|
+
{"not_configured": {"disposition": "account_budgets_only"}}
|
|
3306
|
+
if config.get("accounts") else {}
|
|
3307
|
+
)
|
|
3308
|
+
return _ClaudeBudgetCapture(
|
|
3309
|
+
config=config, target=target, period=None,
|
|
3310
|
+
immediate_overlay=overlay,
|
|
3311
|
+
)
|
|
3217
3312
|
period = _tui_claude_budget_period(config)
|
|
3218
3313
|
try:
|
|
3219
|
-
c = _cctally()
|
|
3220
3314
|
window = _tui_claude_budget_window(
|
|
3221
3315
|
stats_conn,
|
|
3222
3316
|
period=period,
|
|
@@ -3225,25 +3319,56 @@ def _tui_claude_budget_domain(
|
|
|
3225
3319
|
week_start_name=week_start_name,
|
|
3226
3320
|
)
|
|
3227
3321
|
if window is None:
|
|
3228
|
-
return
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3322
|
+
return _ClaudeBudgetCapture(
|
|
3323
|
+
config=config, target=target, period=period,
|
|
3324
|
+
immediate_overlay={
|
|
3325
|
+
"status_unavailable": _tui_claude_budget_unavailable(
|
|
3326
|
+
"period_unresolved", budget_usd=target, period=period),
|
|
3327
|
+
},
|
|
3328
|
+
)
|
|
3232
3329
|
start_at, end_at = window
|
|
3233
|
-
|
|
3234
|
-
cache_conn,
|
|
3330
|
+
rows = tuple(_cctally().iter_shared_range_entries(
|
|
3331
|
+
cache_conn, start=start_at, end_exclusive=end_at,
|
|
3332
|
+
))
|
|
3333
|
+
return _ClaudeBudgetCapture(
|
|
3334
|
+
config=config, target=target, period=period,
|
|
3335
|
+
window=(start_at, end_at), rows=rows,
|
|
3336
|
+
)
|
|
3337
|
+
except Exception:
|
|
3338
|
+
_lib_log.get_logger("dashboard").error(
|
|
3339
|
+
"claude budget inputs could not be captured", exc_info=True,
|
|
3235
3340
|
)
|
|
3341
|
+
return _ClaudeBudgetCapture(
|
|
3342
|
+
config=config, target=target, period=period,
|
|
3343
|
+
immediate_overlay={
|
|
3344
|
+
"status_unavailable": _tui_claude_budget_unavailable(
|
|
3345
|
+
"budget_compute_failed", budget_usd=target, period=period),
|
|
3346
|
+
},
|
|
3347
|
+
)
|
|
3348
|
+
|
|
3349
|
+
|
|
3350
|
+
def _tui_build_claude_budget_domain_from_capture(
|
|
3351
|
+
capture: _ClaudeBudgetCapture,
|
|
3352
|
+
*,
|
|
3353
|
+
now_utc: dt.datetime,
|
|
3354
|
+
) -> tuple[dict[str, object], tuple[tuple[dt.datetime, float], ...]]:
|
|
3355
|
+
"""Price and interpret a captured Claude budget after rollback."""
|
|
3356
|
+
if capture.immediate_overlay is not None:
|
|
3357
|
+
return dict(capture.immediate_overlay), ()
|
|
3358
|
+
assert capture.window is not None and capture.period is not None
|
|
3359
|
+
config = capture.config
|
|
3360
|
+
target = capture.target
|
|
3361
|
+
period = capture.period
|
|
3362
|
+
start_at, end_at = capture.window
|
|
3363
|
+
try:
|
|
3364
|
+
c = _cctally()
|
|
3365
|
+
events = _tui_claude_budget_events_from_rows(capture.rows)
|
|
3236
3366
|
recent_start = max(start_at, now_utc - dt.timedelta(hours=24))
|
|
3237
3367
|
status = c.budget_status_payload(
|
|
3238
3368
|
period=period,
|
|
3239
3369
|
window_start_at=start_at,
|
|
3240
3370
|
window_end_at=end_at,
|
|
3241
3371
|
target_usd=target,
|
|
3242
|
-
# stable_sum, not sum: both figures are published on the dashboard
|
|
3243
|
-
# wire and byte-compared by the dashboard goldens, and the built-in
|
|
3244
|
-
# sum() switched to Neumaier compensated summation for floats in
|
|
3245
|
-
# CPython 3.12, so the same spend renders 49.20424485 on 3.12+ and
|
|
3246
|
-
# 49.204244850000016 on 3.11.
|
|
3247
3372
|
spent_usd=stable_sum(
|
|
3248
3373
|
cost for timestamp, cost in events
|
|
3249
3374
|
if start_at <= timestamp < now_utc
|
|
@@ -3263,14 +3388,6 @@ def _tui_claude_budget_domain(
|
|
|
3263
3388
|
"status_unavailable": _tui_claude_budget_unavailable(
|
|
3264
3389
|
"budget_compute_failed", budget_usd=target, period=period),
|
|
3265
3390
|
}, ()
|
|
3266
|
-
# #556 S5 Unit 1 review R4 — retain ONLY the events a later reclock can
|
|
3267
|
-
# read. `_refresh_budget_status_clock` consults the carrier for exactly one
|
|
3268
|
-
# quantity, `recent_24h_usd` over `[max(window_start, now - 24h), now)`, and
|
|
3269
|
-
# `now` advances monotonically, so an event older than `now_utc - 24h` is
|
|
3270
|
-
# unreadable for the life of this retained state. `spent_usd` above is
|
|
3271
|
-
# already summed from the FULL set, so nothing published is lost — while a
|
|
3272
|
-
# `calendar-month` budget would otherwise pin up to 31 days of Claude
|
|
3273
|
-
# `session_entries` and make every reclock tick walk all of them.
|
|
3274
3391
|
reclock_floor = now_utc - dt.timedelta(hours=24)
|
|
3275
3392
|
retained = tuple(
|
|
3276
3393
|
(timestamp, cost) for timestamp, cost in events
|
|
@@ -3279,6 +3396,40 @@ def _tui_claude_budget_domain(
|
|
|
3279
3396
|
return {"status": status}, retained
|
|
3280
3397
|
|
|
3281
3398
|
|
|
3399
|
+
def _tui_claude_budget_domain(
|
|
3400
|
+
cache_conn,
|
|
3401
|
+
stats_conn,
|
|
3402
|
+
*,
|
|
3403
|
+
claude_budget: "Mapping[str, object] | None",
|
|
3404
|
+
now_utc: dt.datetime,
|
|
3405
|
+
display_tz_name: str | None,
|
|
3406
|
+
week_start_name: str,
|
|
3407
|
+
) -> tuple[dict[str, object], tuple[tuple[dt.datetime, float], ...]]:
|
|
3408
|
+
"""Return ``(budget_domain_overlay, cost_events)`` for the Claude provider.
|
|
3409
|
+
|
|
3410
|
+
The status is VENDOR-WIDE (spec §3.4). A populated `budget.accounts` is NOT
|
|
3411
|
+
consumed to simulate per-account scoping: Claude publishes no
|
|
3412
|
+
`account_scopes` for such a map to describe, so an account-only
|
|
3413
|
+
configuration is its own disposition rather than a fabricated status.
|
|
3414
|
+
|
|
3415
|
+
Every failure is caught HERE. The Claude build has no error boundary of its
|
|
3416
|
+
own — unlike the Codex build at `_tui_build_source_bundle`'s
|
|
3417
|
+
`source_build_failed` handler — so an escaping budget error would take the
|
|
3418
|
+
whole bundle down, not merely the provider.
|
|
3419
|
+
"""
|
|
3420
|
+
capture = _tui_capture_claude_budget_domain(
|
|
3421
|
+
cache_conn,
|
|
3422
|
+
stats_conn,
|
|
3423
|
+
claude_budget=claude_budget,
|
|
3424
|
+
now_utc=now_utc,
|
|
3425
|
+
display_tz_name=display_tz_name,
|
|
3426
|
+
week_start_name=week_start_name,
|
|
3427
|
+
)
|
|
3428
|
+
return _tui_build_claude_budget_domain_from_capture(
|
|
3429
|
+
capture, now_utc=now_utc,
|
|
3430
|
+
)
|
|
3431
|
+
|
|
3432
|
+
|
|
3282
3433
|
def _tui_claude_data_with_budget(
|
|
3283
3434
|
base: dict[str, object], overlay: dict[str, object],
|
|
3284
3435
|
) -> dict[str, object]:
|
|
@@ -3343,6 +3494,8 @@ def _tui_build_source_bundle(
|
|
|
3343
3494
|
cache_conn = c.open_cache_db()
|
|
3344
3495
|
cache_read_tx = False
|
|
3345
3496
|
pin_started_ns: "int | None" = None
|
|
3497
|
+
codex_scope_cm = None
|
|
3498
|
+
codex_path_scope_value = None
|
|
3346
3499
|
|
|
3347
3500
|
def _record_cache_pin() -> None:
|
|
3348
3501
|
"""Stamp the elapsed hold exactly once, on whichever path ends it.
|
|
@@ -3529,6 +3682,14 @@ def _tui_build_source_bundle(
|
|
|
3529
3682
|
prior_bundle.sources.get("codex")
|
|
3530
3683
|
if prior_bundle is not None else None
|
|
3531
3684
|
)
|
|
3685
|
+
claude_reuse_version = combined_accounting_version(
|
|
3686
|
+
claude_version,
|
|
3687
|
+
prior_claude.combined_accounting if prior_claude is not None else None,
|
|
3688
|
+
)
|
|
3689
|
+
codex_reuse_version = combined_accounting_version(
|
|
3690
|
+
codex_version,
|
|
3691
|
+
prior_codex.combined_accounting if prior_codex is not None else None,
|
|
3692
|
+
)
|
|
3532
3693
|
if claude_ingest_failed:
|
|
3533
3694
|
warning = SourceDashboardWarning(
|
|
3534
3695
|
"source_ingest_failed", "Source ingest failed.", "ingest",
|
|
@@ -3549,7 +3710,7 @@ def _tui_build_source_bundle(
|
|
|
3549
3710
|
)
|
|
3550
3711
|
else:
|
|
3551
3712
|
claude = reuse_coherent_source_state(
|
|
3552
|
-
prior_claude, data_version=
|
|
3713
|
+
prior_claude, data_version=claude_reuse_version,
|
|
3553
3714
|
)
|
|
3554
3715
|
# #556 S2 §3.6, gate 2 of 2. The version fragment above already
|
|
3555
3716
|
# rejects a failed generation, but this gate is stated explicitly
|
|
@@ -3559,6 +3720,111 @@ def _tui_build_source_bundle(
|
|
|
3559
3720
|
# the life of the process. Gate 1 is the bundle-level idle guard.
|
|
3560
3721
|
if claude is not None and aggregate_scope_failed(claude):
|
|
3561
3722
|
claude = None
|
|
3723
|
+
if codex_ingest_failed:
|
|
3724
|
+
warning = SourceDashboardWarning(
|
|
3725
|
+
"source_ingest_failed", "Source ingest failed.", "ingest",
|
|
3726
|
+
)
|
|
3727
|
+
codex = (
|
|
3728
|
+
degrade_source_state(prior_codex, warning)
|
|
3729
|
+
if prior_codex is not None
|
|
3730
|
+
else unavailable_source_state("codex", warning)
|
|
3731
|
+
)
|
|
3732
|
+
elif codex_ingest_contended:
|
|
3733
|
+
warning = SourceDashboardWarning(
|
|
3734
|
+
"source_ingest_contended", "Source ingest is in progress.", "ingest",
|
|
3735
|
+
)
|
|
3736
|
+
codex = (
|
|
3737
|
+
degrade_source_state(prior_codex, warning)
|
|
3738
|
+
if prior_codex is not None
|
|
3739
|
+
else unavailable_source_state("codex", warning)
|
|
3740
|
+
)
|
|
3741
|
+
else:
|
|
3742
|
+
codex = (
|
|
3743
|
+
None if prior_codex is not None and (
|
|
3744
|
+
_codex_accounting_pending
|
|
3745
|
+
or any(
|
|
3746
|
+
warning.code == "codex_projection_incoherent"
|
|
3747
|
+
for warning in prior_codex.warnings
|
|
3748
|
+
)
|
|
3749
|
+
or codex_decision_deadline_passed(prior_codex, now_utc)
|
|
3750
|
+
) else reuse_coherent_source_state(
|
|
3751
|
+
prior_codex, data_version=codex_reuse_version,
|
|
3752
|
+
)
|
|
3753
|
+
)
|
|
3754
|
+
if codex is not None and aggregate_scope_failed(codex):
|
|
3755
|
+
codex = None
|
|
3756
|
+
_tui_note_codex_regime("active" if codex is None else "idle")
|
|
3757
|
+
|
|
3758
|
+
# Capture every cache-backed carrier while one read transaction names
|
|
3759
|
+
# the generation. Public view construction happens only after rollback.
|
|
3760
|
+
legacy_labels = (
|
|
3761
|
+
c.legacy_project_labels(projects_envelope)
|
|
3762
|
+
if projects_envelope is not None else None
|
|
3763
|
+
)
|
|
3764
|
+
claude_aggregate_capture = None
|
|
3765
|
+
claude_budget_capture = None
|
|
3766
|
+
if claude is None:
|
|
3767
|
+
claude_aggregate_capture = _tui_capture_claude_aggregates(
|
|
3768
|
+
cache_conn,
|
|
3769
|
+
shared_start=common_range_start,
|
|
3770
|
+
shared_end_exclusive=shared_end_exclusive,
|
|
3771
|
+
now_utc=now_utc,
|
|
3772
|
+
display_tz_name=semantics.display_tz_name,
|
|
3773
|
+
legacy_labels=legacy_labels,
|
|
3774
|
+
max_entry_id=signature.max_entry_id,
|
|
3775
|
+
entry_mutation_seq=signature.entry_mutation_seq,
|
|
3776
|
+
generation=signature.generation,
|
|
3777
|
+
)
|
|
3778
|
+
claude_budget_capture = _tui_capture_claude_budget_domain(
|
|
3779
|
+
cache_conn,
|
|
3780
|
+
stats_conn,
|
|
3781
|
+
claude_budget=semantics.claude_budget,
|
|
3782
|
+
now_utc=now_utc,
|
|
3783
|
+
display_tz_name=semantics.display_tz_name,
|
|
3784
|
+
week_start_name=semantics.week_start_name,
|
|
3785
|
+
)
|
|
3786
|
+
|
|
3787
|
+
codex_capture = None
|
|
3788
|
+
codex_capture_failed = False
|
|
3789
|
+
codex_context = None
|
|
3790
|
+
codex_split_seams_unpatched = (
|
|
3791
|
+
build_codex_source_state
|
|
3792
|
+
is sys.modules["_cctally_dashboard_sources"].build_codex_source_state
|
|
3793
|
+
)
|
|
3794
|
+
if codex is None:
|
|
3795
|
+
try:
|
|
3796
|
+
codex_context = DashboardReadContext(
|
|
3797
|
+
cache_conn=cache_conn,
|
|
3798
|
+
stats_conn=stats_conn,
|
|
3799
|
+
range_start=common_range_start,
|
|
3800
|
+
now_utc=now_utc,
|
|
3801
|
+
display_tz_name=semantics.display_tz_name,
|
|
3802
|
+
week_start_idx=semantics.week_start_idx,
|
|
3803
|
+
week_start_name=semantics.week_start_name,
|
|
3804
|
+
speed=semantics.speed,
|
|
3805
|
+
codex_budget=semantics.codex_budget,
|
|
3806
|
+
codex_quota_actual_thresholds=semantics.codex_quota_actual_thresholds,
|
|
3807
|
+
codex_quota_projected_thresholds=semantics.codex_quota_projected_thresholds,
|
|
3808
|
+
cache_report_anomaly_threshold_pp=semantics.cache_report_anomaly_threshold_pp,
|
|
3809
|
+
stats_identity=(stats_digest, accounts_digest, claude_digest),
|
|
3810
|
+
)
|
|
3811
|
+
if codex_split_seams_unpatched:
|
|
3812
|
+
codex_scope_cm = codex_path_scope()
|
|
3813
|
+
codex_path_scope_value = codex_scope_cm.__enter__()
|
|
3814
|
+
codex_capture = capture_codex_source_state(
|
|
3815
|
+
codex_context, path_scope=codex_path_scope_value,
|
|
3816
|
+
)
|
|
3817
|
+
except Exception:
|
|
3818
|
+
codex_capture_failed = True
|
|
3819
|
+
_lib_log.get_logger("dashboard").error(
|
|
3820
|
+
"codex_read_model source capture failed",
|
|
3821
|
+
exc_info=True,
|
|
3822
|
+
)
|
|
3823
|
+
|
|
3824
|
+
if cache_read_tx:
|
|
3825
|
+
_record_cache_pin()
|
|
3826
|
+
cache_conn.rollback()
|
|
3827
|
+
cache_read_tx = False
|
|
3562
3828
|
if claude is None:
|
|
3563
3829
|
claude_available = "ok" if (claude_cost_usd or claude_total_tokens) else "empty"
|
|
3564
3830
|
# #341 Task 4 (Ruling C): the conditional per-account Claude wire,
|
|
@@ -3568,10 +3834,14 @@ def _tui_build_source_bundle(
|
|
|
3568
3834
|
# chip row / hero cards light up automatically for a decorated Claude
|
|
3569
3835
|
# source. A read failure degrades to the byte-stable undecorated shape.
|
|
3570
3836
|
claude_accounts: list[dict[str, object]] = []
|
|
3837
|
+
claude_combined_accounting: dict[str, object] | None = None
|
|
3571
3838
|
try:
|
|
3572
3839
|
import _cctally_account
|
|
3573
3840
|
if _cctally_account.provider_is_decorated(stats_conn, "claude"):
|
|
3574
|
-
|
|
3841
|
+
(
|
|
3842
|
+
claude_accounts,
|
|
3843
|
+
claude_combined_accounting,
|
|
3844
|
+
) = _claude_accounts_wire(stats_conn, now_utc=now_utc)
|
|
3575
3845
|
except Exception:
|
|
3576
3846
|
# #341 Task 4 P3 — degrade to no-accounts-wire on ANY read
|
|
3577
3847
|
# failure, symmetric with the Codex path. Beyond sqlite, the wire
|
|
@@ -3581,50 +3851,43 @@ def _tui_build_source_bundle(
|
|
|
3581
3851
|
# wire must never fail the whole dashboard tick — it just falls
|
|
3582
3852
|
# back to the byte-stable undecorated shape.
|
|
3583
3853
|
claude_accounts = []
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
),
|
|
3611
|
-
max_entry_id=signature.max_entry_id,
|
|
3612
|
-
entry_mutation_seq=signature.entry_mutation_seq,
|
|
3613
|
-
generation=signature.generation,
|
|
3854
|
+
claude_combined_accounting = {
|
|
3855
|
+
"scope": "account_cycles",
|
|
3856
|
+
"status": "unresolved",
|
|
3857
|
+
"cause": "account_cost_unresolved",
|
|
3858
|
+
"contributions": (),
|
|
3859
|
+
}
|
|
3860
|
+
if claude_aggregate_capture is None:
|
|
3861
|
+
raise RuntimeError("Claude aggregate capture is missing")
|
|
3862
|
+
aggregate_payload, aggregate_outcomes = (
|
|
3863
|
+
_tui_build_claude_aggregates_from_capture(
|
|
3864
|
+
claude_aggregate_capture,
|
|
3865
|
+
now_utc=now_utc,
|
|
3866
|
+
display_tz_name=semantics.display_tz_name,
|
|
3867
|
+
# The legacy display keys the drill-down route resolves
|
|
3868
|
+
# against. Published rows adopt them wherever they exist,
|
|
3869
|
+
# so the aggregate identity and the legacy one agree and
|
|
3870
|
+
# the bounded rows stay routable. The raw envelope is
|
|
3871
|
+
# required — `claude_data` has already replaced every
|
|
3872
|
+
# legacy display key with an opaque key and dropped
|
|
3873
|
+
# `bucket_path`, so the map cannot be recovered from it.
|
|
3874
|
+
# `None` — not an empty map — when no envelope was built,
|
|
3875
|
+
# so the fold can tell "the legacy population is empty"
|
|
3876
|
+
# from "the legacy population is unknown" and withhold on
|
|
3877
|
+
# the second.
|
|
3878
|
+
legacy_labels=legacy_labels,
|
|
3879
|
+
)
|
|
3614
3880
|
)
|
|
3615
|
-
# #
|
|
3616
|
-
# function's
|
|
3617
|
-
#
|
|
3618
|
-
#
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
claude_budget_overlay, claude_budget_events =
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
now_utc=now_utc,
|
|
3626
|
-
display_tz_name=semantics.display_tz_name,
|
|
3627
|
-
week_start_name=semantics.week_start_name,
|
|
3881
|
+
# #617: price and interpret the budget rows captured from this
|
|
3882
|
+
# function's one cache generation. The legacy Claude envelope was
|
|
3883
|
+
# constructed before this call, and stats.db deliberately stays in
|
|
3884
|
+
# statement-scoped autocommit, so the claim remains cache-local.
|
|
3885
|
+
if claude_budget_capture is None:
|
|
3886
|
+
raise RuntimeError("Claude budget capture is missing")
|
|
3887
|
+
claude_budget_overlay, claude_budget_events = (
|
|
3888
|
+
_tui_build_claude_budget_domain_from_capture(
|
|
3889
|
+
claude_budget_capture, now_utc=now_utc,
|
|
3890
|
+
)
|
|
3628
3891
|
)
|
|
3629
3892
|
claude_aggregate_scope = build_aggregate_scope(
|
|
3630
3893
|
published_range, aggregate_outcomes,
|
|
@@ -3644,7 +3907,9 @@ def _tui_build_source_bundle(
|
|
|
3644
3907
|
availability=claude_available,
|
|
3645
3908
|
freshness="fresh",
|
|
3646
3909
|
warnings=(),
|
|
3647
|
-
data_version=
|
|
3910
|
+
data_version=combined_accounting_version(
|
|
3911
|
+
claude_version, claude_combined_accounting,
|
|
3912
|
+
),
|
|
3648
3913
|
last_success_at=now_utc,
|
|
3649
3914
|
capabilities={
|
|
3650
3915
|
"hero": CapabilityRecord("supported", "subscription-week"),
|
|
@@ -3698,91 +3963,25 @@ def _tui_build_source_bundle(
|
|
|
3698
3963
|
# recomputes trailing-24h spend by iterating individual events,
|
|
3699
3964
|
# which the status aggregate cannot supply.
|
|
3700
3965
|
clock_data={"claude_budget_cost_events": claude_budget_events},
|
|
3966
|
+
combined_accounting=claude_combined_accounting,
|
|
3701
3967
|
aggregate_scope=claude_aggregate_scope,
|
|
3702
3968
|
)
|
|
3703
|
-
if codex_ingest_failed:
|
|
3704
|
-
warning = SourceDashboardWarning(
|
|
3705
|
-
"source_ingest_failed", "Source ingest failed.", "ingest",
|
|
3706
|
-
)
|
|
3707
|
-
codex = (
|
|
3708
|
-
degrade_source_state(prior_codex, warning)
|
|
3709
|
-
if prior_codex is not None
|
|
3710
|
-
else unavailable_source_state("codex", warning)
|
|
3711
|
-
)
|
|
3712
|
-
elif codex_ingest_contended:
|
|
3713
|
-
warning = SourceDashboardWarning(
|
|
3714
|
-
"source_ingest_contended", "Source ingest is in progress.", "ingest",
|
|
3715
|
-
)
|
|
3716
|
-
codex = (
|
|
3717
|
-
degrade_source_state(prior_codex, warning)
|
|
3718
|
-
if prior_codex is not None
|
|
3719
|
-
else unavailable_source_state("codex", warning)
|
|
3720
|
-
)
|
|
3721
|
-
else:
|
|
3722
|
-
# Projection coherence can recover when S2 writes its certificate
|
|
3723
|
-
# without changing physical accounting. A current, hero-scoped
|
|
3724
|
-
# incoherence generation is intentionally not retained through
|
|
3725
|
-
# that repair opportunity; all other fresh partial states remain
|
|
3726
|
-
# eligible for exact reuse.
|
|
3727
|
-
# #350 spec §3.3: a passed cycle decision deadline forces an
|
|
3728
|
-
# AUTHORITATIVE rebuild. Weekly-cycle resolution is time-dependent
|
|
3729
|
-
# even on frozen evidence, and the reuse path would otherwise hand
|
|
3730
|
-
# back the exact prior object with no re-check at all — so an idle
|
|
3731
|
-
# dashboard would diverge from a freshly rebuilt one. Leaving
|
|
3732
|
-
# ``codex = None`` routes to the existing build below. Bounded by
|
|
3733
|
-
# construction: one rebuild per crossing, not one per tick.
|
|
3734
|
-
codex = (
|
|
3735
|
-
None if prior_codex is not None and (
|
|
3736
|
-
_codex_accounting_pending
|
|
3737
|
-
or any(
|
|
3738
|
-
warning.code == "codex_projection_incoherent"
|
|
3739
|
-
for warning in prior_codex.warnings
|
|
3740
|
-
)
|
|
3741
|
-
or codex_decision_deadline_passed(prior_codex, now_utc)
|
|
3742
|
-
) else reuse_coherent_source_state(
|
|
3743
|
-
prior_codex, data_version=codex_version,
|
|
3744
|
-
)
|
|
3745
|
-
)
|
|
3746
|
-
# #556 S2 §3.6: symmetric with Claude. Codex's rows are already
|
|
3747
|
-
# bounded by this same range, so its carrier records no fold of its
|
|
3748
|
-
# own — but a retained failure state must never be reused, and the
|
|
3749
|
-
# gate is stated on both providers so a future Codex-side fold
|
|
3750
|
-
# inherits it.
|
|
3751
|
-
if codex is not None and aggregate_scope_failed(codex):
|
|
3752
|
-
codex = None
|
|
3753
|
-
# #583 S1 §1.5: the realised decision, read at the `codex is None`
|
|
3754
|
-
# predicate this branch has just settled. Stamped only here, so a
|
|
3755
|
-
# tick whose Codex leg degraded on ingest failure or contention
|
|
3756
|
-
# stays `not_observed` — no build reached the decision at all.
|
|
3757
|
-
_tui_note_codex_regime("active" if codex is None else "idle")
|
|
3758
3969
|
if codex is None:
|
|
3759
3970
|
try:
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
# #583 S5: the three digests this function already
|
|
3775
|
-
# computed above, handed to the quota memo's reuse
|
|
3776
|
-
# identity instead of being derived a second time per
|
|
3777
|
-
# memo call. Same values, same order, same connection,
|
|
3778
|
-
# and taken under the `assert_projection_readable` gate
|
|
3779
|
-
# already run at the top of this build.
|
|
3780
|
-
stats_identity=(
|
|
3781
|
-
stats_digest, accounts_digest, claude_digest,
|
|
3782
|
-
),
|
|
3783
|
-
),
|
|
3784
|
-
data_version=codex_version,
|
|
3785
|
-
)
|
|
3971
|
+
if codex_capture_failed or codex_context is None:
|
|
3972
|
+
raise RuntimeError("Codex source capture failed")
|
|
3973
|
+
if codex_split_seams_unpatched:
|
|
3974
|
+
if codex_capture is None:
|
|
3975
|
+
raise RuntimeError("Codex source capture failed")
|
|
3976
|
+
codex = build_codex_source_state_from_capture(
|
|
3977
|
+
codex_capture,
|
|
3978
|
+
data_version=codex_version,
|
|
3979
|
+
path_scope=codex_path_scope_value,
|
|
3980
|
+
)
|
|
3981
|
+
else:
|
|
3982
|
+
codex = build_codex_source_state(
|
|
3983
|
+
codex_context, data_version=codex_version,
|
|
3984
|
+
)
|
|
3786
3985
|
# Attached ONLY on a fresh build, never on the reuse or degrade
|
|
3787
3986
|
# paths: those carry rows this tick did not produce, and their
|
|
3788
3987
|
# own carrier already describes the range that bounds them.
|
|
@@ -3836,32 +4035,9 @@ def _tui_build_source_bundle(
|
|
|
3836
4035
|
source_order=("claude", "codex", "all"),
|
|
3837
4036
|
sources={"claude": claude, "codex": codex, "all": combined},
|
|
3838
4037
|
)
|
|
3839
|
-
#
|
|
3840
|
-
#
|
|
3841
|
-
#
|
|
3842
|
-
# data_version names that generation. Reject only stats-side movement,
|
|
3843
|
-
# whose statement-scoped autocommit reads may have mixed generations.
|
|
3844
|
-
# Rejecting ordinary cache advancement starves publication whenever
|
|
3845
|
-
# active Claude/Codex sessions append faster than this build completes.
|
|
3846
|
-
#
|
|
3847
|
-
# ONE EXCEPTION since #583 S5, stated here because a narrowing of this
|
|
3848
|
-
# transaction has to account for it. Every cache read this build ISSUES
|
|
3849
|
-
# runs on the pinned connection and sees the frozen generation, but the
|
|
3850
|
-
# Codex quota memo `_CODEX_QUOTA_OBSERVATION_CACHE` now survives across
|
|
3851
|
-
# builds, so a five-hour correlation read can be answered from a value
|
|
3852
|
-
# another caller loaded -- the share render or the cycle-detail route,
|
|
3853
|
-
# on an unpinned connection at a different generation. That value is
|
|
3854
|
-
# served only while its key is unmoved, and the key covers the ledger
|
|
3855
|
-
# sequence, the attribution revision, the database path and the three
|
|
3856
|
-
# digests taken above, so the evidence behind it provably has not
|
|
3857
|
-
# changed. The full argument is at `_cached_codex_quota_observations`
|
|
3858
|
-
# in `bin/_cctally_dashboard_sources.py`. The dashboard's own bounded
|
|
3859
|
-
# quota read is NOT in that class: it passes `memoize=False` and is
|
|
3860
|
-
# always physically issued on the pinned connection.
|
|
3861
|
-
if cache_read_tx:
|
|
3862
|
-
_record_cache_pin()
|
|
3863
|
-
cache_conn.rollback()
|
|
3864
|
-
cache_read_tx = False
|
|
4038
|
+
# The cache pin ended after carrier capture. These cheap post-build
|
|
4039
|
+
# signatures intentionally reject only stats-side movement; a cache
|
|
4040
|
+
# commit during the pure folds belongs to the next generation.
|
|
3865
4041
|
post_stats_digest = codex_stats_digest(stats_conn)
|
|
3866
4042
|
post_accounts_digest = accounts_identity_digest(stats_conn)
|
|
3867
4043
|
post_claude_digest = claude_stats_digest(stats_conn)
|
|
@@ -3890,6 +4066,8 @@ def _tui_build_source_bundle(
|
|
|
3890
4066
|
_record_cache_pin()
|
|
3891
4067
|
if cache_read_tx:
|
|
3892
4068
|
cache_conn.rollback()
|
|
4069
|
+
if codex_scope_cm is not None:
|
|
4070
|
+
codex_scope_cm.__exit__(None, None, None)
|
|
3893
4071
|
cache_conn.close()
|
|
3894
4072
|
|
|
3895
4073
|
|
|
@@ -7388,13 +7566,20 @@ def _tui_modal_current_week(snap, runtime, width):
|
|
|
7388
7566
|
if not milestones:
|
|
7389
7567
|
return ("{accent.b}Current Week · per-percent{/}",
|
|
7390
7568
|
["", " {dim}No milestones yet — keep recording usage.{/}", ""])
|
|
7391
|
-
|
|
7569
|
+
# #620 S1 D5/A5: `TuiCurrentWeek.dollars_per_percent` is `None` when no
|
|
7570
|
+
# usage has been observed. `or 0.0` printed `avg $/1% $0.00`, which is a
|
|
7571
|
+
# measured-looking answer to a question nothing measured. Every other TUI
|
|
7572
|
+
# site already renders that absence as an em dash; this one did not.
|
|
7573
|
+
avg_dpp_str = (
|
|
7574
|
+
f"${cw.dollars_per_percent:.2f}"
|
|
7575
|
+
if cw.dollars_per_percent is not None else "—"
|
|
7576
|
+
)
|
|
7392
7577
|
cumul = cw.spent_usd
|
|
7393
7578
|
header = [
|
|
7394
7579
|
"",
|
|
7395
7580
|
f" {{dim}}Week{{/}} {{b}}{format_display_dt(cw.week_start_at, runtime.display_tz, fmt='%b %d', suffix=False)} – {format_display_dt(cw.week_end_at, runtime.display_tz, fmt='%b %d', suffix=False)}{{/}} "
|
|
7396
7581
|
f"{{dim}}milestones reached{{/}} {{warn.b}}{len(milestones)}{{/}}",
|
|
7397
|
-
f" {{dim}}avg $/1%{{/}} {{b}}
|
|
7582
|
+
f" {{dim}}avg $/1%{{/}} {{b}}{avg_dpp_str}{{/}} {{dim}}cumulative{{/}} {{b}}${cumul:.2f}{{/}}",
|
|
7398
7583
|
"",
|
|
7399
7584
|
]
|
|
7400
7585
|
bucket = _tui_width_bucket(width)
|
|
@@ -7469,8 +7654,14 @@ def _tui_modal_forecast(snap, runtime, width):
|
|
|
7469
7654
|
for b in fc.budgets:
|
|
7470
7655
|
if b.dollars_per_day is not None:
|
|
7471
7656
|
lines.append(f" {{dim}} ≤{b.target_percent}% {{/}}{{b}}${b.dollars_per_day:.2f}/day{{/}}")
|
|
7472
|
-
|
|
7657
|
+
elif b.pct_headroom is None:
|
|
7473
7658
|
lines.append(f" {{dim}} ≤{b.target_percent}% {{/}}{{dim}}— (already past){{/}}")
|
|
7659
|
+
else:
|
|
7660
|
+
# #620 S1 D5: `dollars_per_day` is also None when the $/1% rate
|
|
7661
|
+
# itself is withheld, and there the target has NOT been passed —
|
|
7662
|
+
# there is headroom and no rate to price it with. Naming the
|
|
7663
|
+
# wrong cause is the defect this session removes.
|
|
7664
|
+
lines.append(f" {{dim}} ≤{b.target_percent}% {{/}}{{dim}}— (no rate observed){{/}}")
|
|
7474
7665
|
lines.append("")
|
|
7475
7666
|
confidence = inp.confidence
|
|
7476
7667
|
lines.append(f" {{dim}}confidence: {confidence} · based on 7-day rate{{/}}")
|