cctally 1.85.1 → 1.87.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.
@@ -702,6 +702,7 @@ def maybe_record_milestone(
702
702
 
703
703
  # Threshold crossed — sync cost before recording so the milestone
704
704
  # captures up-to-date cumulative cost, not a stale snapshot.
705
+ cost_synced = True
705
706
  try:
706
707
  if retained_selection is None:
707
708
  retained_selection = _cctally().WeekSelection(
@@ -740,7 +741,13 @@ def maybe_record_milestone(
740
741
  retained_selection=retained_selection,
741
742
  )
742
743
  except Exception as exc:
743
- eprint(f"[milestone] cost sync failed, using latest available: {exc}")
744
+ # The snapshot read below would now return a row from an EARLIER
745
+ # crossing, so recording here stamps that older cumulative onto
746
+ # this threshold — a write-once row with a $0.00 marginal and a
747
+ # fabricated $/1%. Fall through only far enough to reach the
748
+ # skip guard on the snapshot branch.
749
+ cost_synced = False
750
+ eprint(f"[milestone] cost sync failed: {exc}")
744
751
 
745
752
  week_start = dt.date.fromisoformat(week_start_date)
746
753
  week_end = dt.date.fromisoformat(week_end_date)
@@ -762,17 +769,36 @@ def maybe_record_milestone(
762
769
  effective_ref = adjusted[0]
763
770
 
764
771
  if _week_ref_has_reset_event(conn, effective_ref):
765
- live_cost = _compute_cost_for_weekref(
766
- effective_ref,
767
- account_key=account_key,
768
- as_of=as_of,
769
- )
772
+ import _cctally_cache # fail-closed attribution guard (#341)
773
+ try:
774
+ live_cost = _compute_cost_for_weekref(
775
+ effective_ref,
776
+ account_key=account_key,
777
+ as_of=as_of,
778
+ )
779
+ except _cctally_cache.AccountAttributionUnavailable as exc:
780
+ # Same contract the budget ladder already holds (#341 Task 4):
781
+ # an account-scoped read that fell into the fail-closed guard
782
+ # SKIPS this tick and fires on the next healthy one. Never
783
+ # re-raised — on the passed-conn (ingest) path a bare raise
784
+ # would abort the whole cycle over a transient lock.
785
+ eprint("[milestone] account attribution unavailable, "
786
+ f"skipping this crossing: {exc}")
787
+ return
770
788
  if live_cost is None:
771
789
  eprint("[milestone] could not compute effective-range cost, skipping")
772
790
  return
773
791
  cumulative_cost = live_cost
774
792
  cost_snapshot_id = 0 # no snapshot row to anchor against
775
793
  else:
794
+ if not cost_synced:
795
+ # The latest snapshot predates this crossing. Milestones are
796
+ # write-once, so a stale cumulative here is permanent; skip
797
+ # instead. The next observation still sees current_floor >
798
+ # max_existing and records the crossing with a real cost.
799
+ eprint("[milestone] skipping this crossing — its cost would "
800
+ "come from a snapshot taken before the crossing")
801
+ return
776
802
  # Account-scoped read (#341 P2-1): the cost snapshot was just
777
803
  # materialized under `account_key`, so scope the read to it — the
778
804
  # merged (account-blind) read would return another account's row on
@@ -387,6 +387,9 @@ class CodexBucketUsage:
387
387
  period_end_at: dt.datetime | None = None
388
388
  used_pct: float | None = None
389
389
  dollar_per_pct: float | None = None
390
+ # #424: owning accounts for a pooled native weekly period. Empty for
391
+ # calendar buckets, focused account children, and undecorated providers.
392
+ account_keys: tuple[str, ...] = ()
390
393
 
391
394
 
392
395
  @dataclass
@@ -0,0 +1,173 @@
1
+ """Codex window-scoped spend adoption (pure kernel).
2
+
3
+ Spec: ``docs/superpowers/specs/2026-07-30-codex-window-scoped-spend-adoption.md``
4
+
5
+ ``adopt_unidentified_observations`` (``bin/_lib_quota.py``) applies the #341 §2
6
+ window-account continuity rule to the OBSERVATION axis: inside one physical
7
+ quota window, unidentified observations are adopted by the window's account iff
8
+ exactly one identified account is ever observed for that window key. This
9
+ kernel applies the same inference, with the same grouping key and the same
10
+ guard, to the SPEND axis — ``codex_session_entries.account_key``.
11
+
12
+ A pure leaf module: stdlib only, no cctally imports. The caller supplies window
13
+ descriptors (already grouped on ``_lib_quota._physical_window_key`` and already
14
+ folded, so ``identified_accounts`` is the window's post-fold identified set) and
15
+ candidate entries; this kernel decides, and only decides. Every SQL read and
16
+ every write stays in the glue layer.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import datetime as dt
21
+ from dataclasses import dataclass
22
+ from typing import Iterable
23
+
24
+
25
+ #: Native length of the account-level Codex weekly quota window, in minutes.
26
+ ACCOUNT_WEEKLY_WINDOW_MINUTES = 10_080
27
+
28
+
29
+ #: The reserved "account could not be determined" sentinel
30
+ #: (``_lib_accounts.UNATTRIBUTED``), spelled here so this leaf stays import-free.
31
+ UNATTRIBUTED_SENTINEL = "unattributed"
32
+
33
+
34
+ def entry_is_unattributed(account_key: object) -> bool:
35
+ """Whether a ``codex_session_entries`` row is still up for adoption.
36
+
37
+ ``NULL`` is the stamp every never-decided row carries (#416 spec D1:
38
+ ``stably_absent`` -> ``NULL``), and the empty string is its degenerate
39
+ spelling. The literal ``unattributed`` sentinel is admitted too: no producer
40
+ writes it to this column today, but ``_codex_cache_account_predicate`` counts
41
+ it in the ``unattributed`` BUCKET, so excluding it here would make such a row
42
+ permanently unadoptable — visible as nobody's money and ineligible for the
43
+ only mechanism that could give it an owner.
44
+ """
45
+ return (
46
+ account_key is None
47
+ or account_key == ""
48
+ or account_key == UNATTRIBUTED_SENTINEL
49
+ )
50
+
51
+
52
+ @dataclass(frozen=True)
53
+ class SpendAdoptionWindow:
54
+ """One physical Codex quota window, as the fold left it.
55
+
56
+ ``canonical_resets_at`` is the tolerance-anchored reset (#416 §4.1), never a
57
+ raw jittered provider value — the same anchor
58
+ ``_lib_quota._physical_window_key`` groups on. ``identified_accounts`` is
59
+ the set of non-``unattributed`` account keys observed for that key.
60
+ """
61
+
62
+ source_root_key: str
63
+ window_minutes: int
64
+ canonical_resets_at: dt.datetime
65
+ identified_accounts: frozenset[str] = frozenset()
66
+ model_scoped: bool = False
67
+
68
+ def __post_init__(self) -> None:
69
+ if not isinstance(self.source_root_key, str) or not self.source_root_key:
70
+ raise ValueError("source_root_key must be a non-empty string")
71
+ if (not isinstance(self.window_minutes, int)
72
+ or isinstance(self.window_minutes, bool)
73
+ or self.window_minutes <= 0):
74
+ raise ValueError("window_minutes must be a positive integer")
75
+ reset = self.canonical_resets_at
76
+ if reset.tzinfo is None or reset.utcoffset() is None:
77
+ raise ValueError("canonical_resets_at must be timezone-aware")
78
+ object.__setattr__(
79
+ self, "identified_accounts", frozenset(self.identified_accounts))
80
+
81
+ @property
82
+ def nominal_start_at(self) -> dt.datetime:
83
+ return self.canonical_resets_at - dt.timedelta(
84
+ minutes=self.window_minutes)
85
+
86
+ @property
87
+ def in_scope(self) -> bool:
88
+ """Account-level weekly windows only.
89
+
90
+ A 5h window nests inside the weekly one and adds no evidence; a
91
+ model-scoped pool such as GPT-5.3-Codex-Spark is never account weekly
92
+ quota (#373). Both are excluded from candidacy entirely, so neither
93
+ stamps nor blocks.
94
+ """
95
+ return (
96
+ self.window_minutes == ACCOUNT_WEEKLY_WINDOW_MINUTES
97
+ and not self.model_scoped
98
+ )
99
+
100
+ def covers(self, timestamp: dt.datetime) -> bool:
101
+ """Whether ``timestamp`` falls in the NOMINAL ``[start, reset)`` range.
102
+
103
+ Nominal rather than first-observation: spend before the window's first
104
+ retained observation is still spend inside the cycle.
105
+ """
106
+ return self.nominal_start_at <= timestamp < self.canonical_resets_at
107
+
108
+
109
+ @dataclass(frozen=True)
110
+ class SpendAdoptionCandidate:
111
+ """One ``codex_session_entries`` row offered to the pass."""
112
+
113
+ entry_id: int
114
+ source_root_key: str
115
+ timestamp: dt.datetime
116
+ account_key: "str | None" = None
117
+
118
+
119
+ @dataclass(frozen=True)
120
+ class SpendAdoptionStamp:
121
+ """One decided write: give ``entry_id`` this account."""
122
+
123
+ entry_id: int
124
+ account_key: str
125
+
126
+
127
+ def build_spend_adoption_plan(
128
+ windows: Iterable[SpendAdoptionWindow],
129
+ candidates: Iterable[SpendAdoptionCandidate],
130
+ ) -> tuple[SpendAdoptionStamp, ...]:
131
+ """Return the stamping plan, ordered by ``entry_id``.
132
+
133
+ An in-scope window CLAIMS every candidate its nominal range covers on its own
134
+ root. A candidate is stamped iff the UNION of identified accounts across
135
+ every claiming window is exactly one.
136
+
137
+ A claiming window that identifies no account contributes nothing to that
138
+ union and therefore does NOT block: absence of evidence is not evidence of
139
+ ambiguity. That is the same shape ``adopt_unidentified_observations`` uses on
140
+ the observation axis, which likewise resolves a window from its *identified*
141
+ observations only and treats an unidentified population as no evidence. The
142
+ first implementation blocked on any claiming window that resolved to nothing
143
+ and measured ZERO stamped rows on a real store: weekly resets move by days,
144
+ so one cycle overlaps many neighbours and pre-attribution history is
145
+ unidentified by construction — one such neighbour was always enough to veto.
146
+
147
+ Two claiming windows naming DIFFERENT accounts still leave the entry alone
148
+ (union of two), and so does a single window that itself saw two accounts
149
+ (#341 never-combine). An already-identified row is never re-stamped, so
150
+ re-running over this kernel's own output returns an empty plan.
151
+ """
152
+ by_root: dict[str, list[SpendAdoptionWindow]] = {}
153
+ for window in windows:
154
+ if window.in_scope:
155
+ by_root.setdefault(window.source_root_key, []).append(window)
156
+
157
+ stamps: list[SpendAdoptionStamp] = []
158
+ for candidate in candidates:
159
+ if not entry_is_unattributed(candidate.account_key):
160
+ continue
161
+ identified: set[str] = set()
162
+ for window in by_root.get(candidate.source_root_key, ()):
163
+ if window.covers(candidate.timestamp):
164
+ identified |= window.identified_accounts
165
+ if len(identified) > 1:
166
+ break
167
+ if len(identified) != 1:
168
+ continue
169
+ account = next(iter(identified))
170
+ stamps.append(SpendAdoptionStamp(
171
+ entry_id=candidate.entry_id, account_key=account))
172
+ stamps.sort(key=lambda stamp: (stamp.entry_id, stamp.account_key))
173
+ return tuple(stamps)
@@ -2607,6 +2607,33 @@ def _check_accounts_attribution(s: DoctorState) -> CheckResult:
2607
2607
  )
2608
2608
 
2609
2609
 
2610
+ def _check_accounts_codex_reset_anchors(s: DoctorState) -> CheckResult:
2611
+ """Surface Codex quota rows that landed after migration 032 with no anchor."""
2612
+ st = s.accounts_state or {}
2613
+ try:
2614
+ null_rows = int(st.get("codex_null_reset_anchors") or 0)
2615
+ except (TypeError, ValueError):
2616
+ null_rows = 0
2617
+ details = {"null_anchor_rows": null_rows}
2618
+ if null_rows > 0:
2619
+ return CheckResult(
2620
+ id="accounts.codex_reset_anchors",
2621
+ title="Codex reset anchors",
2622
+ severity="warn",
2623
+ summary=f"{null_rows} Codex quota observation(s) lack a canonical reset anchor",
2624
+ remediation="Run `cctally cache-sync --source codex --rebuild`",
2625
+ details=details,
2626
+ )
2627
+ return CheckResult(
2628
+ id="accounts.codex_reset_anchors",
2629
+ title="Codex reset anchors",
2630
+ severity="ok",
2631
+ summary="Codex reset anchors complete",
2632
+ remediation=None,
2633
+ details=details,
2634
+ )
2635
+
2636
+
2610
2637
  _CATEGORY_DEFINITIONS: tuple[tuple[str, str, tuple[tuple[str, str], ...]], ...] = (
2611
2638
  ("install", "Install", (
2612
2639
  ("install.mode", "_check_install_dev_mode"),
@@ -2667,6 +2694,7 @@ _CATEGORY_DEFINITIONS: tuple[tuple[str, str, tuple[tuple[str, str], ...]], ...]
2667
2694
  ("accounts.registry", "_check_accounts_registry"),
2668
2695
  ("accounts.freshness", "_check_accounts_freshness"),
2669
2696
  ("accounts.attribution", "_check_accounts_attribution"),
2697
+ ("accounts.codex_reset_anchors", "_check_accounts_codex_reset_anchors"),
2670
2698
  )),
2671
2699
  ("pricing", "Pricing", (
2672
2700
  ("pricing.coverage", "_check_pricing_coverage"),
package/bin/_lib_quota.py CHANGED
@@ -78,11 +78,11 @@ def resolve_reset_anchor(
78
78
  anchor already in ``anchors`` or the observation's own raw value, never a
79
79
  recomputed centroid. Assignment picks the NEAREST anchor within tolerance,
80
80
  breaking a tie on the earlier anchor, so for a given anchor set the answer
81
- does not depend on the order the anchors were established in. (The anchor
82
- SET can still depend on arrival order in the pathological case of a chain of
83
- observations each within tolerance of its neighbour but not of the first;
84
- real jitter is seconds wide, so a real cluster collapses to one anchor under
85
- every order.)
81
+ does not depend on the order the anchors were established in. The anchor
82
+ SET can still depend on arrival order in the chain-of-neighbours case. This
83
+ is retained as migration 032's reference rule; production ingest uses
84
+ :class:`ResetAnchorComponents` after #425 real-data evidence proved those
85
+ chains occur.
86
86
  """
87
87
  _require_aware(raw_reset, "raw_reset")
88
88
  if (isinstance(anchors, ResetAnchorIndex)
@@ -121,7 +121,9 @@ class ResetAnchorIndex:
121
121
  nearest within tolerance, ties broken on the earlier anchor, first sight
122
122
  wins, the anchor never moves, and the pathological chain-of-neighbours
123
123
  order-dependence that docstring documents is preserved exactly, because the
124
- anchor SET is still whatever the caller established in whatever order.
124
+ anchor SET is still whatever the caller established in whatever order. This
125
+ class remains the exact migration-032 oracle; current ingest uses
126
+ ``ResetAnchorComponents``.
125
127
 
126
128
  Only the LOOKUP changes. The linear scan did a full ``datetime`` subtraction
127
129
  against every established anchor, and a 5h group accumulates ~1,750 anchors
@@ -204,6 +206,156 @@ class ResetAnchorIndex:
204
206
  return len(self._order)
205
207
 
206
208
 
209
+ class ResetAnchorComponents:
210
+ """Tolerance-connected raw-reset components with first-sight anchors.
211
+
212
+ ``ResetAnchorIndex`` intentionally preserves the original migration-032
213
+ rule: compare a raw reset only with already-established anchors. Real data
214
+ proved that rule can split a chain whose adjacent members are all within
215
+ tolerance. This index retains every distinct raw reset as evidence and
216
+ unions adjacent members transitively. The member with the smallest stable
217
+ physical ``order_key`` remains the completed component's canonical anchor,
218
+ independent of filesystem traversal or journal batch arrival. Callers that
219
+ omit an order key retain insertion-order behavior for compatibility.
220
+
221
+ ``add`` returns both the winning anchor and any formerly independent
222
+ component anchors retired by the union. Writers use the retired set to
223
+ converge rows materialized before a later bridge observation arrived.
224
+ """
225
+
226
+ __slots__ = (
227
+ "_tolerance", "_buckets", "_parent", "_rank",
228
+ "_anchor", "_first_order", "_member_order", "_next_order",
229
+ )
230
+
231
+ def __init__(
232
+ self, raws: Iterable[dt.datetime] = (),
233
+ *, tolerance_seconds: int = CODEX_RESET_ANCHOR_TOLERANCE_SECONDS,
234
+ ) -> None:
235
+ if not isinstance(tolerance_seconds, int) or isinstance(
236
+ tolerance_seconds, bool):
237
+ raise ValueError("tolerance_seconds must be an int")
238
+ if tolerance_seconds <= 0:
239
+ raise ValueError("tolerance_seconds must be positive")
240
+ self._tolerance = tolerance_seconds
241
+ self._buckets: dict[int, list[dt.datetime]] = {}
242
+ self._parent: dict[dt.datetime, dt.datetime] = {}
243
+ self._rank: dict[dt.datetime, int] = {}
244
+ self._anchor: dict[dt.datetime, dt.datetime] = {}
245
+ self._first_order: dict[
246
+ dt.datetime, tuple[str, int, int]
247
+ ] = {}
248
+ self._member_order: dict[
249
+ dt.datetime, tuple[str, int, int]
250
+ ] = {}
251
+ self._next_order = 0
252
+ for raw in raws:
253
+ self.add(raw)
254
+
255
+ @property
256
+ def tolerance_seconds(self) -> int:
257
+ return self._tolerance
258
+
259
+ def _bucket(self, value: dt.datetime) -> int:
260
+ return int(value.timestamp() // self._tolerance)
261
+
262
+ def _find(self, value: dt.datetime) -> dt.datetime:
263
+ parent = self._parent[value]
264
+ if parent != value:
265
+ self._parent[value] = self._find(parent)
266
+ return self._parent[value]
267
+
268
+ def _union(
269
+ self, left: dt.datetime, right: dt.datetime,
270
+ ) -> dt.datetime:
271
+ left_root = self._find(left)
272
+ right_root = self._find(right)
273
+ if left_root == right_root:
274
+ return left_root
275
+ if self._rank[left_root] < self._rank[right_root]:
276
+ left_root, right_root = right_root, left_root
277
+ self._parent[right_root] = left_root
278
+ if self._rank[left_root] == self._rank[right_root]:
279
+ self._rank[left_root] += 1
280
+ if self._first_order[right_root] < self._first_order[left_root]:
281
+ self._anchor[left_root] = self._anchor[right_root]
282
+ self._first_order[left_root] = self._first_order[right_root]
283
+ del self._anchor[right_root]
284
+ del self._first_order[right_root]
285
+ return left_root
286
+
287
+ def add(
288
+ self, raw_reset: dt.datetime,
289
+ *, order_key: "tuple[str, int, int] | None" = None,
290
+ ) -> tuple[dt.datetime, tuple[dt.datetime, ...]]:
291
+ """Add one raw reset and return ``(anchor, retired_anchors)``."""
292
+ _require_aware(raw_reset, "raw_reset")
293
+ if order_key is None:
294
+ order_key = ("", self._next_order, 0)
295
+ if (
296
+ not isinstance(order_key, tuple) or len(order_key) != 3
297
+ or not isinstance(order_key[0], str)
298
+ or not isinstance(order_key[1], int)
299
+ or isinstance(order_key[1], bool)
300
+ or not isinstance(order_key[2], int)
301
+ or isinstance(order_key[2], bool)
302
+ ):
303
+ raise ValueError(
304
+ "order_key must be a (source_path, line_offset, row_id) tuple")
305
+ self._next_order += 1
306
+ if raw_reset in self._parent:
307
+ root = self._find(raw_reset)
308
+ previous = self._anchor[root]
309
+ if order_key < self._member_order[raw_reset]:
310
+ self._member_order[raw_reset] = order_key
311
+ if order_key < self._first_order[root]:
312
+ self._first_order[root] = order_key
313
+ self._anchor[root] = raw_reset
314
+ winner = self._anchor[root]
315
+ retired = (previous,) if previous != winner else ()
316
+ return winner, retired
317
+
318
+ probe = self._bucket(raw_reset)
319
+ neighbours: list[dt.datetime] = []
320
+ for bucket_id in (probe - 1, probe, probe + 1):
321
+ for candidate in self._buckets.get(bucket_id, ()):
322
+ if abs((raw_reset - candidate).total_seconds()) <= self._tolerance:
323
+ neighbours.append(candidate)
324
+
325
+ self._parent[raw_reset] = raw_reset
326
+ self._rank[raw_reset] = 0
327
+ self._anchor[raw_reset] = raw_reset
328
+ self._first_order[raw_reset] = order_key
329
+ self._member_order[raw_reset] = order_key
330
+ self._buckets.setdefault(probe, []).append(raw_reset)
331
+
332
+ prior_anchors = {raw_reset}
333
+ prior_anchors.update(
334
+ self._anchor[self._find(candidate)] for candidate in neighbours)
335
+ root = raw_reset
336
+ for candidate in neighbours:
337
+ root = self._union(root, candidate)
338
+ winner = self._anchor[self._find(root)]
339
+ return winner, tuple(sorted(prior_anchors - {winner}))
340
+
341
+ def canonical(self, raw_reset: dt.datetime) -> dt.datetime:
342
+ """Return the completed component's first-sight anchor."""
343
+ _require_aware(raw_reset, "raw_reset")
344
+ return self._anchor[self._find(raw_reset)]
345
+
346
+ def __contains__(self, anchor: object) -> bool:
347
+ if not isinstance(anchor, dt.datetime):
348
+ return False
349
+ return anchor in self._buckets.get(self._bucket(anchor), ())
350
+
351
+ def __iter__(self):
352
+ """Distinct raw-reset evidence in insertion order."""
353
+ return iter(self._parent)
354
+
355
+ def __len__(self) -> int:
356
+ return len(self._parent)
357
+
358
+
207
359
  @dataclass(frozen=True)
208
360
  class QuotaWindowIdentity:
209
361
  """One root-qualified native quota window identity.
@@ -313,6 +465,7 @@ class QuotaBlock:
313
465
  last_observed_at: dt.datetime
314
466
  first_percent: float
315
467
  current_percent: float
468
+ physical_observations: tuple[QuotaObservation, ...] = ()
316
469
 
317
470
 
318
471
  @dataclass(frozen=True)
@@ -577,7 +730,14 @@ def build_blocks(observations: Iterable[QuotaObservation]) -> tuple[QuotaBlock,
577
730
  ``QuotaBlock.resets_at``, which every renderer shows, is that same anchor.
578
731
  """
579
732
  by_block: dict[tuple[QuotaWindowIdentity, dt.datetime], list[QuotaObservation]] = {}
733
+ physical_by_block: dict[
734
+ tuple[QuotaWindowIdentity, dt.datetime], list[QuotaObservation]
735
+ ] = {}
580
736
  for history in build_history(observations):
737
+ for observation in history.physical_observations:
738
+ physical_by_block.setdefault(
739
+ (history.identity, observation.canonical_resets_at), []
740
+ ).append(observation)
581
741
  for observation in history.observations:
582
742
  by_block.setdefault(
583
743
  (history.identity, observation.canonical_resets_at), []
@@ -599,6 +759,10 @@ def build_blocks(observations: Iterable[QuotaObservation]) -> tuple[QuotaBlock,
599
759
  last_observed_at=last.captured_at,
600
760
  first_percent=first.used_percent,
601
761
  current_percent=last.used_percent,
762
+ physical_observations=tuple(sorted(
763
+ physical_by_block[(identity, resets_at)],
764
+ key=physical_order_key,
765
+ )),
602
766
  ))
603
767
  return tuple(blocks)
604
768