cctally 1.97.0 → 1.98.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 +24 -0
- package/bin/_cctally_dashboard.py +595 -46
- package/bin/_cctally_dashboard_envelope.py +43 -23
- package/bin/_cctally_dashboard_sources.py +79 -13
- package/bin/_cctally_tui.py +313 -30
- package/bin/_lib_alert_axes.py +8 -3
- package/bin/_lib_dashboard_sources.py +463 -19
- package/bin/_lib_snapshot_cache.py +12 -0
- package/bin/cctally +13 -0
- package/dashboard/static/assets/index-CC8TTZUC.css +1 -0
- package/dashboard/static/assets/index-CChXFhs_.js +97 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +1 -1
- package/dashboard/static/assets/index-BSESoPIK.css +0 -1
- package/dashboard/static/assets/index-DgsMz5hA.js +0 -97
|
@@ -55,6 +55,8 @@ from _cctally_core import (
|
|
|
55
55
|
)
|
|
56
56
|
from _lib_dashboard_sources import (
|
|
57
57
|
SOURCE_SCHEMA_VERSION,
|
|
58
|
+
canonical_alerted_at as _canonical_alerted_at,
|
|
59
|
+
canonical_alerted_at_sql as _canonical_alerted_at_sql,
|
|
58
60
|
dashboard_resource_key as _dashboard_resource_key,
|
|
59
61
|
)
|
|
60
62
|
from _lib_display_tz import _compute_display_block, format_display_dt
|
|
@@ -361,6 +363,16 @@ def _alert_account_resolver(conn: sqlite3.Connection):
|
|
|
361
363
|
return fields
|
|
362
364
|
|
|
363
365
|
|
|
366
|
+
# #556 S3 §2.3. Every per-axis mapper orders by the CANONICAL firing instant
|
|
367
|
+
# before its ``LIMIT``, because that limit decides which rows exist downstream
|
|
368
|
+
# and a row excluded there is unrecoverable. Two spellings of one instant
|
|
369
|
+
# compare equal here; a nonzero offset — which the ``all-combined`` fixture
|
|
370
|
+
# stores deliberately — orders by its true instant rather than by its
|
|
371
|
+
# local-time text.
|
|
372
|
+
_CANON_ALERTED_AT = _canonical_alerted_at_sql()
|
|
373
|
+
_CANON_ALERTED_AT_M = _canonical_alerted_at_sql("m.alerted_at")
|
|
374
|
+
|
|
375
|
+
|
|
364
376
|
def _envelope_rows_weekly(
|
|
365
377
|
conn, descriptor, limit, severity_for, account_fields,
|
|
366
378
|
) -> list[dict]:
|
|
@@ -377,7 +389,7 @@ def _envelope_rows_weekly(
|
|
|
377
389
|
alerted_at, cumulative_cost_usd, reset_event_id, account_key
|
|
378
390
|
FROM {descriptor.milestone_table}
|
|
379
391
|
WHERE alerted_at IS NOT NULL
|
|
380
|
-
ORDER BY
|
|
392
|
+
ORDER BY {_CANON_ALERTED_AT} DESC
|
|
381
393
|
LIMIT ?
|
|
382
394
|
""",
|
|
383
395
|
(limit,),
|
|
@@ -431,7 +443,7 @@ def _envelope_rows_five_hour(
|
|
|
431
443
|
ON b.five_hour_window_key = m.five_hour_window_key
|
|
432
444
|
AND b.account_key = m.account_key
|
|
433
445
|
WHERE m.alerted_at IS NOT NULL
|
|
434
|
-
ORDER BY
|
|
446
|
+
ORDER BY {_CANON_ALERTED_AT_M} DESC
|
|
435
447
|
LIMIT ?
|
|
436
448
|
""",
|
|
437
449
|
(limit,),
|
|
@@ -506,7 +518,7 @@ def _envelope_rows_budget_family(
|
|
|
506
518
|
budget_usd, spent_usd, consumption_pct, account_key
|
|
507
519
|
FROM {descriptor.milestone_table}
|
|
508
520
|
WHERE vendor = ? AND alerted_at IS NOT NULL
|
|
509
|
-
ORDER BY
|
|
521
|
+
ORDER BY {_CANON_ALERTED_AT} DESC
|
|
510
522
|
LIMIT ?
|
|
511
523
|
""",
|
|
512
524
|
(default_noun, vendor, limit),
|
|
@@ -572,7 +584,7 @@ def _envelope_rows_projected(
|
|
|
572
584
|
denominator, crossed_at_utc, alerted_at, account_key
|
|
573
585
|
FROM {descriptor.milestone_table}
|
|
574
586
|
WHERE alerted_at IS NOT NULL
|
|
575
|
-
ORDER BY
|
|
587
|
+
ORDER BY {_CANON_ALERTED_AT} DESC
|
|
576
588
|
LIMIT ?
|
|
577
589
|
""",
|
|
578
590
|
(limit,),
|
|
@@ -629,7 +641,7 @@ def _envelope_rows_project_budget(
|
|
|
629
641
|
consumption_pct, crossed_at_utc, alerted_at, account_key
|
|
630
642
|
FROM {descriptor.milestone_table}
|
|
631
643
|
WHERE alerted_at IS NOT NULL
|
|
632
|
-
ORDER BY
|
|
644
|
+
ORDER BY {_CANON_ALERTED_AT} DESC
|
|
633
645
|
LIMIT ?
|
|
634
646
|
""",
|
|
635
647
|
(limit,),
|
|
@@ -858,11 +870,29 @@ def _build_alerts_envelope_array(
|
|
|
858
870
|
))
|
|
859
871
|
|
|
860
872
|
# Python's list.sort is stable. When two alerts share the same
|
|
861
|
-
# `alerted_at`
|
|
862
|
-
#
|
|
873
|
+
# `alerted_at` instant (rare; multiple axes firing within the same
|
|
874
|
+
# second), the union order (weekly, then 5h, then budget, then
|
|
863
875
|
# projected) determines the tiebreaker — no extra deterministic key is
|
|
864
876
|
# added because the spec doesn't require one.
|
|
865
|
-
|
|
877
|
+
#
|
|
878
|
+
# #556 S3 §2.3: the slice below is a truncation, so this re-sort compares
|
|
879
|
+
# instants rather than spellings. Each axis reaches here already ordered by
|
|
880
|
+
# its own canonical SQL expression, but the axes are merged raw, so two
|
|
881
|
+
# differently-spelled instants meet for the first time right here.
|
|
882
|
+
def _order_key(alert):
|
|
883
|
+
# A raise here empties the whole legacy array AND the Claude projection
|
|
884
|
+
# derived from it, so the diagnostic must name the offending row the way
|
|
885
|
+
# `_combined_alert_rows` does — otherwise the operator sees an empty
|
|
886
|
+
# panel and a message that identifies nothing.
|
|
887
|
+
try:
|
|
888
|
+
return _canonical_alerted_at(alert["alerted_at"])
|
|
889
|
+
except (KeyError, ValueError) as exc:
|
|
890
|
+
identity = alert.get("key") or alert.get("id")
|
|
891
|
+
raise ValueError(
|
|
892
|
+
f"alert row {identity!r} (axis {alert.get('axis')!r}): {exc}"
|
|
893
|
+
) from exc
|
|
894
|
+
|
|
895
|
+
out.sort(key=_order_key, reverse=True)
|
|
866
896
|
return out[:limit]
|
|
867
897
|
|
|
868
898
|
|
|
@@ -1270,21 +1300,11 @@ def snapshot_to_envelope(snap: "DataSnapshot", *,
|
|
|
1270
1300
|
}
|
|
1271
1301
|
|
|
1272
1302
|
def _daily_row_to_dict(r: "DailyPanelRow") -> dict:
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
"intensity_bucket": r.intensity_bucket,
|
|
1279
|
-
"models": list(r.models),
|
|
1280
|
-
# ---- v2.3 additions ----
|
|
1281
|
-
"input_tokens": r.input_tokens,
|
|
1282
|
-
"output_tokens": r.output_tokens,
|
|
1283
|
-
"cache_creation_tokens": r.cache_creation_tokens,
|
|
1284
|
-
"cache_read_tokens": r.cache_read_tokens,
|
|
1285
|
-
"total_tokens": r.total_tokens,
|
|
1286
|
-
"cache_hit_pct": r.cache_hit_pct,
|
|
1287
|
-
}
|
|
1303
|
+
# #556 S2 §6.3a: one owner for the daily row wire shape. The All-only
|
|
1304
|
+
# `periods.daily_aggregate.rows` sibling publishes the same shape, and
|
|
1305
|
+
# the client reads one shape whichever sibling produced it, so the
|
|
1306
|
+
# dict is built in `_cctally_dashboard` and this renderer delegates.
|
|
1307
|
+
return sys.modules["_cctally_dashboard"].daily_panel_row_to_wire(r)
|
|
1288
1308
|
|
|
1289
1309
|
# Spec §2.7: empty state is `weekly.rows === []`, not `weekly === null`.
|
|
1290
1310
|
# Always emit a `{rows: [...]}` envelope (possibly empty) so the panel
|
|
@@ -37,6 +37,8 @@ from _lib_dashboard_sources import (
|
|
|
37
37
|
SourceDashboardState,
|
|
38
38
|
SourceDashboardWarning,
|
|
39
39
|
assess_codex_projection_coherence,
|
|
40
|
+
canonical_alerted_at,
|
|
41
|
+
canonical_alerted_at_sql,
|
|
40
42
|
dashboard_resource_key,
|
|
41
43
|
)
|
|
42
44
|
from _lib_quota import (
|
|
@@ -860,6 +862,33 @@ _RESOURCE_ROWS = {
|
|
|
860
862
|
"block": ("quota", "blocks"),
|
|
861
863
|
}
|
|
862
864
|
|
|
865
|
+
# #556 S2: additional collections a resource may ALSO be routed through.
|
|
866
|
+
#
|
|
867
|
+
# The primary above stays the capability gate — its absence is still
|
|
868
|
+
# `SourceCapabilityUnavailable`. These are searched only after the primary
|
|
869
|
+
# misses, and their own absence is an ordinary not-found rather than a
|
|
870
|
+
# capability failure, because a provider legitimately need not publish them (a
|
|
871
|
+
# Codex source has no aggregate sibling, and a Claude source whose bounded fold
|
|
872
|
+
# failed publishes none either).
|
|
873
|
+
#
|
|
874
|
+
# Projects needs one because `projects.rows` is the current SUBSCRIPTION WEEK
|
|
875
|
+
# while `projects.aggregate.rows` is folded over the thirty-day shared range.
|
|
876
|
+
# Without this the aggregate ranking would publish rows the drill-down route
|
|
877
|
+
# answers 404 for — in the committed `all-combined` fixture, four of six.
|
|
878
|
+
_RESOURCE_EXTRA_ROWS: Mapping[str, tuple[tuple[str, ...], ...]] = MappingProxyType({
|
|
879
|
+
"project": (("projects", "aggregate", "rows"),),
|
|
880
|
+
})
|
|
881
|
+
|
|
882
|
+
|
|
883
|
+
def _rows_at(data: Mapping, path: "tuple[str, ...]") -> "list | tuple":
|
|
884
|
+
"""Read one optional nested rows collection, or an empty tuple."""
|
|
885
|
+
node: object = data
|
|
886
|
+
for step in path:
|
|
887
|
+
if not isinstance(node, Mapping):
|
|
888
|
+
return ()
|
|
889
|
+
node = node.get(step)
|
|
890
|
+
return node if isinstance(node, (list, tuple)) else ()
|
|
891
|
+
|
|
863
892
|
|
|
864
893
|
def _public_copy(value: object) -> object:
|
|
865
894
|
"""Detach a bounded source row from its immutable published state."""
|
|
@@ -910,6 +939,21 @@ def source_detail_lookup(
|
|
|
910
939
|
row for row in rows
|
|
911
940
|
if isinstance(row, Mapping) and row.get("key") == key
|
|
912
941
|
]
|
|
942
|
+
if not key_matches:
|
|
943
|
+
# Only after the primary collection misses, so every key that resolves
|
|
944
|
+
# today keeps resolving to the same row. The account-ownership branch
|
|
945
|
+
# below does NOT see an unchanged candidate set — a key that resolves
|
|
946
|
+
# only here reaches it as a row the primary collection never carried.
|
|
947
|
+
# That leaks nothing, because an aggregate row carries no
|
|
948
|
+
# `account_key` and the ownership check refuses a row it cannot
|
|
949
|
+
# attribute, but the set is genuinely wider than it was.
|
|
950
|
+
for path in _RESOURCE_EXTRA_ROWS.get(resource, ()):
|
|
951
|
+
key_matches = [
|
|
952
|
+
row for row in _rows_at(data, path)
|
|
953
|
+
if isinstance(row, Mapping) and row.get("key") == key
|
|
954
|
+
]
|
|
955
|
+
if key_matches:
|
|
956
|
+
break
|
|
913
957
|
if not key_matches:
|
|
914
958
|
raise SourceResourceNotFound()
|
|
915
959
|
if account is not None:
|
|
@@ -2430,6 +2474,12 @@ def refresh_codex_source_clock(
|
|
|
2430
2474
|
# combined figure fail closed on an idle tick that changed nothing else.
|
|
2431
2475
|
account_scope=state.account_scope,
|
|
2432
2476
|
private_session_labels=state.private_session_labels,
|
|
2477
|
+
# #556 S2 §3.6: the aggregate carrier travels with the rows it
|
|
2478
|
+
# describes. This clock refreshes presentation axes only and publishes
|
|
2479
|
+
# the SAME rows, so it must carry the range that bounded them —
|
|
2480
|
+
# dropping it here would withhold the aggregate on any idle tick whose
|
|
2481
|
+
# clock moved.
|
|
2482
|
+
aggregate_scope=state.aggregate_scope,
|
|
2433
2483
|
)
|
|
2434
2484
|
return state if refreshed_state == state else refreshed_state
|
|
2435
2485
|
|
|
@@ -2475,39 +2525,55 @@ def _alerts_wire(
|
|
|
2475
2525
|
"accountLabel": label,
|
|
2476
2526
|
}
|
|
2477
2527
|
|
|
2528
|
+
# #556 S3 §2.1/§2.4: the firing instant is what this wire filters on, what
|
|
2529
|
+
# the panel orders by and what it prints, so it is what every leg selects,
|
|
2530
|
+
# orders by and publishes. Two legs previously ordered, truncated and
|
|
2531
|
+
# published the CROSSING instant instead, which is a different moment: a
|
|
2532
|
+
# row that fired most recently but crossed longest ago was dropped at the
|
|
2533
|
+
# LIMIT and never reached the panel at all. `created_at` stays as an
|
|
2534
|
+
# equal-valued compatibility alias for a client reading a pre-v7 envelope.
|
|
2535
|
+
canon = canonical_alerted_at_sql()
|
|
2536
|
+
|
|
2537
|
+
def _instants(raw: object) -> dict[str, str]:
|
|
2538
|
+
value = canonical_alerted_at(raw)
|
|
2539
|
+
return {"alerted_at": value, "created_at": value}
|
|
2540
|
+
|
|
2478
2541
|
try:
|
|
2479
|
-
for period, threshold, consumption_pct, crossed_at, account_key in stats_conn.execute(
|
|
2480
|
-
"SELECT period, threshold, consumption_pct, crossed_at_utc, account_key "
|
|
2542
|
+
for period, threshold, consumption_pct, crossed_at, alerted_at, account_key in stats_conn.execute(
|
|
2543
|
+
"SELECT period, threshold, consumption_pct, crossed_at_utc, alerted_at, account_key "
|
|
2481
2544
|
"FROM budget_milestones WHERE vendor='codex' AND alerted_at IS NOT NULL "
|
|
2482
|
-
"ORDER BY
|
|
2545
|
+
f"ORDER BY {canon} DESC, threshold DESC LIMIT ?",
|
|
2483
2546
|
(SOURCE_HISTORY_LIMIT,),
|
|
2484
2547
|
):
|
|
2485
2548
|
rows.append({
|
|
2549
|
+
# The resource key keeps the crossing instant it has always
|
|
2550
|
+
# carried: it is an opaque identity, and re-keying every
|
|
2551
|
+
# historical Codex alert row is not this session's change.
|
|
2486
2552
|
"key": dashboard_resource_key("alert", "codex", "codex_budget", period, threshold, crossed_at),
|
|
2487
2553
|
"source": "codex",
|
|
2488
2554
|
"axis": "codex_budget", "period": period, "threshold": threshold,
|
|
2489
|
-
"value": consumption_pct,
|
|
2555
|
+
"value": consumption_pct, **_instants(alerted_at),
|
|
2490
2556
|
**_account(account_key),
|
|
2491
2557
|
})
|
|
2492
|
-
for period, threshold, projected_value, crossed_at, account_key in stats_conn.execute(
|
|
2493
|
-
"SELECT period, threshold, projected_value, crossed_at_utc, account_key "
|
|
2558
|
+
for period, threshold, projected_value, crossed_at, alerted_at, account_key in stats_conn.execute(
|
|
2559
|
+
"SELECT period, threshold, projected_value, crossed_at_utc, alerted_at, account_key "
|
|
2494
2560
|
"FROM projected_milestones WHERE metric='codex_budget_usd' AND alerted_at IS NOT NULL "
|
|
2495
|
-
"ORDER BY
|
|
2561
|
+
f"ORDER BY {canon} DESC, threshold DESC LIMIT ?",
|
|
2496
2562
|
(SOURCE_HISTORY_LIMIT,),
|
|
2497
2563
|
):
|
|
2498
2564
|
rows.append({
|
|
2499
2565
|
"key": dashboard_resource_key("alert", "codex", "projected", period, threshold, crossed_at),
|
|
2500
2566
|
"source": "codex",
|
|
2501
2567
|
"axis": "projected", "period": period, "threshold": threshold,
|
|
2502
|
-
"value": projected_value,
|
|
2568
|
+
"value": projected_value, **_instants(alerted_at),
|
|
2503
2569
|
**_account(account_key),
|
|
2504
2570
|
})
|
|
2505
2571
|
for (root_key, logical_key, observed_slot, window_minutes, resets_at,
|
|
2506
|
-
threshold, severity, created_at, account_key) in stats_conn.execute(
|
|
2572
|
+
threshold, severity, created_at, alerted_at, account_key) in stats_conn.execute(
|
|
2507
2573
|
"SELECT source_root_key, logical_limit_key, observed_slot, window_minutes, resets_at_utc, "
|
|
2508
|
-
"threshold, severity, created_at_utc, account_key FROM quota_threshold_events "
|
|
2574
|
+
"threshold, severity, created_at_utc, alerted_at, account_key FROM quota_threshold_events "
|
|
2509
2575
|
"WHERE source='codex' AND disposition='alerted' AND orphaned_at IS NULL "
|
|
2510
|
-
"ORDER BY
|
|
2576
|
+
f"ORDER BY {canon} DESC, source_root_key, logical_limit_key, observed_slot, threshold "
|
|
2511
2577
|
"LIMIT ?",
|
|
2512
2578
|
(SOURCE_HISTORY_LIMIT,),
|
|
2513
2579
|
):
|
|
@@ -2518,14 +2584,14 @@ def _alerts_wire(
|
|
|
2518
2584
|
),
|
|
2519
2585
|
"source": "codex",
|
|
2520
2586
|
"axis": "quota", "threshold": threshold, "severity": severity,
|
|
2521
|
-
|
|
2587
|
+
**_instants(alerted_at),
|
|
2522
2588
|
**_account(account_key),
|
|
2523
2589
|
})
|
|
2524
2590
|
except sqlite3.Error:
|
|
2525
2591
|
return ()
|
|
2526
2592
|
return tuple(sorted(
|
|
2527
2593
|
rows,
|
|
2528
|
-
key=lambda item:
|
|
2594
|
+
key=lambda item: canonical_alerted_at(item["alerted_at"]),
|
|
2529
2595
|
reverse=True,
|
|
2530
2596
|
)[:SOURCE_HISTORY_LIMIT])
|
|
2531
2597
|
|