cctally 1.117.0 → 1.117.2

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.
@@ -1020,14 +1020,22 @@ def _decorated_combined_leg(
1020
1020
  5, provider, "account_reconciliation_failed"),)
1021
1021
 
1022
1022
  total = math.fsum(cost for _key, cost in card_rows)
1023
- hero = _combined_hero(state)
1024
- hero_cost = hero.get("cost_usd") if hero is not None else None
1025
- if (
1026
- _counter_reason(hero_cost, integral=False) is not None
1027
- or abs(float(hero_cost) - total) > 1e-9
1028
- ):
1029
- return None, (_CombinedCause(
1030
- 5, provider, "account_reconciliation_failed"),)
1023
+ # The cards and their certified contributions above (keys, order, costs
1024
+ # within 1e-9, periods) are what certify the leg, and the headline is their
1025
+ # sum. A decorated Codex hero IS that sum (`build_codex_source_state`), so
1026
+ # it must still agree. A decorated Claude hero is not: under #836's U3 it is
1027
+ # the DISPLAYED account's own spend, by design one card rather than all of
1028
+ # them, so requiring equality would withhold the combined headline on every
1029
+ # multi-account Claude install.
1030
+ if provider != "claude":
1031
+ hero = _combined_hero(state)
1032
+ hero_cost = hero.get("cost_usd") if hero is not None else None
1033
+ if (
1034
+ _counter_reason(hero_cost, integral=False) is not None
1035
+ or abs(float(hero_cost) - total) > 1e-9
1036
+ ):
1037
+ return None, (_CombinedCause(
1038
+ 5, provider, "account_reconciliation_failed"),)
1031
1039
  return {
1032
1040
  "state": "current",
1033
1041
  "scope": "account_cycles",
@@ -2206,6 +2214,84 @@ def codex_stats_digest(stats_conn: sqlite3.Connection) -> str:
2206
2214
  )
2207
2215
 
2208
2216
 
2217
+ # #836 spec 5.7 item 1: the Claude weekly REJECTION state. A weekly credit's
2218
+ # retry can retire a stale replica in place with no new snapshot and a
2219
+ # deduplicated reset, and a suppression replacement or a convergence can
2220
+ # restore one; a DELETE of a referenced snapshot rewrites the milestone's FK to
2221
+ # the `0` sentinel without moving any `MAX(id)`. None of those moves another
2222
+ # signature leg, so this one digests the two relations they change:
2223
+ #
2224
+ # * the suppression-retired snapshots (held, without the held-write payload
2225
+ # marker), which the card's sample selection depends on;
2226
+ # * the five-hour milestones whose effective weekly join is unavailable
2227
+ # (absent snapshot, the `0` sentinel, or retired), which every published
2228
+ # `effective_seven_day_pct_at_crossing` depends on.
2229
+ #
2230
+ # The predicates are `_cctally_five_hour.effective_weekly_value_sql` /
2231
+ # `weekly_join_unavailable_sql`'s, spelled here because this kernel may not
2232
+ # import the glue. Ids only, rowid-ordered (no sorter); both relations are
2233
+ # empty on a store that never retired a row, and the wrapper maps that state to
2234
+ # "" so `data_version` and the Claude source version keep their bytes.
2235
+ _WEEKLY_RETIRED_PREDICATE = (
2236
+ "{a}weekly_observation_held = 1 AND "
2237
+ "json_extract({a}payload_json, '$.weeklyObservationHeld') IS NOT 1"
2238
+ )
2239
+
2240
+
2241
+ def _claude_weekly_retired_relations(
2242
+ stats_conn: sqlite3.Connection,
2243
+ ) -> tuple[tuple[str, str], ...]:
2244
+ try:
2245
+ cols = {str(row[1]) for row in stats_conn.execute(
2246
+ "PRAGMA table_info(weekly_usage_snapshots)")}
2247
+ except sqlite3.Error:
2248
+ cols = set()
2249
+ if "weekly_observation_held" in cols:
2250
+ retired = _WEEKLY_RETIRED_PREDICATE.format(a="")
2251
+ unavailable = ("s.id IS NULL OR ("
2252
+ + _WEEKLY_RETIRED_PREDICATE.format(a="s.") + ")")
2253
+ else:
2254
+ # A store without the column cannot hold a held row
2255
+ # (`_cctally_core.weekly_held_exclusion`).
2256
+ retired = "0"
2257
+ unavailable = "s.id IS NULL"
2258
+ return (
2259
+ (
2260
+ "weekly_usage_snapshots_retired",
2261
+ f"SELECT id FROM weekly_usage_snapshots WHERE {retired} "
2262
+ "ORDER BY id",
2263
+ ),
2264
+ (
2265
+ "five_hour_milestones_unavailable_join",
2266
+ "SELECT m.id FROM five_hour_milestones AS m "
2267
+ "LEFT JOIN weekly_usage_snapshots AS s ON s.id = m.usage_snapshot_id "
2268
+ f"WHERE {unavailable} ORDER BY m.id",
2269
+ ),
2270
+ )
2271
+
2272
+
2273
+ #: `_stats_relations_digest` over two EMPTY relations: the no-rejection state.
2274
+ _EMPTY_TWO_RELATION_DIGEST = hashlib.sha256(b"[[],[]]").hexdigest()
2275
+
2276
+
2277
+ def claude_weekly_retired_digest(stats_conn: sqlite3.Connection) -> str:
2278
+ """One digest over the Claude weekly rejection state (#836, spec 5.7
2279
+ item 1), or ``""`` when no snapshot is retired and no five-hour
2280
+ milestone's join is unavailable.
2281
+
2282
+ Read through `_stats_relations_digest` under its own memo namespace,
2283
+ statement-scoped and outside any stats transaction (as
2284
+ `_tui_compute_dispatch_signature` reads every stats leg), so a settled
2285
+ rollback-journal index costs no scan on an idle tick.
2286
+ """
2287
+ digest = _stats_relations_digest(
2288
+ stats_conn,
2289
+ _claude_weekly_retired_relations(stats_conn),
2290
+ memo_namespace="claude_weekly_retired",
2291
+ )
2292
+ return "" if digest == _EMPTY_TWO_RELATION_DIGEST else digest
2293
+
2294
+
2209
2295
  # #1015 spec 5.4: the two relation digests of the dashboard dispatch key's
2210
2296
  # `calibration_dependency` component. Neither is a `SnapshotSignature` leg, so
2211
2297
  # the published `data_version` contract is unchanged; both ride the dispatch
@@ -866,6 +866,24 @@ def family_participation(family: str | None) -> str:
866
866
  return FAMILY_PARTICIPATION.get(family, "unknown")
867
867
 
868
868
 
869
+ def _float_total(values) -> float:
870
+ """`math.fsum` of `values`, or the plain `sum()` when `fsum` cannot.
871
+
872
+ `fsum` is correctly rounded, so a total is the same on Python 3.11, whose
873
+ float `sum()` is uncompensated, and on 3.12+, whose `sum()` is not; a
874
+ plain sum printed different last digits from one interpreter to the next.
875
+ But `fsum` raises where `sum()` saturates: `OverflowError` once finite
876
+ terms pass the float range, `ValueError` on `inf + -inf`. The plain sum's
877
+ `inf`/`nan` is what the section-39 `_finite_quantity` guards withhold, so
878
+ that path returns it unchanged rather than raising out of the kernel.
879
+ """
880
+ items = list(values)
881
+ try:
882
+ return math.fsum(items)
883
+ except (OverflowError, ValueError):
884
+ return sum(items)
885
+
886
+
869
887
  def _finite_quantity(value) -> bool:
870
888
  """True when a token quantity is a real, finite number.
871
889
 
@@ -1001,8 +1019,8 @@ def class_units(classes, era: CoefficientEra) -> float:
1001
1019
  a multiplication of its own.
1002
1020
  """
1003
1021
  weights = era.weight_map()
1004
- return sum(float(classes.get(name, 0.0)) * weights[name]
1005
- for name in TOKEN_CLASSES)
1022
+ return _float_total(float(classes.get(name, 0.0)) * weights[name]
1023
+ for name in TOKEN_CLASSES)
1006
1024
 
1007
1025
 
1008
1026
  class EntryWeighting(typing.NamedTuple):
@@ -1463,7 +1481,7 @@ def build_daily_series(segments, entries, *, now, newest_entry_at,
1463
1481
  entry_units = class_units(quantities, era)
1464
1482
  units += entry_units
1465
1483
  families[family] = families.get(family, 0.0) + entry_units
1466
- raw_total = sum(raw.values())
1484
+ raw_total = _float_total(raw.values())
1467
1485
 
1468
1486
  # Cause precedence follows STATUS_PRECEDENCE: an unknown split
1469
1487
  # outranks incomplete history, which outranks an unsupported mix.
@@ -1698,7 +1716,7 @@ def composition_centre(vectors) -> dict[str, float] | None:
1698
1716
  return None
1699
1717
  keys = sorted({k for row in rows for k in row})
1700
1718
  raw = {k: median([row.get(k, 0.0) for row in rows]) for k in keys}
1701
- total = sum(raw.values())
1719
+ total = _float_total(raw.values())
1702
1720
  if not math.isfinite(total) or total <= 0.0:
1703
1721
  return None
1704
1722
  return {k: v / total for k, v in raw.items()}
@@ -1825,8 +1843,8 @@ def aggregate_composition(entries, *, era: CoefficientEra) -> tuple | None:
1825
1843
  raw[name] += qty
1826
1844
  units = class_units(quantities, era)
1827
1845
  families[family] = families.get(family, 0.0) + units
1828
- raw_total = sum(raw.values())
1829
- unit_total = sum(families.values())
1846
+ raw_total = _float_total(raw.values())
1847
+ unit_total = _float_total(families.values())
1830
1848
  if not _finite_quantity(raw_total) or not _finite_quantity(unit_total):
1831
1849
  return None
1832
1850
  if raw_total <= 0.0 or unit_total <= 0.0:
@@ -2060,8 +2078,8 @@ def jackknife_fit(days) -> tuple[float, Interval] | None:
2060
2078
  if any(not _finite_quantity(d.units) or not _finite_quantity(d.meter_delta)
2061
2079
  for d in rows):
2062
2080
  return None
2063
- total_units = sum(d.units for d in rows)
2064
- total_meter = sum(d.meter_delta for d in rows)
2081
+ total_units = _float_total(d.units for d in rows)
2082
+ total_meter = _float_total(d.meter_delta for d in rows)
2065
2083
  if total_meter <= 0.0 or total_units <= 0.0:
2066
2084
  return None
2067
2085
  point = total_units / total_meter
@@ -2071,10 +2089,10 @@ def jackknife_fit(days) -> tuple[float, Interval] | None:
2071
2089
  if rest_meter <= 0.0:
2072
2090
  return None
2073
2091
  replicates.append((total_units - row.units) / rest_meter)
2074
- mean = sum(replicates) / n
2092
+ mean = _float_total(replicates) / n
2075
2093
  # A product, not `** 2`, which is the C library's `pow`.
2076
- se = math.sqrt((n - 1) / n * sum((v - mean) * (v - mean)
2077
- for v in replicates))
2094
+ se = math.sqrt((n - 1) / n * _float_total((v - mean) * (v - mean)
2095
+ for v in replicates))
2078
2096
  half = t_quantile_975(n - 1) * se
2079
2097
  return point, Interval(max(0.0, point - half), point + half)
2080
2098
 
@@ -131,6 +131,12 @@ class SnapshotSignature(NamedTuple):
131
131
  # legs above, this one is a digest and is never empty: a store with no
132
132
  # armed Claude alert carries a constant hash, not the empty string.
133
133
  claude_stats_digest: str = ""
134
+ # #836 spec 5.7 item 1: the Claude weekly rejection state
135
+ # (`_lib_dashboard_sources.claude_weekly_retired_digest`) — a credit
136
+ # retry's in-place retirement, a restoration, or the deletion of a
137
+ # referenced snapshot moves no leg above. Empty on a store with no retired
138
+ # snapshot and no unavailable five-hour join, so it is byte-neutral there.
139
+ claude_weekly_retired_digest: str = ""
134
140
 
135
141
 
136
142
  def _max_id(conn: sqlite3.Connection, table: str) -> int:
@@ -247,6 +253,7 @@ def compute_signature(
247
253
  codex_stats_digest: str = "",
248
254
  accounts_digest: str = "",
249
255
  claude_stats_digest: str = "",
256
+ claude_weekly_retired_digest: str = "",
250
257
  ) -> SnapshotSignature:
251
258
  """Composite data-version signature across cache.db + stats.db (spec §3).
252
259
 
@@ -273,6 +280,7 @@ def compute_signature(
273
280
  accounts_digest=str(accounts_digest),
274
281
  codex_ingest_backlog_sig=_codex_ingest_backlog_sig(cache_conn),
275
282
  claude_stats_digest=str(claude_stats_digest),
283
+ claude_weekly_retired_digest=str(claude_weekly_retired_digest),
276
284
  )
277
285
 
278
286
 
@@ -35,12 +35,17 @@ import types
35
35
  import _lib_write_io
36
36
 
37
37
  RING_CAPACITY = 64
38
- MEMORY_BUDGET_BYTES = 65536
38
+ #: Raised from 65,536 for the v1.117.1 cut: the #939 D5 admission-hold
39
+ #: state put the full rings plus the write state at 65,032 bytes on
40
+ #: Python 3.12 and 3.13 but 66,000 on 3.11, whose objects are larger.
41
+ #: The bound stays structural (ring capacities, PUBLISHED_AT_MAX_CHARS);
42
+ #: this figure only checks it on every supported interpreter.
43
+ MEMORY_BUDGET_BYTES = 73728
39
44
 
40
45
  #: `published_at` is the only variable-length field in a record, so it is the
41
46
  #: only one that can put the whole state over `MEMORY_BUDGET_BYTES`. Measured:
42
47
  #: the stored field set is 27,189 bytes over a full ring, and 64 distinct 4 KiB
43
- #: strings would be 283,039 — 4.3x over budget. All three production callers
48
+ #: strings would be 283,039 — 3.8x over budget. All three production callers
44
49
  #: pass a UTC ISO-8601 instant (32 characters), so this is not reachable today;
45
50
  #: the cap makes the budget hold by CONSTRUCTION rather than by convention.
46
51
  PUBLISHED_AT_MAX_CHARS = 64
@@ -154,7 +159,7 @@ class ConversationSyncRecord:
154
159
  MAINTENANCE_PHASES = ("delete", "reclaim", "checkpoint", "other")
155
160
 
156
161
  #: The maintenance ring is deliberately SHORTER than the two tick rings. The
157
- #: whole published state is capped at a frozen 65,536 bytes, and a third
162
+ #: whole published state is capped at a frozen 73,728 bytes, and a third
158
163
  #: full-length ring would spend that budget on a phase that runs about once a
159
164
  #: day rather than about once a second. Sixteen records is several weeks of
160
165
  #: maintenance history at the throttled cadence, and many continuation passes
@@ -181,7 +186,7 @@ MAINTENANCE_SKIP_REASONS = (
181
186
  SQLITE_SOURCE_ID_MAX_CHARS = 96
182
187
  PLAN_DIGEST_MAX_CHARS = 16
183
188
  #: One shared object per distinct SQLite source id, so sixteen records of
184
- #: one runtime hold one string (the 65,536-byte budget).
189
+ #: one runtime hold one string (the 73,728-byte budget).
185
190
  _SOURCE_IDS: "dict[str, str]" = {}
186
191
 
187
192
 
@@ -224,7 +229,7 @@ class MaintenancePhaseRecord:
224
229
  # #901 Q8 (a deletion's reservation inputs) and Q9 (a reclaim chunk's
225
230
  # plan and outcome, §5.5): `OPERATION_INT_FIELDS` packed into ONE
226
231
  # array, -1 for "not recorded", because sixteen records of twenty
227
- # separate int objects do not fit the frozen 65,536-byte budget. Read
232
+ # separate int objects do not fit the frozen 73,728-byte budget. Read
228
233
  # them by name (`record.page_count`); `as_wire` publishes them by name.
229
234
  numbers: "array.array | None" = None # typecode "i"
230
235
 
@@ -499,7 +499,7 @@ def local_instance() -> "dict | None":
499
499
 
500
500
 
501
501
  def state_for_budget() -> tuple:
502
- """Every object this module retains, for the 65,536-byte state test."""
502
+ """Every object this module retains, for the tick-stats state budget."""
503
503
  state = _STATE
504
504
  return (state.t, state.b, state.p, state.ledger, state.ex[0],
505
505
  state.ex[1:], state.open, state.hysteresis, state.local_instance,
package/bin/cctally CHANGED
@@ -1772,6 +1772,10 @@ BudgetRow = _cctally_forecast.BudgetRow
1772
1772
  _resolve_forecast_now = _cctally_forecast._resolve_forecast_now
1773
1773
  _fetch_current_week_snapshots = _cctally_forecast._fetch_current_week_snapshots
1774
1774
  _apply_midweek_reset_override = _cctally_forecast._apply_midweek_reset_override
1775
+ ClaudeCycleScope = _cctally_forecast.ClaudeCycleScope
1776
+ _claude_select_current_owner = _cctally_forecast._claude_select_current_owner
1777
+ _claude_current_cycle_scope = _cctally_forecast._claude_current_cycle_scope
1778
+ _claude_cycle_scope_samples = _cctally_forecast._claude_cycle_scope_samples
1775
1779
  _current_week_five_hour_percent = _cctally_forecast._current_week_five_hour_percent
1776
1780
  _current_week_five_hour_reading = _cctally_forecast._current_week_five_hour_reading
1777
1781
  _resolve_current_budget_window = _cctally_forecast._resolve_current_budget_window