cctally 1.98.0 → 1.99.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.
@@ -1698,7 +1698,7 @@ def _iter_segment_lines(seg_path, lo: int, hi: int, *, on_bytes=None):
1698
1698
  buf_at += start
1699
1699
 
1700
1700
 
1701
- def iter_range(cursor, hw):
1701
+ def iter_range(cursor, hw, segments=None):
1702
1702
  """Stream `cursor -> HW` across segments in canonical order (spec §5.2.2).
1703
1703
 
1704
1704
  Prior segments (before HW's) are immutable and read to their full size;
@@ -1710,8 +1710,14 @@ def iter_range(cursor, hw):
1710
1710
  the size of the whole journal on the hot path. `_read_range` remains the
1711
1711
  materialized form for the ingest cycle, which genuinely needs the batch as
1712
1712
  an indexable sequence (prefix-stop indices address into it).
1713
+
1714
+ `segments` lets a caller supply the enumeration it already took. The fused
1715
+ Codex rehydration needs it: it derives per-family range floors as indices
1716
+ into a segment list, and those keys only address the same positions the
1717
+ traversal yields while both come from ONE enumeration.
1713
1718
  """
1714
- yield from _iter_range_with_segments(cursor, hw, list_segments())
1719
+ yield from _iter_range_with_segments(
1720
+ cursor, hw, list_segments() if segments is None else segments)
1715
1721
 
1716
1722
 
1717
1723
  def _iter_range_with_segments(cursor, hw, segments, *, on_segment=None,
@@ -1746,13 +1752,21 @@ def _iter_range_with_segments(cursor, hw, segments, *, on_segment=None,
1746
1752
  if hw_seg not in segments:
1747
1753
  return
1748
1754
  hw_idx = segments.index(hw_seg)
1749
- if cursor is None:
1750
- start_idx, start_off = 0, 0
1751
- else:
1752
- cur_seg, cur_off = cursor
1753
- if cur_seg in segments:
1754
- start_idx, start_off = segments.index(cur_seg), cur_off
1755
- else:
1755
+ start_idx, start_off = 0, 0
1756
+ if cursor is not None:
1757
+ # The unpack sits INSIDE the guard, which is what
1758
+ # `_journal_cursor_order_key`'s docstring already claims the two
1759
+ # functions do alike: a cursor this enumeration cannot place restarts
1760
+ # the pass from the beginning rather than raising. Unreachable today
1761
+ # because both loaders validate before calling, and a from-zero pass is
1762
+ # always sound because every apply is idempotent on its natural key —
1763
+ # but a raise here would turn an unreadable cursor into a failed pass
1764
+ # instead of a complete one.
1765
+ try:
1766
+ cur_seg, cur_off = cursor
1767
+ if cur_seg in segments:
1768
+ start_idx, start_off = segments.index(cur_seg), int(cur_off)
1769
+ except (TypeError, ValueError):
1756
1770
  start_idx, start_off = 0, 0
1757
1771
  for idx in range(start_idx, hw_idx + 1):
1758
1772
  seg = segments[idx]
@@ -2005,6 +2019,269 @@ def _file_account_values(rec: dict) -> tuple:
2005
2019
  )
2006
2020
 
2007
2021
 
2022
+ # --------------------------------------------------------------------------
2023
+ # #500: the third family this leg carries — the operator's durable attribution
2024
+ # of one recorded Codex quota window group to one account (spec §5, §6.1). It
2025
+ # shares the leg for the same reason the attribution map does: `run_stats_ingest`
2026
+ # invokes exactly ONE applier and truncates the batch afterwards, so a second
2027
+ # independent prefix-stopping applier could commit past this one's stop.
2028
+ # --------------------------------------------------------------------------
2029
+
2030
+ _WINDOW_ATTRIBUTION_KIND = _lib_journal.WINDOW_ATTRIBUTION_KIND
2031
+ _WINDOW_ATTRIBUTION_RETRACT_KIND = _lib_journal.WINDOW_ATTRIBUTION_RETRACT_KIND
2032
+ # Byte prefilter for the streamed replay, exactly as the file-account family
2033
+ # uses: the canonical encoder is `json.dumps(..., ensure_ascii=False)`, which
2034
+ # never escapes an ASCII token, so a genuine op carries the kind verbatim. The
2035
+ # assertion kind is a PREFIX of the retraction kind, so one marker matches both.
2036
+ _WINDOW_ATTRIBUTION_KIND_MARKER = f'"{_WINDOW_ATTRIBUTION_KIND}'.encode("ascii")
2037
+
2038
+ # FIRST-WINS on the op id, which is a content digest — so a duplicate line is
2039
+ # byte-identical and the insert is a genuine no-op rather than a silent
2040
+ # overwrite. `OR IGNORE` for the same reason both attribution-map statements
2041
+ # carry it: an `IntegrityError` raised here prefix-stops `_cache_applier`, and
2042
+ # the scalar cursor could then never advance past that record.
2043
+ #
2044
+ # `OR IGNORE` suppresses EVERY constraint violation, not only the op-id
2045
+ # conflict, so it cannot be the gate on a malformed record: the cursor advances
2046
+ # past that record in the same transaction, which makes the drop permanent.
2047
+ # `_window_attribution_defect` runs BEFORE the statement and routes such a
2048
+ # record to a counted, reported skip instead — the silence removed, the
2049
+ # tolerance kept (review finding F2).
2050
+ _WINDOW_ATTRIBUTION_INSERT = (
2051
+ "INSERT OR IGNORE INTO codex_window_attributions "
2052
+ "(op_id, account_key, source_root_key, logical_limit_key, observed_slot, "
2053
+ " window_minutes, raw_resets_at_utc, canonical_resets_at_utc, "
2054
+ " asserted_at_utc) "
2055
+ "VALUES (?,?,?,?,?,?,?,?,?) "
2056
+ "ON CONFLICT(op_id) DO NOTHING"
2057
+ )
2058
+
2059
+ # A retraction stamps only assertions that are not already retracted, so replay
2060
+ # is order-deterministic and idempotent: the FIRST tombstone in journal order
2061
+ # owns the row, and re-running the same prefix changes nothing.
2062
+ _WINDOW_ATTRIBUTION_RETRACT_UPDATE = (
2063
+ "UPDATE codex_window_attributions SET retracted_by_op_id = ? "
2064
+ "WHERE op_id = ? AND retracted_by_op_id IS NULL"
2065
+ )
2066
+
2067
+
2068
+ class CodexWindowAttributionReplayFailed(RuntimeError):
2069
+ """The #500 attribution replay could not materialize its records.
2070
+
2071
+ Typed rather than bare so the quota cache leg can distinguish it from an
2072
+ ordinary SQLite write failure and report `attributionReplayFailed`, which
2073
+ lands in `stats_quota_projection_state.incomplete` inside the publication
2074
+ transaction. The surrounding rebuild code deliberately flattens failures
2075
+ into logged no-ops (spec §6.2), so a replay failure that was NOT typed and
2076
+ routed here would be published as a complete projection that silently omits
2077
+ the operator's attribution.
2078
+ """
2079
+
2080
+
2081
+ def _is_codex_window_attribution_op(rec: dict) -> bool:
2082
+ return (
2083
+ rec.get("t") == "op"
2084
+ and (rec.get("payload") or {}).get("kind") == _WINDOW_ATTRIBUTION_KIND
2085
+ )
2086
+
2087
+
2088
+ def _is_codex_window_attribution_retract_op(rec: dict) -> bool:
2089
+ return (
2090
+ rec.get("t") == "op"
2091
+ and (rec.get("payload") or {}).get("kind")
2092
+ == _WINDOW_ATTRIBUTION_RETRACT_KIND
2093
+ )
2094
+
2095
+
2096
+ def _is_codex_window_attribution_record(rec: dict) -> bool:
2097
+ return (
2098
+ _is_codex_window_attribution_op(rec)
2099
+ or _is_codex_window_attribution_retract_op(rec)
2100
+ )
2101
+
2102
+
2103
+ #: Payload fields an assertion row cannot be built without. Each is NOT NULL in
2104
+ #: `codex_window_attributions`, and the builder already refuses to mint an op
2105
+ #: that omits one — so a record missing any of them is damaged, not merely old.
2106
+ _WINDOW_ATTRIBUTION_REQUIRED_TEXT = (
2107
+ "account_key", "source_root_key", "logical_limit_key", "observed_slot",
2108
+ )
2109
+
2110
+
2111
+ def _window_attribution_defect(rec: dict) -> "str | None":
2112
+ """Why this record cannot be materialized, or `None` when it is sound.
2113
+
2114
+ Runs BEFORE the INSERT, because `OR IGNORE` cannot distinguish a duplicate
2115
+ op id (a genuine no-op) from a NOT NULL violation (a permanent silent
2116
+ drop). The two shapes that motivated it, review finding F2:
2117
+
2118
+ * a payload missing a required field inserts nothing, raises nothing and
2119
+ leaves `asserted` at 0, so the batch looks like a clean no-op;
2120
+ * `raw_resets_at_utc: null` is WORSE — `json.dumps(None)` is the literal
2121
+ text `null`, which satisfies NOT NULL, so the row lands and
2122
+ `load_active_window_attributions` then skips it as undecodable. The
2123
+ assertion is durable, dormant and invisible.
2124
+
2125
+ Returns a short reason rather than a bool so the caller can name the defect
2126
+ in its diagnostic instead of reporting an anonymous count.
2127
+ """
2128
+ if not isinstance(rec.get("id"), str) or not rec.get("id"):
2129
+ return "record carries no op id"
2130
+ if not isinstance(rec.get("at"), str) or not rec.get("at"):
2131
+ return "record carries no `at` instant"
2132
+ payload = rec.get("payload")
2133
+ if not isinstance(payload, dict):
2134
+ return "record carries no payload"
2135
+ for field in _WINDOW_ATTRIBUTION_REQUIRED_TEXT:
2136
+ value = payload.get(field)
2137
+ if not isinstance(value, str) or not value:
2138
+ return f"payload field {field!r} is missing or not a non-empty string"
2139
+ minutes = payload.get("window_minutes")
2140
+ if not isinstance(minutes, int) or isinstance(minutes, bool):
2141
+ return "payload field 'window_minutes' is missing or not an integer"
2142
+ witnesses = payload.get("raw_resets_at_utc")
2143
+ if not isinstance(witnesses, list) or not witnesses:
2144
+ return ("payload field 'raw_resets_at_utc' is missing or not a "
2145
+ "non-empty list")
2146
+ for witness in witnesses:
2147
+ if not isinstance(witness, str) or not witness:
2148
+ return "payload field 'raw_resets_at_utc' holds a non-string witness"
2149
+ if _is_codex_window_attribution_retract_op(rec):
2150
+ targets = payload.get("retracted_assertion_ids")
2151
+ if not isinstance(targets, list) or not targets:
2152
+ return ("payload field 'retracted_assertion_ids' is missing or "
2153
+ "not a non-empty list")
2154
+ for target in targets:
2155
+ if not isinstance(target, str) or not target:
2156
+ return ("payload field 'retracted_assertion_ids' holds a "
2157
+ "non-string assertion id")
2158
+ return None
2159
+
2160
+
2161
+ def _report_window_attribution_skips(skipped: int, *,
2162
+ quiet: bool = False) -> None:
2163
+ """One stderr line for a run of replayed attribution records that were
2164
+ STRUCTURALLY INVALID and therefore not applied (review finding F2).
2165
+
2166
+ The journal is append-only and the builders refuse every shape
2167
+ `_window_attribution_defect` rejects, so reaching this line means a journal
2168
+ line was damaged or hand-edited. The record is skipped rather than raised
2169
+ on for the reason its `OR IGNORE` sibling exists: an `IntegrityError` here
2170
+ would prefix-stop the ingest cycle forever on one bad record. What is
2171
+ removed is the silence, not the tolerance.
2172
+
2173
+ Every call site must invoke this AFTER its commit, the rule
2174
+ `_report_file_account_conflicts` states: a rolled-back transaction applied
2175
+ nothing, so a line printed from inside it would describe a skip on a cycle
2176
+ that is about to be retried.
2177
+
2178
+ `quiet` is the reconciliation's caller, exactly as it is for the sibling.
2179
+ """
2180
+ if skipped > 0 and not quiet:
2181
+ print(
2182
+ f"[ingest] codex window attribution replay skipped {skipped} "
2183
+ "structurally invalid record(s); each is missing a field the "
2184
+ "assertion cannot be built without, so no attribution was applied "
2185
+ "for it — re-record the attribution, and report the journal damage "
2186
+ "if it recurs",
2187
+ file=sys.stderr,
2188
+ )
2189
+
2190
+
2191
+ def _window_attribution_values(rec: dict) -> tuple:
2192
+ """INSERT values for one assertion.
2193
+
2194
+ `raw_resets_at_utc` is stored as canonical JSON — the same separators and
2195
+ key order the journal encoder uses — so the stored text is byte-stable
2196
+ across replays and a reader can decode it back to the exact witness list.
2197
+ """
2198
+ p = rec.get("payload") or {}
2199
+ witnesses = p.get("raw_resets_at_utc")
2200
+ return (
2201
+ rec.get("id"),
2202
+ p.get("account_key"),
2203
+ p.get("source_root_key"),
2204
+ p.get("logical_limit_key"),
2205
+ p.get("observed_slot"),
2206
+ p.get("window_minutes"),
2207
+ json.dumps(witnesses, separators=(",", ":"), sort_keys=True,
2208
+ ensure_ascii=False),
2209
+ p.get("canonical_resets_at_utc"),
2210
+ rec.get("at"),
2211
+ )
2212
+
2213
+
2214
+ def _apply_window_attribution_records(cache, records) -> "tuple[int, int, int]":
2215
+ """Materialize `codex_window_attribution` / `..._retract` ops into an OPEN
2216
+ cache.db transaction; return `(asserted, retracted, skipped)` — how many
2217
+ assertion rows were ABSENT before and actually landed, how many assertions a
2218
+ retraction in this batch tombstoned, and how many records were structurally
2219
+ invalid and therefore not applied at all.
2220
+
2221
+ Never opens or commits a transaction itself: every call site owns the flocks
2222
+ and the single `BEGIN IMMEDIATE`, so the attribution family stays atomic
2223
+ with the quota rows and the attribution map beside it.
2224
+
2225
+ Records apply in the order given, which is journal order at every call site.
2226
+ That is what makes a retraction reach the assertion it names: the assertion
2227
+ was recorded first, so its row exists by the time the tombstone runs.
2228
+
2229
+ A record `_window_attribution_defect` rejects is SKIPPED and counted rather
2230
+ than inserted or raised on (review finding F2). Raising would prefix-stop
2231
+ the ingest cycle forever on one damaged line, and inserting is what produced
2232
+ the permanently dormant `raw_resets_at_utc = 'null'` row. Every caller must
2233
+ pass the count to `_report_window_attribution_skips` after its commit.
2234
+
2235
+ Any OTHER failure is re-raised as `CodexWindowAttributionReplayFailed` so
2236
+ the rebuild's quota leg reports an incomplete projection rather than
2237
+ flattening it into a successful publish (spec §6.2).
2238
+ """
2239
+ asserted = retracted = skipped = 0
2240
+ try:
2241
+ for rec in records:
2242
+ # Kind first, so a record of some other family that reached this
2243
+ # list is ignored as before rather than counted as damaged.
2244
+ if not _is_codex_window_attribution_record(rec):
2245
+ continue
2246
+ if _window_attribution_defect(rec) is not None:
2247
+ skipped += 1
2248
+ continue
2249
+ if _is_codex_window_attribution_op(rec):
2250
+ before = cache.total_changes
2251
+ cache.execute(_WINDOW_ATTRIBUTION_INSERT,
2252
+ _window_attribution_values(rec))
2253
+ if cache.total_changes > before:
2254
+ asserted += 1
2255
+ continue
2256
+ if not _is_codex_window_attribution_retract_op(rec):
2257
+ continue
2258
+ payload = rec.get("payload") or {}
2259
+ targets = payload.get("retracted_assertion_ids") or ()
2260
+ for target in targets:
2261
+ before = cache.total_changes
2262
+ cache.execute(_WINDOW_ATTRIBUTION_RETRACT_UPDATE,
2263
+ (rec.get("id"), target))
2264
+ if cache.total_changes > before:
2265
+ retracted += 1
2266
+ if asserted or retracted:
2267
+ # #500 §8.3: the semantic revision the quota projection certificate
2268
+ # binds. Advanced HERE and only here, in the same transaction as the
2269
+ # rows, so a certificate can never claim to have been computed
2270
+ # against attributions this replay had not yet applied. Deliberately
2271
+ # not the journal replay cursor, which advances on all traffic and
2272
+ # would invalidate the quota projection continuously.
2273
+ import _cctally_cache as _cc
2274
+
2275
+ _cc.bump_codex_window_attribution_revision(cache)
2276
+ except CodexWindowAttributionReplayFailed:
2277
+ raise
2278
+ except Exception as exc:
2279
+ raise CodexWindowAttributionReplayFailed(
2280
+ f"could not replay a Codex window attribution record: {exc}"
2281
+ ) from exc
2282
+ return asserted, retracted, skipped
2283
+
2284
+
2008
2285
  def _apply_file_account_records(cache, records) -> "tuple[int, int]":
2009
2286
  """Materialize the given ``codex_file_account`` ops into an OPEN cache.db
2010
2287
  transaction; return ``(restored, conflicts)`` — how many rows were ABSENT
@@ -2359,9 +2636,17 @@ class CoverageInvariantViolation(RuntimeError):
2359
2636
  """
2360
2637
 
2361
2638
 
2362
- def _assert_coverage_already_invalidated(cache_conn) -> None:
2639
+ def _assert_coverage_already_invalidated(
2640
+ cache_conn, *,
2641
+ caller: str = "rehydrate_codex_file_accounts(authoritative=True)",
2642
+ ) -> None:
2363
2643
  """Refuse an authoritative replay while a coverage certificate stands.
2364
2644
 
2645
+ ``caller`` names the authoritative replay in the refusal. #500 added a
2646
+ second one (`rehydrate_codex_window_attributions`) over the same covered
2647
+ families, and a message naming the wrong function would send the next
2648
+ debugger to the wrong call site.
2649
+
2365
2650
  `rehydrate_codex_file_accounts(authoritative=True)` empties
2366
2651
  `codex_file_accounts`, a member of `COVERAGE_CACHE_FAMILIES`, before
2367
2652
  replaying, yet its inventory entry is `preserve`. That holds only because
@@ -2393,12 +2678,230 @@ def _assert_coverage_already_invalidated(cache_conn) -> None:
2393
2678
  return
2394
2679
  if row is not None:
2395
2680
  raise CoverageInvariantViolation(
2396
- "rehydrate_codex_file_accounts(authoritative=True) clears a covered "
2681
+ f"{caller} clears a covered "
2397
2682
  f"family while {row[0]!r} is still stored; the caller must "
2398
2683
  "invalidate coverage in the transaction that clears"
2399
2684
  )
2400
2685
 
2401
2686
 
2687
+ #: What one fused rehydration pass materialized, per family (#500 review
2688
+ #: finding F4). A record rather than a flat tuple because the two families
2689
+ #: report different things and a positional 5-tuple would be unreadable at the
2690
+ #: call sites.
2691
+ CodexJournalRehydration = collections.namedtuple(
2692
+ "CodexJournalRehydration",
2693
+ ("file_accounts_applied", "file_accounts_high_water",
2694
+ "file_accounts_declined",
2695
+ "window_attributions_applied", "window_attributions_skipped"),
2696
+ )
2697
+
2698
+
2699
+ def _journal_cursor_order_key(cursor, segments) -> "tuple[int, int]":
2700
+ """Sort key for a `(segment, offset)` journal cursor within `segments`.
2701
+
2702
+ A cursor this enumeration cannot place sorts FIRST — a segment the list does
2703
+ not carry, and equally a value that is not a `(segment, offset)` pair at all.
2704
+ That is the same thing `_iter_range_with_segments` does with it: it restarts
2705
+ from the beginning. Ordering it any later would skip bytes, and raising on it
2706
+ would turn an unreadable cursor into a failed pass rather than a from-zero
2707
+ one. The unpack therefore sits INSIDE the guard (review round 2, finding
2708
+ R2-11), where the docstring always claimed it was.
2709
+
2710
+ `(-1, 0)` is below every real record position, whose segment index is `>= 0`,
2711
+ so a family floor derived from an unplaceable cursor admits every record.
2712
+ """
2713
+ try:
2714
+ seg, off = cursor
2715
+ return (segments.index(seg), int(off))
2716
+ except (ValueError, TypeError):
2717
+ return (-1, 0)
2718
+
2719
+
2720
+ def _earliest_journal_cursor(cursors, segments) -> "tuple[str, int] | None":
2721
+ """The earliest of several per-family cursors, or `None` if any is `None`.
2722
+
2723
+ `None` means "replay from the beginning", so it dominates: a pass that must
2724
+ satisfy a from-zero family has to start at zero regardless of how far the
2725
+ other family has already been carried.
2726
+
2727
+ This decides only where the TRAVERSAL starts. It does not decide what each
2728
+ family applies — that is a per-family floor, because a from-zero family must
2729
+ not drag its sibling back over settled history (review round 2, finding
2730
+ R2-2).
2731
+
2732
+ `segments` is the caller's enumeration, taken once and shared with
2733
+ `iter_range`, so the keys this returns address the same positions the
2734
+ traversal yields.
2735
+ """
2736
+ chosen = None
2737
+ chosen_key = None
2738
+ for cursor in cursors:
2739
+ if cursor is None:
2740
+ return None
2741
+ key = _journal_cursor_order_key(cursor, segments)
2742
+ if chosen_key is None or key < chosen_key:
2743
+ chosen, chosen_key = cursor, key
2744
+ return chosen
2745
+
2746
+
2747
+ def rehydrate_codex_journal_families(
2748
+ cache_conn, *, authoritative: bool = False,
2749
+ file_account_since=None, window_attribution_since=None,
2750
+ want_file_accounts: bool = True, want_window_attributions: bool = True,
2751
+ caller: str = "rehydrate_codex_journal_families(authoritative=True)",
2752
+ ) -> CodexJournalRehydration:
2753
+ """ONE journal traversal that materializes BOTH journal-derived Codex cache
2754
+ families — the ``codex_file_account`` attribution map and the #500 operator
2755
+ window attributions (review finding F4).
2756
+
2757
+ Why fused. Both families are rehydrated inside the SAME locked phase of
2758
+ ``sync_codex_cache``, and with no cursor — a fresh install, ``rm cache.db``,
2759
+ the corruption auto-heal's re-sync, or ``cache-sync --rebuild``, which forces
2760
+ a from-zero replay on both — two independent passes stream the whole journal
2761
+ while the global ``cache.db.lock`` AND the Codex provider flock are held.
2762
+ ``rehydrate_codex_file_accounts`` calls a single such traversal "a
2763
+ multi-second global cache-writer stall — itself a ``database is locked``
2764
+ trigger" (#297). One pass, two byte prefilters, two appliers.
2765
+
2766
+ The two families keep INDEPENDENT cursors, because each is advanced by call
2767
+ sites the other never reaches (``_cache_applier`` carries the window cursor
2768
+ forward on an ordinary ingest tick; ``sync_codex_cache`` carries the map's).
2769
+ The pass therefore starts at the EARLIER of the two, and each family then
2770
+ applies only from its OWN cursor within that one traversal (review round 2,
2771
+ finding R2-2). The traversal range and the application range are separate
2772
+ decisions and must stay separate: one family's absent cursor forces the
2773
+ traversal to zero, but letting it also force the SIBLING's appliers to zero
2774
+ couples the two families' replay ranges to each other's cursor health.
2775
+
2776
+ Row idempotence is not enough to make that coupling safe, which is why the
2777
+ first round's argument for it was wrong. ``_apply_file_account_records`` is
2778
+ idempotent in its rows but not in its REPORTING: it re-counts every
2779
+ historical first-wins decline inside whatever range it re-reads, and
2780
+ ``sync_codex_cache`` prints that count, telling the operator to run
2781
+ ``cache-sync --rebuild`` over decisions settled months ago. A from-zero
2782
+ sibling would also reinstate exactly the whole-journal traversal under both
2783
+ cache flocks that this fusion exists to remove.
2784
+
2785
+ The caller owns the flocks, the transaction and the commit; this function
2786
+ only runs the idempotent statements, so it can sit inside
2787
+ ``sync_codex_cache``'s already-locked phases without inverting the lock
2788
+ order. Both declined/skipped counts are RETURNED rather than reported here,
2789
+ the rule ``_report_file_account_conflicts`` states: this runs inside a
2790
+ transaction the caller may roll back.
2791
+ """
2792
+ import _cctally_cache
2793
+
2794
+ if authoritative:
2795
+ _assert_coverage_already_invalidated(cache_conn, caller=caller)
2796
+ hw = journal_high_water()
2797
+ if hw is None:
2798
+ # No journal at all: an authoritative pass still says "the journal is
2799
+ # the truth", and the truth is that there is nothing to attribute.
2800
+ if authoritative and want_file_accounts:
2801
+ cache_conn.execute("DELETE FROM codex_file_accounts")
2802
+ # Review round 2, finding R2-4: the same rule F10 states for the
2803
+ # window cursor, and the file family needs it stated here because
2804
+ # `sync_codex_cache` writes no replacement when the pass returns no
2805
+ # high-water. `_iter_range_with_segments` restarting from zero for a
2806
+ # vanished segment does not cover this: `segment_name` is
2807
+ # `observations-YYYY-MM.jsonl`, so a wiped journal that receives a
2808
+ # record in the same calendar month re-creates the SAME segment name
2809
+ # at offset 0 and a stale non-zero cursor skips the new bytes.
2810
+ cache_conn.execute(
2811
+ "DELETE FROM cache_meta WHERE key = ?",
2812
+ (_cctally_cache.CODEX_FILE_ACCOUNT_CURSOR_KEY,))
2813
+ if authoritative and want_window_attributions:
2814
+ cache_conn.execute("DELETE FROM codex_window_attributions")
2815
+ # #500 review finding F10: the cursor describes a table that no
2816
+ # longer has any rows, so leaving it would let the NEXT delta pass
2817
+ # skip journal bytes on the strength of a claim this branch just
2818
+ # falsified.
2819
+ cache_conn.execute(
2820
+ "DELETE FROM cache_meta WHERE key = ?",
2821
+ (_cctally_cache.CODEX_WINDOW_ATTRIBUTION_CURSOR_KEY,))
2822
+ return CodexJournalRehydration(0, None, 0, 0, 0)
2823
+ if authoritative:
2824
+ if want_file_accounts:
2825
+ cache_conn.execute("DELETE FROM codex_file_accounts")
2826
+ if want_window_attributions:
2827
+ cache_conn.execute("DELETE FROM codex_window_attributions")
2828
+ # A clear-then-replay is only correct from the beginning of the journal.
2829
+ file_account_since = window_attribution_since = None
2830
+
2831
+ # ONE enumeration, shared by the range floors below and by the traversal, so
2832
+ # a floor's segment index and a yielded record's segment index name the same
2833
+ # position. Two enumerations could disagree by a segment appended between
2834
+ # them, and a floor computed against the other list would then admit or
2835
+ # exclude the wrong records.
2836
+ segments = list_segments()
2837
+ starts = []
2838
+ if want_file_accounts:
2839
+ starts.append(file_account_since)
2840
+ if want_window_attributions:
2841
+ starts.append(window_attribution_since)
2842
+ since = _earliest_journal_cursor(starts, segments) if starts else hw
2843
+
2844
+ # The traversal starts at the EARLIER cursor; each family applies only from
2845
+ # its OWN (review round 2, finding R2-2). Without this, one family's absent
2846
+ # cursor drags the other back over settled history — and
2847
+ # `_apply_file_account_records` is idempotent in its ROWS but not in its
2848
+ # REPORTING, so it re-counts every historical first-wins decline and
2849
+ # `sync_codex_cache` tells the operator to `cache-sync --rebuild` over
2850
+ # decisions settled long ago. It also reinstates the whole-journal traversal
2851
+ # under both cache flocks that this fusion exists to remove. `None` means no
2852
+ # floor at all, which is a from-zero replay for that family.
2853
+ file_floor = (None if file_account_since is None
2854
+ else _journal_cursor_order_key(file_account_since, segments))
2855
+ window_floor = (
2856
+ None if window_attribution_since is None
2857
+ else _journal_cursor_order_key(window_attribution_since, segments))
2858
+ segment_positions = {name: idx for idx, name in enumerate(segments)}
2859
+
2860
+ applied = conflicts = attributed = skipped = 0
2861
+ # Streamed, never materialized, for the reason above: this runs on the FIRST
2862
+ # ordinary sync of every cache.db while both cache flocks are held. The
2863
+ # cheap byte prefilters skip the JSON decode for every uninteresting line;
2864
+ # the canonical encoder is `json.dumps(..., ensure_ascii=False)`, which never
2865
+ # escapes an ASCII kind token, so a genuine op always carries its marker
2866
+ # substring verbatim. A false positive is harmless — it is decoded and
2867
+ # rejected by the real predicate.
2868
+ for _seg, _off, raw in iter_range(since, hw, segments):
2869
+ position = (segment_positions[_seg], int(_off))
2870
+ want_file_here = want_file_accounts and (
2871
+ file_floor is None or position >= file_floor)
2872
+ want_window_here = want_window_attributions and (
2873
+ window_floor is None or position >= window_floor)
2874
+ file_hit = want_file_here and _FILE_ACCOUNT_KIND_MARKER in raw
2875
+ window_hit = want_window_here and _WINDOW_ATTRIBUTION_KIND_MARKER in raw
2876
+ if not (file_hit or window_hit):
2877
+ continue
2878
+ # ONE decode per line (review round 2, finding R2-10). The prefilters are
2879
+ # substring tests, so a single line can match both; decoding inside each
2880
+ # branch parsed such a line twice.
2881
+ rec = _lib_journal.decode_line(raw)
2882
+ if rec is None:
2883
+ continue
2884
+ if file_hit and _is_codex_file_account_op(rec):
2885
+ _restored, _conflicts = _apply_file_account_records(
2886
+ cache_conn, (rec,))
2887
+ applied += _restored
2888
+ conflicts += _conflicts
2889
+ continue
2890
+ if window_hit and _is_codex_window_attribution_record(rec):
2891
+ _asserted, _retracted, _skipped = (
2892
+ _apply_window_attribution_records(cache_conn, (rec,)))
2893
+ attributed += _asserted
2894
+ skipped += _skipped
2895
+ if want_window_attributions and (
2896
+ authoritative or window_attribution_since != (str(hw[0]), int(hw[1]))):
2897
+ # Only when it actually moves. This runs on every ordinary Codex sync,
2898
+ # and a cursor rewritten per tick would dirty the transaction each time
2899
+ # for no change at all.
2900
+ _cctally_cache.store_codex_window_attribution_cursor(cache_conn, hw)
2901
+ return CodexJournalRehydration(
2902
+ applied, hw, conflicts, attributed, skipped)
2903
+
2904
+
2402
2905
  def rehydrate_codex_file_accounts(
2403
2906
  cache_conn, *, authoritative: bool = False, since=None,
2404
2907
  ) -> "tuple[int, tuple[str, int] | None, int]":
@@ -2465,39 +2968,23 @@ def rehydrate_codex_file_accounts(
2465
2968
  ``auth.json`` branch and re-decides — the original defect. Since the MAX-set
2466
2969
  upsert already converges the counter, a clear has no upside and that
2467
2970
  downside.
2971
+ Since #500 review finding F4 this is a thin wrapper over
2972
+ ``rehydrate_codex_journal_families``, which owns the one traversal both
2973
+ journal-derived Codex families share. The semantics above are unchanged —
2974
+ the window-attribution family is simply switched off — so a caller that
2975
+ needs only the map still pays for only the map.
2468
2976
  """
2469
- if authoritative:
2470
- _assert_coverage_already_invalidated(cache_conn)
2471
- hw = journal_high_water()
2472
- if hw is None:
2473
- if authoritative:
2474
- # No journal at all: an authoritative pass still says "the journal
2475
- # is the truth", and the truth is that there are no decisions.
2476
- cache_conn.execute("DELETE FROM codex_file_accounts")
2477
- return 0, None, 0
2478
- if authoritative:
2479
- cache_conn.execute("DELETE FROM codex_file_accounts")
2480
- since = None
2481
- applied = 0
2482
- conflicts = 0
2483
- # Streamed, never materialized: this runs on the FIRST ordinary sync of
2484
- # every cache.db (hook-tick, the dashboard, the corruption auto-heal's
2485
- # re-sync) while both cache flocks are held, so a whole-journal transient
2486
- # here is a multi-second global cache-writer stall — itself a
2487
- # `database is locked` trigger. The cheap byte prefilter skips the JSON
2488
- # decode for every non-decision line; the canonical encoder is
2489
- # `json.dumps(..., ensure_ascii=False)`, which never escapes an ASCII kind
2490
- # token, so a genuine op always carries this substring verbatim. A false
2491
- # positive is harmless — it is decoded and rejected by the real predicate.
2492
- for _seg, _off, raw in iter_range(since, hw):
2493
- if _FILE_ACCOUNT_KIND_MARKER not in raw:
2494
- continue
2495
- rec = _lib_journal.decode_line(raw)
2496
- if rec is not None and _is_codex_file_account_op(rec):
2497
- _restored, _conflicts = _apply_file_account_records(cache_conn, (rec,))
2498
- applied += _restored
2499
- conflicts += _conflicts
2500
- return applied, hw, conflicts
2977
+ result = rehydrate_codex_journal_families(
2978
+ cache_conn,
2979
+ authoritative=authoritative,
2980
+ file_account_since=since,
2981
+ want_file_accounts=True,
2982
+ want_window_attributions=False,
2983
+ caller="rehydrate_codex_file_accounts(authoritative=True)",
2984
+ )
2985
+ return (result.file_accounts_applied,
2986
+ result.file_accounts_high_water,
2987
+ result.file_accounts_declined)
2501
2988
 
2502
2989
 
2503
2990
  def _bounded_covered_offset(segment, raw_offset, covered_offset, decoded_end):
@@ -2572,22 +3059,33 @@ def _coverage_advance_plan(cursor, covered_to, decoded_end=None):
2572
3059
 
2573
3060
  def _cache_applier(decoded, *, cursor=None, covered_to=None,
2574
3061
  decoded_end=None) -> int | None:
2575
- """Composite cache leg (spec §5.2 step 3 + #416 spec §3.4): materialize this
2576
- batch's Codex quota obs into `quota_window_snapshots` AND its
2577
- `codex_file_account` ops into the attribution map, under the NON-BLOCKING
2578
- global cache writer lock followed by `cache.db.codex.lock`, in ONE
2579
- `BEGIN IMMEDIATE`. Contract (journal seam): `(decoded) -> stop | None`,
3062
+ """Composite cache leg (spec §5.2 step 3 + #416 spec §3.4 + #500 spec §6.2):
3063
+ materialize this batch's THREE Codex families — quota obs into
3064
+ `quota_window_snapshots`, `codex_file_account` ops into the attribution map,
3065
+ and `codex_window_attribution`/`..._retract` ops into
3066
+ `codex_window_attributions` — under the NON-BLOCKING global cache writer
3067
+ lock followed by `cache.db.codex.lock`, in ONE `BEGIN IMMEDIATE`. Contract
3068
+ (journal seam): `(decoded) -> stop | None`,
2580
3069
  `decoded = [(record, segment, offset), ...]` in canonical order.
2581
3070
 
2582
- - Neither family present in the batch → return None (no flock taken).
3071
+ - NO family present in the batch → return None (no flock taken).
2583
3072
  - Busy global/provider flock, OR a cache write it cannot complete → PREFIX-STOP:
2584
- return the EARLIEST index across BOTH families having committed NEITHER, so
2585
- the cycle processes only `decoded[:stop]` and advances the cursor to
2586
- `decoded[stop]`'s offset, retrying the remainder next cycle (the scalar
2587
- cursor never advances past an unmaterialized record — spec §5.2 step 3).
3073
+ return the EARLIEST index across ALL THREE families having committed NONE
3074
+ of them, so the cycle processes only `decoded[:stop]` and advances the
3075
+ cursor to `decoded[stop]`'s offset, retrying the remainder next cycle (the
3076
+ scalar cursor never advances past an unmaterialized record — spec §5.2
3077
+ step 3). "A cache write it cannot complete" covers
3078
+ `CodexWindowAttributionReplayFailed` as well as `sqlite3.Error` since
3079
+ review finding F3: the typed attribution failure takes the same
3080
+ prefix-stop, rather than escaping to `_run_stats_ingest_once`, which
3081
+ re-raises under `mode="authoritative"` and would hard-fail `record-usage`,
3082
+ `record-credit`, `sync-week` and statusline publication.
2588
3083
  - Flock acquired + everything upserted → return None (full consumption).
2589
3084
  A quota-row change advances ``codex_physical_mutation_seq`` in the same
2590
- transaction; an idempotent replay leaves the sequence unchanged.
3085
+ transaction; an idempotent replay leaves the sequence unchanged. A
3086
+ structurally invalid attribution record is SKIPPED, counted, and reported
3087
+ after the commit (review finding F2) — it never prefix-stops, because a
3088
+ damaged line would otherwise wedge the cursor forever.
2591
3089
 
2592
3090
  ``cursor``, ``covered_to`` and ``decoded_end`` carry the cycle's journal
2593
3091
  range so this leg can ADVANCE the #496 S5b coverage certificate (spec §4.3).
@@ -2604,15 +3102,22 @@ def _cache_applier(decoded, *, cursor=None, covered_to=None,
2604
3102
  if _is_codex_quota_obs(rec)]
2605
3103
  file_idx = [i for i, (rec, _s, _o) in enumerate(decoded)
2606
3104
  if _is_codex_file_account_op(rec)]
2607
- if not quota_idx and not file_idx:
3105
+ # #500: the operator's window attributions are a covered family too, so the
3106
+ # certificate this leg advances would otherwise claim coverage for records
3107
+ # nobody materialized — and a later rebuild would trust it and skip them.
3108
+ # Materializing here is also what makes spec §6.2's "ingest reconciles the
3109
+ # tail" true on the ordinary status-line tick.
3110
+ attr_idx = [i for i, (rec, _s, _o) in enumerate(decoded)
3111
+ if _is_codex_window_attribution_record(rec)]
3112
+ if not quota_idx and not file_idx and not attr_idx:
2608
3113
  return None
2609
3114
  # BEFORE the flocks, for the reason `_coverage_advance_plan` states, and
2610
3115
  # before them for a second reason too: it is journal file I/O, and the leg's
2611
3116
  # whole purpose is to hold the global cache writer lock as briefly as it can.
2612
3117
  plan = _coverage_advance_plan(cursor, covered_to, decoded_end)
2613
- # All-or-nothing across the two families: one stop, the earliest of either.
2614
- stop_idx = min(quota_idx[0] if quota_idx else file_idx[0],
2615
- file_idx[0] if file_idx else quota_idx[0])
3118
+ # All-or-nothing across the three families: one stop, the earliest of any.
3119
+ stop_idx = min(
3120
+ idx[0] for idx in (quota_idx, file_idx, attr_idx) if idx)
2616
3121
  from _lib_cache_writer_lock import (
2617
3122
  acquire_cache_writer_flocks,
2618
3123
  release_cache_writer_flocks,
@@ -2660,6 +3165,24 @@ def _cache_applier(decoded, *, cursor=None, covered_to=None,
2660
3165
  # must already govern the observations it covers.
2661
3166
  _, _file_conflicts = _apply_file_account_records(
2662
3167
  cache, [decoded[i][0] for i in file_idx])
3168
+ _attr_skipped = 0
3169
+ if attr_idx:
3170
+ # In journal order, so a retraction reaches the assertion it
3171
+ # names.
3172
+ _, _, _attr_skipped = _apply_window_attribution_records(
3173
+ cache, [decoded[i][0] for i in attr_idx])
3174
+ # The cursor moves only when the CYCLE supplied its contiguous
3175
+ # range. A direct call (a test, or any caller that does not know
3176
+ # the range) leaves it alone: a cursor behind the table is
3177
+ # harmless, because every apply is idempotent on its op id, while
3178
+ # a cursor ahead of it skips a durable assertion forever. Bounded
3179
+ # by what the cycle DECODED, for the same reason the coverage
3180
+ # claim is.
3181
+ _attr_through = decoded_end if decoded_end is not None else covered_to
3182
+ if _attr_through is not None:
3183
+ _cctally_cache.store_codex_window_attribution_cursor(
3184
+ cache,
3185
+ (str(_attr_through[0]), int(_attr_through[1])))
2663
3186
  quota_changes_before = cache.total_changes
2664
3187
  _apply_quota_records(cache, [decoded[i][0] for i in quota_idx])
2665
3188
  if cache.total_changes != quota_changes_before:
@@ -2677,14 +3200,29 @@ def _cache_applier(decoded, *, cursor=None, covered_to=None,
2677
3200
  applied_through=applied_through, pinned_vector=vector)
2678
3201
  cache.commit()
2679
3202
  _report_file_account_conflicts(_file_conflicts)
2680
- except sqlite3.Error as exc:
3203
+ _report_window_attribution_skips(_attr_skipped)
3204
+ except (sqlite3.Error, CodexWindowAttributionReplayFailed) as exc:
2681
3205
  try:
2682
3206
  cache.rollback()
2683
3207
  except sqlite3.Error:
2684
3208
  pass
2685
3209
  # Could not materialize -> prefix-stop so the cursor holds and the
2686
3210
  # next cycle retries (the records stay durable in the journal
2687
- # regardless). NEITHER family is committed.
3211
+ # regardless). NO family is committed.
3212
+ #
3213
+ # #500 review finding F3: the typed attribution failure belongs
3214
+ # HERE, not escaping to the caller. It subclasses `RuntimeError`, so
3215
+ # without this clause it propagated out of `_cache_applier`, out of
3216
+ # `_run_cycle`, and reached only `_run_stats_ingest_once`'s broad
3217
+ # handler — which RE-RAISES under `mode="authoritative"`. That mode
3218
+ # is `record-usage`, `record-credit`, `sync-week` and statusline
3219
+ # publication, so a transient `database is locked` on this one table
3220
+ # would hard-fail those commands while the two families beside it
3221
+ # merely held the cursor. Spec §6.2's fail-loud obligation is about
3222
+ # stats-rebuild PUBLICATION and `_run_bounded_recovery` discharges
3223
+ # it separately at its own typed handler; the ingest leg's fail-safe
3224
+ # is the prefix-stop, and holding the cursor gives the identical
3225
+ # guarantee.
2688
3226
  print(f"[ingest] cache leg write failed: {exc}", file=sys.stderr)
2689
3227
  return stop_idx
2690
3228
  finally:
@@ -3039,9 +3577,26 @@ _EVT_KIND_PROVIDER = {
3039
3577
  # `op_kinds` set, so the kind MUST additionally carry a
3040
3578
  # `_lib_rederive._OP_CLASSIFICATIONS` entry or the re-derive planner raises
3041
3579
  # `RederiveConflict` on every run.
3580
+ # #500: the operator's attribution of an already-recorded Codex quota window,
3581
+ # and its retraction. Machinery, not data-bearing: the payload's `account_key`
3582
+ # is the SUBJECT of the assertion — the account the operator says the window
3583
+ # belongs to — rather than the two-shaped stamp naming which account wrote the
3584
+ # record.
3585
+ #
3586
+ # Registration is NOT what makes `classify_legacy_provider` and
3587
+ # `_normalize_legacy_account_stamp` leave that field alone (review finding F7).
3588
+ # Both already do: the former's `t == "op"` branch returns None for every kind
3589
+ # except `weekly_credit_floor`, and the latter is gated on
3590
+ # `_REAL_ACCOUNT_EVT_OP_KINDS`, which these kinds were never in. Removing these
3591
+ # two names changes neither function's answer — verified by experiment.
3592
+ # Registration's real effect is that `_cctally_rederive` unions this set into
3593
+ # the `op_kinds` it hands `validate_family_registry`, so a registered kind MUST
3594
+ # also carry a `_lib_rederive._OP_CLASSIFICATIONS` entry or the re-derive
3595
+ # planner raises `RederiveConflict` on every run.
3042
3596
  _ACCOUNTS_MACHINERY_KINDS = frozenset(
3043
3597
  ("account_observe", "account_label", "accounts_cutover",
3044
- "codex_file_account"))
3598
+ "codex_file_account",
3599
+ "codex_window_attribution", "codex_window_attribution_retract"))
3045
3600
 
3046
3601
  # Legacy-classifier exhaustiveness guard (#341, review finding P2-1). EVERY evt
3047
3602
  # kind in `_EVT_SPECS` and every harvest kind in `_HARVEST_SPECS` must carry a
@@ -5712,6 +6267,7 @@ def _run_stats_ingest_once(
5712
6267
  reconcile_config=None,
5713
6268
  codex_apply=None,
5714
6269
  post_commit=None,
6270
+ locks_held: bool = False,
5715
6271
  ) -> IngestResult:
5716
6272
  """Run one single-flight attempt, without correction-recovery orchestration.
5717
6273
 
@@ -5745,7 +6301,33 @@ def _run_stats_ingest_once(
5745
6301
  returns `IngestResult(ran=True, error=<exc>)` so a statusline/hook tick is
5746
6302
  never broken; an AUTHORITATIVE ingest re-raises so its caller (record-usage,
5747
6303
  record-credit, sync-week, statusline publication) sees the failure.
6304
+
6305
+ `locks_held=True` (#500 spec §8.1) declares that the CALLER already owns
6306
+ stats maintenance and `journal.ingest.lock` for the duration, so this cycle
6307
+ neither acquires nor releases them. Without it, `account attribute`'s apply
6308
+ sequence — which has to hold both across its cache transaction and its stats
6309
+ transaction — would call in here and block on locks it already owns, and a
6310
+ same-process flock request on a second descriptor is a timeout rather than a
6311
+ reentrant no-op. It is deliberately a PARAMETER on this one function and not
6312
+ a second entry point: the cycle, its transaction boundaries, its
6313
+ correction-recovery signal and its alert dispatch are all the same, because a
6314
+ second subtly different ingest path is a worse outcome than the deadlock it
6315
+ was written to avoid. The one behavioural difference lives in
6316
+ `run_stats_ingest`, which declines automatic correction recovery under this
6317
+ flag exactly as it already declines it for a caller-owned connection.
5748
6318
  """
6319
+ if locks_held and _cctally_core.holds_attribution_apply_cache_flocks():
6320
+ # #500 §8.1 / the lock-order law in `docs/journal-gotchas.md`: every
6321
+ # cache write must be committed and unlocked BEFORE the stats
6322
+ # transaction opens. `codex_attribution_apply_locks` yields an owner
6323
+ # whose `release_cache_flocks()` is what satisfies that; a caller that
6324
+ # forgets it would violate the law silently, so the violation is refused
6325
+ # here rather than diagnosed later from a deadlock or a torn generation.
6326
+ raise ValueError(
6327
+ "run_stats_ingest(locks_held=True) requires the apply set's cache "
6328
+ "writer flocks to be released first — call "
6329
+ "owner.release_cache_flocks() before the stats transaction"
6330
+ )
5749
6331
  own_conn = conn is None
5750
6332
  maintenance_fd = None
5751
6333
  lock_fd = None
@@ -5758,7 +6340,17 @@ def _run_stats_ingest_once(
5758
6340
  # For a current/mismatched epoch, open first, then take maintenance SH
5759
6341
  # and verify the main-file identity did not change across the open; if
5760
6342
  # a sibling rebuilt in that gap, discard the stale handle and retry.
5761
- if own_conn:
6343
+ if locks_held:
6344
+ # #500 §8.1: the caller already owns maintenance EXCLUSIVE, which is
6345
+ # strictly stronger than the SHARED hold this branch would take, so
6346
+ # the whole resolution dance has nothing left to serialize — no
6347
+ # sibling can rebuild the main file underneath this open. Requesting
6348
+ # either lock again on a second descriptor would be a same-process
6349
+ # timeout, not a reentrant no-op, which is the deadlock this
6350
+ # parameter exists to avoid.
6351
+ if own_conn:
6352
+ conn = _cctally_core.open_db()
6353
+ elif own_conn:
5762
6354
  while True:
5763
6355
  raw_epoch = _stats_db_user_version()
5764
6356
  if (
@@ -5822,18 +6414,19 @@ def _run_stats_ingest_once(
5822
6414
  alerts=[],
5823
6415
  )
5824
6416
 
5825
- lock_fd = _acquire_ingest_lock(mode, timeout_s)
5826
- if lock_fd is None:
5827
- if own_conn and conn is not None:
5828
- conn.close()
5829
- conn = None
5830
- return IngestResult(
5831
- ran=False,
5832
- consumed=0,
5833
- malformed=0,
5834
- events_emitted=0,
5835
- alerts=[],
5836
- )
6417
+ if not locks_held:
6418
+ lock_fd = _acquire_ingest_lock(mode, timeout_s)
6419
+ if lock_fd is None:
6420
+ if own_conn and conn is not None:
6421
+ conn.close()
6422
+ conn = None
6423
+ return IngestResult(
6424
+ ran=False,
6425
+ consumed=0,
6426
+ malformed=0,
6427
+ events_emitted=0,
6428
+ alerts=[],
6429
+ )
5837
6430
  try:
5838
6431
  # #386: declare the sanctioned steady-state write regime for the
5839
6432
  # duration of the cycle. Two consumers: the Stage 3 authorizer, and
@@ -6112,6 +6705,7 @@ def run_stats_ingest(
6112
6705
  reconcile_config=None,
6113
6706
  codex_apply=None,
6114
6707
  post_commit=None,
6708
+ locks_held: bool = False,
6115
6709
  ) -> IngestResult:
6116
6710
  """Run one cycle, healing one completed-correction mismatch when safe.
6117
6711
 
@@ -6120,6 +6714,14 @@ def run_stats_ingest(
6120
6714
  triggering commit, releases both locks, and retries once on a freshly opened
6121
6715
  current-family connection. Caller-owned connections are never closed or
6122
6716
  replaced. A second correction signal is surfaced with the manual remedy.
6717
+
6718
+ `locks_held=True` (#500 spec §8.1) says the caller already owns stats
6719
+ maintenance and `journal.ingest.lock`, so the cycle neither acquires nor
6720
+ releases them. It declines automatic correction recovery for the same reason
6721
+ a caller-owned CONNECTION does — recovery works by unwinding every lock and
6722
+ then seeking maintenance EXCLUSIVE in total order, which it cannot do while
6723
+ the caller owns the set — and surfaces the same signal with the same manual
6724
+ remedy rather than inventing a second recovery path.
6123
6725
  """
6124
6726
  kwargs = {
6125
6727
  "mode": mode,
@@ -6128,17 +6730,21 @@ def run_stats_ingest(
6128
6730
  "reconcile_config": reconcile_config,
6129
6731
  "codex_apply": codex_apply,
6130
6732
  "post_commit": post_commit,
6733
+ "locks_held": locks_held,
6131
6734
  }
6132
6735
  try:
6133
6736
  return _run_stats_ingest_once(**kwargs)
6134
6737
  except CorrectionRebuildRequired as signal:
6135
6738
  if not signal.recovery_eligible:
6136
6739
  raise
6137
- if conn is not None:
6740
+ if conn is not None or locks_held:
6138
6741
  raise CorrectionRebuildRequired(
6139
6742
  _correction_recovery_guidance(
6140
6743
  "automatic correction recovery cannot replace a "
6141
6744
  "caller-owned stats.db connection"
6745
+ if conn is not None else
6746
+ "automatic correction recovery cannot run while the "
6747
+ "caller holds the stats maintenance and ingest locks"
6142
6748
  ),
6143
6749
  batch_id=signal.batch_id,
6144
6750
  event_id=signal.event_id,
@@ -8544,6 +9150,16 @@ def _rebuild_quota_cache_leg_raw(
8544
9150
  file_accounts = [
8545
9151
  r for r in decoded if r is not None and _is_codex_file_account_op(r)
8546
9152
  ]
9153
+ # #500 spec §6.3: the operator's attribution assertions ride the SAME leg,
9154
+ # so they are replayed under the same flocks, in the same transaction, and
9155
+ # under the same coverage certificate as the quota rows and the attribution
9156
+ # map. A separate pass after this one could not be covered by that
9157
+ # certificate, and a failure in it would be flattened into a successful
9158
+ # publish (§6.2).
9159
+ attributions = [
9160
+ r for r in decoded
9161
+ if r is not None and _is_codex_window_attribution_record(r)
9162
+ ]
8547
9163
  if coverage is not None:
8548
9164
  # `complete` is TRUE for a skip, and that is not a slip. Spec §4.7
8549
9165
  # distinguishes stats publication success from cache-recovery
@@ -8559,7 +9175,7 @@ def _rebuild_quota_cache_leg_raw(
8559
9175
  # that elided every quota-bearing segment has an empty `quota_raw` and a
8560
9176
  # cache that may still need those observations if the certificate stopped
8561
9177
  # being valid while the pass ran.
8562
- if not quota_raw and not file_accounts and not elision_gaps:
9178
+ if not quota_raw and not file_accounts and not attributions and not elision_gaps:
8563
9179
  return 0.0
8564
9180
  cache_path = _cctally_core.CACHE_DB_PATH
8565
9181
  if not cache_path.exists():
@@ -8630,6 +9246,7 @@ def _rebuild_quota_cache_leg_raw(
8630
9246
  quota_raw, file_accounts, cutover_claude, counters,
8631
9247
  cache_path=cache_path, vector=vector, covered=covered,
8632
9248
  high_water=high_water, coverage=coverage, quiet=quiet,
9249
+ attributions=attributions,
8633
9250
  )
8634
9251
 
8635
9252
 
@@ -8688,6 +9305,7 @@ def _recovery_state(cache):
8688
9305
  def _run_bounded_recovery(
8689
9306
  quota_raw, file_accounts, cutover_claude, counters, *,
8690
9307
  cache_path, vector, covered, high_water, coverage, quiet=False,
9308
+ attributions=(),
8691
9309
  ) -> float:
8692
9310
  """Recovery as resumable chunks, each capped by bytes AND record count.
8693
9311
 
@@ -8854,11 +9472,27 @@ def _run_bounded_recovery(
8854
9472
  counters.update(counter_baseline)
8855
9473
  continue
8856
9474
  file_conflicts = 0
9475
+ attr_skipped = 0
8857
9476
  if with_decisions:
8858
9477
  # Decisions FIRST inside this transaction — the same §3.5
8859
9478
  # precedence ordering `_cache_applier` keeps.
8860
9479
  _restored, file_conflicts = _apply_file_account_records(
8861
9480
  cache, file_accounts)
9481
+ # #500: the whole attribution population rides chunk 0 for
9482
+ # the same reason the decisions do — it is bounded by the
9483
+ # operator's assertions (dozens of records on a store with
9484
+ # years of history), not by the observations, so it cannot
9485
+ # reintroduce the unbounded hold the chunking removed. The
9486
+ # cursor commits with them: once this transaction lands, the
9487
+ # table IS current through the pinned high water, because
9488
+ # every attribution record in the prefix is in this list.
9489
+ if attributions:
9490
+ _, _, attr_skipped = _apply_window_attribution_records(
9491
+ cache, attributions)
9492
+ if attributions and high_water is not None:
9493
+ _cctally_cache.store_codex_window_attribution_cursor(
9494
+ cache,
9495
+ (str(high_water[0]), int(high_water[1])))
8862
9496
  if decoded_chunk is not None:
8863
9497
  _apply_quota_records(
8864
9498
  cache, decoded_chunk,
@@ -8878,6 +9512,7 @@ def _run_bounded_recovery(
8878
9512
  committed_chunks += 1
8879
9513
  chunk_index += 1
8880
9514
  _report_file_account_conflicts(file_conflicts, quiet=quiet)
9515
+ _report_window_attribution_skips(attr_skipped, quiet=quiet)
8881
9516
  outcome, stop_reason = "incomplete", "noCoverageEstablished"
8882
9517
  break
8883
9518
  if last and covered is not None:
@@ -8907,6 +9542,7 @@ def _run_bounded_recovery(
8907
9542
  committed_chunks += 1
8908
9543
  chunk_index += 1
8909
9544
  _report_file_account_conflicts(file_conflicts, quiet=quiet)
9545
+ _report_window_attribution_skips(attr_skipped, quiet=quiet)
8910
9546
  outcome, stop_reason = "incomplete", "mintRefused"
8911
9547
  break
8912
9548
  else:
@@ -8925,6 +9561,26 @@ def _run_bounded_recovery(
8925
9561
  committed_chunks += 1
8926
9562
  chunk_index += 1
8927
9563
  _report_file_account_conflicts(file_conflicts, quiet=quiet)
9564
+ _report_window_attribution_skips(attr_skipped, quiet=quiet)
9565
+ except CodexWindowAttributionReplayFailed as exc:
9566
+ # #500 spec §6.2. Typed, and reported through the SAME coverage
9567
+ # channel a write failure uses, so `_write_quota_projection_state`
9568
+ # stamps `stats_quota_projection_state.incomplete` inside the
9569
+ # publication transaction. It must never be flattened into a
9570
+ # successful empty result: a generation published with
9571
+ # `incomplete = 0` while omitting the operator's attribution is
9572
+ # exactly the silent under-attribution this design exists to
9573
+ # prevent.
9574
+ if cache is not None:
9575
+ try:
9576
+ cache.rollback()
9577
+ except sqlite3.Error:
9578
+ pass
9579
+ if not quiet:
9580
+ print("[rebuild] Codex window attribution replay failed: "
9581
+ f"{exc}", file=sys.stderr)
9582
+ outcome, stop_reason = "failed", "attributionReplayFailed"
9583
+ break
8928
9584
  except sqlite3.Error as exc:
8929
9585
  if cache is not None:
8930
9586
  try: