cctally 1.73.0 → 1.74.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 +11 -0
- package/bin/_cctally_cache.py +26 -5
- package/bin/_cctally_dashboard.py +35 -1
- package/bin/_cctally_dashboard_share.py +38 -3
- package/bin/_cctally_dashboard_sources.py +812 -74
- package/bin/_cctally_db.py +104 -1
- package/bin/_cctally_doctor.py +27 -4
- package/bin/_cctally_source_analytics.py +288 -3
- package/bin/_cctally_tui.py +17 -14
- package/bin/_lib_aggregators.py +48 -34
- package/bin/_lib_dashboard_sources.py +20 -7
- package/bin/_lib_doctor.py +44 -0
- package/bin/_lib_jsonl.py +11 -0
- package/bin/_lib_pricing.py +7 -4
- package/bin/_lib_pricing_check.py +7 -1
- package/bin/_lib_quota.py +12 -11
- package/bin/_lib_view_models.py +50 -14
- package/dashboard/static/assets/index-DeQjEMO6.js +80 -0
- package/dashboard/static/assets/index-ZiPO8veo.css +1 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +1 -1
- package/dashboard/static/assets/index-BWadAHxC.js +0 -80
- package/dashboard/static/assets/index-D2nwo_ln.css +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,17 @@ based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [1.74.0] - 2026-07-19
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- Dashboard Daily, Weekly, and Monthly details now use the same bounded history density across Claude, Codex, and All sources—30 days, 12 weeks, and 8 months—with compact date labels and contained mobile tables; Codex Blocks remains explicitly unavailable when native 5-hour activity does not exist. (#319)
|
|
12
|
+
- Dashboard Trend, Projects, Cache Report, Forecast, and Alerts details now keep their canonical composed structure for Codex and All sources, show provider-native values or explicit unavailable reasons in every slot, and route provider-qualified project rows through the source-bound detail foundation. (#319)
|
|
13
|
+
- Dashboard source-qualified Session and Project details now preserve Codex accounting totals when project metadata is incomplete; the combined source retains server-issued Claude Block identities and opens the canonical Block modal; source details share the standard focus, Escape, return-focus, and scroll-lock lifecycle; and desktop Share previews retain the captured provider. (#319)
|
|
14
|
+
- Codex accounting no longer double-counts parent token events copied into a forked/subagent rollout before its first model context. Those records remain available to the physical conversation reader but are excluded from accounting; cache migration 027 safely replays retained Codex rollouts to remove existing `unknown` duplicates. Consequently, `cctally pricing-check` and `doctor` no longer report those ingest artifacts as a missing embedded price, while real unknown model IDs remain actionable. The complete scoped OpenAI model set and tracked rates were reverified against the live pricing sources, and the embedded pricing snapshot date is now 2026-07-19.
|
|
15
|
+
- Dashboard Codex Weekly rows now follow observed native seven-day quota-cycle re-anchors instead of calendar weeks, collapsing reset jitter and clipping an older cycle when OpenAI grants an early reset. Codex quota forecasts now use elapsed native-cycle pace rather than activity-only bursts, avoiding misleading projections pinned at 100%. (#312)
|
|
16
|
+
- Dashboard Codex source reads now retain accounting, quota, budget, session, and forensics panels when retained historical rows still lack project attribution. The cache replay path repairs this metadata when possible; until then the source is fresh-but-partial and disables Projects only, with a rebuild remedy. (#312)
|
|
17
|
+
- Dashboard Codex hero cost and token counters now cover the active native seven-day reset cycle, rather than an unrelated calendar slice. A missing or conflicting active seven-day boundary leaves quota and budget visible while clearly marking the hero unavailable; Codex panels regain bounded native chrome and source warnings degrade only the affected domain. (#312)
|
|
18
|
+
|
|
8
19
|
## [1.73.0] - 2026-07-18
|
|
9
20
|
|
|
10
21
|
### Added
|
package/bin/_cctally_cache.py
CHANGED
|
@@ -813,8 +813,30 @@ def _delete_codex_file_derived_rows(
|
|
|
813
813
|
)
|
|
814
814
|
|
|
815
815
|
|
|
816
|
-
def _clear_codex_derived_rows(conn: sqlite3.Connection) ->
|
|
817
|
-
"""Clear every re-derivable Codex row family
|
|
816
|
+
def _clear_codex_derived_rows(conn: sqlite3.Connection) -> bool:
|
|
817
|
+
"""Clear every re-derivable Codex row family and report whether state changed.
|
|
818
|
+
|
|
819
|
+
The FTS5 ``delete-all`` command can increment SQLite's cumulative change
|
|
820
|
+
counter even when the semantic Codex surface was already empty. Callers
|
|
821
|
+
use this return value, rather than ``Connection.total_changes``, when
|
|
822
|
+
deciding whether to advance the physical-mutation sequence.
|
|
823
|
+
"""
|
|
824
|
+
state_changed = any(
|
|
825
|
+
conn.execute(query).fetchone() is not None
|
|
826
|
+
for query in (
|
|
827
|
+
"SELECT 1 FROM codex_session_entries LIMIT 1",
|
|
828
|
+
"SELECT 1 FROM quota_window_snapshots WHERE source = 'codex' LIMIT 1",
|
|
829
|
+
"SELECT 1 FROM codex_conversation_threads LIMIT 1",
|
|
830
|
+
"SELECT 1 FROM codex_conversation_events LIMIT 1",
|
|
831
|
+
"SELECT 1 FROM codex_session_files LIMIT 1",
|
|
832
|
+
"SELECT 1 FROM codex_source_roots LIMIT 1",
|
|
833
|
+
"SELECT 1 FROM codex_conversation_messages LIMIT 1",
|
|
834
|
+
"SELECT 1 FROM codex_conversation_file_touches LIMIT 1",
|
|
835
|
+
"SELECT 1 FROM codex_conversation_rollups LIMIT 1",
|
|
836
|
+
"SELECT 1 FROM cache_meta "
|
|
837
|
+
"WHERE key='codex_quota_projection_certificate' LIMIT 1",
|
|
838
|
+
)
|
|
839
|
+
)
|
|
818
840
|
conn.execute("DELETE FROM codex_session_entries")
|
|
819
841
|
conn.execute("DELETE FROM quota_window_snapshots WHERE source = 'codex'")
|
|
820
842
|
conn.execute("DELETE FROM codex_conversation_threads")
|
|
@@ -832,6 +854,7 @@ def _clear_codex_derived_rows(conn: sqlite3.Connection) -> None:
|
|
|
832
854
|
conn.execute(
|
|
833
855
|
"DELETE FROM cache_meta WHERE key='codex_quota_projection_certificate'"
|
|
834
856
|
)
|
|
857
|
+
return state_changed
|
|
835
858
|
|
|
836
859
|
|
|
837
860
|
def _bump_codex_physical_mutation_seq(conn: sqlite3.Connection) -> None:
|
|
@@ -3903,9 +3926,7 @@ def sync_codex_cache(
|
|
|
3903
3926
|
# Clear INSIDE the lock — see sync_cache() for the full
|
|
3904
3927
|
# rationale. Done before the existing SELECT so delta
|
|
3905
3928
|
# detection sees an empty baseline.
|
|
3906
|
-
|
|
3907
|
-
_clear_codex_derived_rows(conn)
|
|
3908
|
-
if conn.total_changes != before_clear:
|
|
3929
|
+
if _clear_codex_derived_rows(conn):
|
|
3909
3930
|
_bump_codex_physical_mutation_seq(conn)
|
|
3910
3931
|
conn.commit()
|
|
3911
3932
|
eprint("[cache-sync] rebuild: cleared Codex cached entries")
|
|
@@ -763,6 +763,35 @@ def _build_codex_source_detail(snapshot, *, resource: str, key: str) -> dict[str
|
|
|
763
763
|
raise SourceResourceNotFound()
|
|
764
764
|
|
|
765
765
|
|
|
766
|
+
def _codex_partial_source_detail(snapshot, row: Mapping, *, resource: str,
|
|
767
|
+
key: str) -> dict[str, Any]:
|
|
768
|
+
"""Keep a published cache-only row usable when project metadata is partial."""
|
|
769
|
+
detail = _source_safe_native_detail(
|
|
770
|
+
row, source="codex", resource=resource, key=key,
|
|
771
|
+
)
|
|
772
|
+
detail.update({
|
|
773
|
+
"metadata_availability": "partial",
|
|
774
|
+
"metadata_reason": (
|
|
775
|
+
"Codex project metadata is incomplete; available accounting totals "
|
|
776
|
+
"are preserved."
|
|
777
|
+
),
|
|
778
|
+
})
|
|
779
|
+
if resource == "session":
|
|
780
|
+
detail.setdefault("models", [])
|
|
781
|
+
detail["model_breakdowns"] = []
|
|
782
|
+
elif resource == "project":
|
|
783
|
+
generated_at = getattr(snapshot, "generated_at", None)
|
|
784
|
+
detail.setdefault("range_start", detail.get("first_seen"))
|
|
785
|
+
detail.setdefault(
|
|
786
|
+
"range_end",
|
|
787
|
+
generated_at.astimezone(dt.timezone.utc).isoformat()
|
|
788
|
+
if isinstance(generated_at, dt.datetime) else detail.get("last_seen"),
|
|
789
|
+
)
|
|
790
|
+
detail["models"] = []
|
|
791
|
+
detail["sessions"] = []
|
|
792
|
+
return detail
|
|
793
|
+
|
|
794
|
+
|
|
766
795
|
def _source_safe_claude_session_detail(detail, *, key: str) -> dict[str, Any]:
|
|
767
796
|
"""Adapt the existing Claude session detail builder to S4's safe route."""
|
|
768
797
|
payload = _session_detail_to_envelope(detail)
|
|
@@ -958,7 +987,12 @@ def build_source_detail(*, snapshot, source: str, resource: str,
|
|
|
958
987
|
row = source_detail_lookup(snapshot.source_bundle, source, resource, key)
|
|
959
988
|
if source == "claude":
|
|
960
989
|
return _build_claude_source_detail(snapshot, resource=resource, key=key)
|
|
961
|
-
|
|
990
|
+
try:
|
|
991
|
+
return _build_codex_source_detail(snapshot, resource=resource, key=key)
|
|
992
|
+
except sys.modules["_cctally_source_analytics"].QualifiedMetadataUnavailable:
|
|
993
|
+
return _codex_partial_source_detail(
|
|
994
|
+
snapshot, row, resource=resource, key=key,
|
|
995
|
+
)
|
|
962
996
|
|
|
963
997
|
_ensure_sibling_loaded("_cctally_dashboard_share")
|
|
964
998
|
from _cctally_dashboard_share import (
|
|
@@ -1404,6 +1404,8 @@ def _build_codex_source_share_snapshot(ls, *, state, panel: str,
|
|
|
1404
1404
|
raise ValueError("source capability unavailable")
|
|
1405
1405
|
required_domain = {
|
|
1406
1406
|
"current-week": "hero",
|
|
1407
|
+
"trend": "periods",
|
|
1408
|
+
"forecast": "quota",
|
|
1407
1409
|
"daily": "periods",
|
|
1408
1410
|
"monthly": "periods",
|
|
1409
1411
|
"weekly": "periods",
|
|
@@ -1425,14 +1427,47 @@ def _build_codex_source_share_snapshot(ls, *, state, panel: str,
|
|
|
1425
1427
|
source_rows = all_rows[-1:] if all_rows else ()
|
|
1426
1428
|
command = "codex-weekly"
|
|
1427
1429
|
display_tz = str(weekly.get("display_tz") or "UTC")
|
|
1428
|
-
elif panel in ("daily", "monthly", "weekly"):
|
|
1430
|
+
elif panel in ("daily", "monthly", "weekly", "trend"):
|
|
1429
1431
|
periods = data.get("periods")
|
|
1430
|
-
|
|
1432
|
+
period_key = "weekly" if panel == "trend" else panel
|
|
1433
|
+
panel_data = periods.get(period_key) if isinstance(periods, Mapping) else {}
|
|
1431
1434
|
if not isinstance(panel_data, Mapping):
|
|
1432
1435
|
raise ValueError("source capability unavailable")
|
|
1433
1436
|
source_rows = tuple(panel_data.get("rows", ()))
|
|
1434
|
-
command = f"codex-{
|
|
1437
|
+
command = f"codex-{period_key}"
|
|
1435
1438
|
display_tz = str(panel_data.get("display_tz") or "UTC")
|
|
1439
|
+
elif panel == "forecast":
|
|
1440
|
+
quota = data.get("quota")
|
|
1441
|
+
panel_data = quota if isinstance(quota, Mapping) else {}
|
|
1442
|
+
source_rows = tuple(panel_data.get("histories", ())) if isinstance(panel_data, Mapping) else ()
|
|
1443
|
+
start, end = _share_codex_period_bounds(
|
|
1444
|
+
state=state, panel="weekly", options=options, rows=source_rows,
|
|
1445
|
+
)
|
|
1446
|
+
rows = []
|
|
1447
|
+
for row in source_rows:
|
|
1448
|
+
if not isinstance(row, Mapping):
|
|
1449
|
+
continue
|
|
1450
|
+
forecast = row.get("forecast") if isinstance(row.get("forecast"), Mapping) else {}
|
|
1451
|
+
current = row.get("current_percent")
|
|
1452
|
+
projected = forecast.get("projected_percent")
|
|
1453
|
+
rows.append(ls.Row(cells={
|
|
1454
|
+
"limit": ls.TextCell(str(row.get("label") or "Codex quota")),
|
|
1455
|
+
"current": ls.TextCell("—" if current is None else f"{float(current):.1f}%"),
|
|
1456
|
+
"projected": ls.TextCell("—" if projected is None else f"{float(projected):.1f}%"),
|
|
1457
|
+
}))
|
|
1458
|
+
return ls.ShareSnapshot(
|
|
1459
|
+
cmd="codex-quota", title="Codex Quota Forecast", subtitle=None,
|
|
1460
|
+
period=ls.PeriodSpec(start=start, end=end, display_tz="UTC", label=None),
|
|
1461
|
+
columns=(
|
|
1462
|
+
ls.ColumnSpec(key="limit", label="Limit"),
|
|
1463
|
+
ls.ColumnSpec(key="current", label="Current", align="right"),
|
|
1464
|
+
ls.ColumnSpec(key="projected", label="Projected", align="right"),
|
|
1465
|
+
),
|
|
1466
|
+
rows=tuple(rows), chart=None, totals=(), notes=(), generated_at=end,
|
|
1467
|
+
version=sys.modules["cctally"]._share_resolve_version(),
|
|
1468
|
+
template_id=template_id, source="codex", source_label="Codex",
|
|
1469
|
+
availability=availability, availability_reason=reason,
|
|
1470
|
+
)
|
|
1436
1471
|
elif panel == "sessions":
|
|
1437
1472
|
panel_data = data.get(panel) if isinstance(data.get(panel), Mapping) else {}
|
|
1438
1473
|
source_rows = tuple(panel_data.get("rows", ())) if isinstance(panel_data, Mapping) else ()
|