cctally 1.100.0 → 1.102.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 +73 -0
- package/README.md +8 -2
- 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 +1140 -282
- package/bin/_cctally_dashboard_conversation.py +12 -0
- package/bin/_cctally_dashboard_envelope.py +53 -61
- package/bin/_cctally_dashboard_share.py +101 -29
- package/bin/_cctally_dashboard_sources.py +663 -192
- package/bin/_cctally_diagnosis.py +1172 -0
- package/bin/_cctally_diagnosis_sources.py +4054 -0
- package/bin/_cctally_diff.py +20 -0
- package/bin/_cctally_forecast.py +329 -111
- package/bin/_cctally_milestone_history.py +10 -2
- package/bin/_cctally_parser.py +84 -0
- package/bin/_cctally_project.py +155 -47
- package/bin/_cctally_quota.py +14 -0
- package/bin/_cctally_record.py +151 -71
- package/bin/_cctally_refresh.py +105 -93
- package/bin/_cctally_share.py +9 -2
- package/bin/_cctally_source_analytics.py +40 -4
- package/bin/_cctally_statusline.py +8 -1
- 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_blocks.py +12 -0
- package/bin/_lib_cache_report.py +110 -1
- package/bin/_lib_codex_conversation.py +14 -0
- package/bin/_lib_codex_conversation_query.py +22 -8
- package/bin/_lib_codex_pools.py +20 -8
- package/bin/_lib_conversation.py +6 -3
- package/bin/_lib_conversation_query.py +256 -69
- package/bin/_lib_dashboard_sources.py +212 -24
- package/bin/_lib_diagnosis.py +1261 -0
- package/bin/_lib_forecast.py +62 -4
- package/bin/_lib_perf.py +12 -0
- package/bin/_lib_pricing.py +8 -7
- package/bin/_lib_readme_refresh.py +26 -5
- 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 +103 -16
- package/bin/cctally-explain +5 -0
- package/dashboard/static/assets/dashboardStream.shared-worker-1XTMV3nr.js +1 -0
- package/dashboard/static/assets/index-Di2hljvB.css +1 -0
- package/dashboard/static/assets/index-XYCIWjVG.js +97 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +6 -1
- package/dashboard/static/assets/index-B5YfQEtn.css +0 -1
- package/dashboard/static/assets/index-Bt59nMMO.js +0 -97
|
@@ -42,6 +42,7 @@ from _lib_dashboard_sources import (
|
|
|
42
42
|
canonical_alerted_at,
|
|
43
43
|
canonical_alerted_at_sql,
|
|
44
44
|
claude_stats_digest,
|
|
45
|
+
combined_accounting_version,
|
|
45
46
|
codex_stats_digest,
|
|
46
47
|
dashboard_resource_key,
|
|
47
48
|
)
|
|
@@ -72,6 +73,7 @@ from _lib_source_analytics import (
|
|
|
72
73
|
build_codex_project_result,
|
|
73
74
|
collision_safe_project_label_map,
|
|
74
75
|
)
|
|
76
|
+
from _lib_source_identity import identity_path
|
|
75
77
|
from _lib_view_models import (
|
|
76
78
|
CodexWeeklyView,
|
|
77
79
|
build_codex_daily_view,
|
|
@@ -2258,7 +2260,9 @@ def _session_wire(
|
|
|
2258
2260
|
# session paths from distinct $CODEX_HOME roots. The opaque detail
|
|
2259
2261
|
# key must use that same grouping identity or two visible rows route
|
|
2260
2262
|
# to one another's detail payload.
|
|
2261
|
-
root_identity =
|
|
2263
|
+
root_identity = (
|
|
2264
|
+
identity_path(row.codex_root) if row.codex_root else "single-root"
|
|
2265
|
+
)
|
|
2262
2266
|
row_metadata = (metadata or {}).get((str(row.codex_root or ""), str(row.session_id_path)))
|
|
2263
2267
|
if row_metadata is None and metadata is not None:
|
|
2264
2268
|
row_metadata = next((
|
|
@@ -2705,6 +2709,86 @@ def _codex_account_admits(scope_key: str | None, row_key: object) -> bool:
|
|
|
2705
2709
|
return key in (scope_key, _lib_accounts.UNATTRIBUTED)
|
|
2706
2710
|
|
|
2707
2711
|
|
|
2712
|
+
def _quota_breakdown_capture_key(
|
|
2713
|
+
account_key: str | None,
|
|
2714
|
+
identity: QuotaWindowIdentity,
|
|
2715
|
+
resets_at: dt.datetime,
|
|
2716
|
+
) -> tuple[object, ...]:
|
|
2717
|
+
return (
|
|
2718
|
+
account_key,
|
|
2719
|
+
identity.source_root_key,
|
|
2720
|
+
identity.logical_limit_key,
|
|
2721
|
+
identity.observed_slot,
|
|
2722
|
+
identity.window_minutes,
|
|
2723
|
+
identity.account_key,
|
|
2724
|
+
resets_at.astimezone(UTC).isoformat(),
|
|
2725
|
+
)
|
|
2726
|
+
|
|
2727
|
+
|
|
2728
|
+
def _capture_quota_breakdown_evidence(
|
|
2729
|
+
context: DashboardReadContext,
|
|
2730
|
+
observations: Iterable[object],
|
|
2731
|
+
) -> Mapping[tuple[object, ...], tuple[tuple, tuple]]:
|
|
2732
|
+
"""Capture every fold-dependent weekly read from one cache generation.
|
|
2733
|
+
|
|
2734
|
+
The account partitions and `build_blocks` fold are intentionally cheap and
|
|
2735
|
+
may run inside the pin: their only purpose is to enumerate the bounded
|
|
2736
|
+
identity set whose durable breakdown and 5h-correlation reads cannot be
|
|
2737
|
+
issued after rollback. Expensive row aggregation remains in
|
|
2738
|
+
`_quota_read_model` and consumes these values later.
|
|
2739
|
+
"""
|
|
2740
|
+
values = tuple(observations)
|
|
2741
|
+
partitions: list[tuple[str | None, tuple[object, ...]]] = [(None, values)]
|
|
2742
|
+
account_keys = sorted({
|
|
2743
|
+
str(observation.identity.account_key) for observation in values
|
|
2744
|
+
})
|
|
2745
|
+
partitions.extend((key, tuple(
|
|
2746
|
+
observation for observation in values
|
|
2747
|
+
if str(observation.identity.account_key) == key
|
|
2748
|
+
)) for key in account_keys)
|
|
2749
|
+
captured: dict[tuple[object, ...], tuple[tuple, tuple]] = {}
|
|
2750
|
+
for account_key, partition in partitions:
|
|
2751
|
+
for block in build_blocks(partition):
|
|
2752
|
+
identity = block.identity
|
|
2753
|
+
if identity.window_minutes != 10_080 or block.resets_at <= context.now_utc:
|
|
2754
|
+
continue
|
|
2755
|
+
key = _quota_breakdown_capture_key(
|
|
2756
|
+
account_key, identity, block.resets_at)
|
|
2757
|
+
try:
|
|
2758
|
+
canonical_rows = codex_quota_breakdown(
|
|
2759
|
+
identity,
|
|
2760
|
+
block.resets_at,
|
|
2761
|
+
speed=context.speed,
|
|
2762
|
+
cache_conn=context.cache_conn,
|
|
2763
|
+
stats_conn=context.stats_conn,
|
|
2764
|
+
account_key=account_key,
|
|
2765
|
+
)
|
|
2766
|
+
except sqlite3.Error:
|
|
2767
|
+
canonical_rows = ()
|
|
2768
|
+
correlated_five_hour: tuple = ()
|
|
2769
|
+
if canonical_rows:
|
|
2770
|
+
try:
|
|
2771
|
+
correlated_five_hour = tuple(
|
|
2772
|
+
observation
|
|
2773
|
+
for observation in _cached_codex_quota_observations(
|
|
2774
|
+
source_root_keys={identity.source_root_key},
|
|
2775
|
+
cache_conn=context.cache_conn,
|
|
2776
|
+
stats_conn=context.stats_conn,
|
|
2777
|
+
stats_identity=context.stats_identity,
|
|
2778
|
+
captured_at_or_after=block.nominal_start_at,
|
|
2779
|
+
)
|
|
2780
|
+
if observation.identity.window_minutes == 300
|
|
2781
|
+
and observation.identity.observed_slot == identity.observed_slot
|
|
2782
|
+
and observation.identity.limit_id == identity.limit_id
|
|
2783
|
+
and _codex_account_admits(
|
|
2784
|
+
account_key, observation.identity.account_key)
|
|
2785
|
+
)
|
|
2786
|
+
except sqlite3.Error:
|
|
2787
|
+
correlated_five_hour = ()
|
|
2788
|
+
captured[key] = (tuple(canonical_rows), correlated_five_hour)
|
|
2789
|
+
return MappingProxyType(captured)
|
|
2790
|
+
|
|
2791
|
+
|
|
2708
2792
|
def _quota_read_model(
|
|
2709
2793
|
context: DashboardReadContext,
|
|
2710
2794
|
observations: Iterable[object],
|
|
@@ -2712,6 +2796,7 @@ def _quota_read_model(
|
|
|
2712
2796
|
accounting_entries: Iterable[object] = (),
|
|
2713
2797
|
account_key: str | None = None,
|
|
2714
2798
|
decorated: bool,
|
|
2799
|
+
captured_breakdowns: "Mapping[tuple[object, ...], tuple[tuple, tuple]] | None" = None,
|
|
2715
2800
|
) -> dict[str, object]:
|
|
2716
2801
|
"""Use S2's pure history/block/forecast kernels over cache evidence.
|
|
2717
2802
|
|
|
@@ -2824,43 +2909,52 @@ def _quota_read_model(
|
|
|
2824
2909
|
< block.resets_at
|
|
2825
2910
|
)
|
|
2826
2911
|
canonical_rows = ()
|
|
2912
|
+
correlated_five_hour = ()
|
|
2827
2913
|
if identity.window_minutes == 10_080 and block.resets_at > context.now_utc:
|
|
2828
|
-
|
|
2829
|
-
canonical_rows =
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
cache_conn=context.cache_conn,
|
|
2834
|
-
stats_conn=context.stats_conn,
|
|
2835
|
-
account_key=account_key,
|
|
2914
|
+
if captured_breakdowns is not None:
|
|
2915
|
+
canonical_rows, correlated_five_hour = captured_breakdowns.get(
|
|
2916
|
+
_quota_breakdown_capture_key(
|
|
2917
|
+
account_key, identity, block.resets_at),
|
|
2918
|
+
((), ()),
|
|
2836
2919
|
)
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
try:
|
|
2844
|
-
# #416 Slice 3A review F4: this load is bounded by root, slot
|
|
2845
|
-
# and `limit_id` only, so under focus the crossing was annotated
|
|
2846
|
-
# with whichever ACCOUNT's 5h observation happened to sort last.
|
|
2847
|
-
correlated_five_hour = tuple(
|
|
2848
|
-
observation
|
|
2849
|
-
for observation in _cached_codex_quota_observations(
|
|
2850
|
-
source_root_keys={identity.source_root_key},
|
|
2920
|
+
else:
|
|
2921
|
+
try:
|
|
2922
|
+
canonical_rows = codex_quota_breakdown(
|
|
2923
|
+
identity,
|
|
2924
|
+
block.resets_at,
|
|
2925
|
+
speed=context.speed,
|
|
2851
2926
|
cache_conn=context.cache_conn,
|
|
2852
2927
|
stats_conn=context.stats_conn,
|
|
2853
|
-
|
|
2854
|
-
captured_at_or_after=block.nominal_start_at,
|
|
2928
|
+
account_key=account_key,
|
|
2855
2929
|
)
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2930
|
+
except sqlite3.Error:
|
|
2931
|
+
# Older or partially migrated stores retain the bounded
|
|
2932
|
+
# observation-derived fallback below. A coherent current
|
|
2933
|
+
# store always has the durable projection used by the CLI.
|
|
2934
|
+
canonical_rows = ()
|
|
2935
|
+
if canonical_rows:
|
|
2936
|
+
if captured_breakdowns is None:
|
|
2937
|
+
try:
|
|
2938
|
+
# #416 Slice 3A review F4: this load is bounded by root,
|
|
2939
|
+
# slot and `limit_id` only, so under focus the crossing was
|
|
2940
|
+
# once annotated with whichever account sorted last.
|
|
2941
|
+
correlated_five_hour = tuple(
|
|
2942
|
+
observation
|
|
2943
|
+
for observation in _cached_codex_quota_observations(
|
|
2944
|
+
source_root_keys={identity.source_root_key},
|
|
2945
|
+
cache_conn=context.cache_conn,
|
|
2946
|
+
stats_conn=context.stats_conn,
|
|
2947
|
+
stats_identity=context.stats_identity,
|
|
2948
|
+
captured_at_or_after=block.nominal_start_at,
|
|
2949
|
+
)
|
|
2950
|
+
if observation.identity.window_minutes == 300
|
|
2951
|
+
and observation.identity.observed_slot == identity.observed_slot
|
|
2952
|
+
and observation.identity.limit_id == identity.limit_id
|
|
2953
|
+
and _codex_account_admits(
|
|
2954
|
+
account_key, observation.identity.account_key)
|
|
2955
|
+
)
|
|
2956
|
+
except sqlite3.Error:
|
|
2957
|
+
correlated_five_hour = ()
|
|
2864
2958
|
|
|
2865
2959
|
for row in canonical_rows:
|
|
2866
2960
|
milestone_rows.append({
|
|
@@ -3359,6 +3453,7 @@ def refresh_codex_source_clock(
|
|
|
3359
3453
|
# omission silently drops the authoritative account count and makes the
|
|
3360
3454
|
# combined figure fail closed on an idle tick that changed nothing else.
|
|
3361
3455
|
account_scope=state.account_scope,
|
|
3456
|
+
combined_accounting=state.combined_accounting,
|
|
3362
3457
|
private_session_labels=state.private_session_labels,
|
|
3363
3458
|
# #556 S2 §3.6: the aggregate carrier travels with the rows it
|
|
3364
3459
|
# describes. This clock refreshes presentation axes only and publishes
|
|
@@ -4103,8 +4198,10 @@ def _codex_accounts_wire(
|
|
|
4103
4198
|
accounting_start: dt.datetime,
|
|
4104
4199
|
accounting_end: dt.datetime,
|
|
4105
4200
|
population: tuple[object, ...],
|
|
4106
|
-
) -> tuple[
|
|
4107
|
-
|
|
4201
|
+
) -> tuple[
|
|
4202
|
+
list[dict[str, object]], list[dict[str, object]], dict[str, object]
|
|
4203
|
+
]:
|
|
4204
|
+
"""Return cards, hero cycles, and their server-only spend certificate.
|
|
4108
4205
|
|
|
4109
4206
|
``population`` is the merged, encounter-ordered accounting read every card
|
|
4110
4207
|
is derived from (#583 S5 change 2). This function issues no accounting SQL
|
|
@@ -4270,6 +4367,7 @@ def _codex_accounts_wire(
|
|
|
4270
4367
|
|
|
4271
4368
|
accounts_wire: list[dict[str, object]] = []
|
|
4272
4369
|
hero_cycles_wire: list[dict[str, object]] = []
|
|
4370
|
+
contributions: list[dict[str, object]] = []
|
|
4273
4371
|
for key in ordered_keys:
|
|
4274
4372
|
cyc = cycle_by_account.get(key)
|
|
4275
4373
|
is_unattributed = key == _lib_accounts.UNATTRIBUTED
|
|
@@ -4319,6 +4417,23 @@ def _codex_accounts_wire(
|
|
|
4319
4417
|
# which is true of several unrelated states (#564 D3).
|
|
4320
4418
|
card["spendWindow"] = fallback_window
|
|
4321
4419
|
accounts_wire.append(card)
|
|
4420
|
+
if cyc is not None and not is_unattributed:
|
|
4421
|
+
contribution_period = {
|
|
4422
|
+
"kind": "native_7_day_cycle",
|
|
4423
|
+
"start_at": cyc.start_at.astimezone(UTC).isoformat(),
|
|
4424
|
+
"end_at": cyc.resets_at.astimezone(UTC).isoformat(),
|
|
4425
|
+
}
|
|
4426
|
+
else:
|
|
4427
|
+
contribution_period = {
|
|
4428
|
+
"kind": "trailing_7_day_cycle",
|
|
4429
|
+
"start_at": fallback_start.astimezone(UTC).isoformat(),
|
|
4430
|
+
"end_at": context.now_utc.astimezone(UTC).isoformat(),
|
|
4431
|
+
}
|
|
4432
|
+
contributions.append({
|
|
4433
|
+
"account_key": key,
|
|
4434
|
+
"cost_usd": totals["spendUsd"],
|
|
4435
|
+
"period": contribution_period,
|
|
4436
|
+
})
|
|
4322
4437
|
if cyc is not None and not is_unattributed:
|
|
4323
4438
|
hero_cycles_wire.append({
|
|
4324
4439
|
"accountKey": key,
|
|
@@ -4329,7 +4444,11 @@ def _codex_accounts_wire(
|
|
|
4329
4444
|
"cost_usd": totals["spendUsd"],
|
|
4330
4445
|
"total_tokens": totals["totalTokens"],
|
|
4331
4446
|
})
|
|
4332
|
-
return accounts_wire, hero_cycles_wire
|
|
4447
|
+
return accounts_wire, hero_cycles_wire, {
|
|
4448
|
+
"scope": "account_cycles",
|
|
4449
|
+
"status": "certified",
|
|
4450
|
+
"contributions": tuple(contributions),
|
|
4451
|
+
}
|
|
4333
4452
|
|
|
4334
4453
|
|
|
4335
4454
|
def _codex_partition_by_account(
|
|
@@ -4447,6 +4566,7 @@ def _codex_account_scopes_wire(
|
|
|
4447
4566
|
changed_new_by_account: Mapping[str, tuple[CodexEntry, ...]] | None = None,
|
|
4448
4567
|
changed_old_rows_by_account: Mapping[str, tuple[object, ...]] | None = None,
|
|
4449
4568
|
changed_new_rows_by_account: Mapping[str, tuple[object, ...]] | None = None,
|
|
4569
|
+
captured_breakdowns: "Mapping[tuple[object, ...], tuple[tuple, tuple]] | None" = None,
|
|
4450
4570
|
) -> dict[str, dict[str, object]]:
|
|
4451
4571
|
"""The per-account CHILDREN of the merged Codex read model (spec §5.3).
|
|
4452
4572
|
|
|
@@ -4526,6 +4646,7 @@ def _codex_account_scopes_wire(
|
|
|
4526
4646
|
# #429 §4.4: this wire is only ever reached under decoration — the
|
|
4527
4647
|
# caller gates the whole `account_scopes` surface on it.
|
|
4528
4648
|
decorated=True,
|
|
4649
|
+
captured_breakdowns=captured_breakdowns,
|
|
4529
4650
|
)
|
|
4530
4651
|
# Each decorated child owns the only honest cycle index for that
|
|
4531
4652
|
# account. Reusing a merged parent index here would render account A's
|
|
@@ -4755,7 +4876,7 @@ def _claude_accounts_wire(
|
|
|
4755
4876
|
stats_conn: sqlite3.Connection,
|
|
4756
4877
|
*,
|
|
4757
4878
|
now_utc: dt.datetime,
|
|
4758
|
-
) -> list[dict[str, object]]:
|
|
4879
|
+
) -> tuple[list[dict[str, object]], dict[str, object]]:
|
|
4759
4880
|
"""Per-account Claude hero cards (#341 Task 4, Ruling C).
|
|
4760
4881
|
|
|
4761
4882
|
Symmetric with ``_codex_accounts_wire``: the caller gates on
|
|
@@ -4784,13 +4905,38 @@ def _claude_accounts_wire(
|
|
|
4784
4905
|
(key,),
|
|
4785
4906
|
).fetchone()
|
|
4786
4907
|
|
|
4787
|
-
def _latest_cost(key: str)
|
|
4908
|
+
def _latest_cost(key: str):
|
|
4788
4909
|
row = stats_conn.execute(
|
|
4789
|
-
"SELECT cost_usd
|
|
4910
|
+
"SELECT cost_usd, week_start_at, week_end_at "
|
|
4911
|
+
"FROM weekly_cost_snapshots WHERE account_key=? "
|
|
4790
4912
|
"ORDER BY captured_at_utc DESC LIMIT 1",
|
|
4791
4913
|
(key,),
|
|
4792
4914
|
).fetchone()
|
|
4793
|
-
|
|
4915
|
+
if row is None or row[0] is None:
|
|
4916
|
+
return 0.0, None, "account_cost_unresolved"
|
|
4917
|
+
try:
|
|
4918
|
+
start = dt.datetime.fromisoformat(str(row[1]).replace("Z", "+00:00"))
|
|
4919
|
+
end = dt.datetime.fromisoformat(str(row[2]).replace("Z", "+00:00"))
|
|
4920
|
+
except (TypeError, ValueError):
|
|
4921
|
+
return float(row[0]), None, "account_cycle_unresolved"
|
|
4922
|
+
if (
|
|
4923
|
+
start.tzinfo is None or start.utcoffset() is None
|
|
4924
|
+
or end.tzinfo is None or end.utcoffset() is None
|
|
4925
|
+
or start >= end
|
|
4926
|
+
):
|
|
4927
|
+
return float(row[0]), None, "account_cycle_unresolved"
|
|
4928
|
+
start_at = start.astimezone(UTC).isoformat().replace("+00:00", "Z")
|
|
4929
|
+
end_at = end.astimezone(UTC).isoformat().replace("+00:00", "Z")
|
|
4930
|
+
contribution = {
|
|
4931
|
+
"account_key": key,
|
|
4932
|
+
"cost_usd": float(row[0]),
|
|
4933
|
+
"period": {
|
|
4934
|
+
"kind": "subscription_week",
|
|
4935
|
+
"start_at": start_at,
|
|
4936
|
+
"end_at": end_at,
|
|
4937
|
+
},
|
|
4938
|
+
}
|
|
4939
|
+
return float(row[0]), contribution, None
|
|
4794
4940
|
|
|
4795
4941
|
_claude_label_map = _cctally_account.display_label_map(stats_conn, "claude")
|
|
4796
4942
|
|
|
@@ -4804,12 +4950,20 @@ def _claude_accounts_wire(
|
|
|
4804
4950
|
ordered_keys.append(_lib_accounts.UNATTRIBUTED)
|
|
4805
4951
|
|
|
4806
4952
|
cards: list[dict[str, object]] = []
|
|
4953
|
+
contributions: list[dict[str, object]] = []
|
|
4954
|
+
unresolved_causes: set[str] = set()
|
|
4807
4955
|
for key in ordered_keys:
|
|
4808
4956
|
is_unattributed = key == _lib_accounts.UNATTRIBUTED
|
|
4809
4957
|
usage = _latest_usage(key)
|
|
4810
4958
|
weekly_pct = usage[0] if usage is not None else None
|
|
4811
4959
|
five_hour_pct = usage[1] if usage is not None else None
|
|
4812
4960
|
resets_at = usage[2] if usage is not None else None
|
|
4961
|
+
spend_usd, contribution, unresolved_cause = _latest_cost(key)
|
|
4962
|
+
if contribution is None:
|
|
4963
|
+
unresolved_causes.add(
|
|
4964
|
+
unresolved_cause or "account_reconciliation_failed")
|
|
4965
|
+
else:
|
|
4966
|
+
contributions.append(contribution)
|
|
4813
4967
|
card: dict[str, object] = {
|
|
4814
4968
|
"accountKey": key,
|
|
4815
4969
|
"label": _claude_label_map.get(key) or _cctally_account.account_label(stats_conn, key),
|
|
@@ -4818,12 +4972,30 @@ def _claude_accounts_wire(
|
|
|
4818
4972
|
"weeklyPercent": None if is_unattributed else weekly_pct,
|
|
4819
4973
|
"fiveHourPercent": None if is_unattributed else five_hour_pct,
|
|
4820
4974
|
"resetsAt": None if is_unattributed else resets_at,
|
|
4821
|
-
"spendUsd":
|
|
4975
|
+
"spendUsd": spend_usd,
|
|
4822
4976
|
}
|
|
4823
4977
|
if is_unattributed:
|
|
4824
4978
|
card["unattributed"] = True
|
|
4979
|
+
if contribution is not None:
|
|
4980
|
+
period = contribution["period"]
|
|
4981
|
+
card["spendWindow"] = {
|
|
4982
|
+
"kind": "subscription-week",
|
|
4983
|
+
"startAt": period["start_at"],
|
|
4984
|
+
"endAt": period["end_at"],
|
|
4985
|
+
}
|
|
4825
4986
|
cards.append(card)
|
|
4826
|
-
|
|
4987
|
+
certificate: dict[str, object] = {
|
|
4988
|
+
"scope": "account_cycles",
|
|
4989
|
+
"status": "unresolved" if unresolved_causes else "certified",
|
|
4990
|
+
"contributions": tuple(contributions),
|
|
4991
|
+
}
|
|
4992
|
+
if unresolved_causes:
|
|
4993
|
+
certificate["cause"] = (
|
|
4994
|
+
"account_cost_unresolved"
|
|
4995
|
+
if "account_cost_unresolved" in unresolved_causes
|
|
4996
|
+
else "account_cycle_unresolved"
|
|
4997
|
+
)
|
|
4998
|
+
return cards, certificate
|
|
4827
4999
|
|
|
4828
5000
|
|
|
4829
5001
|
def _codex_ingest_backlog_wire(
|
|
@@ -4904,6 +5076,297 @@ def reset_codex_source_caches() -> None:
|
|
|
4904
5076
|
_lib_snapshot_cache.reset_codex_accounting_cache_state()
|
|
4905
5077
|
|
|
4906
5078
|
|
|
5079
|
+
@dataclass(frozen=True)
|
|
5080
|
+
class _CodexAccountingCapture:
|
|
5081
|
+
qualified_entries: tuple[object, ...]
|
|
5082
|
+
dirty_accounts: tuple[str, ...]
|
|
5083
|
+
changed_old: tuple[object, ...]
|
|
5084
|
+
changed_new: tuple[object, ...]
|
|
5085
|
+
entries: tuple[object, ...]
|
|
5086
|
+
metadata_incomplete: bool
|
|
5087
|
+
|
|
5088
|
+
|
|
5089
|
+
def _capture_codex_accounting(
|
|
5090
|
+
context: DashboardReadContext,
|
|
5091
|
+
*,
|
|
5092
|
+
accounting_start: dt.datetime,
|
|
5093
|
+
accounting_end: dt.datetime,
|
|
5094
|
+
active_roots: tuple[str, ...],
|
|
5095
|
+
path_scope: object,
|
|
5096
|
+
metadata_incomplete: bool,
|
|
5097
|
+
) -> _CodexAccountingCapture:
|
|
5098
|
+
"""Read the incremental accounting carrier without folding public views."""
|
|
5099
|
+
qualified_entries: tuple[object, ...] = ()
|
|
5100
|
+
dirty_accounts: tuple[str, ...] = ()
|
|
5101
|
+
changed_old: tuple[object, ...] = ()
|
|
5102
|
+
changed_new: tuple[object, ...] = ()
|
|
5103
|
+
if not metadata_incomplete:
|
|
5104
|
+
try:
|
|
5105
|
+
cached_accounting = _lib_snapshot_cache.build_cached_codex_accounting(
|
|
5106
|
+
cache_conn=context.cache_conn,
|
|
5107
|
+
range_start=accounting_start,
|
|
5108
|
+
range_end=accounting_end,
|
|
5109
|
+
extra_signature=(
|
|
5110
|
+
context.speed,
|
|
5111
|
+
tuple(str(root) for root in path_scope.roots),
|
|
5112
|
+
active_roots,
|
|
5113
|
+
),
|
|
5114
|
+
load_all=lambda: load_qualified_codex_entries(
|
|
5115
|
+
accounting_start,
|
|
5116
|
+
accounting_end,
|
|
5117
|
+
speed=context.speed,
|
|
5118
|
+
sync=False,
|
|
5119
|
+
cache_conn=context.cache_conn,
|
|
5120
|
+
),
|
|
5121
|
+
load_paths=lambda identities: load_qualified_codex_entries(
|
|
5122
|
+
accounting_start,
|
|
5123
|
+
accounting_end,
|
|
5124
|
+
speed=context.speed,
|
|
5125
|
+
sync=False,
|
|
5126
|
+
cache_conn=context.cache_conn,
|
|
5127
|
+
source_identities=identities,
|
|
5128
|
+
),
|
|
5129
|
+
path_of=lambda entry: (
|
|
5130
|
+
str(entry.source_root_key), str(entry.source_path),
|
|
5131
|
+
),
|
|
5132
|
+
account_of=lambda entry: str(entry.account_key),
|
|
5133
|
+
order_key=lambda entry: (
|
|
5134
|
+
entry.timestamp,
|
|
5135
|
+
str(entry.source_root_key),
|
|
5136
|
+
str(entry.conversation_key),
|
|
5137
|
+
int(entry.cache_entry_id),
|
|
5138
|
+
),
|
|
5139
|
+
identity_of=lambda entry: int(entry.cache_entry_id),
|
|
5140
|
+
)
|
|
5141
|
+
qualified_entries = cached_accounting.entries
|
|
5142
|
+
dirty_accounts = cached_accounting.dirty_accounts
|
|
5143
|
+
changed_old = cached_accounting.changed_old
|
|
5144
|
+
changed_new = cached_accounting.changed_new
|
|
5145
|
+
entries: tuple[object, ...] = qualified_entries
|
|
5146
|
+
except QualifiedMetadataUnavailable:
|
|
5147
|
+
_lib_log.get_logger("dashboard").warning(
|
|
5148
|
+
"Codex qualified metadata read became unavailable; using cache-only accounting fallback"
|
|
5149
|
+
)
|
|
5150
|
+
metadata_incomplete = True
|
|
5151
|
+
_lib_snapshot_cache.reset_codex_accounting_cache_state()
|
|
5152
|
+
entries = load_cached_rooted_codex_accounting_entries(
|
|
5153
|
+
accounting_start,
|
|
5154
|
+
accounting_end,
|
|
5155
|
+
speed=context.speed,
|
|
5156
|
+
cache_conn=context.cache_conn,
|
|
5157
|
+
)
|
|
5158
|
+
dirty_accounts = tuple(sorted({
|
|
5159
|
+
str(getattr(entry, "account_key", "") or
|
|
5160
|
+
_lib_accounts.UNATTRIBUTED)
|
|
5161
|
+
for entry in entries
|
|
5162
|
+
}))
|
|
5163
|
+
else:
|
|
5164
|
+
_lib_snapshot_cache.reset_codex_accounting_cache_state()
|
|
5165
|
+
entries = load_cached_rooted_codex_accounting_entries(
|
|
5166
|
+
accounting_start,
|
|
5167
|
+
accounting_end,
|
|
5168
|
+
speed=context.speed,
|
|
5169
|
+
cache_conn=context.cache_conn,
|
|
5170
|
+
)
|
|
5171
|
+
dirty_accounts = tuple(sorted({
|
|
5172
|
+
str(getattr(entry, "account_key", "") or
|
|
5173
|
+
_lib_accounts.UNATTRIBUTED)
|
|
5174
|
+
for entry in entries
|
|
5175
|
+
}))
|
|
5176
|
+
return _CodexAccountingCapture(
|
|
5177
|
+
qualified_entries=qualified_entries,
|
|
5178
|
+
dirty_accounts=dirty_accounts,
|
|
5179
|
+
changed_old=changed_old,
|
|
5180
|
+
changed_new=changed_new,
|
|
5181
|
+
entries=tuple(entries),
|
|
5182
|
+
metadata_incomplete=metadata_incomplete,
|
|
5183
|
+
)
|
|
5184
|
+
|
|
5185
|
+
|
|
5186
|
+
@dataclass(frozen=True)
|
|
5187
|
+
class CodexSourceCapture:
|
|
5188
|
+
"""Every cache-backed input for a post-transaction Codex source build."""
|
|
5189
|
+
|
|
5190
|
+
context: DashboardReadContext
|
|
5191
|
+
active_roots: tuple[str, ...]
|
|
5192
|
+
quota_observations: tuple[object, ...]
|
|
5193
|
+
projection_incoherent: bool
|
|
5194
|
+
ingest_backlog: object
|
|
5195
|
+
accounting_start: dt.datetime
|
|
5196
|
+
accounting_end: dt.datetime
|
|
5197
|
+
health: object
|
|
5198
|
+
accounting: _CodexAccountingCapture
|
|
5199
|
+
accounting_exists: bool
|
|
5200
|
+
accounting_exists_error: Exception | None
|
|
5201
|
+
conversation_metadata: Mapping
|
|
5202
|
+
decorated: bool
|
|
5203
|
+
card_population: tuple[object, ...]
|
|
5204
|
+
breakdowns: Mapping
|
|
5205
|
+
cache_checkpoint: tuple[dict, ...]
|
|
5206
|
+
accounting_checkpoint: object
|
|
5207
|
+
|
|
5208
|
+
|
|
5209
|
+
def _restore_codex_source_capture(capture: CodexSourceCapture) -> None:
|
|
5210
|
+
for cache, prior in zip(_codex_source_caches(), capture.cache_checkpoint):
|
|
5211
|
+
cache.clear()
|
|
5212
|
+
cache.update(prior)
|
|
5213
|
+
_lib_snapshot_cache.restore_codex_accounting_cache_state(
|
|
5214
|
+
capture.accounting_checkpoint)
|
|
5215
|
+
|
|
5216
|
+
|
|
5217
|
+
def capture_codex_source_state(
|
|
5218
|
+
context: DashboardReadContext,
|
|
5219
|
+
*,
|
|
5220
|
+
path_scope: object,
|
|
5221
|
+
) -> CodexSourceCapture:
|
|
5222
|
+
"""Capture Codex cache evidence without constructing public view models."""
|
|
5223
|
+
caches = _codex_source_caches()
|
|
5224
|
+
cache_checkpoint = tuple(dict(cache) for cache in caches)
|
|
5225
|
+
accounting_checkpoint = (
|
|
5226
|
+
_lib_snapshot_cache.checkpoint_codex_accounting_cache_state()
|
|
5227
|
+
)
|
|
5228
|
+
try:
|
|
5229
|
+
active_roots = tuple(sorted(
|
|
5230
|
+
str(row[0]) for row in context.cache_conn.execute(
|
|
5231
|
+
"SELECT source_root_key FROM codex_source_roots"
|
|
5232
|
+
)
|
|
5233
|
+
))
|
|
5234
|
+
quota_observations = tuple(_cached_codex_quota_observations(
|
|
5235
|
+
source_root_keys=active_roots,
|
|
5236
|
+
cache_conn=context.cache_conn,
|
|
5237
|
+
stats_conn=context.stats_conn,
|
|
5238
|
+
stats_identity=context.stats_identity,
|
|
5239
|
+
memoize=False,
|
|
5240
|
+
captured_at_or_after=(
|
|
5241
|
+
context.now_utc - dt.timedelta(days=DASHBOARD_QUOTA_RECENT_DAYS)
|
|
5242
|
+
),
|
|
5243
|
+
active_at=context.now_utc,
|
|
5244
|
+
max_rows=DASHBOARD_QUOTA_OBSERVATION_LIMIT,
|
|
5245
|
+
))
|
|
5246
|
+
projection_incoherent = not codex_projection_coherence(context).coherent
|
|
5247
|
+
ingest_backlog = _codex_ingest_backlog_wire(context.cache_conn)
|
|
5248
|
+
accounting_end = context.now_utc + dt.timedelta(microseconds=1)
|
|
5249
|
+
accounting_start = context.range_start
|
|
5250
|
+
if context.codex_budget is not None:
|
|
5251
|
+
try:
|
|
5252
|
+
_period, budget_start, _budget_end = (
|
|
5253
|
+
_configured_codex_budget_window(context)
|
|
5254
|
+
)
|
|
5255
|
+
except Exception:
|
|
5256
|
+
_warn_codex_budget_window_once("accounting_range")
|
|
5257
|
+
else:
|
|
5258
|
+
accounting_start = min(accounting_start, budget_start)
|
|
5259
|
+
health = load_codex_project_metadata_health(
|
|
5260
|
+
cache_conn=context.cache_conn,
|
|
5261
|
+
start=accounting_start,
|
|
5262
|
+
end=accounting_end,
|
|
5263
|
+
)
|
|
5264
|
+
accounting = _capture_codex_accounting(
|
|
5265
|
+
context,
|
|
5266
|
+
accounting_start=accounting_start,
|
|
5267
|
+
accounting_end=accounting_end,
|
|
5268
|
+
active_roots=active_roots,
|
|
5269
|
+
path_scope=path_scope,
|
|
5270
|
+
metadata_incomplete=health.incomplete_rows > 0,
|
|
5271
|
+
)
|
|
5272
|
+
try:
|
|
5273
|
+
accounting_exists = has_cached_codex_accounting_entries(
|
|
5274
|
+
cache_conn=context.cache_conn)
|
|
5275
|
+
accounting_exists_error = None
|
|
5276
|
+
except Exception as exc:
|
|
5277
|
+
# This probe is semantically relevant only when quota evidence
|
|
5278
|
+
# cannot resolve a native cycle. Capture the fault beside the
|
|
5279
|
+
# value, then defer it until the build knows that branch is live.
|
|
5280
|
+
accounting_exists = False
|
|
5281
|
+
accounting_exists_error = exc
|
|
5282
|
+
conversation_metadata = MappingProxyType(
|
|
5283
|
+
_codex_conversation_metadata(context.cache_conn))
|
|
5284
|
+
try:
|
|
5285
|
+
import _cctally_account
|
|
5286
|
+
decorated = _cctally_account.provider_is_decorated(
|
|
5287
|
+
context.stats_conn, "codex")
|
|
5288
|
+
except Exception:
|
|
5289
|
+
decorated = False
|
|
5290
|
+
card_population_start = accounting_start
|
|
5291
|
+
if decorated:
|
|
5292
|
+
card_population_start = min(
|
|
5293
|
+
context.range_start,
|
|
5294
|
+
context.now_utc - dt.timedelta(
|
|
5295
|
+
minutes=ACCOUNT_WEEKLY_WINDOW_MINUTES),
|
|
5296
|
+
accounting_start,
|
|
5297
|
+
)
|
|
5298
|
+
else:
|
|
5299
|
+
# The undecorated parent has no account cards. It needs an earlier
|
|
5300
|
+
# population only when a healthy representative cycle actually
|
|
5301
|
+
# resolves; an unresolved narrow share must not pay for a seven-day
|
|
5302
|
+
# read whose rows no published domain can consume.
|
|
5303
|
+
try:
|
|
5304
|
+
captured_cycles = _resolve_codex_weekly_cycle(
|
|
5305
|
+
quota_observations, context.now_utc)
|
|
5306
|
+
except CodexCycleUnavailable:
|
|
5307
|
+
captured_cycles = ()
|
|
5308
|
+
if captured_cycles:
|
|
5309
|
+
card_population_start = min(
|
|
5310
|
+
accounting_start, captured_cycles[0].start_at)
|
|
5311
|
+
card_population = (
|
|
5312
|
+
accounting.entries
|
|
5313
|
+
if card_population_start == accounting_start
|
|
5314
|
+
else load_cached_rooted_codex_accounting_entries(
|
|
5315
|
+
card_population_start,
|
|
5316
|
+
accounting_end,
|
|
5317
|
+
speed=context.speed,
|
|
5318
|
+
cache_conn=context.cache_conn,
|
|
5319
|
+
)
|
|
5320
|
+
)
|
|
5321
|
+
breakdowns = _capture_quota_breakdown_evidence(
|
|
5322
|
+
context, quota_observations)
|
|
5323
|
+
return CodexSourceCapture(
|
|
5324
|
+
context=context,
|
|
5325
|
+
active_roots=active_roots,
|
|
5326
|
+
quota_observations=quota_observations,
|
|
5327
|
+
projection_incoherent=projection_incoherent,
|
|
5328
|
+
ingest_backlog=ingest_backlog,
|
|
5329
|
+
accounting_start=accounting_start,
|
|
5330
|
+
accounting_end=accounting_end,
|
|
5331
|
+
health=health,
|
|
5332
|
+
accounting=accounting,
|
|
5333
|
+
accounting_exists=accounting_exists,
|
|
5334
|
+
accounting_exists_error=accounting_exists_error,
|
|
5335
|
+
conversation_metadata=conversation_metadata,
|
|
5336
|
+
decorated=decorated,
|
|
5337
|
+
card_population=tuple(card_population),
|
|
5338
|
+
breakdowns=breakdowns,
|
|
5339
|
+
cache_checkpoint=cache_checkpoint,
|
|
5340
|
+
accounting_checkpoint=accounting_checkpoint,
|
|
5341
|
+
)
|
|
5342
|
+
except Exception:
|
|
5343
|
+
for cache, prior in zip(caches, cache_checkpoint):
|
|
5344
|
+
cache.clear()
|
|
5345
|
+
cache.update(prior)
|
|
5346
|
+
_lib_snapshot_cache.restore_codex_accounting_cache_state(
|
|
5347
|
+
accounting_checkpoint)
|
|
5348
|
+
raise
|
|
5349
|
+
|
|
5350
|
+
|
|
5351
|
+
def build_codex_source_state_from_capture(
|
|
5352
|
+
capture: CodexSourceCapture,
|
|
5353
|
+
*,
|
|
5354
|
+
data_version: str,
|
|
5355
|
+
path_scope: object,
|
|
5356
|
+
) -> SourceDashboardState:
|
|
5357
|
+
"""Fold one captured Codex generation without issuing cache SQL."""
|
|
5358
|
+
try:
|
|
5359
|
+
return _build_codex_source_state(
|
|
5360
|
+
capture.context,
|
|
5361
|
+
data_version=data_version,
|
|
5362
|
+
path_scope=path_scope,
|
|
5363
|
+
capture=capture,
|
|
5364
|
+
)
|
|
5365
|
+
except Exception:
|
|
5366
|
+
_restore_codex_source_capture(capture)
|
|
5367
|
+
raise
|
|
5368
|
+
|
|
5369
|
+
|
|
4907
5370
|
def build_codex_source_state(
|
|
4908
5371
|
context: DashboardReadContext,
|
|
4909
5372
|
*,
|
|
@@ -4927,23 +5390,29 @@ def build_codex_source_state(
|
|
|
4927
5390
|
# `_codex_quota_reuse_identity`. The clear that used to stand here existed
|
|
4928
5391
|
# because a decoration or account-registry change advances no cache.db leg
|
|
4929
5392
|
# and so could not move a cache-only key. Do not narrow that identity.
|
|
4930
|
-
|
|
4931
|
-
|
|
4932
|
-
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
5393
|
+
with codex_path_scope() as path_scope:
|
|
5394
|
+
capture = capture_codex_source_state(
|
|
5395
|
+
context, path_scope=path_scope)
|
|
5396
|
+
return build_codex_source_state_from_capture(
|
|
5397
|
+
capture, data_version=data_version, path_scope=path_scope)
|
|
5398
|
+
|
|
5399
|
+
|
|
5400
|
+
def _build_codex_cycle_accounting(
|
|
5401
|
+
rows: Iterable[object],
|
|
5402
|
+
*,
|
|
5403
|
+
now_utc: dt.datetime,
|
|
5404
|
+
display_tz_name: str | None,
|
|
5405
|
+
speed: str,
|
|
5406
|
+
) -> tuple[list[CodexEntry], float]:
|
|
5407
|
+
"""Price the hero's already-captured native-cycle accounting rows."""
|
|
5408
|
+
entries = _codex_entries_from_accounting(rows)
|
|
5409
|
+
cost_usd = build_codex_daily_view(
|
|
5410
|
+
entries,
|
|
5411
|
+
now_utc=now_utc,
|
|
5412
|
+
tz_name=display_tz_name,
|
|
5413
|
+
speed=speed,
|
|
5414
|
+
).total_cost_usd
|
|
5415
|
+
return entries, cost_usd
|
|
4947
5416
|
|
|
4948
5417
|
|
|
4949
5418
|
def _build_codex_source_state(
|
|
@@ -4951,12 +5420,17 @@ def _build_codex_source_state(
|
|
|
4951
5420
|
*,
|
|
4952
5421
|
data_version: str,
|
|
4953
5422
|
path_scope: object,
|
|
5423
|
+
capture: "CodexSourceCapture | None" = None,
|
|
4954
5424
|
) -> SourceDashboardState:
|
|
4955
|
-
active_roots =
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
5425
|
+
active_roots = (
|
|
5426
|
+
capture.active_roots
|
|
5427
|
+
if capture is not None
|
|
5428
|
+
else tuple(sorted(
|
|
5429
|
+
str(row[0]) for row in context.cache_conn.execute(
|
|
5430
|
+
"SELECT source_root_key FROM codex_source_roots"
|
|
5431
|
+
)
|
|
5432
|
+
))
|
|
5433
|
+
)
|
|
4960
5434
|
# This read is a function of the tick instant, so it does NOT reuse across
|
|
4961
5435
|
# ticks and the cross-build memo above cannot help it. Both bounds move
|
|
4962
5436
|
# with `context.now_utc`, which in production is the tick's wall clock
|
|
@@ -4987,34 +5461,51 @@ def _build_codex_source_state(
|
|
|
4987
5461
|
# superset fix, which is why the design conclusion above stands. The
|
|
4988
5462
|
# doctor's read is about 73% of the un-profiled total and this memo never
|
|
4989
5463
|
# sees it.
|
|
4990
|
-
quota_observations =
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5464
|
+
quota_observations = (
|
|
5465
|
+
capture.quota_observations
|
|
5466
|
+
if capture is not None
|
|
5467
|
+
else _cached_codex_quota_observations(
|
|
5468
|
+
source_root_keys=active_roots,
|
|
5469
|
+
cache_conn=context.cache_conn,
|
|
5470
|
+
stats_conn=context.stats_conn,
|
|
5471
|
+
stats_identity=context.stats_identity,
|
|
5472
|
+
# A key carrying the tick instant cannot match a retained entry, so the
|
|
5473
|
+
# memo is bypassed rather than asked. On the share path
|
|
5474
|
+
# `stats_identity` is None as well, and establishing an identity for an
|
|
5475
|
+
# unreachable key cost 35 ms of stats.db work per render.
|
|
5476
|
+
memoize=False,
|
|
5477
|
+
captured_at_or_after=(
|
|
5478
|
+
context.now_utc - dt.timedelta(days=DASHBOARD_QUOTA_RECENT_DAYS)
|
|
5479
|
+
),
|
|
5480
|
+
active_at=context.now_utc,
|
|
5481
|
+
max_rows=DASHBOARD_QUOTA_OBSERVATION_LIMIT,
|
|
5482
|
+
)
|
|
5005
5483
|
)
|
|
5006
|
-
|
|
5007
|
-
|
|
5484
|
+
projection_incoherent = (
|
|
5485
|
+
capture.projection_incoherent
|
|
5486
|
+
if capture is not None
|
|
5487
|
+
else not codex_projection_coherence(context).coherent
|
|
5488
|
+
)
|
|
5489
|
+
ingest_backlog = (
|
|
5490
|
+
capture.ingest_backlog
|
|
5491
|
+
if capture is not None
|
|
5492
|
+
else _codex_ingest_backlog_wire(context.cache_conn)
|
|
5008
5493
|
)
|
|
5009
|
-
projection_incoherent = not coherence.coherent
|
|
5010
|
-
ingest_backlog = _codex_ingest_backlog_wire(context.cache_conn)
|
|
5011
5494
|
# The cache reader's established report surface treats the ``now`` instant
|
|
5012
5495
|
# as inclusive. The qualified adapter is half-open, so extend only its
|
|
5013
5496
|
# query/result boundary by one microsecond and keep all live budget sums
|
|
5014
5497
|
# explicitly half-open at ``now`` below.
|
|
5015
|
-
accounting_end =
|
|
5016
|
-
|
|
5017
|
-
|
|
5498
|
+
accounting_end = (
|
|
5499
|
+
capture.accounting_end
|
|
5500
|
+
if capture is not None
|
|
5501
|
+
else context.now_utc + dt.timedelta(microseconds=1)
|
|
5502
|
+
)
|
|
5503
|
+
accounting_start = (
|
|
5504
|
+
capture.accounting_start
|
|
5505
|
+
if capture is not None
|
|
5506
|
+
else context.range_start
|
|
5507
|
+
)
|
|
5508
|
+
if capture is None and context.codex_budget is not None:
|
|
5018
5509
|
# #556 S5 Unit 2 (Unit 1 review R6, widened) — this is the SECOND
|
|
5019
5510
|
# unguarded call to the window resolver, and unlike
|
|
5020
5511
|
# `_codex_budget_cost_events` it had no boundary of its own. It sits
|
|
@@ -5032,10 +5523,14 @@ def _build_codex_source_state(
|
|
|
5032
5523
|
_warn_codex_budget_window_once("accounting_range")
|
|
5033
5524
|
else:
|
|
5034
5525
|
accounting_start = min(accounting_start, budget_start)
|
|
5035
|
-
health =
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5526
|
+
health = (
|
|
5527
|
+
capture.health
|
|
5528
|
+
if capture is not None
|
|
5529
|
+
else load_codex_project_metadata_health(
|
|
5530
|
+
cache_conn=context.cache_conn,
|
|
5531
|
+
start=accounting_start,
|
|
5532
|
+
end=accounting_end,
|
|
5533
|
+
)
|
|
5039
5534
|
)
|
|
5040
5535
|
metadata_incomplete = health.incomplete_rows > 0
|
|
5041
5536
|
metadata_warning_message = (
|
|
@@ -5045,85 +5540,24 @@ def _build_codex_source_state(
|
|
|
5045
5540
|
else "Codex project metadata could not be read; "
|
|
5046
5541
|
"run `cctally cache-sync --source codex --rebuild`."
|
|
5047
5542
|
)
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
extra_signature=(
|
|
5059
|
-
context.speed,
|
|
5060
|
-
tuple(str(root) for root in path_scope.roots),
|
|
5061
|
-
active_roots,
|
|
5062
|
-
),
|
|
5063
|
-
load_all=lambda: load_qualified_codex_entries(
|
|
5064
|
-
accounting_start,
|
|
5065
|
-
accounting_end,
|
|
5066
|
-
speed=context.speed,
|
|
5067
|
-
sync=False,
|
|
5068
|
-
cache_conn=context.cache_conn,
|
|
5069
|
-
),
|
|
5070
|
-
load_paths=lambda identities: load_qualified_codex_entries(
|
|
5071
|
-
accounting_start,
|
|
5072
|
-
accounting_end,
|
|
5073
|
-
speed=context.speed,
|
|
5074
|
-
sync=False,
|
|
5075
|
-
cache_conn=context.cache_conn,
|
|
5076
|
-
source_identities=identities,
|
|
5077
|
-
),
|
|
5078
|
-
path_of=lambda entry: (
|
|
5079
|
-
str(entry.source_root_key), str(entry.source_path),
|
|
5080
|
-
),
|
|
5081
|
-
account_of=lambda entry: str(entry.account_key),
|
|
5082
|
-
order_key=lambda entry: (
|
|
5083
|
-
entry.timestamp,
|
|
5084
|
-
str(entry.source_root_key),
|
|
5085
|
-
str(entry.conversation_key),
|
|
5086
|
-
int(entry.cache_entry_id),
|
|
5087
|
-
),
|
|
5088
|
-
identity_of=lambda entry: int(entry.cache_entry_id),
|
|
5089
|
-
)
|
|
5090
|
-
qualified_entries = cached_accounting.entries
|
|
5091
|
-
accounting_dirty_accounts = cached_accounting.dirty_accounts
|
|
5092
|
-
accounting_changed_old = cached_accounting.changed_old
|
|
5093
|
-
accounting_changed_new = cached_accounting.changed_new
|
|
5094
|
-
accounting_entries: tuple[object, ...] = qualified_entries
|
|
5095
|
-
except QualifiedMetadataUnavailable:
|
|
5096
|
-
# A cached read must be internally coherent, but retain accounting
|
|
5097
|
-
# once if a defensive race or malformed row violates that premise.
|
|
5098
|
-
_lib_log.get_logger("dashboard").warning(
|
|
5099
|
-
"Codex qualified metadata read became unavailable; using cache-only accounting fallback"
|
|
5100
|
-
)
|
|
5101
|
-
metadata_incomplete = True
|
|
5102
|
-
_lib_snapshot_cache.reset_codex_accounting_cache_state()
|
|
5103
|
-
accounting_entries = load_cached_rooted_codex_accounting_entries(
|
|
5104
|
-
accounting_start,
|
|
5105
|
-
accounting_end,
|
|
5106
|
-
speed=context.speed,
|
|
5107
|
-
cache_conn=context.cache_conn,
|
|
5108
|
-
)
|
|
5109
|
-
accounting_dirty_accounts = tuple(sorted({
|
|
5110
|
-
str(getattr(entry, "account_key", "") or
|
|
5111
|
-
_lib_accounts.UNATTRIBUTED)
|
|
5112
|
-
for entry in accounting_entries
|
|
5113
|
-
}))
|
|
5114
|
-
else:
|
|
5115
|
-
_lib_snapshot_cache.reset_codex_accounting_cache_state()
|
|
5116
|
-
accounting_entries = load_cached_rooted_codex_accounting_entries(
|
|
5117
|
-
accounting_start,
|
|
5118
|
-
accounting_end,
|
|
5119
|
-
speed=context.speed,
|
|
5120
|
-
cache_conn=context.cache_conn,
|
|
5543
|
+
accounting_capture = (
|
|
5544
|
+
capture.accounting
|
|
5545
|
+
if capture is not None
|
|
5546
|
+
else _capture_codex_accounting(
|
|
5547
|
+
context,
|
|
5548
|
+
accounting_start=accounting_start,
|
|
5549
|
+
accounting_end=accounting_end,
|
|
5550
|
+
active_roots=active_roots,
|
|
5551
|
+
path_scope=path_scope,
|
|
5552
|
+
metadata_incomplete=metadata_incomplete,
|
|
5121
5553
|
)
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
5554
|
+
)
|
|
5555
|
+
qualified_entries = accounting_capture.qualified_entries
|
|
5556
|
+
accounting_dirty_accounts = accounting_capture.dirty_accounts
|
|
5557
|
+
accounting_changed_old = accounting_capture.changed_old
|
|
5558
|
+
accounting_changed_new = accounting_capture.changed_new
|
|
5559
|
+
accounting_entries = accounting_capture.entries
|
|
5560
|
+
metadata_incomplete = accounting_capture.metadata_incomplete
|
|
5127
5561
|
budget_entries = _codex_entries_from_accounting(accounting_entries)
|
|
5128
5562
|
cycles_all: list[CodexCycleBoundary] = []
|
|
5129
5563
|
try:
|
|
@@ -5141,29 +5575,43 @@ def _build_codex_source_state(
|
|
|
5141
5575
|
# here leaves `cycle` unresolved, which `cycle_failure` below turns
|
|
5142
5576
|
# into the hero failure the accounting axis actually reports.
|
|
5143
5577
|
cycle = None
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5578
|
+
if cycle is None and capture is not None:
|
|
5579
|
+
if capture.accounting_exists_error is not None:
|
|
5580
|
+
raise capture.accounting_exists_error
|
|
5581
|
+
accounting_exists = capture.accounting_exists
|
|
5582
|
+
elif cycle is None:
|
|
5583
|
+
accounting_exists = has_cached_codex_accounting_entries(
|
|
5584
|
+
cache_conn=context.cache_conn)
|
|
5585
|
+
else:
|
|
5586
|
+
accounting_exists = False
|
|
5587
|
+
cycle_failure = cycle is None and accounting_exists
|
|
5147
5588
|
hero_failure = projection_incoherent or cycle_failure
|
|
5148
5589
|
if cycle is None or hero_failure:
|
|
5149
5590
|
cycle_entries: list[CodexEntry] = []
|
|
5150
5591
|
cycle_cost_usd: float | None = None if hero_failure else 0.0
|
|
5151
5592
|
else:
|
|
5152
5593
|
cycle_end = min(accounting_end, cycle.resets_at)
|
|
5153
|
-
cycle_rows =
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
|
|
5594
|
+
cycle_rows = (
|
|
5595
|
+
tuple(
|
|
5596
|
+
row for row in capture.card_population
|
|
5597
|
+
if cycle.start_at <= row.timestamp.astimezone(UTC) < cycle_end
|
|
5598
|
+
and str(row.source_root_key) in cycle.source_root_keys
|
|
5599
|
+
)
|
|
5600
|
+
if capture is not None
|
|
5601
|
+
else load_cached_rooted_codex_accounting_entries(
|
|
5602
|
+
cycle.start_at,
|
|
5603
|
+
cycle_end,
|
|
5604
|
+
speed=context.speed,
|
|
5605
|
+
cache_conn=context.cache_conn,
|
|
5606
|
+
source_root_keys=cycle.source_root_keys,
|
|
5607
|
+
)
|
|
5159
5608
|
)
|
|
5160
|
-
cycle_entries =
|
|
5161
|
-
|
|
5162
|
-
cycle_entries,
|
|
5609
|
+
cycle_entries, cycle_cost_usd = _build_codex_cycle_accounting(
|
|
5610
|
+
cycle_rows,
|
|
5163
5611
|
now_utc=context.now_utc,
|
|
5164
|
-
|
|
5612
|
+
display_tz_name=context.display_tz_name,
|
|
5165
5613
|
speed=context.speed,
|
|
5166
|
-
)
|
|
5614
|
+
)
|
|
5167
5615
|
visible_accounting_entries = tuple(
|
|
5168
5616
|
entry for entry in accounting_entries
|
|
5169
5617
|
if context.range_start <= getattr(entry, "timestamp").astimezone(UTC) < accounting_end
|
|
@@ -5232,12 +5680,15 @@ def _build_codex_source_state(
|
|
|
5232
5680
|
# R8 gate, resolved once before the parent weekly projection so that only
|
|
5233
5681
|
# a decorated merged row gains the additive account axis. Focused children
|
|
5234
5682
|
# and <=1-real-account providers remain byte-identical.
|
|
5235
|
-
|
|
5236
|
-
|
|
5237
|
-
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
|
|
5683
|
+
if capture is not None:
|
|
5684
|
+
_codex_decorated = capture.decorated
|
|
5685
|
+
else:
|
|
5686
|
+
try:
|
|
5687
|
+
import _cctally_account
|
|
5688
|
+
_codex_decorated = _cctally_account.provider_is_decorated(
|
|
5689
|
+
context.stats_conn, "codex")
|
|
5690
|
+
except Exception:
|
|
5691
|
+
_codex_decorated = False
|
|
5241
5692
|
weekly = _cached_codex_native_weekly_view(
|
|
5242
5693
|
context.stats_conn,
|
|
5243
5694
|
visible_accounting_entries,
|
|
@@ -5271,6 +5722,8 @@ def _build_codex_source_state(
|
|
|
5271
5722
|
quota_observations,
|
|
5272
5723
|
accounting_entries=visible_accounting_entries,
|
|
5273
5724
|
decorated=_codex_decorated,
|
|
5725
|
+
captured_breakdowns=(
|
|
5726
|
+
capture.breakdowns if capture is not None else None),
|
|
5274
5727
|
)
|
|
5275
5728
|
# R8 gate, resolved ONCE and threaded (#341 Task 4 / #416 §5.8). Every
|
|
5276
5729
|
# per-account decoration below — block/alert/budget `account_key`, the
|
|
@@ -5309,7 +5762,11 @@ def _build_codex_source_state(
|
|
|
5309
5762
|
context, budget_entries, cost_events=budget_cost_events,
|
|
5310
5763
|
)
|
|
5311
5764
|
configured_budget = configured_budget_domain["status"]
|
|
5312
|
-
conversation_metadata =
|
|
5765
|
+
conversation_metadata = (
|
|
5766
|
+
capture.conversation_metadata
|
|
5767
|
+
if capture is not None
|
|
5768
|
+
else _codex_conversation_metadata(context.cache_conn)
|
|
5769
|
+
)
|
|
5313
5770
|
cache_report = _codex_cache_report_wire(
|
|
5314
5771
|
visible_accounting_entries,
|
|
5315
5772
|
metadata=conversation_metadata,
|
|
@@ -5379,6 +5836,7 @@ def _build_codex_source_state(
|
|
|
5379
5836
|
# projection (client-side chip filter); the hero renders per-account cards.
|
|
5380
5837
|
accounts_wire: list[dict[str, object]] = []
|
|
5381
5838
|
hero_cycles_wire: list[dict[str, object]] = []
|
|
5839
|
+
combined_accounting: dict[str, object] | None = None
|
|
5382
5840
|
account_scopes: dict[str, dict[str, object]] = {}
|
|
5383
5841
|
# #556 S5 §3.8: bound OUTSIDE the try, because the degrade path below has to
|
|
5384
5842
|
# be able to clear it, and the retained `clock_data` reads it either way.
|
|
@@ -5405,7 +5863,9 @@ def _build_codex_source_state(
|
|
|
5405
5863
|
minutes=ACCOUNT_WEEKLY_WINDOW_MINUTES),
|
|
5406
5864
|
accounting_start,
|
|
5407
5865
|
)
|
|
5408
|
-
if
|
|
5866
|
+
if capture is not None:
|
|
5867
|
+
card_population = capture.card_population
|
|
5868
|
+
elif card_population_start == accounting_start:
|
|
5409
5869
|
# The build already holds this exact population, so reading it
|
|
5410
5870
|
# again would be one extra full-range pass over every retained
|
|
5411
5871
|
# Codex accounting row, per tick. The equality is the ordinary
|
|
@@ -5436,7 +5896,7 @@ def _build_codex_source_state(
|
|
|
5436
5896
|
speed=context.speed,
|
|
5437
5897
|
cache_conn=context.cache_conn,
|
|
5438
5898
|
)
|
|
5439
|
-
accounts_wire, hero_cycles_wire = _codex_accounts_wire(
|
|
5899
|
+
accounts_wire, hero_cycles_wire, combined_accounting = _codex_accounts_wire(
|
|
5440
5900
|
context,
|
|
5441
5901
|
quota_observations=quota_observations,
|
|
5442
5902
|
cycles=cycles_all,
|
|
@@ -5490,6 +5950,8 @@ def _build_codex_source_state(
|
|
|
5490
5950
|
changed_new_by_account=changed_new_by_account,
|
|
5491
5951
|
changed_old_rows_by_account=changed_old_rows_by_account,
|
|
5492
5952
|
changed_new_rows_by_account=changed_new_rows_by_account,
|
|
5953
|
+
captured_breakdowns=(
|
|
5954
|
+
capture.breakdowns if capture is not None else None),
|
|
5493
5955
|
# Quota/stat generations are already represented by each
|
|
5494
5956
|
# child's quota observations, cycle and alert/budget rows in
|
|
5495
5957
|
# `_codex_account_scopes_wire`'s outer signature. Reuse the
|
|
@@ -5557,6 +6019,12 @@ def _build_codex_source_state(
|
|
|
5557
6019
|
# degrade to the byte-stable undecorated shape.
|
|
5558
6020
|
accounts_wire = []
|
|
5559
6021
|
hero_cycles_wire = []
|
|
6022
|
+
combined_accounting = {
|
|
6023
|
+
"scope": "account_cycles",
|
|
6024
|
+
"status": "unresolved",
|
|
6025
|
+
"cause": "account_cost_unresolved",
|
|
6026
|
+
"contributions": (),
|
|
6027
|
+
}
|
|
5560
6028
|
account_scopes = {}
|
|
5561
6029
|
budget_events_by_account = {}
|
|
5562
6030
|
# #416 QA P0-A — the "All accounts" headline is the MERGED spend and tokens
|
|
@@ -5593,7 +6061,9 @@ def _build_codex_source_state(
|
|
|
5593
6061
|
# observation age and weekly-cycle evidence live on their own axes.
|
|
5594
6062
|
freshness="fresh",
|
|
5595
6063
|
warnings=tuple(warnings),
|
|
5596
|
-
data_version=
|
|
6064
|
+
data_version=combined_accounting_version(
|
|
6065
|
+
data_version, combined_accounting,
|
|
6066
|
+
),
|
|
5597
6067
|
last_success_at=context.now_utc,
|
|
5598
6068
|
capabilities={
|
|
5599
6069
|
"hero": (
|
|
@@ -5717,4 +6187,5 @@ def _build_codex_source_state(
|
|
|
5717
6187
|
),
|
|
5718
6188
|
},
|
|
5719
6189
|
private_session_labels=private_session_labels,
|
|
6190
|
+
combined_accounting=combined_accounting,
|
|
5720
6191
|
)
|