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.
@@ -162,6 +162,7 @@ class CodexWeeklyPeriod:
162
162
  end_at: dt.datetime
163
163
  source_root_keys: tuple[str, ...]
164
164
  used_percent: float | None = None
165
+ account_keys: tuple[str, ...] = ()
165
166
 
166
167
 
167
168
  def _codex_history_row_is_model_scoped(row: object) -> bool:
@@ -506,8 +507,9 @@ def _codex_weekly_periods(
506
507
  ``max(...)`` hands the focused account the OTHER account's percentage —
507
508
  the never-combine violation D6 forbids — while ``end_at = min(resets_at,
508
509
  next_start)`` clips one account's week at the other's start. ``None`` keeps
509
- the merged "All accounts" read, which is byte-stable and is what the parent
510
- still uses.
510
+ the merged "All accounts" read used by the parent. Every merged boundary
511
+ retains the account-key set that contributed its percentage; the parent
512
+ publishes that set only under the existing multi-account decoration gate.
511
513
 
512
514
  The predicate is strict equality, deliberately NOT the one-directional
513
515
  ``(account, unattributed)`` widening ``_codex_five_hour_rows`` uses: that
@@ -528,8 +530,8 @@ def _codex_weekly_periods(
528
530
  account_params: tuple = () if account_key is None else (account_key,)
529
531
  try:
530
532
  rows = stats_conn.execute(
531
- "SELECT source_root_key, logical_limit_key, limit_name, resets_at_utc, "
532
- "nominal_start_at_utc, current_percent "
533
+ "SELECT source_root_key, account_key, logical_limit_key, limit_name, "
534
+ "resets_at_utc, nominal_start_at_utc, current_percent "
533
535
  "FROM quota_window_blocks "
534
536
  "WHERE source='codex' AND window_minutes=10080 "
535
537
  f"AND source_root_key IN ({placeholders}) AND orphaned_at IS NULL "
@@ -547,10 +549,10 @@ def _codex_weekly_periods(
547
549
  # live boundary together with its own durable row, so the flag is OR-ed on
548
550
  # merge rather than taken from either side.
549
551
  raw_boundaries: list[
550
- tuple[dt.datetime, dt.datetime, set[str], list[float], bool]
552
+ tuple[dt.datetime, dt.datetime, set[str], list[float], set[str], bool]
551
553
  ] = []
552
554
 
553
- for (root_key, logical_limit_key, limit_name, resets_at_raw,
555
+ for (root_key, row_account_key, logical_limit_key, limit_name, resets_at_raw,
554
556
  start_at_raw, current_percent) in rows:
555
557
  if is_model_scoped_codex_quota(logical_limit_key, limit_name):
556
558
  continue
@@ -568,7 +570,14 @@ def _codex_weekly_periods(
568
570
  used_values = []
569
571
  if isinstance(current_percent, (int, float)) and not isinstance(current_percent, bool):
570
572
  used_values.append(float(current_percent))
571
- raw_boundaries.append((start_at, resets_at, {str(root_key)}, used_values, False))
573
+ raw_boundaries.append((
574
+ start_at,
575
+ resets_at,
576
+ {str(root_key)},
577
+ used_values,
578
+ {str(row_account_key or _lib_accounts.UNATTRIBUTED)},
579
+ False,
580
+ ))
572
581
 
573
582
  # `active_cycle is None` is the case §7.4 calls out explicitly: no boundary
574
583
  # is live, so nothing is exempt and every period clips exactly as before.
@@ -578,13 +587,18 @@ def _codex_weekly_periods(
578
587
  active_cycle.resets_at.astimezone(UTC),
579
588
  set(active_cycle.source_root_keys),
580
589
  [active_cycle.used_percent] if active_cycle.used_percent is not None else [],
590
+ {
591
+ active_cycle.quota_identity.account_key
592
+ if active_cycle.quota_identity is not None
593
+ else _lib_accounts.UNATTRIBUTED
594
+ },
581
595
  True,
582
596
  ))
583
597
 
584
598
  ordered: list[
585
- tuple[dt.datetime, dt.datetime, set[str], list[float], bool]
599
+ tuple[dt.datetime, dt.datetime, set[str], list[float], set[str], bool]
586
600
  ] = []
587
- for start_at, resets_at, period_roots, used_values, is_live in sorted(
601
+ for start_at, resets_at, period_roots, used_values, period_accounts, is_live in sorted(
588
602
  raw_boundaries, key=lambda item: (item[0], item[1]),
589
603
  ):
590
604
  if (
@@ -592,19 +606,26 @@ def _codex_weekly_periods(
592
606
  and (start_at - ordered[-1][0]).total_seconds()
593
607
  < _FIVE_HOUR_JITTER_FLOOR_SECONDS
594
608
  ):
595
- first_start, latest_reset, existing_roots, existing_used, existing_live = ordered[-1]
609
+ (
610
+ first_start, latest_reset, existing_roots, existing_used,
611
+ existing_accounts, existing_live,
612
+ ) = ordered[-1]
596
613
  existing_roots.update(period_roots)
597
614
  existing_used.extend(used_values)
615
+ existing_accounts.update(period_accounts)
598
616
  ordered[-1] = (
599
617
  first_start, max(latest_reset, resets_at), existing_roots, existing_used,
600
- existing_live or is_live,
618
+ existing_accounts, existing_live or is_live,
601
619
  )
602
620
  else:
603
621
  ordered.append((
604
- start_at, resets_at, set(period_roots), list(used_values), is_live,
622
+ start_at, resets_at, set(period_roots), list(used_values),
623
+ set(period_accounts), is_live,
605
624
  ))
606
625
  periods: list[CodexWeeklyPeriod] = []
607
- for index, (start_at, resets_at, period_roots, used_values, is_live) in enumerate(ordered):
626
+ for index, (
627
+ start_at, resets_at, period_roots, used_values, period_accounts, is_live,
628
+ ) in enumerate(ordered):
608
629
  next_start = ordered[index + 1][0] if index + 1 < len(ordered) else None
609
630
  # The live cycle always ends at its own reset (#373 §7.4).
610
631
  if is_live:
@@ -617,6 +638,7 @@ def _codex_weekly_periods(
617
638
  end_at=end_at,
618
639
  source_root_keys=tuple(sorted(period_roots)),
619
640
  used_percent=max(used_values) if used_values else None,
641
+ account_keys=tuple(sorted(period_accounts)),
620
642
  ))
621
643
  return tuple(periods)
622
644
 
@@ -929,6 +951,9 @@ def _bucket_wire(bucket: Any) -> dict[str, object]:
929
951
  value = getattr(bucket, name, None)
930
952
  if value is not None:
931
953
  result[name] = value
954
+ account_keys = getattr(bucket, "account_keys", ())
955
+ if account_keys:
956
+ result["account_keys"] = tuple(account_keys)
932
957
  return result
933
958
 
934
959
 
@@ -2474,11 +2499,13 @@ def _build_codex_native_weekly_view(
2474
2499
  display_tz_name: str | None,
2475
2500
  speed: str,
2476
2501
  account_key: str | None = None,
2502
+ include_account_keys: bool = False,
2477
2503
  ) -> CodexWeeklyView:
2478
2504
  """Aggregate Codex cost into observed native quota-cycle segments.
2479
2505
 
2480
2506
  ``account_key`` scopes the durable boundary read to one account (#416
2481
- Slice 3A review B1); ``None`` is the merged parent read and is byte-stable.
2507
+ Slice 3A review B1); ``None`` is the merged parent read. The additive
2508
+ ownership axis is emitted only when ``include_account_keys`` is true.
2482
2509
  """
2483
2510
  periods = _codex_weekly_periods(
2484
2511
  stats_conn,
@@ -2532,6 +2559,10 @@ def _build_codex_native_weekly_view(
2532
2559
  and periods_by_bucket[row.bucket].used_percent > 0
2533
2560
  else None
2534
2561
  ),
2562
+ account_keys=(
2563
+ periods_by_bucket[row.bucket].account_keys
2564
+ if include_account_keys else ()
2565
+ ),
2535
2566
  )
2536
2567
  for row in rows
2537
2568
  )
@@ -2802,14 +2833,11 @@ def _codex_account_scopes_wire(
2802
2833
  context, account_observations, accounting_entries=rows,
2803
2834
  account_key=key,
2804
2835
  )
2805
- # #416 Slice 3A review B3. The parent sets `quota.cycle_index` and the
2806
- # client reads `codex.quota.cycle_index`, so a child without the key
2807
- # forces the client into a fallback — and the tempting one (reuse the
2808
- # parent's) would render account A's milestone HISTORY on account B's
2809
- # hero. The index is derivable per account from what the child already
2810
- # has (its own `CodexCycleBoundary` plus `stats_conn`), so it is built
2811
- # genuinely rather than declared parent-only. No cycle => `()`, an
2812
- # honest empty state, never another account's ledger.
2836
+ # Each decorated child owns the only honest cycle index for that
2837
+ # account. Reusing a merged parent index here would render account A's
2838
+ # milestone HISTORY on account B's hero. The index is derivable from
2839
+ # the child's own `CodexCycleBoundary` plus `stats_conn`; no cycle =>
2840
+ # `()`, an honest empty state, never another account's ledger.
2813
2841
  cycle_index: tuple = ()
2814
2842
  if cycle is not None and not hero_failure:
2815
2843
  try:
@@ -3166,6 +3194,15 @@ def build_codex_source_state(
3166
3194
  monthly = build_codex_monthly_view(
3167
3195
  entries, now_utc=context.now_utc, tz_name=context.display_tz_name, speed=context.speed,
3168
3196
  )
3197
+ # R8 gate, resolved once before the parent weekly projection so that only
3198
+ # a decorated merged row gains the additive account axis. Focused children
3199
+ # and <=1-real-account providers remain byte-identical.
3200
+ try:
3201
+ import _cctally_account
3202
+ _codex_decorated = _cctally_account.provider_is_decorated(
3203
+ context.stats_conn, "codex")
3204
+ except Exception:
3205
+ _codex_decorated = False
3169
3206
  weekly = _build_codex_native_weekly_view(
3170
3207
  context.stats_conn,
3171
3208
  visible_accounting_entries,
@@ -3174,6 +3211,7 @@ def build_codex_source_state(
3174
3211
  now_utc=context.now_utc,
3175
3212
  display_tz_name=context.display_tz_name,
3176
3213
  speed=context.speed,
3214
+ include_account_keys=_codex_decorated,
3177
3215
  )
3178
3216
  sessions = (
3179
3217
  build_rooted_codex_session_view(
@@ -3196,12 +3234,6 @@ def build_codex_source_state(
3196
3234
  # `accounts[]` cards, `hero.cycles[]`, and the `account_scopes` children —
3197
3235
  # hangs off this single boolean, so a <=1-real-account install is provably
3198
3236
  # byte-identical by construction rather than by golden observation.
3199
- try:
3200
- import _cctally_account
3201
- _codex_decorated = _cctally_account.provider_is_decorated(
3202
- context.stats_conn, "codex")
3203
- except Exception:
3204
- _codex_decorated = False
3205
3237
  quota_blocks = _quota_wire(
3206
3238
  context.stats_conn,
3207
3239
  accounting_entries=visible_accounting_entries,
@@ -3210,12 +3242,13 @@ def build_codex_source_state(
3210
3242
  display_tz_name=context.display_tz_name,
3211
3243
  decorated=_codex_decorated,
3212
3244
  )
3213
- # Hero-modal historical-milestone navigation index (spec §1c, §3). Built
3214
- # here on the non-idle codex source rebuild (idle ticks reuse the stored
3215
- # bundle) over the durable projection — a pure serializer never touches it.
3216
- # Guarded: an index failure must never fail the codex source build.
3245
+ # Hero-modal historical-milestone navigation index (spec §1c, §3). A
3246
+ # decorated parent has no single cycle history: its representative
3247
+ # `cycle` belongs to one account, while every child builds its own index
3248
+ # above. Preserve the parent index only for the byte-stable undecorated
3249
+ # shape. Guarded: an index failure must never fail the codex source build.
3217
3250
  cycle_index: tuple = ()
3218
- if cycle is not None and not hero_failure:
3251
+ if not _codex_decorated and cycle is not None and not hero_failure:
3219
3252
  try:
3220
3253
  cycle_index = tuple(
3221
3254
  sys.modules["cctally"].build_codex_cycle_index(
@@ -5997,7 +5997,7 @@ def _032_codex_canonical_reset_anchor(conn: sqlite3.Connection) -> None:
5997
5997
  updates: list[tuple[str, int]] = []
5998
5998
  for row in conn.execute(
5999
5999
  "SELECT id, source_root_key, observed_slot, logical_limit_key, "
6000
- " window_minutes, resets_at_utc "
6000
+ " window_minutes, resets_at_utc, source_path, line_offset "
6001
6001
  " FROM quota_window_snapshots "
6002
6002
  " WHERE source = 'codex' AND canonical_resets_at_utc IS NULL "
6003
6003
  " AND source_root_key IS NOT NULL AND observed_slot IS NOT NULL "
@@ -6007,6 +6007,7 @@ def _032_codex_canonical_reset_anchor(conn: sqlite3.Connection) -> None:
6007
6007
  source_root_key=row[1], observed_slot=row[2],
6008
6008
  logical_limit_key=row[3], window_minutes=row[4],
6009
6009
  resets_at_utc=row[5],
6010
+ source_path=row[6], line_offset=row[7],
6010
6011
  )
6011
6012
  if anchor is not None:
6012
6013
  updates.append((anchor, int(row[0])))
@@ -6019,6 +6020,121 @@ def _032_codex_canonical_reset_anchor(conn: sqlite3.Connection) -> None:
6019
6020
  conn.commit()
6020
6021
 
6021
6022
 
6023
+ @cache_migration("033_codex_reset_anchor_component_closure")
6024
+ def _033_codex_reset_anchor_component_closure(
6025
+ conn: sqlite3.Connection,
6026
+ ) -> None:
6027
+ """#425: converge migration-032 chain-of-neighbours splits.
6028
+
6029
+ Migration 032 compared each raw reset only with established anchors. Real
6030
+ production history contains tolerance-connected chains whose endpoints are
6031
+ more than 600 seconds apart, leaving one physical window under multiple
6032
+ anchors. Rebuild the component closure over the complete population while
6033
+ retaining the deterministic first observation as the winning anchor.
6034
+
6035
+ Raw ``resets_at_utc`` evidence is never rewritten. Re-running computes the
6036
+ same full-population mapping and therefore changes no rows.
6037
+ """
6038
+ import _cctally_cache as cache_mod
6039
+ import _lib_quota
6040
+
6041
+ cols = {
6042
+ str(row[1]) for row in conn.execute(
6043
+ "PRAGMA table_info(quota_window_snapshots)")
6044
+ }
6045
+ if "canonical_resets_at_utc" not in cols:
6046
+ return
6047
+
6048
+ groups: dict[tuple, _lib_quota.ResetAnchorComponents] = {}
6049
+ evidence: list[
6050
+ tuple[int, dt.datetime, _lib_quota.ResetAnchorComponents, str | None]
6051
+ ] = []
6052
+ for row in conn.execute(
6053
+ "SELECT id, source_root_key, observed_slot, logical_limit_key, "
6054
+ " window_minutes, resets_at_utc, canonical_resets_at_utc, "
6055
+ " source_path, line_offset "
6056
+ " FROM quota_window_snapshots "
6057
+ " WHERE source = 'codex' "
6058
+ " AND source_root_key IS NOT NULL AND observed_slot IS NOT NULL "
6059
+ " ORDER BY source_path, line_offset, id"
6060
+ ).fetchall():
6061
+ raw = cache_mod._parse_anchor_iso(row[5])
6062
+ if raw is None:
6063
+ continue
6064
+ group = cache_mod.CodexResetAnchorResolver.group_key(
6065
+ row[1], row[2], row[3], row[4])
6066
+ components = groups.setdefault(
6067
+ group, _lib_quota.ResetAnchorComponents())
6068
+ components.add(
6069
+ raw,
6070
+ order_key=(str(row[7]), int(row[8]), int(row[0])),
6071
+ )
6072
+ evidence.append((int(row[0]), raw, components, row[6]))
6073
+
6074
+ updates: list[tuple[str, int]] = []
6075
+ for row_id, raw, components, stored in evidence:
6076
+ canonical = cache_mod._codex_anchor_iso(components.canonical(raw))
6077
+ if canonical != stored:
6078
+ updates.append((canonical, row_id))
6079
+ if updates:
6080
+ conn.executemany(
6081
+ "UPDATE quota_window_snapshots SET canonical_resets_at_utc = ? "
6082
+ "WHERE id = ?",
6083
+ updates,
6084
+ )
6085
+ conn.commit()
6086
+
6087
+
6088
+ @cache_migration("034_codex_window_spend_adoption")
6089
+ def _034_codex_window_spend_adoption(conn: sqlite3.Connection) -> None:
6090
+ """One-time window-scoped spend adoption over existing Codex history.
6091
+
6092
+ Spec:
6093
+ ``docs/superpowers/specs/2026-07-30-codex-window-scoped-spend-adoption.md``.
6094
+
6095
+ Ingest stamps every window forward from here, but rollout bytes that were
6096
+ already walked keep whatever per-file attribution decided for them — and for
6097
+ history predating the durable attribution map that is nothing at all (#416
6098
+ spec D1). A cycle's milestone ladder then renders ``$0.00`` for a crossing
6099
+ whose spend is real, because the crossing came from the folded observation
6100
+ axis and the dollars from the per-file axis. This handler runs the SAME pass
6101
+ ingest now runs, unbounded, so existing history converges in one pass.
6102
+
6103
+ The whole decision lives in ``_lib_codex_account_adoption`` and the read/write
6104
+ in ``_cctally_cache.apply_codex_window_spend_adoption``; re-implementing the
6105
+ rule here would let the migration and ingest drift.
6106
+
6107
+ Idempotent by construction: only rows whose ``account_key`` is ``NULL`` or
6108
+ empty are candidates, so a re-run over its own output writes nothing. NO
6109
+ self-stamp — the dispatcher central-stamps on a clean return (#140).
6110
+
6111
+ Takes the Codex provider flock like handlers 024-027: this writes a
6112
+ Codex-derived table, so a mid-walk ``sync_codex_cache`` must not interleave.
6113
+ On contention it DEFERS (``MigrationGateNotMet``) before touching any data —
6114
+ the safe side, and free here because that same sync runs the identical pass
6115
+ at its own end anyway.
6116
+
6117
+ ``BEGIN IMMEDIATE`` before the read, like handler 024: the pass reads the
6118
+ folded window evidence and then writes back a plan derived from it, so it
6119
+ must hold the write lock for the whole read-then-write rather than upgrade
6120
+ part way through it.
6121
+ """
6122
+ import _cctally_cache as cache_mod
6123
+
6124
+ held = _acquire_cache_db_codex_provider_flock(
6125
+ conn, migration="034 window spend adoption")
6126
+ try:
6127
+ conn.execute("BEGIN IMMEDIATE")
6128
+ try:
6129
+ cache_mod.apply_codex_window_spend_adoption(conn)
6130
+ conn.commit()
6131
+ except Exception:
6132
+ conn.rollback()
6133
+ raise
6134
+ finally:
6135
+ _release_cache_db_writer_flocks(held)
6136
+
6137
+
6022
6138
  # === Region 7d: Stats migration 008_recompute_weekly_cost_snapshots_dedup_fix ===
6023
6139
 
6024
6140
  @stats_migration("008_recompute_weekly_cost_snapshots_dedup_fix")
@@ -995,6 +995,7 @@ def _doctor_gather_state_impl(
995
995
  codex_last_entry_at = None
996
996
  codex_project_metadata_health = None
997
997
  codex_project_metadata_error = None
998
+ codex_null_reset_anchors = 0
998
999
  try:
999
1000
  if _cache_probe_allowed and _cctally_core.CACHE_DB_PATH.exists():
1000
1001
  conn = sqlite3.connect(str(_cctally_core.CACHE_DB_PATH))
@@ -1008,6 +1009,18 @@ def _doctor_gather_state_impl(
1008
1009
  codex_last_entry_at = parse_iso_datetime(
1009
1010
  row[1], "codex_session_entries.timestamp_utc",
1010
1011
  ).astimezone(dt.timezone.utc)
1012
+ try:
1013
+ row = conn.execute(
1014
+ "SELECT COUNT(*) FROM quota_window_snapshots "
1015
+ "WHERE source = 'codex' "
1016
+ "AND canonical_resets_at_utc IS NULL"
1017
+ ).fetchone()
1018
+ if row and row[0] is not None:
1019
+ codex_null_reset_anchors = int(row[0])
1020
+ except sqlite3.OperationalError:
1021
+ # Pre-anchor cache shapes have no column to inspect. Their
1022
+ # pending migration is reported by the DB checks instead.
1023
+ pass
1011
1024
  # Keep the health probe on the existing read-only cache
1012
1025
  # connection. A failed probe is health evidence, not an
1013
1026
  # empty corpus: the kernel renders it as a distinct FAIL.
@@ -1645,6 +1658,8 @@ def _doctor_gather_state_impl(
1645
1658
  cctally_version = (
1646
1659
  cctally_version_tuple[0] if cctally_version_tuple else "unknown"
1647
1660
  )
1661
+ accounts_state = _gather_accounts_state(now_utc)
1662
+ accounts_state["codex_null_reset_anchors"] = codex_null_reset_anchors
1648
1663
 
1649
1664
  return _lib_doctor.DoctorState(
1650
1665
  symlink_state=symlink_state,
@@ -1742,7 +1757,7 @@ def _doctor_gather_state_impl(
1742
1757
  journal_heal_incidents=journal_heal_incidents,
1743
1758
  journal_writer_guard=journal_writer_guard,
1744
1759
  # Multi-account attribution legs (#341).
1745
- accounts_state=_gather_accounts_state(now_utc),
1760
+ accounts_state=accounts_state,
1746
1761
  cache_repair_marker=cache_repair_marker,
1747
1762
  backup_sync_state=backup_sync_state,
1748
1763
  )
@@ -1489,6 +1489,8 @@ def _resolve_obs_anchor(resolver, rec: dict) -> "str | None":
1489
1489
  source_root_key=root, observed_slot=slot, logical_limit_key=key,
1490
1490
  window_minutes=p.get("window_minutes"),
1491
1491
  resets_at_utc=p.get("resets_at_utc"),
1492
+ source_path=p.get("source_path"),
1493
+ line_offset=p.get("line_offset"),
1492
1494
  )
1493
1495
  except Exception: # pragma: no cover — never fail an ingest over a label
1494
1496
  return None
@@ -1523,6 +1525,9 @@ def _apply_quota_records(cache, records) -> None:
1523
1525
  for rec in records:
1524
1526
  covered, decided = oracle.resolve(rec)
1525
1527
  anchor = _resolve_obs_anchor(anchors, rec)
1528
+ if anchors is not None:
1529
+ anchors.apply_pending_merges()
1530
+ anchors.mark_file_committed()
1526
1531
  row_values = _quota_snapshot_values(rec, anchor)
1527
1532
  if not has_anchor:
1528
1533
  row_values = row_values[:-1]
@@ -240,6 +240,7 @@ def load_codex_quota_observations(
240
240
  active_at: dt.datetime | None = None,
241
241
  max_rows: int | None = None,
242
242
  physical_signatures: dict[str, str] | None = None,
243
+ canonical_resets_between: "tuple[dt.datetime, dt.datetime] | None" = None,
243
244
  ) -> tuple[QuotaObservation, ...]:
244
245
  """Load only valid root-qualified S1 physical quota rows.
245
246
 
@@ -258,6 +259,14 @@ def load_codex_quota_observations(
258
259
  supplied, exact S2 signatures are accumulated from the same cursor before
259
260
  presentation bounds are applied, so coherence validation does not require
260
261
  a second unbounded observation load.
262
+
263
+ ``canonical_resets_between`` is an INCLUSIVE ``(low, high)`` bound on the
264
+ window's CANONICAL reset, applied in SQL. Unlike ``captured_at_or_after``
265
+ it is a window-IDENTITY bound, so it never fractures a window group: every
266
+ observation of one physical window shares one canonical anchor, and the
267
+ continuity fold below therefore still sees each retained window whole. That
268
+ is what lets the ingest-side spend-adoption pass bound itself to the windows
269
+ one sync touched instead of materializing all history every hook tick.
261
270
  """
262
271
  for name, value in (
263
272
  ("captured_at_or_after", captured_at_or_after), ("active_at", active_at),
@@ -269,6 +278,19 @@ def load_codex_quota_observations(
269
278
  captured_at_or_after = value.astimezone(UTC)
270
279
  else:
271
280
  active_at = value.astimezone(UTC)
281
+ if canonical_resets_between is not None:
282
+ if len(canonical_resets_between) != 2:
283
+ raise ValueError(
284
+ "canonical_resets_between must be a (low, high) pair")
285
+ bounds = []
286
+ for value in canonical_resets_between:
287
+ if value.tzinfo is None or value.utcoffset() is None:
288
+ raise ValueError(
289
+ "canonical_resets_between must be timezone-aware")
290
+ bounds.append(value.astimezone(UTC))
291
+ if bounds[0] > bounds[1]:
292
+ raise ValueError("canonical_resets_between must be ordered")
293
+ canonical_resets_between = (bounds[0], bounds[1])
272
294
  if max_rows is not None:
273
295
  if not isinstance(max_rows, int) or isinstance(max_rows, bool) or max_rows <= 0:
274
296
  raise ValueError("max_rows must be a positive integer or None")
@@ -352,6 +374,23 @@ def load_codex_quota_observations(
352
374
  return ()
353
375
  sql += " AND source_root_key IN (" + ",".join("?" for _ in requested) + ")"
354
376
  params.extend(sorted(requested))
377
+ if canonical_resets_between is not None:
378
+ # COALESCE, not the bare column: a pre-032 row (or one the backfill
379
+ # never reached) carries NULL there and the reader falls back to the
380
+ # raw reset, so the bound has to fall back with it or the row would
381
+ # silently drop out of its own window.
382
+ reset_expr = (
383
+ "COALESCE(canonical_resets_at_utc, resets_at_utc)"
384
+ if has_anchor else "resets_at_utc"
385
+ )
386
+ sql += (
387
+ f" AND unixepoch({reset_expr}) >= unixepoch(?)"
388
+ f" AND unixepoch({reset_expr}) <= unixepoch(?)"
389
+ )
390
+ params.extend((
391
+ _utc_iso(canonical_resets_between[0]),
392
+ _utc_iso(canonical_resets_between[1]),
393
+ ))
355
394
  # When exact signatures are requested this first cursor must cover the
356
395
  # complete root history. Otherwise apply dashboard presentation bounds
357
396
  # in SQL so only the capped evidence crosses the SQLite/Python boundary.
@@ -494,6 +533,7 @@ def load_codex_quota_observations(
494
533
  captured_at_or_after=captured_at_or_after,
495
534
  active_at=active_at,
496
535
  max_rows=max_rows,
536
+ canonical_resets_between=canonical_resets_between,
497
537
  )
498
538
  if max_rows is not None and len(result) > max_rows:
499
539
  result = sorted(
@@ -966,7 +1006,9 @@ def _evaluate_quota_alerts(
966
1006
  return queued
967
1007
 
968
1008
 
969
- def _reanchor_terminal_events_sql(key_slots: int, minute_slots: int) -> str:
1009
+ def _reanchor_terminal_events_sql(
1010
+ key_slots: int, minute_slots: int, reset_slots: int,
1011
+ ) -> str:
970
1012
  # `UPDATE OR IGNORE`, not a plain UPDATE: if this identity already carries an
971
1013
  # anchored row at the same threshold, moving the jittered twin onto it would
972
1014
  # violate the UNIQUE key. OR IGNORE SKIPS that move (it does not delete the
@@ -978,6 +1020,11 @@ def _reanchor_terminal_events_sql(key_slots: int, minute_slots: int) -> str:
978
1020
  # transaction.
979
1021
  keys = ",".join(f":key{i}" for i in range(key_slots))
980
1022
  minutes = ",".join(f":min{i}" for i in range(minute_slots))
1023
+ member_epochs = ",".join(f":reset{i}" for i in range(reset_slots))
1024
+ member_clause = (
1025
+ f" OR unixepoch(resets_at_utc) IN ({member_epochs})"
1026
+ if member_epochs else ""
1027
+ )
981
1028
  return (
982
1029
  "UPDATE OR IGNORE quota_threshold_events "
983
1030
  " SET resets_at_utc = :anchor, "
@@ -989,7 +1036,9 @@ def _reanchor_terminal_events_sql(key_slots: int, minute_slots: int) -> str:
989
1036
  f" AND observed_slot = :slot AND window_minutes IN ({minutes}) "
990
1037
  " AND (resets_at_utc <> :anchor OR logical_limit_key <> :limit_key "
991
1038
  " OR window_minutes <> :minutes) "
992
- " AND abs(unixepoch(resets_at_utc) - unixepoch(:anchor)) <= :tolerance"
1039
+ " AND ("
1040
+ " abs(unixepoch(resets_at_utc) - unixepoch(:anchor)) <= :tolerance"
1041
+ f"{member_clause})"
993
1042
  )
994
1043
 
995
1044
 
@@ -1020,15 +1069,24 @@ def _reanchor_terminal_events(conn: sqlite3.Connection, block) -> None:
1020
1069
  re-materialization (they share this body), and is idempotent: a row already
1021
1070
  on the canonical identity is excluded by the three-way `<>` guard.
1022
1071
 
1023
- Bounded on both axes by the tolerances that produced the canonical identity —
1024
- 600s on the reset, ±1 minute on the length — so it can only ever collapse
1025
- rows the canonicalization itself merged. Two genuinely different cycles are
1026
- five hours or seven days apart, and a `10200` window is a different window,
1027
- not jitter.
1072
+ The reset match accepts either the original 600-second anchor neighbourhood
1073
+ or an exact raw reset retained by this block. The latter is required by
1074
+ #425's transitive component closure: an endpoint can be farther than 600s
1075
+ from the first-sight anchor while still joining it through retained bridge
1076
+ observations. Exact membership keeps the widened reach evidence-bound; a
1077
+ genuinely different cycle is never inferred from distance alone. The length
1078
+ axis remains bounded by its ±1 minute snap.
1028
1079
  """
1029
1080
  identity = block.identity
1030
1081
  keys = codex_snap_equivalent_limit_keys(identity.logical_limit_key)
1031
1082
  minutes = codex_snap_equivalent_window_minutes(identity.window_minutes)
1083
+ membership_evidence = (
1084
+ block.physical_observations or block.observations
1085
+ )
1086
+ reset_epochs = sorted({
1087
+ int(observation.resets_at.timestamp())
1088
+ for observation in membership_evidence
1089
+ })
1032
1090
  params: dict[str, object] = {
1033
1091
  "anchor": _utc_iso(block.resets_at),
1034
1092
  "source": identity.source,
@@ -1041,8 +1099,13 @@ def _reanchor_terminal_events(conn: sqlite3.Connection, block) -> None:
1041
1099
  }
1042
1100
  params.update({f"key{i}": value for i, value in enumerate(keys)})
1043
1101
  params.update({f"min{i}": value for i, value in enumerate(minutes)})
1102
+ params.update({
1103
+ f"reset{i}": value for i, value in enumerate(reset_epochs)})
1044
1104
  conn.execute(
1045
- _reanchor_terminal_events_sql(len(keys), len(minutes)), params)
1105
+ _reanchor_terminal_events_sql(
1106
+ len(keys), len(minutes), len(reset_epochs)),
1107
+ params,
1108
+ )
1046
1109
 
1047
1110
 
1048
1111
  def _apply_quota_projection_rows(
@@ -1441,13 +1504,30 @@ def _codex_cache_account_predicate(
1441
1504
  dollars are these" — widening it IS attribution, which D1 forbids, and it
1442
1505
  puts one row in two scopes.
1443
1506
 
1444
- Three stamping mechanisms exist and must never be conflated: the
1507
+ FOUR stamping mechanisms exist and must never be conflated: the
1445
1508
  quota-observation fold (``adopt_unidentified_observations``, per physical-
1446
1509
  window group, landing post-fold in ``quota_window_blocks`` /
1447
1510
  ``quota_percent_milestones`` and NEVER written back to
1448
1511
  ``quota_window_snapshots``); per-file-range attribution
1449
1512
  (``codex_file_accounts`` -> ``codex_session_entries.account_key``,
1450
- ``stably_absent`` -> NULL); and the stats ``accounts`` registry.
1513
+ ``stably_absent`` -> NULL); WINDOW-SCOPED SPEND ADOPTION (the 2026-07-30
1514
+ spec — ``_lib_codex_account_adoption`` +
1515
+ ``_cctally_cache.apply_codex_window_spend_adoption``, which stamps that same
1516
+ ``codex_session_entries.account_key`` column at ingest from the window's
1517
+ single identified account); and the stats ``accounts`` registry.
1518
+
1519
+ So ``codex_session_entries.account_key`` now carries window-derived
1520
+ attribution IN ADDITION to per-file decisions, and that is precisely what
1521
+ keeps the cost read strict rather than forcing it to widen. The rule above
1522
+ genuinely pointed both ways for that read: the scope key comes from the
1523
+ observation fold while the rows came from per-file attribution — DIFFERENT
1524
+ mechanisms, which reads as "widen" — yet widening a cost read is attribution
1525
+ D1 forbids and puts one row in two scopes. The resolution is to make the ROW
1526
+ carry the window's answer durably, so scope key and row now agree by
1527
+ construction and the strict flavour is correct rather than merely safe. A
1528
+ row the adoption pass declined to stamp (zero or ambiguous identified
1529
+ accounts, an overlap whose windows disagree) is genuinely nobody's and
1530
+ stays in the ``unattributed`` scope, which is the honest answer.
1451
1531
  """
1452
1532
  if account_key is None:
1453
1533
  return "", ()
@@ -1575,7 +1655,19 @@ def codex_quota_breakdown(
1575
1655
  renders an honest ``$0.00``; the dollars stay visible in the
1576
1656
  ``unattributed`` scope, which owns them.
1577
1657
 
1578
- The full rule, and the three stamping mechanisms it turns on, are in
1658
+ That honest ``$0.00`` used to fire for spend that was NOT nobody's. The
1659
+ crossing carries the window's account (the observation fold put it there)
1660
+ while the rows behind it carried none, because per-file attribution had no
1661
+ decision covering those bytes. ``codex_session_entries.account_key`` now also
1662
+ carries WINDOW-DERIVED attribution — stamped durably at ingest by
1663
+ ``apply_codex_window_spend_adoption`` under the same window key and the same
1664
+ single-identified-account guard the fold uses — so the ladder and the
1665
+ dollars agree by construction. The read stays strict on top of it precisely
1666
+ BECAUSE the inference is now durable: doing it here instead would re-file one
1667
+ row under two scopes on every read, while doing it once at ingest moves the
1668
+ row out of ``unattributed`` and into exactly one owner.
1669
+
1670
+ The full rule, and the four stamping mechanisms it turns on, are in
1579
1671
  ``_codex_cache_account_predicate``.
1580
1672
  """
1581
1673
  reset = _parse_utc(resets_at, "resets_at") if isinstance(resets_at, str) else resets_at