cctally 1.83.0 → 1.84.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 +34 -0
- package/README.md +2 -4
- package/bin/_cctally_account.py +144 -11
- package/bin/_cctally_alerts.py +3 -1
- package/bin/_cctally_cache.py +1396 -33
- package/bin/_cctally_dashboard.py +46 -2
- package/bin/_cctally_dashboard_conversation.py +16 -7
- package/bin/_cctally_dashboard_envelope.py +12 -0
- package/bin/_cctally_dashboard_share.py +58 -5
- package/bin/_cctally_dashboard_sources.py +641 -38
- package/bin/_cctally_db.py +173 -7
- package/bin/_cctally_doctor.py +112 -27
- package/bin/_cctally_journal.py +761 -74
- package/bin/_cctally_milestone_history.py +73 -16
- package/bin/_cctally_quota.py +313 -15
- package/bin/_cctally_source_analytics.py +4 -1
- package/bin/_cctally_tui.py +7 -3
- package/bin/_lib_doctor.py +67 -2
- package/bin/_lib_journal.py +48 -0
- package/bin/_lib_jsonl.py +119 -0
- package/bin/_lib_quota.py +222 -13
- package/bin/_lib_rederive.py +12 -0
- package/bin/_lib_source_analytics.py +13 -0
- package/bin/_lib_source_identity.py +24 -0
- package/dashboard/static/assets/index-DlVVJeS4.js +92 -0
- package/dashboard/static/assets/index-OYBkyglj.css +1 -0
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +1 -1
- package/dashboard/static/assets/index-3bgCMVHb.js +0 -92
- package/dashboard/static/assets/index-D27EIHEI.css +0 -1
|
@@ -658,13 +658,25 @@ def _select_physical_cycles(cycles: list, *, preferred_identity=None,
|
|
|
658
658
|
|
|
659
659
|
def _load_codex_cycles(stats_conn, root_keys, *, include_orphaned=False,
|
|
660
660
|
preferred_identity=None, current_boundary=None,
|
|
661
|
-
now_utc=None) -> list:
|
|
662
|
-
"""Unbounded reset-defined 7-day cycles, one selected identity per boundary.
|
|
661
|
+
now_utc=None, account_key=None) -> list:
|
|
662
|
+
"""Unbounded reset-defined 7-day cycles, one selected identity per boundary.
|
|
663
|
+
|
|
664
|
+
``account_key`` (#416 Slice 3A review B3) scopes the enumeration to one
|
|
665
|
+
account. ``_CodexCycle.identity_parts`` is ``(root, limit, slot, window)``
|
|
666
|
+
and deliberately excludes the account, so without the predicate two accounts
|
|
667
|
+
on one root are jitter-clustered TOGETHER and one account's cycle index
|
|
668
|
+
lists the other's boundaries — the milestone-history equivalent of B1.
|
|
669
|
+
``None`` keeps the merged read, which is byte-stable and is what the
|
|
670
|
+
cycle-DETAIL route and every existing caller still use.
|
|
671
|
+
"""
|
|
663
672
|
roots = tuple(sorted({r for r in root_keys if isinstance(r, str) and r}))
|
|
664
673
|
if not roots:
|
|
665
674
|
return []
|
|
666
675
|
placeholders = ",".join("?" for _ in roots)
|
|
667
676
|
orphan_clause = "" if include_orphaned else "AND orphaned_at IS NULL "
|
|
677
|
+
# `quota_window_blocks.account_key` is NOT NULL DEFAULT 'unattributed'.
|
|
678
|
+
account_clause = "" if account_key is None else "AND account_key=? "
|
|
679
|
+
account_params = () if account_key is None else (account_key,)
|
|
668
680
|
rows = stats_conn.execute(
|
|
669
681
|
"SELECT source_root_key, logical_limit_key, observed_slot, "
|
|
670
682
|
" window_minutes, limit_id, limit_name, account_key, "
|
|
@@ -672,9 +684,10 @@ def _load_codex_cycles(stats_conn, root_keys, *, include_orphaned=False,
|
|
|
672
684
|
"FROM quota_window_blocks "
|
|
673
685
|
"WHERE source='codex' AND window_minutes=10080 "
|
|
674
686
|
f"{orphan_clause}"
|
|
687
|
+
f"{account_clause}"
|
|
675
688
|
f"AND source_root_key IN ({placeholders}) "
|
|
676
689
|
"ORDER BY unixepoch(resets_at_utc) DESC",
|
|
677
|
-
(*roots
|
|
690
|
+
(*account_params, *roots),
|
|
678
691
|
).fetchall()
|
|
679
692
|
|
|
680
693
|
cycles: list = []
|
|
@@ -867,16 +880,21 @@ def _codex_cycle_label(cyc, tz) -> str:
|
|
|
867
880
|
)
|
|
868
881
|
|
|
869
882
|
|
|
870
|
-
def build_codex_cycle_index(stats_conn, *, identity, now_utc
|
|
883
|
+
def build_codex_cycle_index(stats_conn, *, identity, now_utc,
|
|
884
|
+
account_key=None) -> list:
|
|
871
885
|
"""Newest-first Codex cycle index over the durable projection (spec §1c,
|
|
872
886
|
§3). Enumerates the hero root's reset-defined ledger with no depth cap;
|
|
873
|
-
one selected full native identity per physical reset.
|
|
887
|
+
one selected full native identity per physical reset.
|
|
888
|
+
|
|
889
|
+
``account_key`` (#416 Slice 3A review B3) builds the index for ONE account,
|
|
890
|
+
so a focused hero renders its own milestone history instead of the first
|
|
891
|
+
account's. ``None`` is the merged parent index and is byte-stable."""
|
|
874
892
|
now = now_utc.astimezone(UTC) if now_utc.tzinfo else now_utc.replace(tzinfo=UTC)
|
|
875
893
|
tz = _resolve_display_tz()
|
|
876
894
|
cycles = _load_codex_cycles(
|
|
877
895
|
stats_conn, getattr(identity, "source_root_keys", ()),
|
|
878
896
|
preferred_identity=getattr(identity, "quota_identity", None),
|
|
879
|
-
current_boundary=identity, now_utc=now,
|
|
897
|
+
current_boundary=identity, now_utc=now, account_key=account_key,
|
|
880
898
|
)
|
|
881
899
|
return [_codex_cycle_entry(stats_conn, c, tz) for c in cycles]
|
|
882
900
|
|
|
@@ -905,16 +923,26 @@ def _shape_codex_milestone(row, *, key_parts, block_key) -> dict:
|
|
|
905
923
|
}
|
|
906
924
|
|
|
907
925
|
|
|
908
|
-
def _codex_breakdown_rows(ident, reset, speed, cache_conn, stats_conn
|
|
926
|
+
def _codex_breakdown_rows(ident, reset, speed, cache_conn, stats_conn,
|
|
927
|
+
account_key=None):
|
|
909
928
|
try:
|
|
929
|
+
# #416 closeout F3: the flavour is NOT this route's to pick. Every key
|
|
930
|
+
# that reaches here comes from the POST-fold durable projection, so the
|
|
931
|
+
# pre-fold BOUNDARY read widens — but `codex_quota_breakdown` now
|
|
932
|
+
# decides that per read, because the two reads behind it have opposite
|
|
933
|
+
# correctness requirements and the single flag conflated them. The
|
|
934
|
+
# accounting read stays strict, so a crossing whose spend is
|
|
935
|
+
# unattributed renders $0.00 rather than adopting another scope's rows.
|
|
910
936
|
return codex_quota_breakdown(
|
|
911
937
|
ident, reset, speed=speed, cache_conn=cache_conn, stats_conn=stats_conn,
|
|
938
|
+
account_key=account_key,
|
|
912
939
|
)
|
|
913
940
|
except sqlite3.Error:
|
|
914
941
|
return None
|
|
915
942
|
|
|
916
943
|
|
|
917
|
-
def _union_cluster_milestones(cluster, block_key, speed, cache_conn, stats_conn
|
|
944
|
+
def _union_cluster_milestones(cluster, block_key, speed, cache_conn, stats_conn,
|
|
945
|
+
account_key=None):
|
|
918
946
|
"""Union ``codex_quota_breakdown`` milestone rows across every cluster
|
|
919
947
|
member (each jittered sibling reset via the existing per-reset path).
|
|
920
948
|
|
|
@@ -922,6 +950,19 @@ def _union_cluster_milestones(cluster, block_key, speed, cache_conn, stats_conn)
|
|
|
922
950
|
full ledger is the union — shaped and ordered by ``(captured_at, percent)``.
|
|
923
951
|
Returns ``None`` if any member's projection is incoherent (mirrors the
|
|
924
952
|
single-reset signal so the caller can surface ``projection_incoherent``).
|
|
953
|
+
|
|
954
|
+
``account_key`` (#416) is the ROUTE's focus, deliberately not
|
|
955
|
+
``member.account_key``. The milestone read keeps using the member's own
|
|
956
|
+
stamp — those rows were written under that exact identity — but the two
|
|
957
|
+
CACHE reads are scoped to the account whose view this is. The two differ for
|
|
958
|
+
the 5h leg: ``_codex_five_hour_rows`` admits a still-``unattributed`` block
|
|
959
|
+
into a real account's cycle (the #373 per-window-group rule). SELECTION is
|
|
960
|
+
therefore the cycle's scope, and so is the block-start boundary — but COST
|
|
961
|
+
is not widened to match (#416 closeout F3): an unattributed block selected
|
|
962
|
+
into a real account's cycle renders its crossings at ``$0.00``, because
|
|
963
|
+
costing it under the cycle's account would file those dollars twice, in the
|
|
964
|
+
focused scope and in the ``unattributed`` one. ``None`` keeps the merged
|
|
965
|
+
read.
|
|
925
966
|
"""
|
|
926
967
|
shaped: list = []
|
|
927
968
|
for member in cluster.cluster_members:
|
|
@@ -932,7 +973,8 @@ def _union_cluster_milestones(cluster, block_key, speed, cache_conn, stats_conn)
|
|
|
932
973
|
limit_id=member.limit_id, limit_name=member.limit_name,
|
|
933
974
|
)
|
|
934
975
|
breakdown = _codex_breakdown_rows(
|
|
935
|
-
ident, member.reset, speed, cache_conn, stats_conn
|
|
976
|
+
ident, member.reset, speed, cache_conn, stats_conn,
|
|
977
|
+
account_key=account_key,
|
|
936
978
|
)
|
|
937
979
|
if breakdown is None:
|
|
938
980
|
return None # projection-incoherent signal to the caller
|
|
@@ -955,12 +997,14 @@ def _union_cluster_milestones(cluster, block_key, speed, cache_conn, stats_conn)
|
|
|
955
997
|
)
|
|
956
998
|
|
|
957
999
|
|
|
958
|
-
def _codex_cycle_blocks(stats_conn, cache_conn, cyc, speed
|
|
1000
|
+
def _codex_cycle_blocks(stats_conn, cache_conn, cyc, speed,
|
|
1001
|
+
account_key=None) -> "list | None":
|
|
959
1002
|
out: list = []
|
|
960
1003
|
for block in _codex_five_hour_clusters(stats_conn, cyc):
|
|
961
1004
|
block_key = block.block_key
|
|
962
1005
|
milestones = _union_cluster_milestones(
|
|
963
|
-
block, block_key, speed, cache_conn, stats_conn
|
|
1006
|
+
block, block_key, speed, cache_conn, stats_conn,
|
|
1007
|
+
account_key=account_key,
|
|
964
1008
|
)
|
|
965
1009
|
if milestones is None:
|
|
966
1010
|
return None # projection-incoherent signal to the caller
|
|
@@ -987,13 +1031,24 @@ def _now_guard(cyc):
|
|
|
987
1031
|
|
|
988
1032
|
|
|
989
1033
|
def build_codex_cycle_detail(
|
|
990
|
-
stats_conn, cache_conn, *, identity, key, speed, now_utc
|
|
1034
|
+
stats_conn, cache_conn, *, identity, key, speed, now_utc, account_key=None
|
|
991
1035
|
):
|
|
992
1036
|
"""Complete payload for one Codex cycle (spec §1c). On success returns a
|
|
993
1037
|
dict mirroring the Claude detail shape (segments = a single Codex segment,
|
|
994
1038
|
dividers = []). On failure returns ``(None, reason)`` where reason ∈
|
|
995
1039
|
{pruned, rebuild_pending, projection_incoherent, unknown} — Task 4 maps it
|
|
996
1040
|
into the 404 body.
|
|
1041
|
+
|
|
1042
|
+
``account_key`` (#416) focuses the whole route on ONE account. It scopes the
|
|
1043
|
+
cycle ENUMERATION and the milestone LADDER together, which has to be one
|
|
1044
|
+
parameter rather than two: ``build_codex_cycle_index`` already takes it
|
|
1045
|
+
(Slice 3A review B3) and builds each key from THAT account's cluster
|
|
1046
|
+
representative, while ``_canonicalize_codex_cluster`` takes ``max(reset)``
|
|
1047
|
+
across the cluster — so a merged enumeration resolves a jitter-separated
|
|
1048
|
+
sibling's reset and the focused account's own index key 404s.
|
|
1049
|
+
|
|
1050
|
+
``None`` is the merged route every shipped caller uses and is byte-stable by
|
|
1051
|
+
construction: nothing below it changes shape.
|
|
997
1052
|
"""
|
|
998
1053
|
now = now_utc.astimezone(UTC) if now_utc.tzinfo else now_utc.replace(tzinfo=UTC)
|
|
999
1054
|
tz = _resolve_display_tz()
|
|
@@ -1001,7 +1056,7 @@ def build_codex_cycle_detail(
|
|
|
1001
1056
|
cycles = _load_codex_cycles(
|
|
1002
1057
|
stats_conn, getattr(identity, "source_root_keys", ()),
|
|
1003
1058
|
preferred_identity=getattr(identity, "quota_identity", None),
|
|
1004
|
-
current_boundary=identity, now_utc=now,
|
|
1059
|
+
current_boundary=identity, now_utc=now, account_key=account_key,
|
|
1005
1060
|
)
|
|
1006
1061
|
except sqlite3.Error:
|
|
1007
1062
|
return (None, "projection_incoherent")
|
|
@@ -1014,7 +1069,7 @@ def build_codex_cycle_detail(
|
|
|
1014
1069
|
stats_conn, getattr(identity, "source_root_keys", ()),
|
|
1015
1070
|
include_orphaned=True,
|
|
1016
1071
|
preferred_identity=getattr(identity, "quota_identity", None),
|
|
1017
|
-
current_boundary=identity, now_utc=now,
|
|
1072
|
+
current_boundary=identity, now_utc=now, account_key=account_key,
|
|
1018
1073
|
)
|
|
1019
1074
|
except sqlite3.Error:
|
|
1020
1075
|
return (None, "projection_incoherent")
|
|
@@ -1023,11 +1078,13 @@ def build_codex_cycle_detail(
|
|
|
1023
1078
|
return (None, "unknown")
|
|
1024
1079
|
|
|
1025
1080
|
milestones = _union_cluster_milestones(
|
|
1026
|
-
match, match.block_key, speed, cache_conn, stats_conn
|
|
1081
|
+
match, match.block_key, speed, cache_conn, stats_conn,
|
|
1082
|
+
account_key=account_key,
|
|
1027
1083
|
)
|
|
1028
1084
|
if milestones is None:
|
|
1029
1085
|
return (None, "projection_incoherent")
|
|
1030
|
-
blocks = _codex_cycle_blocks(
|
|
1086
|
+
blocks = _codex_cycle_blocks(
|
|
1087
|
+
stats_conn, cache_conn, match, speed, account_key=account_key)
|
|
1031
1088
|
if blocks is None:
|
|
1032
1089
|
return (None, "projection_incoherent")
|
|
1033
1090
|
|