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.
@@ -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) -> list:
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) -> "list | None":
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(stats_conn, cache_conn, match, speed)
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