cctally 1.97.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.
@@ -98,6 +98,7 @@ Spec: docs/superpowers/specs/2026-05-13-bin-cctally-split-design.md
98
98
  from __future__ import annotations
99
99
 
100
100
  import argparse
101
+ import bisect
101
102
  import contextlib
102
103
  import datetime as dt
103
104
  import fcntl
@@ -113,7 +114,7 @@ import sys
113
114
  import tempfile
114
115
  import time
115
116
  from dataclasses import asdict, dataclass, field
116
- from typing import Any, Callable, Iterator, NamedTuple
117
+ from typing import Any, Callable, Iterable, Iterator, NamedTuple
117
118
 
118
119
 
119
120
  def _cctally():
@@ -1079,6 +1080,261 @@ def resolve_codex_file_account(
1079
1080
  account_key=row[0], incarnation=int(row[1]), from_offset=int(row[2]))
1080
1081
 
1081
1082
 
1083
+ # --------------------------------------------------------------------------
1084
+ # #500 spec §6.1/§6.2 — the derived index over operator window attributions.
1085
+ #
1086
+ # The journal holds the truth (two op kinds); `codex_window_attributions` is a
1087
+ # disposable index over it, carried by the same cache leg that carries the
1088
+ # quota observations and the attribution map. A high-water cursor in
1089
+ # `cache_meta` records the journal position the table was materialized through,
1090
+ # so the ordinary sync reconciles only the tail.
1091
+ # --------------------------------------------------------------------------
1092
+
1093
+ #: `cache_meta` key for the derived table's journal high-water cursor.
1094
+ CODEX_WINDOW_ATTRIBUTION_CURSOR_KEY = "codex_window_attribution_cursor"
1095
+
1096
+ #: `cache_meta` key for the ATTRIBUTION REVISION — a counter advanced only when
1097
+ #: an attribution record actually lands or is tombstoned (#500 spec §8.3).
1098
+ #:
1099
+ #: Deliberately NOT the journal replay cursor beside it, and the two must never
1100
+ #: be collapsed. The cursor advances on all journal traffic, so a certificate
1101
+ #: keyed on it would be invalidated by every unrelated observation and the
1102
+ #: targeted quota projection would degrade into continuous re-projection. The
1103
+ #: revision that matters is the last attribution-CHANGING one.
1104
+ CODEX_WINDOW_ATTRIBUTION_REVISION_KEY = "codex_window_attribution_revision"
1105
+
1106
+ #: `cache_meta` key for the #416 attribution MAP's journal high-water cursor
1107
+ #: (`codex_file_accounts`). Named here beside its sibling rather than only as a
1108
+ #: local inside `sync_codex_cache`, because the no-journal authoritative branch
1109
+ #: of `rehydrate_codex_journal_families` must drop it in the same breath as the
1110
+ #: rows it describes (review round 2, finding R2-4) and cannot spell a literal
1111
+ #: that lives in another module's function body.
1112
+ CODEX_FILE_ACCOUNT_CURSOR_KEY = "codex_attribution_rehydrated_hw"
1113
+
1114
+
1115
+ def load_codex_window_attribution_cursor(
1116
+ conn: sqlite3.Connection,
1117
+ ) -> "tuple[str, int] | None":
1118
+ """The journal position `codex_window_attributions` was replayed through.
1119
+
1120
+ `None` means "never replayed", which is also what a malformed or
1121
+ unreadable value means: a cursor nobody can parse must not be trusted to
1122
+ skip journal bytes, and a from-zero replay is always sound because every
1123
+ apply is idempotent on its natural key.
1124
+ """
1125
+ try:
1126
+ row = conn.execute(
1127
+ "SELECT value FROM cache_meta WHERE key = ? LIMIT 1",
1128
+ (CODEX_WINDOW_ATTRIBUTION_CURSOR_KEY,),
1129
+ ).fetchone()
1130
+ except sqlite3.DatabaseError:
1131
+ return None
1132
+ if not row or not row[0]:
1133
+ return None
1134
+ try:
1135
+ stored = json.loads(row[0])
1136
+ segment, offset = stored["high_water"]
1137
+ except (ValueError, TypeError, KeyError):
1138
+ return None
1139
+ if not isinstance(segment, str) or not segment:
1140
+ return None
1141
+ try:
1142
+ return (segment, int(offset))
1143
+ except (TypeError, ValueError):
1144
+ return None
1145
+
1146
+
1147
+ def store_codex_window_attribution_cursor(
1148
+ conn: sqlite3.Connection, high_water, *, at_utc: "str | None" = None,
1149
+ ) -> None:
1150
+ """Record the journal position the table is now materialized through.
1151
+
1152
+ Runs inside the caller's transaction, which is the one that applied the
1153
+ records it describes — the cursor and the rows must commit or roll back
1154
+ together, or a crash between them would skip a durable assertion forever.
1155
+ """
1156
+ _set_cache_meta(
1157
+ conn,
1158
+ CODEX_WINDOW_ATTRIBUTION_CURSOR_KEY,
1159
+ json.dumps(
1160
+ {
1161
+ "high_water": [str(high_water[0]), int(high_water[1])],
1162
+ "at": at_utc or dt.datetime.now(
1163
+ dt.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
1164
+ },
1165
+ separators=(",", ":"), sort_keys=True,
1166
+ ),
1167
+ )
1168
+
1169
+
1170
+ def codex_window_attribution_revision(conn: sqlite3.Connection) -> int:
1171
+ """The current attribution revision; `0` when nothing has ever landed.
1172
+
1173
+ An unreadable or malformed value reads as `0`, which is the FAIL-SAFE
1174
+ direction here: a stored certificate carrying any other number then
1175
+ disagrees, so the projection is treated as uncertified and re-derived,
1176
+ rather than a stale projection being trusted.
1177
+ """
1178
+ try:
1179
+ row = conn.execute(
1180
+ "SELECT value FROM cache_meta WHERE key = ? LIMIT 1",
1181
+ (CODEX_WINDOW_ATTRIBUTION_REVISION_KEY,),
1182
+ ).fetchone()
1183
+ except sqlite3.DatabaseError:
1184
+ return 0
1185
+ if not row or row[0] in (None, ""):
1186
+ return 0
1187
+ try:
1188
+ return int(row[0])
1189
+ except (TypeError, ValueError):
1190
+ return 0
1191
+
1192
+
1193
+ def bump_codex_window_attribution_revision(conn: sqlite3.Connection) -> int:
1194
+ """Advance the revision inside the caller's OPEN transaction.
1195
+
1196
+ Runs in the same transaction as the rows it describes, for the reason the
1197
+ cursor beside it does: a revision that committed without its records would
1198
+ certify a projection computed against attributions nobody applied.
1199
+ """
1200
+ nxt = codex_window_attribution_revision(conn) + 1
1201
+ _set_cache_meta(conn, CODEX_WINDOW_ATTRIBUTION_REVISION_KEY, str(nxt))
1202
+ return nxt
1203
+
1204
+
1205
+ def load_active_window_attributions(
1206
+ conn: sqlite3.Connection, *, source_root_keys=None,
1207
+ retracted_only: bool = False,
1208
+ ) -> "tuple[dict, ...]":
1209
+ """Every NON-RETRACTED assertion, oldest assertion first.
1210
+
1211
+ ``retracted_only=True`` inverts exactly that one predicate and returns the
1212
+ tombstoned records instead (#500 §7.1). A retraction removes an assertion's
1213
+ EFFECT, not the record of it, and the spend axis needs the record: a stored
1214
+ stamp has no un-stamp path, so the reconciliation has to know which group's
1215
+ rows an assertion used to own before it can restore them. Every other rule
1216
+ below — weekly only, never the sentinel, undecodable witnesses skipped —
1217
+ applies unchanged, because a tombstoned record the overlay would have
1218
+ refused names no range worth visiting either.
1219
+
1220
+ `raw_resets_at_utc` comes back as a decoded `tuple[str, ...]`, not the
1221
+ stored JSON text: the group binding is an INTERSECTION against those
1222
+ witnesses and it happens in Python, because cardinality is dozens of rows
1223
+ and the tolerance-connected component cannot be expressed in SQL.
1224
+
1225
+ `source_root_keys` bounds the read to the roots a caller is about to
1226
+ resolve against. It is a bound on the ASSERTIONS loaded, never on the group
1227
+ evidence they are resolved against — spec §6.4.1 requires resolution to run
1228
+ against complete group evidence, and that is the caller's obligation.
1229
+
1230
+ A row whose stored witness JSON cannot be decoded is SKIPPED rather than
1231
+ raising: it can bind to no group, so it is dormant, which is exactly the
1232
+ honest outcome for an assertion nobody can match.
1233
+
1234
+ Two predicates repeat rules the BUILDERS already enforce, and that
1235
+ duplication is the point (review finding F8). The builders bind the WRITE
1236
+ path; this read is what the overlay trusts, and the replay path can land a
1237
+ row the builders never minted — a hand-edited journal line, or a record
1238
+ written before a rule existed. Only account-level weekly quota is
1239
+ attributable, and "the operator asserted nobody" is not a fact, so neither
1240
+ shape may reach the overlay. The existing index
1241
+ `idx_codex_window_attributions_root(source_root_key, window_minutes)`
1242
+ already anticipates the first. Task 2's overlay re-checks the model-scoped
1243
+ axis independently; this is defense in depth, not the only check.
1244
+ """
1245
+ sql = (
1246
+ "SELECT op_id, account_key, source_root_key, logical_limit_key, "
1247
+ " observed_slot, window_minutes, raw_resets_at_utc, "
1248
+ " canonical_resets_at_utc, asserted_at_utc "
1249
+ "FROM codex_window_attributions "
1250
+ "WHERE retracted_by_op_id IS "
1251
+ + ("NOT NULL " if retracted_only else "NULL ")
1252
+ + " AND window_minutes = ? "
1253
+ " AND account_key <> ?"
1254
+ )
1255
+ params: list = [
1256
+ _lib_codex_account_adoption.ACCOUNT_WEEKLY_WINDOW_MINUTES,
1257
+ _lib_codex_account_adoption.UNATTRIBUTED_SENTINEL,
1258
+ ]
1259
+ if source_root_keys is not None:
1260
+ keys = sorted({str(k) for k in source_root_keys})
1261
+ if not keys:
1262
+ return ()
1263
+ sql += " AND source_root_key IN (%s)" % ",".join("?" * len(keys))
1264
+ params.extend(keys)
1265
+ sql += " ORDER BY asserted_at_utc ASC, op_id ASC"
1266
+ rows = []
1267
+ for row in conn.execute(sql, params):
1268
+ try:
1269
+ witnesses = json.loads(row[6])
1270
+ except (ValueError, TypeError):
1271
+ continue
1272
+ if not isinstance(witnesses, list) or not witnesses:
1273
+ continue
1274
+ rows.append({
1275
+ "op_id": row[0],
1276
+ "account_key": row[1],
1277
+ "source_root_key": row[2],
1278
+ "logical_limit_key": row[3],
1279
+ "observed_slot": row[4],
1280
+ "window_minutes": int(row[5]),
1281
+ "raw_resets_at_utc": tuple(str(w) for w in witnesses),
1282
+ "canonical_resets_at_utc": row[7],
1283
+ "asserted_at_utc": row[8],
1284
+ })
1285
+ return tuple(rows)
1286
+
1287
+
1288
+ def rehydrate_codex_window_attributions(
1289
+ conn: sqlite3.Connection, *, authoritative: bool = False,
1290
+ ) -> "tuple[int, int]":
1291
+ """Replay journaled window-attribution ops into an OPEN cache.db
1292
+ connection and advance the cursor; return
1293
+ `(assertion_rows_landed, structurally_invalid_records_skipped)`
1294
+ (spec §6.3).
1295
+
1296
+ The skip count is RETURNED rather than reported here, the rule
1297
+ `_report_file_account_conflicts` states and #500 review finding F2 requires:
1298
+ this runs inside the caller's transaction, and that caller rolls back on
1299
+ failure, so a line printed from in here would describe a skip on a cycle
1300
+ that is about to be retried. Every caller must pass it to
1301
+ `_cctally_journal._report_window_attribution_skips` after its commit.
1302
+
1303
+ The caller owns the flocks, the transaction and the commit — this only runs
1304
+ the idempotent statements, so it can sit inside `sync_codex_cache`'s already
1305
+ locked phases without inverting the lock order. Since review finding F4 the
1306
+ traversal itself belongs to `rehydrate_codex_journal_families`, so a sync
1307
+ that rehydrates BOTH journal-derived Codex families reads the journal once.
1308
+
1309
+ `authoritative=True` is the clear-then-replay form used under
1310
+ `cache-sync --rebuild`, mirroring `rehydrate_codex_file_accounts`: it makes
1311
+ the replay a convergence operator rather than an inserter, so a row that has
1312
+ drifted away from the journal — including a `retracted_by_op_id` stamp that
1313
+ should no longer stand — is corrected instead of preserved by the
1314
+ `DO NOTHING` conflict clause. It is lossless because the journal is
1315
+ append-only and is the only source this table ever had.
1316
+
1317
+ Otherwise it is a DELTA replay from the stored cursor. A cursor is never a
1318
+ one-shot "already rehydrated" marker: the command that appends these ops can
1319
+ die between its append and its cache commit, and only a cursor makes the
1320
+ retry replay the record it never materialized.
1321
+ """
1322
+ import _cctally_journal as _jr
1323
+
1324
+ result = _jr.rehydrate_codex_journal_families(
1325
+ conn,
1326
+ authoritative=authoritative,
1327
+ window_attribution_since=(
1328
+ None if authoritative
1329
+ else load_codex_window_attribution_cursor(conn)),
1330
+ want_file_accounts=False,
1331
+ want_window_attributions=True,
1332
+ caller="rehydrate_codex_window_attributions(authoritative=True)",
1333
+ )
1334
+ return (result.window_attributions_applied,
1335
+ result.window_attributions_skipped)
1336
+
1337
+
1082
1338
  # --------------------------------------------------------------------------
1083
1339
  # #416 spec §4.1/§4.2 — the canonical reset anchor, resolved at INGEST.
1084
1340
  #
@@ -1788,6 +2044,43 @@ def _delete_codex_file_derived_rows(
1788
2044
  _invalidate_codex_journal_coverage_certificate(conn)
1789
2045
 
1790
2046
 
2047
+ def _prune_codex_accounting_change_log(
2048
+ conn: sqlite3.Connection,
2049
+ *,
2050
+ retain_sequences: int = 50_000,
2051
+ ) -> int:
2052
+ """Bound #582's ledger while preserving cold-fallback gap detection.
2053
+
2054
+ The in-process dashboard cursor is intentionally not durable. Retaining a
2055
+ generous sequence tail serves ordinary ticks; a process older than that
2056
+ tail observes a non-contiguous first sequence and safely cold-loads.
2057
+ """
2058
+ if retain_sequences < 1:
2059
+ raise ValueError("retain_sequences must be positive")
2060
+ try:
2061
+ row = conn.execute(
2062
+ "SELECT value FROM cache_meta "
2063
+ "WHERE key='codex_accounting_mutation_seq'"
2064
+ ).fetchone()
2065
+ current = 0 if row is None else int(row[0])
2066
+ cutoff = current - retain_sequences
2067
+ if cutoff <= 0:
2068
+ return 0
2069
+ if conn.execute(
2070
+ "SELECT 1 FROM codex_accounting_change_log "
2071
+ "WHERE mutation_seq <= ? LIMIT 1",
2072
+ (cutoff,),
2073
+ ).fetchone() is None:
2074
+ return 0
2075
+ cursor = conn.execute(
2076
+ "DELETE FROM codex_accounting_change_log WHERE mutation_seq <= ?",
2077
+ (cutoff,),
2078
+ )
2079
+ return max(0, int(cursor.rowcount))
2080
+ except (sqlite3.Error, TypeError, ValueError):
2081
+ return 0
2082
+
2083
+
1791
2084
  def _clear_codex_derived_rows(conn: sqlite3.Connection) -> bool:
1792
2085
  """Clear every re-derivable Codex row family and report whether state changed.
1793
2086
 
@@ -1804,6 +2097,14 @@ def _clear_codex_derived_rows(conn: sqlite3.Connection) -> bool:
1804
2097
  account switch. Wiping it is the defect. It is re-derivable only from the
1805
2098
  journal, and ``sync_codex_cache`` rehydrates it from there immediately after
1806
2099
  this call.
2100
+
2101
+ ``codex_window_attributions`` (#500 spec §6.3) is protected for the same
2102
+ reason and must not be added either. It holds the operator's durable
2103
+ assertions about which account owns a recorded quota window; no rollout byte
2104
+ carries that fact, so a clear that did not replay would erase it outright.
2105
+ ``sync_codex_cache`` rehydrates it from the journal — AUTHORITATIVELY under
2106
+ ``--rebuild`` — immediately after this call, in the same position the
2107
+ attribution map's rehydration occupies and BEFORE the rollout walk.
1807
2108
  """
1808
2109
  state_changed = any(
1809
2110
  conn.execute(query).fetchone() is not None
@@ -1825,7 +2126,39 @@ def _clear_codex_derived_rows(conn: sqlite3.Connection) -> bool:
1825
2126
  f"WHERE key='{_lib_cache_coverage.PROGRESS_KEY}' LIMIT 1",
1826
2127
  )
1827
2128
  )
1828
- conn.execute("DELETE FROM codex_session_entries")
2129
+ # #582: a whole-provider clear is one cache invalidation, not N path
2130
+ # tombstones. Suppress the row trigger while deleting, then publish one
2131
+ # durable full marker under the same transaction. Older schemas have no
2132
+ # ledger and safely ignore the extra cache_meta writes.
2133
+ _has_accounting_ledger = conn.execute(
2134
+ "SELECT 1 FROM sqlite_master WHERE type='table' "
2135
+ "AND name='codex_accounting_change_log'"
2136
+ ).fetchone() is not None
2137
+ if _has_accounting_ledger:
2138
+ conn.execute(
2139
+ "INSERT OR REPLACE INTO cache_meta(key, value) VALUES "
2140
+ "('codex_accounting_bulk_clear', '1')"
2141
+ )
2142
+ try:
2143
+ conn.execute("DELETE FROM codex_session_entries")
2144
+ finally:
2145
+ if _has_accounting_ledger:
2146
+ conn.execute(
2147
+ "DELETE FROM cache_meta WHERE key='codex_accounting_bulk_clear'"
2148
+ )
2149
+ if _has_accounting_ledger and state_changed:
2150
+ conn.execute(
2151
+ "INSERT INTO cache_meta(key, value) VALUES "
2152
+ "('codex_accounting_mutation_seq', '1') "
2153
+ "ON CONFLICT(key) DO UPDATE "
2154
+ "SET value=CAST(value AS INTEGER) + 1"
2155
+ )
2156
+ conn.execute(
2157
+ "INSERT INTO codex_accounting_change_log "
2158
+ "(mutation_seq, change_kind) "
2159
+ "SELECT CAST(value AS INTEGER), 'full' FROM cache_meta "
2160
+ "WHERE key='codex_accounting_mutation_seq'"
2161
+ )
1829
2162
  conn.execute("DELETE FROM quota_window_snapshots WHERE source = 'codex'")
1830
2163
  conn.execute("DELETE FROM codex_conversation_threads")
1831
2164
  conn.execute("DELETE FROM codex_conversation_events")
@@ -1857,6 +2190,21 @@ COVERAGE_CACHE_FAMILIES: "tuple[str, ...]" = (
1857
2190
  "quota_window_snapshots",
1858
2191
  "codex_file_accounts",
1859
2192
  "codex_file_incarnations",
2193
+ # #500 spec §6.2. The certificate's promise is "every journal record in this
2194
+ # prefix is materialized", and a `codex_window_attribution` op materializes
2195
+ # HERE. Leaving the family out would let a certificate certify a prefix
2196
+ # containing an assertion nobody applied — and a rebuild would then trust it
2197
+ # and skip the replay, publishing a projection with `incomplete = 0` that
2198
+ # silently omits the operator's attribution.
2199
+ #
2200
+ # Membership does NOT route a typed replay failure into
2201
+ # `stats_quota_projection_state.incomplete` (review finding F7): this tuple
2202
+ # has no RUNTIME consumer at all — every reader of it is a test. That
2203
+ # routing is the `except CodexWindowAttributionReplayFailed` handler in
2204
+ # `_cctally_journal._run_bounded_recovery`. What membership really does is
2205
+ # force every writer of this table into `COVERAGE_WRITER_ACTIONS` below,
2206
+ # which the static inventory guard enforces.
2207
+ "codex_window_attributions",
1860
2208
  )
1861
2209
 
1862
2210
  #: Every path that mutates or materializes those families, mapped to the ONE
@@ -1928,31 +2276,40 @@ COVERAGE_WRITER_ACTIONS: "dict[str, str]" = {
1928
2276
  # transaction owner above them, not here.
1929
2277
  "_cctally_cache.record_codex_file_account": "preserve",
1930
2278
  "_cctally_cache.set_codex_file_incarnation": "preserve",
1931
- # The two journal-to-cache appliers. Same reasoning: INSERT OR IGNORE on the
1932
- # natural key, no deletes, and the certificate decision belongs to the
2279
+ # The three journal-to-cache appliers. Same reasoning: INSERT OR IGNORE on
2280
+ # the natural key, no deletes, and the certificate decision belongs to the
1933
2281
  # composite that owns their transaction.
1934
2282
  "_cctally_journal._apply_quota_records": "preserve",
1935
2283
  "_cctally_journal._apply_file_account_records": "preserve",
2284
+ "_cctally_journal._apply_window_attribution_records": "preserve",
2285
+ # The ONE fused journal-to-cache rehydration for both journal-derived Codex
2286
+ # families (#500 review finding F4). `rehydrate_codex_file_accounts` and
2287
+ # `_cctally_cache.rehydrate_codex_window_attributions` are now thin wrappers
2288
+ # over it and hold no DML of their own, which is why this key replaced both
2289
+ # of theirs — the scanner found the writes here, and the inventory names
2290
+ # where the writes ARE.
2291
+ #
1936
2292
  # Two branches, and only the additive one leaves the covered families
1937
- # untouched. `authoritative=False` replays journaled decisions into
1938
- # `codex_file_accounts` with an idempotent upsert and writes no quota row at
1939
- # all, so the coverage statement is unaffected.
2293
+ # untouched. `authoritative=False` replays journaled decisions and
2294
+ # assertions with idempotent upserts and writes no quota row at all, so the
2295
+ # coverage statement is unaffected.
1940
2296
  #
1941
- # `authoritative=True` runs `DELETE FROM codex_file_accounts`, which IS a
1942
- # covered family. `preserve` holds there for a reason that lives in the
1943
- # caller rather than in this function, so it is written down here instead of
1944
- # left as an ordering nobody stated: `sync_codex_cache` passes
2297
+ # `authoritative=True` runs `DELETE FROM codex_file_accounts` and
2298
+ # `DELETE FROM codex_window_attributions`, both COVERED families.
2299
+ # `preserve` holds there for a reason that lives in the caller rather than
2300
+ # in this function, so it is written down here instead of left as an
2301
+ # ordering nobody stated: `sync_codex_cache` passes
1945
2302
  # `authoritative=bool(rebuild)`, and that same `rebuild` flag already ran
1946
2303
  # `_clear_codex_derived_rows` — which invalidates the certificate and the
1947
2304
  # progress record — and committed, before this call. The certificate is
1948
- # therefore already gone when the delete runs, and the clear-then-replay
1949
- # re-derives the whole map from `since=None` to the journal high water. A
2305
+ # therefore already gone when the deletes run, and the clear-then-replay
2306
+ # re-derives both families from `since=None` to the journal high water. A
1950
2307
  # second `authoritative=True` caller, or a reordering inside
1951
2308
  # `sync_codex_cache`, would break that silently, and the static scanner
1952
2309
  # cannot catch it because this key is already in the inventory with a green
1953
- # label. `rehydrate_codex_file_accounts` therefore checks the invariant
1954
- # itself and raises `CoverageInvariantViolation` rather than relying on it.
1955
- "_cctally_journal.rehydrate_codex_file_accounts": "preserve",
2310
+ # label. The function therefore checks the invariant itself and raises
2311
+ # `CoverageInvariantViolation` rather than relying on it.
2312
+ "_cctally_journal.rehydrate_codex_journal_families": "preserve",
1956
2313
  # Spec §4.3's migrations row, enumerated rather than named — and enumerated
1957
2314
  # by the action each one TAKES, not by the action the spec's one-line row
1958
2315
  # assumed. Only `_024` deletes rows the journal still retains, and it is the
@@ -2820,8 +3177,7 @@ def _write_codex_file_batch(
2820
3177
  )
2821
3178
  rows_changed = 0
2822
3179
  if accounting_rows:
2823
- before = conn.total_changes
2824
- conn.executemany(
3180
+ cursor = conn.executemany(
2825
3181
  """INSERT OR IGNORE INTO codex_session_entries
2826
3182
  (source_path, line_offset, timestamp_utc, session_id, model,
2827
3183
  input_tokens, cached_input_tokens, output_tokens,
@@ -2830,7 +3186,10 @@ def _write_codex_file_batch(
2830
3186
  VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)""",
2831
3187
  accounting_rows,
2832
3188
  )
2833
- rows_changed = conn.total_changes - before
3189
+ # `total_changes` includes #582's accounting-ledger trigger writes;
3190
+ # cursor.rowcount is the top-level accounting-row count this metric
3191
+ # has always promised.
3192
+ rows_changed = max(0, int(cursor.rowcount))
2834
3193
  if quota_rows:
2835
3194
  if anchor_resolver is not None:
2836
3195
  anchor_resolver.apply_pending_merges()
@@ -2961,9 +3320,9 @@ class IngestStats:
2961
3320
  # Count of session_entries rows written by this sync — both genuinely-
2962
3321
  # new INSERTs and ccusage-parity ON CONFLICT DO UPDATE replacements
2963
3322
  # (the dedup tiebreaker swaps a streaming-intermediate row for the
2964
- # post-stream finalization). SQLite's `total_changes` counter
2965
- # increments on both, so this field is "rows changed", not "rows
2966
- # newly inserted". Pre-dedup builds used INSERT OR IGNORE where
3323
+ # post-stream finalization). SQLite cursor rowcount increments on both, so
3324
+ # this field is "rows changed", not "rows newly inserted", while excluding
3325
+ # trigger-maintained ledger rows. Pre-dedup builds used INSERT OR IGNORE where
2967
3326
  # conflicts did NOT bump the counter; the name change preserves the
2968
3327
  # observability metric without misrepresenting UPSERT updates as
2969
3328
  # new inserts.
@@ -3088,6 +3447,11 @@ def _ensure_session_files_row(conn: sqlite3.Connection, source_path: str) -> Non
3088
3447
  parent = os.path.basename(os.path.dirname(source_path))
3089
3448
  cwd = _decode_escaped_cwd(parent)
3090
3449
 
3450
+ metadata_changed = existing is not None and (
3451
+ (existing[0] is None and session_id is not None)
3452
+ or (existing[1] is None and cwd is not None)
3453
+ )
3454
+
3091
3455
  now_iso = dt.datetime.now(dt.timezone.utc).isoformat()
3092
3456
  conn.execute(
3093
3457
  """
@@ -3101,6 +3465,21 @@ def _ensure_session_files_row(conn: sqlite3.Connection, source_path: str) -> Non
3101
3465
  """,
3102
3466
  (source_path, now_iso, session_id, cwd),
3103
3467
  )
3468
+ if metadata_changed:
3469
+ # `session_files` is part of every project/session aggregate row. A
3470
+ # NULL-to-known lazy backfill changes the joined accounting identity
3471
+ # without inserting a `session_entries` row, so stamp every retained
3472
+ # row for this file with the same durable mutation signal used by cost
3473
+ # finalizations. Snapshot caches then invalidate from their existing
3474
+ # O(1) counter instead of scanning all session-file metadata each tick.
3475
+ sync_seq = _bump_mutation_seq(conn)
3476
+ conn.execute(
3477
+ "UPDATE session_entries "
3478
+ "SET mutation_seq = ?, "
3479
+ " mutation_min_ts = COALESCE(mutation_min_ts, timestamp_utc) "
3480
+ "WHERE source_path = ?",
3481
+ (sync_seq, source_path),
3482
+ )
3104
3483
  # Commit per-call so the write lock is released before the caller's
3105
3484
  # subsequent JSONL read+parse. Leaving the implicit transaction open
3106
3485
  # across the per-file loop would both hold a writer lock across reads
@@ -6318,7 +6697,7 @@ def sync_codex_cache(
6318
6697
  #
6319
6698
  # The cursor keeps the Claude-only case cheap too — once it equals the
6320
6699
  # high-water, the replay reads no bytes and writes nothing.
6321
- _ATTR_CURSOR_KEY = "codex_attribution_rehydrated_hw"
6700
+ _ATTR_CURSOR_KEY = CODEX_FILE_ACCOUNT_CURSOR_KEY
6322
6701
  try:
6323
6702
  _cursor_row = conn.execute(
6324
6703
  "SELECT value FROM cache_meta WHERE key = ? LIMIT 1",
@@ -6333,8 +6712,33 @@ def sync_codex_cache(
6333
6712
  _since = (_seg, int(_off))
6334
6713
  try:
6335
6714
  import _cctally_journal as _jr
6336
- restored, _applied_hw, _declined = _jr.rehydrate_codex_file_accounts(
6337
- conn, authoritative=bool(rebuild), since=_since)
6715
+ # #500 spec §6.3: the operator's window attributions are rehydrated
6716
+ # in the SAME transaction and the same position as the attribution
6717
+ # map, and for the same reason — `_clear_codex_derived_rows`
6718
+ # deliberately leaves the table standing, so the only thing that can
6719
+ # converge it after a rebuild is a replay from the journal. Ordered
6720
+ # BEFORE the rollout walk so every durable assertion visible to this
6721
+ # locked walk is already in place. It keeps its own cursor, distinct
6722
+ # from the attribution map's.
6723
+ #
6724
+ # ONE fused traversal (review finding F4). Two independent passes
6725
+ # streamed the whole journal twice while the global cache-writer
6726
+ # flock and the Codex provider flock were both held, which is the
6727
+ # `database is locked` trigger #297 documents.
6728
+ _rehydration = _jr.rehydrate_codex_journal_families(
6729
+ conn,
6730
+ authoritative=bool(rebuild),
6731
+ file_account_since=_since,
6732
+ window_attribution_since=(
6733
+ None if rebuild
6734
+ else load_codex_window_attribution_cursor(conn)),
6735
+ caller="sync_codex_cache(rebuild=True)",
6736
+ )
6737
+ restored = _rehydration.file_accounts_applied
6738
+ _applied_hw = _rehydration.file_accounts_high_water
6739
+ _declined = _rehydration.file_accounts_declined
6740
+ _attributed = _rehydration.window_attributions_applied
6741
+ _attr_skipped = _rehydration.window_attributions_skipped
6338
6742
  _new_cursor = (
6339
6743
  None if _applied_hw is None
6340
6744
  else f"{_applied_hw[0]}:{_applied_hw[1]}")
@@ -6352,7 +6756,12 @@ def sync_codex_cache(
6352
6756
  # bytes), and `rebuild` covers the authoritative DELETE, which
6353
6757
  # happens even when there is nothing to replay. Getting this wrong
6354
6758
  # strands an open transaction across the whole walk.
6355
- if rebuild or _new_cursor != _cursor_text:
6759
+ # `conn.in_transaction` is the precise witness — it is true iff
6760
+ # uncommitted DML is pending — and it covers #500's rehydration too
6761
+ # without the caller having to model that function's write
6762
+ # conditions. Getting this wrong strands an open transaction across
6763
+ # the whole walk.
6764
+ if rebuild or _new_cursor != _cursor_text or conn.in_transaction:
6356
6765
  conn.commit()
6357
6766
  # Both reports come AFTER the commit (closeout review C5): the
6358
6767
  # `except` below rolls back, so anything printed before it would
@@ -6361,13 +6770,23 @@ def sync_codex_cache(
6361
6770
  eprint(
6362
6771
  "[cache-sync] rehydrated "
6363
6772
  f"{restored} Codex attribution decision(s) from the journal")
6773
+ if _attributed:
6774
+ eprint(
6775
+ "[cache-sync] rehydrated "
6776
+ f"{_attributed} Codex window attribution(s) from the journal")
6364
6777
  _jr._report_file_account_conflicts(_declined)
6778
+ _jr._report_window_attribution_skips(_attr_skipped)
6365
6779
  except Exception as exc:
6366
6780
  conn.rollback()
6367
6781
  stats.deferred_reason = "attribution_rehydration"
6782
+ # Both journal-derived Codex families are rehydrated by the one
6783
+ # fused pass above, so this message names both (review finding F7):
6784
+ # a failure here is just as likely to be a window attribution as an
6785
+ # attribution decision, and a reader who saw only "decisions" would
6786
+ # look in the wrong place.
6368
6787
  eprint(
6369
- "[cache-sync] could not rehydrate Codex attribution "
6370
- f"decisions: {exc}; deferring the Codex walk")
6788
+ "[cache-sync] could not rehydrate Codex attribution decisions "
6789
+ f"or window attributions: {exc}; deferring the Codex walk")
6371
6790
  return stats
6372
6791
 
6373
6792
  # Pure read (glob + is_file only); safe to run before the SELECT and
@@ -7461,6 +7880,33 @@ def sync_codex_cache(
7461
7880
  # boundary, never to a best-effort local except.
7462
7881
  raise
7463
7882
  eprint(f"[cache-sync] could not adopt Codex window spend: {exc}")
7883
+ # #500 §7.1: the STANDING half of operator attribution. The condition it
7884
+ # repairs is created by ordinary ingest, not only by an operator command
7885
+ # — native evidence arriving for an attributed group, a second assertion,
7886
+ # a component split, a window re-materializing as model-scoped — so it
7887
+ # has to run wherever ingest runs, not only where `account attribute`
7888
+ # does. Ordered AFTER the pass above so the reconciliation has the last
7889
+ # word on any row that pass could also have touched, and inside the same
7890
+ # flocks for the same reason: the observation evidence and the accounting
7891
+ # rows it restores are one committed generation. Costs a store with no
7892
+ # attribution records one indexed read; best-effort, like the pass above.
7893
+ try:
7894
+ restored, readopted = reconcile_codex_window_attribution_spend(conn)
7895
+ conn.commit()
7896
+ if restored:
7897
+ eprint(f"[cache-sync] restored {restored} Codex row(s) whose "
7898
+ "operator attribution no longer resolves; "
7899
+ f"re-attributed {readopted}")
7900
+ except sqlite3.DatabaseError as exc:
7901
+ conn.rollback()
7902
+ if _cctally_db_sib._is_sqlite_corruption_error(exc):
7903
+ raise
7904
+ eprint("[cache-sync] could not reconcile Codex window "
7905
+ f"attribution spend: {exc}")
7906
+ # #582: keep only a generous mutation-sequence tail. A dashboard that
7907
+ # falls behind it detects the gap and cold-loads, preserving truth.
7908
+ if _prune_codex_accounting_change_log(conn):
7909
+ conn.commit()
7464
7910
  # Codex creates/extends cache.db sidecars independently of Claude's
7465
7911
  # sync path. Harden them while both cache flocks are still held and
7466
7912
  # after all Codex writes, before the optional checkpoint can rotate a
@@ -7570,9 +8016,20 @@ def apply_codex_window_spend_adoption(
7570
8016
  conn: sqlite3.Connection,
7571
8017
  *,
7572
8018
  touched: "dict[str, tuple[dt.datetime, dt.datetime]] | None" = None,
8019
+ strict: bool = False,
7573
8020
  ) -> int:
7574
8021
  """Stamp window-derived attribution onto unattributed Codex spend.
7575
8022
 
8023
+ ``strict`` (#500 spec §8.2) propagates every failure instead of converting a
8024
+ schema or loader database error into a successful zero-row result. The
8025
+ default stays best-effort because this pass's ordinary caller is a sync that
8026
+ must not fail a whole ingest over an adoption problem, and the stamp is fully
8027
+ re-derivable on the next one. Inside ``account attribute``'s transaction that
8028
+ forgiveness is wrong in a specific and silent way: the journal op lands, the
8029
+ percentage axis moves, adoption swallows the failure, and the command reports
8030
+ success with the spend axis untouched — which is exactly the all-or-nothing
8031
+ apply it promised not to break. Only that command passes ``strict=True``.
8032
+
7576
8033
  The I/O half of ``_lib_codex_account_adoption``: read the folded window
7577
8034
  evidence and the candidate rows, hand both to the pure kernel, write back the
7578
8035
  plan it returns. Cache-only by construction — the window's identified
@@ -7636,8 +8093,19 @@ def apply_codex_window_spend_adoption(
7636
8093
  "PRAGMA table_info(codex_session_entries)")
7637
8094
  }
7638
8095
  except sqlite3.DatabaseError:
8096
+ if strict:
8097
+ raise
7639
8098
  return 0
7640
8099
  if not {"account_key", "source_root_key", "timestamp_utc"} <= columns:
8100
+ if strict:
8101
+ # A dropped or pre-#341 table returns NO rows from `table_info`
8102
+ # rather than raising, so the strict caller has to be told in the
8103
+ # one currency it can act on. Reported as a database error because
8104
+ # that is what it is: the schema this pass writes to is not there.
8105
+ raise sqlite3.OperationalError(
8106
+ "codex_session_entries is missing the columns window spend "
8107
+ "adoption requires (account_key, source_root_key, "
8108
+ "timestamp_utc)")
7641
8109
  return 0
7642
8110
 
7643
8111
  try:
@@ -7646,6 +8114,8 @@ def apply_codex_window_spend_adoption(
7646
8114
  canonical_resets_between=reset_bounds,
7647
8115
  )
7648
8116
  except sqlite3.DatabaseError:
8117
+ if strict:
8118
+ raise
7649
8119
  return 0
7650
8120
 
7651
8121
  # Group on the SAME key the observation fold groups on
@@ -7733,15 +8203,328 @@ def apply_codex_window_spend_adoption(
7733
8203
  plan = adopt.build_spend_adoption_plan(windows, candidates)
7734
8204
  if not plan:
7735
8205
  return 0
7736
- before = conn.total_changes
7737
- conn.executemany(
8206
+ cursor = conn.executemany(
7738
8207
  "UPDATE codex_session_entries SET account_key = ? "
7739
8208
  " WHERE id = ? AND (account_key IS NULL OR account_key = '' "
7740
8209
  " OR account_key = ?)",
7741
8210
  [(stamp.account_key, stamp.entry_id, _lib_accounts.UNATTRIBUTED)
7742
8211
  for stamp in plan],
7743
8212
  )
7744
- return conn.total_changes - before
8213
+ # Trigger-maintained change ledgers are intentionally excluded from this
8214
+ # public semantic row count.
8215
+ return max(0, int(cursor.rowcount))
8216
+
8217
+
8218
+ # --------------------------------------------------------------------------
8219
+ # #500 §7.1 — suppression must un-stamp spend, not merely stop attributing
8220
+ # --------------------------------------------------------------------------
8221
+ #
8222
+ # The precedence table suppresses an assertion at fold time whenever the world
8223
+ # changes under it. On the PERCENTAGE axis that is sufficient, because the
8224
+ # overlay is re-derived from scratch on every load: stop applying the assertion
8225
+ # and the percentage reverts by construction.
8226
+ #
8227
+ # The SPEND axis does not behave that way. `codex_session_entries.account_key`
8228
+ # is a stored stamp, and the adoption kernel never revisits an already-identified
8229
+ # row — it skips every non-sentinel candidate, and the surrounding SQL only
8230
+ # selects rows that are NULL, empty or the sentinel. There is no un-stamp path,
8231
+ # so without this the two axes disagree permanently after native evidence
8232
+ # arrives, with no error and no operator action that would reveal it.
8233
+ #
8234
+ # This reconciliation is STATELESS by design. The spec sketched recording, per
8235
+ # assertion, the ownership it last applied; that state would have to survive an
8236
+ # authoritative rehydrate of the assertions table (which deletes and replays it)
8237
+ # while the spend stamps it describes survive independently, and the two going
8238
+ # out of step is a worse failure than the one being fixed. Restoring to the
8239
+ # per-file baseline and re-running the bounded adoption pass converges to the
8240
+ # same answer from the durable inputs alone, and is idempotent because a row
8241
+ # already at its correct owner is not rewritten.
8242
+
8243
+
8244
+ def codex_file_key_for_entry_path(
8245
+ source_root_key: str, source_path: str,
8246
+ ) -> str:
8247
+ """The durable file identity behind a stored ``source_path``.
8248
+
8249
+ ``codex_session_entries`` retains the first configured WALK spelling, while
8250
+ the attribution map is keyed on ``(root, canonical physical path)``, so
8251
+ recovering the identity means re-running the same canonicalization the walk
8252
+ ran (``codex_file_identity`` over ``CodexDiscoveredFile.physical_path``).
8253
+ """
8254
+ from _lib_source_identity import codex_file_key
8255
+
8256
+ return codex_file_key(
8257
+ str(source_root_key),
8258
+ str(_canonical_codex_path(pathlib.Path(str(source_path)))),
8259
+ )
8260
+
8261
+
8262
+ class _CodexFileBaselineResolver:
8263
+ """Per-row ``codex_file_accounts`` baselines, memoised per file.
8264
+
8265
+ ``(covered, account_key)`` semantics are ``codex_account_for_offset``'s:
8266
+ ``(True, None)`` is the stably-absent DECISION and ``(False, None)`` means
8267
+ no decision covers those bytes at all. Both restore to ``NULL``, which is
8268
+ what makes the row adoptable again; the distinction matters to the caller
8269
+ only as documentation of why.
8270
+ """
8271
+
8272
+ def __init__(self, conn: sqlite3.Connection) -> None:
8273
+ self._conn = conn
8274
+ self._ranges: "dict[tuple[str, str], list[tuple[int, str | None]]]" = {}
8275
+
8276
+ def baseline(
8277
+ self, source_root_key: str, source_path: str, line_offset: int,
8278
+ ) -> "str | None":
8279
+ key = (str(source_root_key), str(source_path))
8280
+ ranges = self._ranges.get(key)
8281
+ if ranges is None:
8282
+ identity = codex_file_key_for_entry_path(*key)
8283
+ incarnation = codex_file_incarnation(self._conn, identity)
8284
+ # Resolved at the file's CURRENT incarnation, never by path alone:
8285
+ # a truncation resets the file to offset zero, so an older
8286
+ # incarnation's ranges must not cover reused offsets.
8287
+ ranges = self._ranges[key] = load_codex_file_account_ranges(
8288
+ self._conn, identity, incarnation)
8289
+ return codex_account_for_offset(ranges, int(line_offset))[1]
8290
+
8291
+
8292
+ def _codex_span_index(
8293
+ spans: "Iterable[tuple[dt.datetime, dt.datetime]]",
8294
+ ) -> "tuple[list[dt.datetime], list[dt.datetime]]":
8295
+ """Index half-open spans for O(log n) containment.
8296
+
8297
+ Sorted starts plus the PREFIX MAXIMUM of the ends. Spans overlap — a
8298
+ straddling weekly window and its neighbour share hours — so "some span
8299
+ starting at or before ``t`` also ends after ``t``" is not answerable from
8300
+ the immediately preceding span alone; the running maximum end over the
8301
+ whole prefix is, and it is what keeps the lookup a single ``bisect``.
8302
+ """
8303
+ ordered = sorted(spans)
8304
+ starts = [start for start, _end in ordered]
8305
+ prefix_max_end: "list[dt.datetime]" = []
8306
+ running: "dt.datetime | None" = None
8307
+ for _start, end in ordered:
8308
+ running = end if running is None or end > running else running
8309
+ prefix_max_end.append(running)
8310
+ return starts, prefix_max_end
8311
+
8312
+
8313
+ def _codex_span_covers(index, instant: dt.datetime) -> bool:
8314
+ """True iff some indexed span half-open-contains ``instant``."""
8315
+ starts, prefix_max_end = index
8316
+ position = bisect.bisect_right(starts, instant)
8317
+ return position > 0 and prefix_max_end[position - 1] > instant
8318
+
8319
+
8320
+ def reconcile_codex_window_attribution_spend(
8321
+ conn: sqlite3.Connection, *, strict: bool = False,
8322
+ ) -> "tuple[int, int]":
8323
+ """Restore stranded attribution stamps, then re-adopt; ``(restored, adopted)``.
8324
+
8325
+ Standing, not one-shot: it runs at the end of every Codex sync and inside
8326
+ ``account attribute``'s own transaction, because the condition it repairs is
8327
+ created by ORDINARY ingest — native evidence arriving for a group an operator
8328
+ had attributed, a second assertion appearing, a component splitting, a
8329
+ re-materialized window turning out to be model-scoped — and by retraction
8330
+ alike. Whichever of those happened, the effect is the same: a spend row is
8331
+ stamped to an account the current resolution no longer names.
8332
+
8333
+ The restore is scoped in three ways, and each one bounds the blast radius of
8334
+ a mistake:
8335
+
8336
+ * to the nominal ranges of groups some attribution record names, currently
8337
+ or at assertion time;
8338
+ * to rows whose stamp is an account some attribution record ASSERTED, so a
8339
+ per-file decision the operator never touched is never a candidate;
8340
+ * to rows whose stamp is not already an account the CURRENT WORLD can
8341
+ justify for a group containing that instant — a resolved assertion's
8342
+ owner, or a group's own native evidence — which is what makes a second
8343
+ run write nothing.
8344
+
8345
+ Costs a store with no attribution records exactly one indexed read.
8346
+
8347
+ ``strict`` propagates failures for ``account attribute``'s all-or-nothing
8348
+ apply; the default swallows them the way the sync-time adoption pass does,
8349
+ because everything here is re-derivable on the next sync.
8350
+ """
8351
+ from _cctally_quota import resolve_codex_window_attributions_with_evidence
8352
+
8353
+ try:
8354
+ active = load_active_window_attributions(conn)
8355
+ retracted = load_active_window_attributions(conn, retracted_only=True)
8356
+ except sqlite3.DatabaseError:
8357
+ if strict:
8358
+ raise
8359
+ return (0, 0)
8360
+ records = (*active, *retracted)
8361
+ if not records:
8362
+ return (0, 0)
8363
+
8364
+ roots = sorted({str(record["source_root_key"]) for record in records})
8365
+ try:
8366
+ _resolutions, ownership, groups = (
8367
+ resolve_codex_window_attributions_with_evidence(
8368
+ conn, source_root_keys=roots, include_retracted=True))
8369
+ except sqlite3.DatabaseError:
8370
+ if strict:
8371
+ raise
8372
+ return (0, 0)
8373
+
8374
+ asserted_keys = sorted({str(record["account_key"]) for record in records})
8375
+ week = _CODEX_ACCOUNT_WEEK
8376
+
8377
+ # The ranges to visit: every group an assertion currently owns, plus every
8378
+ # group any record named at assertion time. The second half is what reaches
8379
+ # a retraction, a newly dormant assertion and a component split — all three
8380
+ # move the rows OUT of a currently-resolved group, so a currently-resolved
8381
+ # set alone would never look at them again.
8382
+ ranges: "dict[str, list[tuple[dt.datetime, dt.datetime]]]" = {}
8383
+ # Two deliberate loosenesses in `justified`, recorded because they are
8384
+ # KNOWN and neither is worth restructuring this map for:
8385
+ #
8386
+ # 1. The predicate is "some covering group justifies this account", while
8387
+ # the adoption fold's rule is "the UNION of identified accounts across
8388
+ # every claiming window is exactly one". The two agree wherever one
8389
+ # account claims an instant and disagree where two do: the fold declines
8390
+ # to stamp, this map still justifies keeping a stamp already there. The
8391
+ # error is one-directional — it retains a stamp the operator's own
8392
+ # assertion put on a row the fold now declines to re-derive — and the
8393
+ # alternative, evaluating the union per instant, would cost a per-row
8394
+ # fold rather than a bisect.
8395
+ # 2. It is keyed on `(root, account)` with no group axes, so a
8396
+ # justification earned by one group covers a same-account stamp inside
8397
+ # any other group whose span contains that instant. Weekly spans on one
8398
+ # root overlap by construction, so this is reachable rather than
8399
+ # theoretical; it is also the same one-directional retention.
8400
+ justified: "dict[tuple[str, str], list[tuple[dt.datetime, dt.datetime]]]" = {}
8401
+ for group_key, account_key in ownership.items():
8402
+ root_key, reset = str(group_key[1]), group_key[5]
8403
+ ranges.setdefault(root_key, []).append((reset - week, reset))
8404
+ justified.setdefault((root_key, str(account_key)), []).append(
8405
+ (reset - week, reset))
8406
+ # A group's own NATIVE accounts justify a stamp exactly as ownership does:
8407
+ # the fold adopts every unattributed member of such a group into that
8408
+ # account, so a spend row already carrying it is at the answer the current
8409
+ # world gives. Reading it from the loaded GROUPS rather than from a
8410
+ # SUPPRESSED_NATIVE resolution also covers the dormant and split shapes,
8411
+ # whose resolutions name no group at all.
8412
+ #
8413
+ # Without this the reconciliation churns forever whenever a group resolving
8414
+ # SUPPRESSED_NATIVE names an account the operator also asserted elsewhere —
8415
+ # which is ordinary, not exotic: the row is restored to a different per-file
8416
+ # baseline and immediately re-adopted to the same account, on every sync,
8417
+ # each time printing a line claiming an attribution no longer resolves. The
8418
+ # end state was right; the write behaviour was not idempotent.
8419
+ #
8420
+ # An OUT-OF-SCOPE group justifies nothing, though. The kernel files a
8421
+ # model-scoped group as SUPPRESSED_MODEL_SCOPED — it "neither stamps nor
8422
+ # blocks" — so it can never be the source of a stamp, and letting it
8423
+ # authorize keeping one is one-directional damage: the span below is a whole
8424
+ # week, so a single Spark-labelled capture naming the account the operator
8425
+ # asserted covers every stamped row in that week and a retraction over it
8426
+ # silently accomplishes nothing. Such a group reaches this loop because it
8427
+ # shares the assertion's four stored axes and its anchor while interpreting
8428
+ # to a different limit key. `SUPPRESSED_NATIVE` is unaffected: that shape is
8429
+ # in-scope by construction.
8430
+ for group in groups:
8431
+ if not group.in_scope:
8432
+ continue
8433
+ reset = group.group_key[5]
8434
+ for account_key in group.identified_accounts:
8435
+ justified.setdefault(
8436
+ (group.source_root_key, str(account_key)), []).append(
8437
+ (reset - week, reset))
8438
+ for record in records:
8439
+ reset = _parse_anchor_iso(record["canonical_resets_at_utc"])
8440
+ if reset is None:
8441
+ continue
8442
+ ranges.setdefault(str(record["source_root_key"]), []).append(
8443
+ (reset - week, reset))
8444
+ if not ranges:
8445
+ return (0, 0)
8446
+
8447
+ # Both membership questions are asked once per candidate row, and the SQL
8448
+ # range is the union of a root's spans, so on a whole-history attribution
8449
+ # that is every stamped row against every span. Indexing them turns two
8450
+ # linear scans into two binary searches: measured read-only on the
8451
+ # maintainer's store at the §9.1 shape (58,806 stamped rows, 180 spans, 90
8452
+ # justifications), the filter alone went from 358.8 ms to 30.3 ms — per
8453
+ # `sync_codex_cache`, inside both cache flocks.
8454
+ span_index = {
8455
+ root_key: _codex_span_index(spans)
8456
+ for root_key, spans in ranges.items()
8457
+ }
8458
+ justified_index = {
8459
+ key: _codex_span_index(spans) for key, spans in justified.items()
8460
+ }
8461
+
8462
+ resolver = _CodexFileBaselineResolver(conn)
8463
+ restored = 0
8464
+ try:
8465
+ for root_key, spans in sorted(ranges.items()):
8466
+ low = min(span[0] for span in spans)
8467
+ high = max(span[1] for span in spans)
8468
+ updates: "list[tuple[str | None, int]]" = []
8469
+ for row in conn.execute(
8470
+ "SELECT id, source_path, line_offset, timestamp_utc, "
8471
+ " account_key "
8472
+ " FROM codex_session_entries "
8473
+ " WHERE source_root_key = ? "
8474
+ " AND account_key IN ("
8475
+ + ",".join("?" * len(asserted_keys)) + ") "
8476
+ " AND unixepoch(timestamp_utc) >= unixepoch(?) "
8477
+ " AND unixepoch(timestamp_utc) <= unixepoch(?)",
8478
+ (root_key, *asserted_keys,
8479
+ _codex_anchor_iso(low), _codex_anchor_iso(high)),
8480
+ ):
8481
+ account_key = str(row[4])
8482
+ timestamp = _parse_anchor_iso(row[3])
8483
+ if timestamp is None:
8484
+ continue
8485
+ if not _codex_span_covers(span_index[root_key], timestamp):
8486
+ continue
8487
+ owner = justified_index.get((root_key, account_key))
8488
+ if owner is not None and _codex_span_covers(owner, timestamp):
8489
+ continue
8490
+ try:
8491
+ baseline = resolver.baseline(root_key, row[1], row[2])
8492
+ except (ValueError, OSError):
8493
+ # `codex_file_key` refuses a blank path and `pathlib`
8494
+ # refuses a NUL byte; neither is a `sqlite3.DatabaseError`,
8495
+ # so an unrecoverable row would have failed the WHOLE Codex
8496
+ # sync rather than degrading. A row whose durable file
8497
+ # identity cannot be recovered has no baseline to restore
8498
+ # to, so it is skipped — the same rule the quota loader
8499
+ # applies window-by-window rather than suppressing valid
8500
+ # ones. `strict` still propagates, because the command's
8501
+ # all-or-nothing apply must not report success over it.
8502
+ if strict:
8503
+ raise
8504
+ continue
8505
+ if baseline == account_key:
8506
+ continue
8507
+ updates.append((baseline, int(row[0])))
8508
+ if updates:
8509
+ cursor = conn.executemany(
8510
+ "UPDATE codex_session_entries SET account_key = ? "
8511
+ " WHERE id = ?",
8512
+ updates,
8513
+ )
8514
+ restored += max(0, int(cursor.rowcount))
8515
+ except sqlite3.DatabaseError:
8516
+ if strict:
8517
+ raise
8518
+ return (0, 0)
8519
+
8520
+ touched = {
8521
+ root_key: (min(span[0] for span in spans),
8522
+ max(span[1] for span in spans))
8523
+ for root_key, spans in ranges.items()
8524
+ }
8525
+ adopted = apply_codex_window_spend_adoption(
8526
+ conn, touched=touched, strict=strict)
8527
+ return (restored, adopted)
7745
8528
 
7746
8529
 
7747
8530
  def iter_codex_entries(
@@ -8596,6 +9379,21 @@ def open_cache_db() -> sqlite3.Connection:
8596
9379
  conn.execute("PRAGMA journal_mode").fetchone()[0]
8597
9380
  ).lower()
8598
9381
 
9382
+ # #566: the refusal must precede the DDL it exists to prevent. The
9383
+ # dispatcher's #142 guard runs inside
9384
+ # `_run_pending_cache_migrations_under_writer_lock`, which this path
9385
+ # reaches only after `apply_policy`, `_apply_cache_schema` and the
9386
+ # `last_total_tokens` ALTER-plus-purge. A dev-checkout binary pointed at
9387
+ # the real prod dir therefore modified the production schema and only
9388
+ # then refused. Evaluated here, under both flocks and after the gates
9389
+ # were re-read, nothing persistent has been written yet.
9390
+ try:
9391
+ _cctally_db_sib._refuse_prod_migration_before_schema_write(
9392
+ conn, _CACHE_MIGRATIONS, "cache.db")
9393
+ except _cctally_db_sib.ProdMigrationRefused:
9394
+ conn.close()
9395
+ raise
9396
+
8599
9397
  if not schema_current or journal_mode != "wal":
8600
9398
  _cctally_store.apply_policy(conn, "cache")
8601
9399
  else: