cctally 1.77.0 → 1.79.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 +28 -0
- package/README.md +2 -0
- package/bin/_cctally_cache.py +140 -2
- package/bin/_cctally_config.py +50 -0
- package/bin/_cctally_core.py +7 -4
- package/bin/_cctally_dashboard.py +352 -58
- package/bin/_cctally_dashboard_conversation.py +4 -3
- package/bin/_cctally_dashboard_envelope.py +12 -0
- package/bin/_cctally_dashboard_sources.py +25 -3
- package/bin/_cctally_doctor.py +35 -4
- package/bin/_cctally_milestone_history.py +845 -0
- package/bin/_cctally_statusline.py +128 -3
- package/bin/_cctally_tui.py +23 -0
- package/bin/_cctally_update.py +338 -15
- package/bin/_cctally_weekrefs.py +6 -2
- package/bin/_lib_codex_conversation.py +1342 -18
- package/bin/_lib_codex_conversation_export.py +21 -1
- package/bin/_lib_codex_conversation_query.py +530 -67
- package/bin/_lib_conversation_dispatch.py +2 -2
- package/bin/_lib_dashboard_json.py +43 -0
- package/bin/_lib_doctor.py +38 -0
- package/bin/_lib_milestone_history.py +158 -0
- package/bin/_lib_source_analytics.py +20 -6
- package/bin/cctally +36 -0
- package/dashboard/static/assets/index-BwvAcS_k.js +92 -0
- package/dashboard/static/assets/index-CrIlpAlQ.css +1 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +4 -1
- package/dashboard/static/assets/index-CvcCeA4L.css +0 -1
- package/dashboard/static/assets/index-DLNXAevv.js +0 -87
|
@@ -38,7 +38,6 @@ edits — the forwarders hit ``sys.modules["_cctally_dashboard"]`` at call time
|
|
|
38
38
|
"""
|
|
39
39
|
from __future__ import annotations
|
|
40
40
|
|
|
41
|
-
import json
|
|
42
41
|
import re
|
|
43
42
|
import socket
|
|
44
43
|
import sqlite3
|
|
@@ -51,6 +50,7 @@ from _cctally_cache import (
|
|
|
51
50
|
sync_codex_conversations,
|
|
52
51
|
_codex_provider_roots,
|
|
53
52
|
)
|
|
53
|
+
from _lib_dashboard_json import encode_dashboard_json
|
|
54
54
|
|
|
55
55
|
# Live-tail watch-loop tuning — used ONLY by _handle_get_conversation_events_impl
|
|
56
56
|
# below, so moved here with the events handler (spec §4.1 / §6).
|
|
@@ -709,7 +709,8 @@ def _run_conversation_events_stream(
|
|
|
709
709
|
import time as _time
|
|
710
710
|
watch = sys.modules["cctally"]._load_sibling("_lib_conversation_watch")
|
|
711
711
|
tail_frame = (
|
|
712
|
-
"event: tail\ndata: " +
|
|
712
|
+
"event: tail\ndata: " + encode_dashboard_json(tail_data) + "\n\n"
|
|
713
|
+
).encode("utf-8")
|
|
713
714
|
|
|
714
715
|
if passive:
|
|
715
716
|
# Frozen-data contract: no ingest, no emit. Keep-alive only.
|
|
@@ -1056,7 +1057,7 @@ def _handle_get_conversation_payload_impl(handler, path: str) -> None:
|
|
|
1056
1057
|
q = _u.parse_qs(handler.path.partition("?")[2])
|
|
1057
1058
|
if session_id.startswith("v1."):
|
|
1058
1059
|
# Qualified payload readback (§3.4): Codex uses block_key + which={call,
|
|
1059
|
-
# output}; a v1.claude key keeps the tool_use_id + which={input,result}
|
|
1060
|
+
# output,event}; a v1.claude key keeps the tool_use_id + which={input,result}
|
|
1060
1061
|
# selector. neutral_payload picks per the resolved source; gone → 410.
|
|
1061
1062
|
which_q = _qs_str(q, "which", "")
|
|
1062
1063
|
tool_use_id_q = _qs_str(q, "tool_use_id", "") or None
|
|
@@ -1287,6 +1287,12 @@ def snapshot_to_envelope(snap: "DataSnapshot", *,
|
|
|
1287
1287
|
update_envelope = {
|
|
1288
1288
|
"state": _update_state_envelope,
|
|
1289
1289
|
"suppress": _update_suppress_envelope,
|
|
1290
|
+
# Beta-channel (spec 2026-07-21 §3): the configured release channel,
|
|
1291
|
+
# derived DIRECTLY from config (never from cached state — a cached
|
|
1292
|
+
# producer field can't seed the settings toggle after a failed fetch
|
|
1293
|
+
# or on brew). The resolved-target fields (latest_version /
|
|
1294
|
+
# resolved_dist_tag / latest_version_channel) ride inside `state`.
|
|
1295
|
+
"configured_channel": sys.modules["cctally"].resolve_update_channel(config),
|
|
1290
1296
|
}
|
|
1291
1297
|
|
|
1292
1298
|
# Doctor aggregate block (spec §5.5). Only the small severity tree
|
|
@@ -1427,6 +1433,12 @@ def snapshot_to_envelope(snap: "DataSnapshot", *,
|
|
|
1427
1433
|
# bound or the data source crashed during sync
|
|
1428
1434
|
# (recorded on ``last_sync_error``).
|
|
1429
1435
|
"five_hour_milestones": getattr(snap, "five_hour_milestones", []) or [],
|
|
1436
|
+
# Hero-modal historical-milestone navigation index (spec §3).
|
|
1437
|
+
# Additive/optional: newest-first per-week entries built on the
|
|
1438
|
+
# non-idle rebuild and stored on the snapshot; ``getattr``
|
|
1439
|
+
# default keeps legacy/positional fixture snapshots serializing
|
|
1440
|
+
# (they emit ``[]``). Historical rows never ride the envelope.
|
|
1441
|
+
"week_index": getattr(snap, "week_index", []) or [],
|
|
1430
1442
|
},
|
|
1431
1443
|
|
|
1432
1444
|
"forecast":
|
|
@@ -49,7 +49,10 @@ from _lib_jsonl import CodexEntry, codex_model_scoped_quota_pool
|
|
|
49
49
|
from _lib_fmt import stable_sum
|
|
50
50
|
from _lib_aggregators import _aggregate_codex_buckets
|
|
51
51
|
from _lib_five_hour import _FIVE_HOUR_JITTER_FLOOR_SECONDS
|
|
52
|
-
from _lib_source_analytics import
|
|
52
|
+
from _lib_source_analytics import (
|
|
53
|
+
build_codex_project_result,
|
|
54
|
+
collision_safe_project_label_map,
|
|
55
|
+
)
|
|
53
56
|
from _lib_view_models import (
|
|
54
57
|
CodexWeeklyView,
|
|
55
58
|
build_codex_daily_view,
|
|
@@ -1783,12 +1786,17 @@ def _partial_projects_wire(
|
|
|
1783
1786
|
model_totals[field] += value
|
|
1784
1787
|
session_totals[field] += value
|
|
1785
1788
|
|
|
1789
|
+
label_map = collision_safe_project_label_map(
|
|
1790
|
+
(f"{root_key}\0{project_key}", str(group["label"]))
|
|
1791
|
+
for (root_key, project_key), group in groups.items()
|
|
1792
|
+
)
|
|
1786
1793
|
rows = []
|
|
1787
1794
|
for (root_key, _project_key), group in groups.items():
|
|
1795
|
+
internal_identity = f"{root_key}\0{group['project_key']}"
|
|
1788
1796
|
rows.append({
|
|
1789
1797
|
"key": dashboard_resource_key("project", "codex", root_key, group["project_key"]),
|
|
1790
1798
|
"source": "codex",
|
|
1791
|
-
"label":
|
|
1799
|
+
"label": label_map[internal_identity],
|
|
1792
1800
|
"session_count": len(group["sessions"]),
|
|
1793
1801
|
"first_seen": group["first_seen"].astimezone(UTC).isoformat(),
|
|
1794
1802
|
"last_seen": group["last_seen"].astimezone(UTC).isoformat(),
|
|
@@ -2072,7 +2080,21 @@ def build_codex_source_state(
|
|
|
2072
2080
|
now_utc=context.now_utc,
|
|
2073
2081
|
display_tz_name=context.display_tz_name,
|
|
2074
2082
|
)
|
|
2075
|
-
|
|
2083
|
+
# Hero-modal historical-milestone navigation index (spec §1c, §3). Built
|
|
2084
|
+
# here on the non-idle codex source rebuild (idle ticks reuse the stored
|
|
2085
|
+
# bundle) over the durable projection — a pure serializer never touches it.
|
|
2086
|
+
# Guarded: an index failure must never fail the codex source build.
|
|
2087
|
+
cycle_index: tuple = ()
|
|
2088
|
+
if cycle is not None and not hero_failure:
|
|
2089
|
+
try:
|
|
2090
|
+
cycle_index = tuple(
|
|
2091
|
+
sys.modules["cctally"].build_codex_cycle_index(
|
|
2092
|
+
context.stats_conn, identity=cycle, now_utc=context.now_utc,
|
|
2093
|
+
)
|
|
2094
|
+
)
|
|
2095
|
+
except sqlite3.Error:
|
|
2096
|
+
cycle_index = ()
|
|
2097
|
+
quota = {**quota, "blocks": quota_blocks, "cycle_index": cycle_index}
|
|
2076
2098
|
budget_rows = _budget_wire(context.stats_conn)
|
|
2077
2099
|
projected_budget_rows = _projected_budget_wire(context.stats_conn)
|
|
2078
2100
|
budget_cost_events = _codex_budget_cost_events(context, budget_entries)
|
package/bin/_cctally_doctor.py
CHANGED
|
@@ -27,6 +27,7 @@ import argparse
|
|
|
27
27
|
import datetime as dt
|
|
28
28
|
import fcntl
|
|
29
29
|
import json
|
|
30
|
+
import math
|
|
30
31
|
import pathlib
|
|
31
32
|
import shutil
|
|
32
33
|
import sqlite3
|
|
@@ -35,6 +36,7 @@ import sys
|
|
|
35
36
|
import _cctally_core
|
|
36
37
|
import _lib_changelog
|
|
37
38
|
from _cctally_core import _now_utc, eprint, now_utc_iso, parse_iso_datetime
|
|
39
|
+
from _lib_dashboard_json import encode_dashboard_json
|
|
38
40
|
|
|
39
41
|
|
|
40
42
|
def _cctally():
|
|
@@ -53,11 +55,25 @@ def _gather_statusline_pipeline(c, *, now_utc: dt.datetime) -> dict:
|
|
|
53
55
|
"tombstones": {"fiveHour": "absent", "sevenDay": "absent"},
|
|
54
56
|
}
|
|
55
57
|
try:
|
|
56
|
-
|
|
58
|
+
age = c._statusline_transport_age_seconds()
|
|
59
|
+
result["transport_age_seconds"] = (
|
|
60
|
+
age
|
|
61
|
+
if isinstance(age, (int, float))
|
|
62
|
+
and not isinstance(age, bool)
|
|
63
|
+
and math.isfinite(float(age))
|
|
64
|
+
else None
|
|
65
|
+
)
|
|
57
66
|
except Exception:
|
|
58
67
|
pass
|
|
59
68
|
try:
|
|
60
|
-
|
|
69
|
+
age = c._statusline_observe_age_seconds()
|
|
70
|
+
result["selected_age_seconds"] = (
|
|
71
|
+
age
|
|
72
|
+
if isinstance(age, (int, float))
|
|
73
|
+
and not isinstance(age, bool)
|
|
74
|
+
and math.isfinite(float(age))
|
|
75
|
+
else None
|
|
76
|
+
)
|
|
61
77
|
except Exception:
|
|
62
78
|
pass
|
|
63
79
|
try:
|
|
@@ -784,14 +800,28 @@ def doctor_gather_state(
|
|
|
784
800
|
# on corruption — both behaviors would hide diagnostic state
|
|
785
801
|
# (codex H1).
|
|
786
802
|
config_json_error = None
|
|
803
|
+
config_parsed: dict = {}
|
|
787
804
|
try:
|
|
788
805
|
if _cctally_core.CONFIG_PATH.exists():
|
|
789
|
-
json.loads(
|
|
806
|
+
config_parsed = json.loads(
|
|
807
|
+
_cctally_core.CONFIG_PATH.read_text(encoding="utf-8")
|
|
808
|
+
)
|
|
790
809
|
except json.JSONDecodeError as exc:
|
|
791
810
|
config_json_error = f"{type(exc).__name__}: {exc}"
|
|
792
811
|
except OSError as exc:
|
|
793
812
|
config_json_error = f"OSError: {exc}"
|
|
794
813
|
|
|
814
|
+
# Configured update (release) channel (beta-channel, spec 2026-07-21 §3):
|
|
815
|
+
# derived from the SAME raw read (never load_config, which auto-creates on
|
|
816
|
+
# first run). Fail-soft to "stable" — resolve_update_channel already
|
|
817
|
+
# tolerates a non-dict block / junk value.
|
|
818
|
+
try:
|
|
819
|
+
update_channel = c.resolve_update_channel(
|
|
820
|
+
config_parsed if isinstance(config_parsed, dict) else {}
|
|
821
|
+
)
|
|
822
|
+
except Exception:
|
|
823
|
+
update_channel = "stable"
|
|
824
|
+
|
|
795
825
|
update_state = None
|
|
796
826
|
update_state_error = None
|
|
797
827
|
try:
|
|
@@ -864,6 +894,7 @@ def doctor_gather_state(
|
|
|
864
894
|
codex_jsonl_present=codex_jsonl_present,
|
|
865
895
|
codex_project_metadata_health=codex_project_metadata_health,
|
|
866
896
|
codex_project_metadata_error=codex_project_metadata_error,
|
|
897
|
+
update_channel=update_channel,
|
|
867
898
|
dashboard_bind_stored=dashboard_bind_stored,
|
|
868
899
|
runtime_bind=runtime_bind,
|
|
869
900
|
# Conversation viewer (Plan 2, spec §5): only consulted on a LAN bind.
|
|
@@ -943,7 +974,7 @@ def cmd_doctor(args: argparse.Namespace) -> int:
|
|
|
943
974
|
state = c.doctor_gather_state(deep=True)
|
|
944
975
|
report = _lib_doctor.run_checks(state)
|
|
945
976
|
if getattr(args, "json", False):
|
|
946
|
-
print(
|
|
977
|
+
print(encode_dashboard_json(
|
|
947
978
|
_lib_doctor.serialize_json(report), indent=2, sort_keys=True,
|
|
948
979
|
))
|
|
949
980
|
else:
|