cctally 1.117.2 → 1.118.1

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.
Files changed (58) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +7 -3
  3. package/bin/_cctally_cache.py +2873 -193
  4. package/bin/_cctally_completion.py +106 -33
  5. package/bin/_cctally_core.py +69 -4
  6. package/bin/_cctally_dashboard.py +188 -13
  7. package/bin/_cctally_dashboard_conversation.py +5 -1
  8. package/bin/_cctally_dashboard_envelope.py +50 -19
  9. package/bin/_cctally_dashboard_sources.py +23 -1
  10. package/bin/_cctally_db.py +193 -153
  11. package/bin/_cctally_diagnosis_sources.py +63 -7
  12. package/bin/_cctally_doctor.py +865 -93
  13. package/bin/_cctally_five_hour.py +67 -50
  14. package/bin/_cctally_forecast.py +35 -13
  15. package/bin/_cctally_journal.py +186 -18
  16. package/bin/_cctally_milestone_history.py +859 -62
  17. package/bin/_cctally_oauth_samples.py +286 -15
  18. package/bin/_cctally_parser.py +2 -2
  19. package/bin/_cctally_quota.py +104 -12
  20. package/bin/_cctally_record.py +153 -29
  21. package/bin/_cctally_rederive.py +608 -68
  22. package/bin/_cctally_statusline.py +108 -81
  23. package/bin/_cctally_store.py +179 -38
  24. package/bin/_cctally_tui.py +892 -181
  25. package/bin/_cctally_update.py +32 -13
  26. package/bin/_lib_codex_conversation_query.py +52 -14
  27. package/bin/_lib_codex_drain_admission.py +224 -6
  28. package/bin/_lib_codex_metadata.py +39 -0
  29. package/bin/_lib_conversation_anon.py +15 -8
  30. package/bin/_lib_conversation_dispatch.py +7 -0
  31. package/bin/_lib_conversation_query.py +289 -70
  32. package/bin/_lib_dashboard_sources.py +37 -1
  33. package/bin/_lib_detector_access_registry.py +497 -26
  34. package/bin/_lib_doctor.py +85 -20
  35. package/bin/_lib_doctor_scan_index.py +321 -0
  36. package/bin/_lib_ingest_frontier.py +20 -1
  37. package/bin/_lib_oauth_samples.py +23 -7
  38. package/bin/_lib_pricing.py +146 -16
  39. package/bin/_lib_quota.py +19 -0
  40. package/bin/_lib_rederive.py +11 -6
  41. package/bin/_lib_retained_records.py +516 -0
  42. package/bin/_lib_snapshot_cache.py +10 -0
  43. package/bin/_lib_stale_proof.py +272 -43
  44. package/bin/_lib_stats_content_revision.py +831 -0
  45. package/bin/_lib_write_io.py +11 -4
  46. package/bin/cctally +4 -1
  47. package/dashboard/static/assets/ConversationsView-D4Iv4ku0.js +72 -0
  48. package/dashboard/static/assets/{DoctorModal-B0DnU9d9.js → DoctorModal-D4QSq6uz.js} +1 -1
  49. package/dashboard/static/assets/ModalRoot-C9F5dhZi.js +1 -0
  50. package/dashboard/static/assets/{ProjectsDrillPanel-DSxXQSa-.js → ProjectsDrillPanel-WxAdRDMt.js} +1 -1
  51. package/dashboard/static/assets/{SourceDetailModal-BleDpQqG.js → SourceDetailModal-NyyEfXmc.js} +1 -1
  52. package/dashboard/static/assets/{UpdateModal-CMqaAkSK.js → UpdateModal-CFLq9pT1.js} +1 -1
  53. package/dashboard/static/assets/{index-BjQV-0-D.js → index-BKTWH9JE.js} +2 -2
  54. package/dashboard/static/assets/{outlineNavigation-CO-VWBm7.js → outlineNavigation-BnGUwCS3.js} +3 -3
  55. package/dashboard/static/dashboard.html +1 -1
  56. package/package.json +4 -1
  57. package/dashboard/static/assets/ConversationsView-oqNsWzWV.js +0 -72
  58. package/dashboard/static/assets/ModalRoot-CYua2wP_.js +0 -1
@@ -105,6 +105,7 @@ import contextvars
105
105
  import datetime as dt
106
106
  import errno
107
107
  import fcntl
108
+ import hashlib
108
109
  import json
109
110
  import os
110
111
  import pathlib
@@ -117,6 +118,7 @@ import subprocess
117
118
  import sys
118
119
  import tempfile
119
120
  import time
121
+ import uuid
120
122
  from dataclasses import asdict, dataclass, field, replace as _dc_replace
121
123
  from typing import Any, Callable, Iterable, Iterator, NamedTuple
122
124
 
@@ -287,21 +289,34 @@ _AI_TITLE_UPSERT_SQL = (
287
289
  "ai_title=excluded.ai_title, source_path=excluded.source_path, byte_offset=excluded.byte_offset"
288
290
  )
289
291
 
290
- def _ai_title_upsert_sql(table: str = "conversation_ai_titles") -> str:
292
+ def _ai_title_upsert_sql(table: str = "conversation_ai_titles", *,
293
+ owner_only: bool = False) -> str:
291
294
  """The AI-title upsert, targeted at the live table or its staging twin.
292
295
 
293
296
  A rebuild builds its replacement generation into staging and leaves the
294
- live table alone until the publish (#752), so the ONE statement that writes
295
- a title has to be able to name either. Parameterised through this helper
296
- rather than by two literals, because two literals drift.
297
+ live table alone until the publish (#752) — except for a file replayed
298
+ under a NEW incarnation, whose live title rows are replaced,
299
+ owner-restricted, at its batch commit (#1062 §4.2). So the ONE statement
300
+ that writes a title has to be able to name either table. Parameterised
301
+ through this helper rather than by several literals, because literals
302
+ drift.
303
+
304
+ ``owner_only`` is that #1062 §4.2 live-table write. The file's own live
305
+ rows were just deleted, so a conflicting live row belongs to ANOTHER file
306
+ of the same session and is kept until the publish installs the replayed
307
+ generation's choice; a later title line of the same file still replaces an
308
+ earlier one, as in the walk.
297
309
  """
298
- return (
310
+ sql = (
299
311
  f"INSERT INTO {table}(session_id,ai_title,source_path,byte_offset) "
300
312
  "VALUES(?,?,?,?) "
301
313
  "ON CONFLICT(session_id) DO UPDATE SET "
302
314
  "ai_title=excluded.ai_title, source_path=excluded.source_path, "
303
315
  "byte_offset=excluded.byte_offset"
304
316
  )
317
+ if owner_only:
318
+ sql += f" WHERE {table}.source_path = excluded.source_path"
319
+ return sql
305
320
 
306
321
 
307
322
  # ---------------------------------------------------------------------------
@@ -708,6 +723,14 @@ CODEX_REPLAY_DRAIN_MARKER_NAME = "codex-replay-drain.last-attempt"
708
723
  #: far more often than the `doctor` WARN it produces is actionable.
709
724
  CODEX_REPLAY_DRAIN_SPAWN_THROTTLE_SECONDS = 3600
710
725
 
726
+ #: #982 spec 4.5a: the same spawn's spacing while a well-formed heal record
727
+ #: exists. A pass that ended `contended` or `degraded` leaves the record (in
728
+ #: phase `units`, a global fence over the population and every one-way stamp),
729
+ #: and the executor resumes it per file, so the retry costs one unit's work,
730
+ #: not a whole-tree re-read; waiting the hourly slot would keep the fence up
731
+ #: for an hour after any stall.
732
+ CODEX_HEAL_RETRY_THROTTLE_SECONDS = 300
733
+
711
734
  #: How long the drain worker waits for the cache writer flocks. The default
712
735
  #: ``None`` is a single non-blocking attempt, which spends the whole hourly slot
713
736
  #: on whichever ordinary sync happened to hold the lock at that instant. Nobody
@@ -871,6 +894,22 @@ def _set_cache_meta_if_changed(
871
894
  )
872
895
 
873
896
 
897
+ def _clear_claude_walk_verdicts(conn: sqlite3.Connection) -> None:
898
+ """Delete both Claude walk verdicts inside a destructive wipe's transaction.
899
+
900
+ The walk-complete marker and #916's residual token each vouch for the
901
+ cache's contents, so a rebuild or truncation wipe drops them in the same
902
+ transaction as the rows: a crash between the wipe's commit and the end of
903
+ its walk must not leave either one vouching for an emptied store. The
904
+ caller commits.
905
+ """
906
+ conn.execute(
907
+ "DELETE FROM cache_meta WHERE key IN (?, ?)",
908
+ ("claude_ingest_walk_complete",
909
+ _ingest_frontier.CLAUDE_WALK_ORPHAN_RESIDUAL_KEY),
910
+ )
911
+
912
+
874
913
  def _withdraw_cache_meta_marker(conn: sqlite3.Connection, key: str) -> None:
875
914
  """Remove a walk-completion marker, writing only when it is present.
876
915
 
@@ -1076,6 +1115,29 @@ class ProjectKey:
1076
1115
  is_no_git: bool = field(default=False, compare=False)
1077
1116
 
1078
1117
 
1118
+ def _is_repository_marker(marker: str) -> bool:
1119
+ """Whether a `.git` entry marks a repository for the project walk (#976).
1120
+
1121
+ A bounded structural filter, not a Git validity check: an absent entry (or
1122
+ a dangling symlink) is no marker; a `.git` DIRECTORY (a symlink to one
1123
+ included) counts only when it holds ``HEAD`` and the ``objects/`` and
1124
+ ``refs/`` directories, so a stray non-repository `.git/` in a worktree
1125
+ container is skipped; anything else (a worktree or submodule `gitdir:`
1126
+ file) counts by presence. Metadata calls only: nothing is opened and no
1127
+ `gitdir:` or `commondir` is followed, which keeps the walk inside the
1128
+ frozen-roots stat contract (`bench/write-attribution/frozen_roots.py`).
1129
+ Only the `git-root` walk uses this; `_is_dev_checkout()` and the other
1130
+ `.git` presence predicates keep their own installation-safety contract.
1131
+ """
1132
+ if not os.path.exists(marker):
1133
+ return False
1134
+ if os.path.isdir(marker):
1135
+ return (os.path.exists(os.path.join(marker, "HEAD"))
1136
+ and os.path.isdir(os.path.join(marker, "objects"))
1137
+ and os.path.isdir(os.path.join(marker, "refs")))
1138
+ return True
1139
+
1140
+
1079
1141
  def _resolve_project_key(
1080
1142
  project_path: str | None,
1081
1143
  mode: str, # "git-root" | "full-path"
@@ -1083,8 +1145,9 @@ def _resolve_project_key(
1083
1145
  ) -> ProjectKey:
1084
1146
  """Resolve a raw project_path to its ProjectKey.
1085
1147
 
1086
- Walks parents looking for `.git` (file or dir) to find the canonical
1087
- git-root. Non-git paths fall back to the normalized path. NULL input
1148
+ Walks parents looking for a repository marker (`_is_repository_marker`: a
1149
+ `.git` file, or a `.git` directory holding HEAD, objects/ and refs/) to
1150
+ find the canonical git-root. Non-git paths fall back to the normalized path. NULL input
1088
1151
  becomes a literal `(unknown)` bucket.
1089
1152
  """
1090
1153
  if project_path is None:
@@ -1136,7 +1199,7 @@ def _resolve_project_key(
1136
1199
  while True:
1137
1200
  if cur == home or cur == "/" or os.path.dirname(cur) == cur:
1138
1201
  break
1139
- if os.path.exists(os.path.join(cur, ".git")):
1202
+ if _is_repository_marker(os.path.join(cur, ".git")):
1140
1203
  key = ProjectKey(
1141
1204
  bucket_path=cur,
1142
1205
  display_key=os.path.basename(cur) or cur,
@@ -1708,12 +1771,18 @@ class CodexResetAnchorResolver:
1708
1771
  """
1709
1772
 
1710
1773
  def __init__(self, conn: sqlite3.Connection, *,
1711
- deadline: "float | None" = None, clock=None):
1774
+ deadline: "float | None" = None, clock=None,
1775
+ seed_source: "Callable[[tuple[str, str, str, object], str], _lib_quota.ResetAnchorComponents | None] | None" = None):
1712
1776
  self._conn = conn
1713
1777
  # #980 F3: a budgeted walk passes its walk deadline and clock, which
1714
1778
  # bound the group seed read; unbudgeted callers pass neither.
1715
1779
  self._deadline = deadline
1716
1780
  self._clock = clock if clock is not None else time.monotonic
1781
+ # #975 (spec 4.2): `seed_source(group, logical_limit_key)` supplies a
1782
+ # group's committed evidence in place of the seed SELECT whenever it
1783
+ # returns components (the provenance drain hands out a private copy of
1784
+ # its committed-evidence base); `None` falls back to the SELECT.
1785
+ self._seed_source = seed_source
1717
1786
  self._groups: "dict[tuple[str, str, str, object], _lib_quota.ResetAnchorComponents]" = {}
1718
1787
  self._pending_merges: list[
1719
1788
  tuple[tuple[str, str, str, object], str, dt.datetime,
@@ -1744,6 +1813,11 @@ class CodexResetAnchorResolver:
1744
1813
  components = self._groups.get(group)
1745
1814
  if components is not None:
1746
1815
  return components
1816
+ if self._seed_source is not None:
1817
+ components = self._seed_source(group, logical_limit_key)
1818
+ if components is not None:
1819
+ self._groups[group] = components
1820
+ return components
1747
1821
  components = _lib_quota.ResetAnchorComponents()
1748
1822
  if not self._seed_failed:
1749
1823
  candidates = _lib_jsonl.codex_snap_equivalent_limit_keys(
@@ -1922,6 +1996,219 @@ def _codex_anchor_iso(value: dt.datetime) -> str:
1922
1996
  return value.astimezone(dt.timezone.utc).isoformat().replace("+00:00", "Z")
1923
1997
 
1924
1998
 
1999
+ class _CodexWalkShare:
2000
+ """History-sized state shared across the admitted units of one pass.
2001
+
2002
+ #982 / #975 spec 4.2. One in-process object per heal-executor or
2003
+ provenance pass, never persisted. Every reuse is guarded by a token read
2004
+ under the writer locks: ``(st_dev, st_ino)`` of cache.db, ``PRAGMA
2005
+ schema_version``, ``quota_window_change_log``'s ``sqlite_sequence`` value
2006
+ and ``codex_physical_mutation_seq``. The ledger's three triggers record
2007
+ every insert, delete and seeded-column update of a Codex quota row, so any
2008
+ other writer's quota mutation between two units changes the token; while
2009
+ the ledger table or a trigger is missing the token is ``None`` and nothing
2010
+ is reused. A bind consumes the seal, so a unit that never sealed (an
2011
+ exception, a rollback, a refusal) leaves the next unit seeding afresh.
2012
+
2013
+ - ``bind_walk`` / ``seal``: the walk memo, one ``CodexResetAnchorResolver``
2014
+ shared across population units, i.e. the single walk's own resolver.
2015
+ - ``identity_resolver`` / ``absorb_identity`` / ``seal``: the provenance
2016
+ committed-evidence seed. Each identity resolves against copies of a
2017
+ per-group base equal to a fresh seed of the committed rows; after the
2018
+ identity commits, the base gains exactly that identity's stored rows.
2019
+ Components and anchors do not depend on insertion order, so base plus
2020
+ absorbed rows equals a fresh seed, and evidence an identity resolved but
2021
+ did not store dies with its copy, exactly as a new resolver per call.
2022
+ - ``ledger_paths``: the accounting ledger's path set, read in full once
2023
+ and then only past the last ``seq`` seen. A positive signal only (I7),
2024
+ so a path whose rows a prune later removed may stay in the set.
2025
+ - ``heal_payload``: the heal record's population and D1 set, parsed once
2026
+ per pass and reused while the fence key keeps the same ``gen`` (a
2027
+ payload is only ever written with a new ``gen``).
2028
+ """
2029
+
2030
+ _LEDGER_TRIGGERS = (
2031
+ "trg_qws_ledger_ins", "trg_qws_ledger_del", "trg_qws_ledger_upd")
2032
+
2033
+ def __init__(self) -> None:
2034
+ self._sealed: "tuple | None" = None
2035
+ self._walk: "CodexResetAnchorResolver | None" = None
2036
+ self._base: "CodexResetAnchorResolver | None" = None
2037
+ self._ledger: "set[str] | None" = None
2038
+ self._ledger_seq = 0
2039
+ self._ledger_identity: "tuple | None" = None
2040
+ self._heal_payload: "CodexHealPayload | None" = None
2041
+
2042
+ def heal_payload(self, conn: sqlite3.Connection,
2043
+ record: "dict | None") -> "CodexHealPayload | None":
2044
+ """The record's parsed payload: the one this pass already parsed
2045
+ while ``record`` keeps its ``gen``, otherwise parsed once now."""
2046
+ if record is None or record.get("malformed"):
2047
+ return None
2048
+ cached = self._heal_payload
2049
+ if cached is not None and cached.gen == record.get("gen"):
2050
+ return cached
2051
+ payload = load_codex_heal_payload(conn, record)
2052
+ if payload is not None:
2053
+ self._heal_payload = payload
2054
+ return payload
2055
+
2056
+ def remember_heal_payload(self, payload: "CodexHealPayload") -> None:
2057
+ """A payload this pass wrote itself (the history start unit)."""
2058
+ self._heal_payload = payload
2059
+
2060
+ def forget_heal_payload(self) -> None:
2061
+ """A pass that ended ``restart`` (spec 4.5a) keeps no payload."""
2062
+ self._heal_payload = None
2063
+
2064
+ @classmethod
2065
+ def token(cls, conn: sqlite3.Connection) -> "tuple | None":
2066
+ """The reuse token, or ``None`` when no reuse may be trusted."""
2067
+ from _cctally_quota import codex_physical_mutation_seq
2068
+ try:
2069
+ main = next((row[2] for row in conn.execute("PRAGMA database_list")
2070
+ if row[1] == "main"), None)
2071
+ if not main:
2072
+ return None
2073
+ st = os.stat(main)
2074
+ present = {row[0] for row in conn.execute(
2075
+ "SELECT name FROM sqlite_master "
2076
+ "WHERE (type='table' AND name='quota_window_change_log') "
2077
+ " OR (type='trigger' AND name IN (?,?,?))",
2078
+ cls._LEDGER_TRIGGERS)}
2079
+ if len(present) != 1 + len(cls._LEDGER_TRIGGERS):
2080
+ return None
2081
+ schema_version = int(
2082
+ conn.execute("PRAGMA schema_version").fetchone()[0])
2083
+ row = conn.execute(
2084
+ "SELECT seq FROM sqlite_sequence "
2085
+ "WHERE name='quota_window_change_log'").fetchone()
2086
+ ledger_seq = 0 if row is None else int(row[0])
2087
+ except (sqlite3.Error, OSError, TypeError, ValueError):
2088
+ return None
2089
+ return (st.st_dev, st.st_ino, schema_version, ledger_seq,
2090
+ codex_physical_mutation_seq(conn))
2091
+
2092
+ def _validate(self, conn: sqlite3.Connection) -> None:
2093
+ token = self.token(conn)
2094
+ if token is None or self._sealed is None or token != self._sealed:
2095
+ self._walk = None
2096
+ self._base = None
2097
+ self._sealed = None
2098
+
2099
+ def bind_walk(self, conn: sqlite3.Connection) -> CodexResetAnchorResolver:
2100
+ """The shared walk resolver rebound to ``conn`` (new on a token change)."""
2101
+ self._validate(conn)
2102
+ if self._walk is None:
2103
+ self._walk = CodexResetAnchorResolver(conn)
2104
+ else:
2105
+ self._walk._conn = conn
2106
+ self._walk._pending_merges.clear()
2107
+ return self._walk
2108
+
2109
+ def seal(self, conn: sqlite3.Connection) -> None:
2110
+ """Record the token after this unit's last commit, under its locks."""
2111
+ self._sealed = self.token(conn)
2112
+
2113
+ def invalidate(self) -> None:
2114
+ """Drop everything: the next unit seeds from the committed store."""
2115
+ self._sealed = None
2116
+ self._walk = None
2117
+ self._base = None
2118
+ self._ledger = None
2119
+ self._ledger_seq = 0
2120
+ self._ledger_identity = None
2121
+
2122
+ def identity_resolver(
2123
+ self, conn: sqlite3.Connection) -> CodexResetAnchorResolver:
2124
+ """A resolver for one provenance identity over copies of the base."""
2125
+ self._validate(conn)
2126
+ if self._base is None:
2127
+ self._base = CodexResetAnchorResolver(conn)
2128
+ else:
2129
+ self._base._conn = conn
2130
+ return CodexResetAnchorResolver(conn, seed_source=self._seed_copy)
2131
+
2132
+ def _seed_copy(self, group, logical_limit_key: str):
2133
+ base = self._base
2134
+ if base is None:
2135
+ return None
2136
+ components = base._components_for(group, logical_limit_key)
2137
+ if base._seed_failed:
2138
+ # Never remember a degraded seed; the identity's own resolver
2139
+ # then degrades exactly as a new resolver per call does.
2140
+ self._base = None
2141
+ return None
2142
+ return components.copy()
2143
+
2144
+ def absorb_identity(
2145
+ self, conn: sqlite3.Connection, source_path: str) -> None:
2146
+ """Add one committed identity's stored quota rows to the base.
2147
+
2148
+ Mirrors the seed SELECT's membership exactly: a row belongs to every
2149
+ seeded group with its root and slot whose snap-equivalent limit keys
2150
+ include the row's key (the seed does not filter on the window), with
2151
+ its real order key ``(source_path, line_offset, id)``. A group not yet
2152
+ seeded is skipped: its first seed reads these rows anyway.
2153
+ """
2154
+ base = self._base
2155
+ if base is None or not base._groups:
2156
+ return
2157
+ by_scope: "dict[tuple[str, str], list]" = {}
2158
+ for group in base._groups:
2159
+ by_scope.setdefault((group[0], group[1]), []).append(group)
2160
+ limit_keys: "dict[tuple, frozenset[str]]" = {}
2161
+ for (slot, limit_key, _window, root, path, offset, row_id,
2162
+ raw_text) in conn.execute(
2163
+ "SELECT observed_slot, logical_limit_key, window_minutes, "
2164
+ "source_root_key, source_path, line_offset, id, resets_at_utc "
2165
+ "FROM quota_window_snapshots "
2166
+ "WHERE source='codex' AND source_path=?", (str(source_path),)):
2167
+ groups = by_scope.get((root, slot))
2168
+ if not groups:
2169
+ continue
2170
+ raw = _parse_anchor_iso(raw_text)
2171
+ if raw is None:
2172
+ continue
2173
+ for group in groups:
2174
+ keys = limit_keys.get(group)
2175
+ if keys is None:
2176
+ keys = limit_keys[group] = frozenset(
2177
+ _lib_jsonl.codex_snap_equivalent_limit_keys(
2178
+ str(group[2])))
2179
+ if limit_key in keys:
2180
+ base._groups[group].add(
2181
+ raw, order_key=(str(path), int(offset), int(row_id)))
2182
+
2183
+ def ledger_paths(self, conn: sqlite3.Connection) -> "frozenset[str]":
2184
+ """``_codex_ledger_paths`` across units: one full read per pass, then
2185
+ only the ledger rows past the last ``seq`` seen. A cache.db file or
2186
+ ``schema_version`` change (or an unusable token) forces a full read."""
2187
+ token = self.token(conn)
2188
+ identity = None if token is None else token[:3]
2189
+ if (identity is None or self._ledger is None
2190
+ or identity != self._ledger_identity):
2191
+ rows = conn.execute(
2192
+ "SELECT seq, source_path FROM codex_accounting_change_log "
2193
+ "WHERE change_kind='path'").fetchall()
2194
+ self._ledger, self._ledger_seq = set(), 0
2195
+ self._ledger_identity = identity
2196
+ else:
2197
+ rows = conn.execute(
2198
+ "SELECT seq, source_path FROM codex_accounting_change_log "
2199
+ "WHERE change_kind='path' AND seq > ?",
2200
+ (self._ledger_seq,)).fetchall()
2201
+ for seq, path in rows:
2202
+ if path:
2203
+ self._ledger.add(path)
2204
+ if seq > self._ledger_seq:
2205
+ self._ledger_seq = seq
2206
+ paths = frozenset(self._ledger)
2207
+ if identity is None:
2208
+ self._ledger = None
2209
+ return paths
2210
+
2211
+
1925
2212
  def _codex_provider_roots() -> list[CodexProviderRoot]:
1926
2213
  """Return configured provider roots with their sessions/direct walk roots.
1927
2214
 
@@ -4258,14 +4545,26 @@ def _codex_barrier_seal_pending(conn, *, priority=(), max_keys=None,
4258
4545
 
4259
4546
  The caller holds the cache writer locks and has committed its own work.
4260
4547
  Reads only cache.db (no stat, no decode) and commits once.
4261
- """
4262
- if allow_init:
4263
- _codex_barrier_init_targets(conn)
4264
4548
 
4549
+ #982 spec 4.3 (seal fence): while a heal fence holds, nothing is sealed
4550
+ (census ``heal-fenced``, no scan needed, so no caller requests the worker
4551
+ for it): a per-file unit could otherwise seal a conversation whose
4552
+ cursorless population contributor is still unread, under a census set
4553
+ before the fence. Sealing resumes after the transition, whose piece sets
4554
+ the census from the whole walk's counters.
4555
+ """
4265
4556
  def remaining() -> int:
4266
4557
  return int(conn.execute(
4267
4558
  "SELECT COUNT(*) FROM codex_barrier_repairs").fetchone()[0])
4268
4559
 
4560
+ if _codex_heal_fence_active(conn):
4561
+ if conn.in_transaction:
4562
+ conn.commit()
4563
+ return BarrierSealResult(0, 0, remaining(), "heal-fenced", False,
4564
+ start_after)
4565
+ if allow_init:
4566
+ _codex_barrier_init_targets(conn)
4567
+
4269
4568
  if not _codex_barrier_census_proven(conn):
4270
4569
  if conn.in_transaction:
4271
4570
  conn.commit()
@@ -4334,7 +4633,10 @@ def _codex_barrier_after_walk(conn, stats, *, targeted: bool, deadline,
4334
4633
  set(stats.drain_requested) | {CODEX_REPLAY_DRAIN_COMMAND}))
4335
4634
 
4336
4635
 
4337
- def _codex_admitted_cache_write(fn, *, wait_for_requests: bool):
4636
+ def _codex_admitted_cache_write(
4637
+ fn, *, wait_for_requests: bool,
4638
+ after_commit: "Callable[[sqlite3.Connection], None] | None" = None,
4639
+ contention_bound: "float | None" = None):
4338
4640
  """Run ``fn(conn)`` through the drain gate under the cache lock domain.
4339
4641
 
4340
4642
  The store is opened BEFORE admission takes the cache flocks (review N5;
@@ -4347,10 +4649,20 @@ def _codex_admitted_cache_write(fn, *, wait_for_requests: bool):
4347
4649
  is busy". A connection opened for a refused admission is closed before
4348
4650
  the backoff, and the next attempt opens a fresh one.
4349
4651
 
4652
+ ``after_commit(conn)`` runs after ``fn``'s transaction committed and
4653
+ before ``adm.finish``, so it still holds both store locks: the provenance
4654
+ drain (#975, spec 4.2) refreshes its committed-evidence seed there, where
4655
+ no other writer can have interleaved. It does not run when ``fn`` raises.
4656
+
4657
+ ``contention_bound`` (a heal unit's, #982): store locks another writer
4658
+ holds continuously (no fresh hook request in between) for that many
4659
+ seconds raise ``_CodexHealContended``, as every other heal unit gives up.
4660
+
4350
4661
  Returns ``(True, result)``, or ``(False, None)`` when a fresh hook request
4351
4662
  made the caller yield and it may not wait.
4352
4663
  """
4353
4664
  import _lib_codex_drain_admission as adm
4665
+ contended_since: "float | None" = None
4354
4666
  while True:
4355
4667
  conn = open_cache_db()
4356
4668
  try:
@@ -4364,12 +4676,23 @@ def _codex_admitted_cache_write(fn, *, wait_for_requests: bool):
4364
4676
  conn.close()
4365
4677
  if not wait_for_requests:
4366
4678
  return False, None
4679
+ if contention_bound is not None:
4680
+ if adm.fresh_requests(_cctally_core.APP_DIR):
4681
+ contended_since = None
4682
+ else:
4683
+ now = time.monotonic()
4684
+ if contended_since is None:
4685
+ contended_since = now
4686
+ if now - contended_since >= contention_bound:
4687
+ raise _CodexHealContended()
4367
4688
  _drain_backoff(adm)
4368
4689
  try:
4369
4690
  try:
4370
4691
  result = fn(conn)
4371
4692
  if conn.in_transaction:
4372
4693
  conn.commit()
4694
+ if after_commit is not None:
4695
+ after_commit(conn)
4373
4696
  return True, result
4374
4697
  except BaseException:
4375
4698
  conn.rollback()
@@ -4608,9 +4931,14 @@ def _delete_codex_file_derived_rows(
4608
4931
  "DELETE FROM codex_session_entries WHERE source_path = ?" + root_clause,
4609
4932
  params,
4610
4933
  )
4934
+ # #971 spec D3: ``+`` keeps the root term out of index selection, so the
4935
+ # statement seeks the UNIQUE (source, source_path, ...) autoindex instead
4936
+ # of walking every quota row of the root through idx_qws_codex_load_order.
4937
+ quota_root_clause = (
4938
+ " AND +source_root_key IS ?" if match_source_root else "")
4611
4939
  conn.execute(
4612
4940
  "DELETE FROM quota_window_snapshots WHERE source = 'codex' "
4613
- "AND source_path = ?" + root_clause,
4941
+ "AND source_path = ?" + quota_root_clause,
4614
4942
  params,
4615
4943
  )
4616
4944
  conn.execute(
@@ -4697,6 +5025,10 @@ def _prune_codex_accounting_change_log(
4697
5025
  if retain_sequences < 1:
4698
5026
  raise ValueError("retain_sequences must be positive")
4699
5027
  try:
5028
+ if _codex_heal_fence_active(conn):
5029
+ # #982 spec 4.3: the heal's shared ledger path set reads these
5030
+ # rows; a due prune stays owed for the executor after the fence.
5031
+ return 0
4700
5032
  cutoff = _codex_change_log_prune_cutoff(conn, retain_sequences)
4701
5033
  if cutoff is None:
4702
5034
  return 0
@@ -4970,16 +5302,14 @@ COVERAGE_WRITER_ACTIONS: "dict[str, str]" = {
4970
5302
  "_cctally_journal.rehydrate_codex_journal_families": "preserve",
4971
5303
  # Spec §4.3's migrations row, enumerated rather than named — and enumerated
4972
5304
  # by the action each one TAKES, not by the action the spec's one-line row
4973
- # assumed. Only `_024` deletes rows the journal still retains, and it is the
4974
- # only one that invalidates. `_038_codex_session_files_ingest_complete` is
4975
- # deliberately ABSENT: the review that asked for this row listed it, but it
4976
- # adds a column to `codex_session_files`, which is not a covered family —
4977
- # which is why the guard scans rather than trusts a hand list.
5305
+ # assumed. No migration deletes rows the journal still retains: since #983
5306
+ # 024 only arms the guarded byte-zero replay, whose clear is the
5307
+ # `_clear_codex_derived_rows` entry above, so it writes no covered family
5308
+ # and is absent. `_038_codex_session_files_ingest_complete` is deliberately
5309
+ # ABSENT too: the review that asked for this row listed it, but it adds a
5310
+ # column to `codex_session_files`, which is not a covered family — which is
5311
+ # why the guard scans rather than trusts a hand list.
4978
5312
  #
4979
- # `DELETE FROM quota_window_snapshots WHERE source = 'codex'` with NO
4980
- # sequence bump. Left standing, the certificate would be stale-VALID over a
4981
- # cache holding zero Codex quota rows.
4982
- "_cctally_db._024_codex_fused_ingest_rebuild": "invalidate",
4983
5313
  # `UPDATE quota_window_snapshots SET observed_model = ?`, and it bumps
4984
5314
  # `codex_physical_mutation_seq` in the same transaction whenever it changes
4985
5315
  # a row. The certificate is left standing and goes invalid on the SEQUENCE
@@ -5222,6 +5552,11 @@ def _codex_conversation_project_attribution(
5222
5552
  label, else git identity, else unassigned) so the browse live-recompute
5223
5553
  fallback matches the stored rollup. Degrades to (None, None) when the S3
5224
5554
  kernel or a valid source root is unavailable — never guesses.
5555
+
5556
+ Each call resolves with a fresh `_resolve_project_key` memo, so the answer
5557
+ never depends on which conversation resolved first: the no-git fallback
5558
+ memoizes the first raw spelling's label under the normalized path, which
5559
+ would hand one symlink alias's basename to another (#972 §5.8).
5225
5560
  """
5226
5561
  if not source_root_key:
5227
5562
  return None, None
@@ -5323,12 +5658,115 @@ def _next_conversation_render_revision(conn: sqlite3.Connection) -> int:
5323
5658
  return revision
5324
5659
 
5325
5660
 
5661
+ def _codex_first_row_root(
5662
+ conn: sqlite3.Connection, conversation_key: str,
5663
+ ) -> "str | None":
5664
+ """The source root of a conversation's first normalized row, in the
5665
+ writer's order (#972 §5.3), without loading any message body.
5666
+
5667
+ Reads ``(timestamp_utc, id)`` index order and stops at the end of the
5668
+ first timestamp tie run, which `in_physical_order` then orders exactly as
5669
+ `_load_codex_normalized_rows` orders it. None when there is no row."""
5670
+ run: list = []
5671
+ cursor = conn.execute(
5672
+ "SELECT source_root_key, timestamp_utc, source_path, line_offset "
5673
+ "FROM codex_conversation_messages WHERE conversation_key=? "
5674
+ "ORDER BY timestamp_utc, id",
5675
+ (conversation_key,),
5676
+ )
5677
+ try:
5678
+ for row in cursor:
5679
+ if run and row[1] != run[0][1]:
5680
+ break
5681
+ run.append(row)
5682
+ finally:
5683
+ cursor.close()
5684
+ if not run:
5685
+ return None
5686
+ return _lib_codex_conversation.in_physical_order(
5687
+ run, timestamp=lambda row: row[1],
5688
+ position=lambda row: (row[2], row[3]))[0][0]
5689
+
5690
+
5691
+ def _codex_rollup_thread_fields(
5692
+ conn: sqlite3.Connection,
5693
+ conversation_key: str,
5694
+ *,
5695
+ first_row_root: "Callable[[], str | None]",
5696
+ attribution_cache: "dict | None" = None,
5697
+ require_thread: bool = False,
5698
+ ) -> "tuple[str | None, str | None, str | None, str | None] | None":
5699
+ """``(source_root_key, parent_thread_id, project_key, project_label)`` of
5700
+ one conversation's rollup, from its thread row (#972 §5.3).
5701
+
5702
+ The one derivation the rollup writer and the per-pass reconcile share, so
5703
+ the reconcile detects exactly what the writer would write. The thread
5704
+ root wins when non-empty; otherwise ``first_row_root()`` supplies the
5705
+ first normalized row's root (called lazily, only then).
5706
+
5707
+ ``attribution_cache`` (#972 §5.8 rule 3) memoizes the project attribution
5708
+ per exact ``(source_root_key, cwd, git_json)`` input; each entry is still
5709
+ computed with a fresh resolver memo, so sharing it across conversations
5710
+ never makes one answer depend on another. With ``require_thread`` a
5711
+ conversation with no thread row answers None instead (rule 2: a missing
5712
+ thread row is no evidence for the reconcile)."""
5713
+ # #845 §4.7. The two columns are read as BLOBs and decoded field by
5714
+ # field, so one undecodable byte no longer raises here and no longer
5715
+ # fails the whole recompute. The table name stays UNQUALIFIED, because
5716
+ # this writer must honor account scoping: under
5717
+ # `scope_conversations_db_to_account` it sees the empty TEMP view on
5718
+ # purpose, and under #968's barrier the last-good view.
5719
+ thread = conn.execute(
5720
+ "SELECT CAST(cwd AS BLOB) AS cwd_blob, "
5721
+ "CAST(git_json AS BLOB) AS git_json_blob, parent_thread_id, "
5722
+ "source_root_key "
5723
+ "FROM codex_conversation_threads WHERE conversation_key = ?",
5724
+ (conversation_key,),
5725
+ ).fetchone()
5726
+ if thread is None and require_thread:
5727
+ return None
5728
+ cwd = git_json = parent_thread_id = thread_root = None
5729
+ thread_malformed = False
5730
+ if thread is not None:
5731
+ cwd_blob, git_json_blob, parent_thread_id, thread_root = thread
5732
+ decoded = _lib_codex_metadata.decode_codex_project_metadata(
5733
+ cwd_blob, git_json_blob)
5734
+ cwd, git_json = decoded.cwd, decoded.git_json
5735
+ # The DIRECT-ONLY predicate: this writer resolves the thread's own
5736
+ # fields through `_codex_conversation_project_attribution`, which
5737
+ # stops at a usable `cwd`, so a valid `cwd` beside an undecodable
5738
+ # `git_json` is still attributed.
5739
+ thread_malformed = _lib_codex_metadata.codex_metadata_is_malformed(
5740
+ decoded, None)
5741
+ source_root_key = thread_root if thread_root else first_row_root()
5742
+ if thread_malformed:
5743
+ # `(unassigned)` is a real answer for a thread with no metadata.
5744
+ # Persisting it for a thread whose metadata could not be READ would
5745
+ # stamp a wrong answer that every later read prefers, which is the
5746
+ # failure class `docs/codex-gotchas.md` records for the materialized
5747
+ # `"(unassigned)"`. NULL is the absence every reader already
5748
+ # handles.
5749
+ return source_root_key, parent_thread_id, None, None
5750
+ if attribution_cache is None:
5751
+ project_key, project_label = _codex_conversation_project_attribution(
5752
+ source_root_key, cwd, git_json)
5753
+ else:
5754
+ inputs = (source_root_key, cwd, git_json)
5755
+ attribution = attribution_cache.get(inputs)
5756
+ if attribution is None:
5757
+ attribution = _codex_conversation_project_attribution(*inputs)
5758
+ attribution_cache[inputs] = attribution
5759
+ project_key, project_label = attribution
5760
+ return source_root_key, parent_thread_id, project_key, project_label
5761
+
5762
+
5326
5763
  def _recompute_codex_rollups(
5327
5764
  conn: sqlite3.Connection,
5328
5765
  conversation_keys,
5329
5766
  *,
5330
5767
  advance_render_revision: bool = True,
5331
5768
  inputs_changed: "bool | set[str] | frozenset[str]" = True,
5769
+ attribution_cache: "dict | None" = None,
5332
5770
  ) -> None:
5333
5771
  """Recompute-affected-or-delete the rollup for each conversation (§3.2).
5334
5772
 
@@ -5346,9 +5784,16 @@ def _recompute_codex_rollups(
5346
5784
  outline memo keyed on it is never stale. Any other conversation's row is
5347
5785
  written only when one of its stored values would change, so a true no-op
5348
5786
  writes nothing (and reserves no revision).
5787
+
5788
+ #972: the thread-derived fields come from `_codex_rollup_thread_fields`,
5789
+ the derivation the per-pass reconcile shares; ``attribution_cache`` is the
5790
+ caller's per-pass attribution memo (none by default: every conversation
5791
+ resolves afresh). §5.8 rule 5: every key's outcome is derived before the
5792
+ first write, because the first write takes the conversations write lock
5793
+ and a key's derivation reads only its own rows and stored rollup.
5349
5794
  """
5350
5795
  kern = _lib_codex_conversation
5351
- keys = {key for key in conversation_keys if key}
5796
+ keys = sorted({key for key in conversation_keys if key})
5352
5797
  if not keys:
5353
5798
  return
5354
5799
  reserved_revision: "int | None" = None
@@ -5361,13 +5806,12 @@ def _recompute_codex_rollups(
5361
5806
  reserved_revision = _next_conversation_render_revision(conn)
5362
5807
  return reserved_revision
5363
5808
 
5809
+ # (conversation_key, None) deletes; (conversation_key, values) upserts.
5810
+ outcomes: list = []
5364
5811
  for conversation_key in keys:
5365
5812
  rows = _load_codex_normalized_rows(conn, conversation_key)
5366
5813
  if not rows:
5367
- conn.execute(
5368
- "DELETE FROM codex_conversation_rollups WHERE conversation_key = ?",
5369
- (conversation_key,),
5370
- )
5814
+ outcomes.append((conversation_key, None))
5371
5815
  continue
5372
5816
  item_count = kern.rollup_item_count(rows)
5373
5817
  title = kern.derive_title(rows)
@@ -5376,46 +5820,11 @@ def _recompute_codex_rollups(
5376
5820
  last_activity = max(timestamps) if timestamps else None
5377
5821
  models = sorted({r.model for r in rows if r.model})
5378
5822
  models_json = json.dumps(models) if models else None
5379
- source_root_key = rows[0].source_root_key
5380
- # #845 §4.7. The two columns are read as BLOBs and decoded field by
5381
- # field, so one undecodable byte no longer raises here and no longer
5382
- # fails the whole recompute. The table name stays UNQUALIFIED, because
5383
- # this writer must honor account scoping: under
5384
- # `scope_conversations_db_to_account` it sees the empty TEMP view on
5385
- # purpose.
5386
- thread = conn.execute(
5387
- "SELECT CAST(cwd AS BLOB) AS cwd_blob, "
5388
- "CAST(git_json AS BLOB) AS git_json_blob, parent_thread_id, "
5389
- "source_root_key "
5390
- "FROM codex_conversation_threads WHERE conversation_key = ?",
5391
- (conversation_key,),
5392
- ).fetchone()
5393
- cwd = git_json = parent_thread_id = None
5394
- thread_malformed = False
5395
- if thread is not None:
5396
- cwd_blob, git_json_blob, parent_thread_id, thread_root = thread
5397
- decoded = _lib_codex_metadata.decode_codex_project_metadata(
5398
- cwd_blob, git_json_blob)
5399
- cwd, git_json = decoded.cwd, decoded.git_json
5400
- # The DIRECT-ONLY predicate: this writer resolves the thread's own
5401
- # fields through `_codex_conversation_project_attribution`, which
5402
- # stops at a usable `cwd`, so a valid `cwd` beside an undecodable
5403
- # `git_json` is still attributed.
5404
- thread_malformed = _lib_codex_metadata.codex_metadata_is_malformed(
5405
- decoded, None)
5406
- if thread_root:
5407
- source_root_key = thread_root
5408
- if thread_malformed:
5409
- # `(unassigned)` is a real answer for a thread with no metadata.
5410
- # Persisting it for a thread whose metadata could not be READ would
5411
- # stamp a wrong answer that every later read prefers, which is the
5412
- # failure class `docs/codex-gotchas.md` records for the materialized
5413
- # `"(unassigned)"`. NULL is the absence every reader already
5414
- # handles.
5415
- project_key = project_label = None
5416
- else:
5417
- project_key, project_label = _codex_conversation_project_attribution(
5418
- source_root_key, cwd, git_json)
5823
+ first_root = rows[0].source_root_key
5824
+ source_root_key, parent_thread_id, project_key, project_label = (
5825
+ _codex_rollup_thread_fields(
5826
+ conn, conversation_key, first_row_root=lambda: first_root,
5827
+ attribution_cache=attribution_cache))
5419
5828
  values = (source_root_key, parent_thread_id, item_count, started,
5420
5829
  last_activity, project_key, project_label, models_json, title)
5421
5830
  if inputs_changed is not True and conversation_key not in (
@@ -5429,6 +5838,14 @@ def _recompute_codex_rollups(
5429
5838
  ).fetchone()
5430
5839
  if stored is not None and tuple(stored) == values:
5431
5840
  continue
5841
+ outcomes.append((conversation_key, values))
5842
+ for conversation_key, values in outcomes:
5843
+ if values is None:
5844
+ conn.execute(
5845
+ "DELETE FROM codex_conversation_rollups WHERE conversation_key = ?",
5846
+ (conversation_key,),
5847
+ )
5848
+ continue
5432
5849
  conn.execute(
5433
5850
  "INSERT INTO codex_conversation_rollups "
5434
5851
  "(conversation_key, source_root_key, parent_thread_id, item_count, "
@@ -5446,6 +5863,254 @@ def _recompute_codex_rollups(
5446
5863
  )
5447
5864
 
5448
5865
 
5866
+ #: #972 §5.4: the most conversations one reconcile transaction rewrites.
5867
+ CODEX_ROLLUP_RECONCILE_BATCH = 64
5868
+
5869
+
5870
+ def _reconcile_codex_rollup_attribution(
5871
+ conn: sqlite3.Connection, stats: "CodexIngestStats",
5872
+ ) -> int:
5873
+ """Rewrite every rollup whose thread-derived fields drifted (#972 §5.4).
5874
+
5875
+ A rollup is materialized when the conversation pass writes it, from the
5876
+ thread metadata and filesystem of that moment. Neither a metadata-only
5877
+ contributor that accounting accepts later (the R4 interleave) nor a
5878
+ worktree removed later rewrites it, so every full pass compares each
5879
+ persisted rollup with what `_codex_rollup_thread_fields` derives now and
5880
+ rewrites the ones that differ through the writer itself.
5881
+
5882
+ Detection runs with no transaction open over a materialized list of
5883
+ `main.codex_conversation_rollups`; repair runs the sorted drifted keys in
5884
+ batches of at most `CODEX_ROLLUP_RECONCILE_BATCH` through
5885
+ `_reconcile_codex_rollup_batch`, each in its own explicit transaction
5886
+ (one snapshot of both stores per batch, one render revision per written
5887
+ batch). A database error rolls back its batch and propagates; earlier
5888
+ batches stay committed and the next pass detects the rest. Never on an
5889
+ account-scoped connection, whose TEMP rollups are a view of the
5890
+ persisted ones. Returns the conversations written, which it also adds to
5891
+ ``stats.rollups_reattributed``.
5892
+
5893
+ Admission (§5.8, revisions 5 and 6) is per conversation and shared by
5894
+ detection and every batch (`_admit_codex_rollup_reconcile`): a
5895
+ conversation is compared only once accounting covers, from the same file
5896
+ generation, every `session_meta` line the conversation pass consumed for
5897
+ it (rule 1; a stale cursor of a replaced or truncated file is not
5898
+ coverage, `_codex_rollup_reconcile_uncovered`), never when it has no thread
5899
+ row (rule 2), and a non-database error deriving one rollup skips it
5900
+ (``stats.rollups_reconcile_skipped``, rule 4). No global condition, such
5901
+ as unrelated prune debt or a pending verification, gates the pass. One
5902
+ attribution cache keyed by the raw thread inputs serves the whole pass,
5903
+ and detection reads the rollups in key order (rule 3).
5904
+ """
5905
+ if conn.execute(
5906
+ "SELECT 1 FROM sqlite_temp_master "
5907
+ "WHERE type='table' AND name='_conversation_account_scope'"
5908
+ ).fetchone() is not None:
5909
+ raise RuntimeError(
5910
+ "the rollup reconcile never runs on an account-scoped connection")
5911
+ if conn.in_transaction:
5912
+ raise RuntimeError("the rollup reconcile needs no open transaction")
5913
+ cursor = conn.execute(
5914
+ "SELECT conversation_key, source_root_key, parent_thread_id, "
5915
+ "project_key, project_label FROM main.codex_conversation_rollups "
5916
+ "ORDER BY conversation_key")
5917
+ try:
5918
+ stored = cursor.fetchall()
5919
+ finally:
5920
+ cursor.close()
5921
+ attributions: dict = {}
5922
+ drifted = _admit_codex_rollup_reconcile(conn, stored, stats, attributions)
5923
+ drifted.sort()
5924
+ written = 0
5925
+ for start in range(0, len(drifted), CODEX_ROLLUP_RECONCILE_BATCH):
5926
+ changed = _reconcile_codex_rollup_batch(
5927
+ conn, drifted[start:start + CODEX_ROLLUP_RECONCILE_BATCH],
5928
+ stats, attributions)
5929
+ written += changed
5930
+ stats.rollups_reattributed += changed
5931
+ return written
5932
+
5933
+
5934
+ def _codex_rollup_reconcile_uncovered(
5935
+ conn: sqlite3.Connection, keys: "list[str] | None" = None,
5936
+ ) -> "set[str]":
5937
+ """Conversations whose consumed `session_meta` accounting has not yet
5938
+ processed from the same file generation (#972 §5.8 rule 1, revision 7).
5939
+
5940
+ A conversation is covered when, for every `(source_path, line_offset)`
5941
+ of its `session_meta` events, (a) `cache_db.codex_session_files` holds
5942
+ that path with `last_byte_offset` beyond that line, and (b) that
5943
+ accounting cursor and the conversation cursor for the same path
5944
+ (`main.codex_conversation_source_files`) agree on `size_bytes` and, when
5945
+ both describe the same physical container (equal `container_kind` and
5946
+ `container_path`), show no inode disagreement under
5947
+ `_lib_ingest_frontier.source_identity_replaced` (the inode decides; a
5948
+ missing or non-integer inode on either side is no evidence).
5949
+
5950
+ A path and an offset are coordinates, not generation evidence (VC-1):
5951
+ the conversation walk resets its cursor and the file's retained events
5952
+ when a different file now occupies the pathname, or when the file
5953
+ shrank, while an accounting walk that has not reached the file yet still
5954
+ holds the earlier incarnation's larger offset; comparing coordinates
5955
+ alone let the reconcile write the other contributor's project from a
5956
+ thread row that never saw the new metadata.
5957
+
5958
+ Only legs both walks converge on for every quiet file are compared, so
5959
+ the rule delays a repair while one walk lags and never withholds a
5960
+ settled conversation forever. Each walk adopts a verified container into
5961
+ its own cursor on its own schedule, so one cursor can describe the
5962
+ adopted `.zst` (its inode) for days while the other still describes the
5963
+ plain file it replaced (26 rows of 9 compressed conversations on a real
5964
+ store, 2026-11-05): inodes of different containers are not compared.
5965
+ Content anchors are not compared at all: after a partial read followed
5966
+ by an in-place shrink the two walks settle at one size on one inode with
5967
+ different anchors. The residual (spec §8, R9): a same-size in-place
5968
+ rewrite, or a same-size replacement under a cursor with no inode, while
5969
+ accounting has not re-read the file can write a project from the earlier
5970
+ generation, which the next full pass after accounting reads the file
5971
+ corrects. Decoded sizes, the logical identity (`_codex_identity_path`)
5972
+ and decoded offsets are shared by both walks for plain, compressed and
5973
+ archived rollouts alike. A conversation with no `session_meta` event is
5974
+ covered: there is nothing for accounting to lag behind.
5975
+
5976
+ One set-based read over the `session_meta` partial index
5977
+ (`idx_codex_events_session_meta_provenance`) joined to both cursors by
5978
+ path: never one query per conversation through the conversation-key
5979
+ index, which loads every event of each conversation (89.6 s for 1,251
5980
+ conversations on a real store, against 0.014 s for this read). For the
5981
+ same reason the statement constrains `conversation_key` only through a
5982
+ unary ``+`` and leaves de-duplication and NULL keys to Python: a
5983
+ ``DISTINCT`` or an ``IS NOT NULL`` there makes SQLite prefer a scan of
5984
+ every event through that index. The statement returns only the rows
5985
+ that fail a leg SQL decides exactly (``hard``) or whose same-container
5986
+ inodes SQL sees as different; the inode verdict itself is the shared
5987
+ Python rule, so this read cannot drift from the walks' replacement test.
5988
+ """
5989
+ hard = (
5990
+ "f.path IS NULL OR c.path IS NULL "
5991
+ "OR f.last_byte_offset <= e.line_offset "
5992
+ "OR f.size_bytes IS NOT c.size_bytes")
5993
+ same_container = (
5994
+ "f.container_kind IS c.container_kind "
5995
+ "AND f.container_path IS c.container_path")
5996
+ sql = (
5997
+ f"SELECT e.conversation_key, CASE WHEN {hard} THEN 1 ELSE 0 END, "
5998
+ "f.device_id, f.inode, c.device_id, c.inode "
5999
+ "FROM main.codex_conversation_events AS e "
6000
+ "LEFT JOIN cache_db.codex_session_files AS f ON f.path = e.source_path "
6001
+ # A conversation cursor exists beside its retained events: the walk
6002
+ # writes a file's events and its cursor in one savepoint, and the
6003
+ # prune drain deletes them together. Should one be missing anyway,
6004
+ # there is no generation to compare, so the line is uncovered.
6005
+ "LEFT JOIN main.codex_conversation_source_files AS c "
6006
+ "ON c.path = e.source_path "
6007
+ f"WHERE e.record_type = 'session_meta' AND ({hard} "
6008
+ f"OR ({same_container} AND f.inode IS NOT NULL "
6009
+ "AND c.inode IS NOT NULL AND f.inode IS NOT c.inode))")
6010
+ params: "tuple | list" = ()
6011
+ if keys is not None:
6012
+ if not keys:
6013
+ return set()
6014
+ sql += (" AND +e.conversation_key IN ("
6015
+ + ",".join("?" * len(keys)) + ")")
6016
+ params = keys
6017
+ uncovered: "set[str]" = set()
6018
+ for key, is_hard, f_device, f_inode, c_device, c_inode in conn.execute(
6019
+ sql, params):
6020
+ if key is None or key in uncovered:
6021
+ continue
6022
+ if is_hard or _ingest_frontier.source_identity_replaced(
6023
+ f_device, f_inode, c_device, c_inode):
6024
+ uncovered.add(key)
6025
+ return uncovered
6026
+
6027
+
6028
+ def _admit_codex_rollup_reconcile(
6029
+ conn: sqlite3.Connection, stored: list,
6030
+ stats: "CodexIngestStats", attributions: dict,
6031
+ *, keys: "list[str] | None" = None,
6032
+ ) -> "list[str]":
6033
+ """The drifted, admitted keys among ``stored`` rows (#972 §5.8 rules 1,
6034
+ 2, 4 and the drift comparison), read on the caller's snapshot.
6035
+
6036
+ ``stored`` holds ``(conversation_key, source_root_key,
6037
+ parent_thread_id, project_key, project_label)`` rows; ``keys`` limits
6038
+ the coverage read to a batch's keys (None: every conversation), which
6039
+ compares both stores' cursors for the generation inside the caller's
6040
+ snapshot. The one admission detection and every repair batch share, so
6041
+ they cannot diverge. Fills ``attributions`` for each admitted
6042
+ derivation."""
6043
+ uncovered = _codex_rollup_reconcile_uncovered(conn, keys)
6044
+ drifted = []
6045
+ for conversation_key, *values in stored:
6046
+ if conversation_key in uncovered:
6047
+ continue
6048
+ try:
6049
+ expected = _codex_rollup_thread_fields(
6050
+ conn, conversation_key,
6051
+ first_row_root=lambda key=conversation_key: (
6052
+ _codex_first_row_root(conn, key)),
6053
+ attribution_cache=attributions, require_thread=True)
6054
+ except (ValueError, OSError, UnicodeError):
6055
+ # e.g. a cwd with an embedded NUL, which `realpath` refuses: this
6056
+ # rollup keeps its stored fields for the pass.
6057
+ stats.rollups_reconcile_skipped += 1
6058
+ continue
6059
+ if expected is not None and expected != tuple(values):
6060
+ drifted.append(conversation_key)
6061
+ return drifted
6062
+
6063
+
6064
+ def _reconcile_codex_rollup_batch(
6065
+ conn: sqlite3.Connection, batch: "list[str]",
6066
+ stats: "CodexIngestStats", attributions: dict,
6067
+ ) -> int:
6068
+ """Repair one batch of detected keys in one transaction (#972 §5.4,
6069
+ §5.8 rule 8); returns the conversations written.
6070
+
6071
+ Accounting writes under its own locks, so a thread row or cursor can
6072
+ change between detection and this batch. After ``BEGIN`` the batch
6073
+ re-admits its keys on the transaction's snapshot of both stores, which
6074
+ fills ``attributions``, and hands only the admitted, still-drifted keys to
6075
+ the writer; the writer reads the same snapshot and takes each attribution
6076
+ from the cache, so it writes nothing the admission did not see and makes
6077
+ no new filesystem call for those keys. A batch with no surviving key
6078
+ ends its read-only transaction and reserves no render revision."""
6079
+ if conn.in_transaction:
6080
+ raise RuntimeError("the rollup reconcile needs no open transaction")
6081
+ marks = ",".join("?" * len(batch))
6082
+ conn.execute("BEGIN")
6083
+ try:
6084
+ rows = conn.execute(
6085
+ "SELECT conversation_key, source_root_key, parent_thread_id, "
6086
+ "project_key, project_label, render_revision "
6087
+ "FROM main.codex_conversation_rollups "
6088
+ f"WHERE conversation_key IN ({marks}) ORDER BY conversation_key",
6089
+ batch).fetchall()
6090
+ before = {row[0]: row[5] for row in rows}
6091
+ admitted = _admit_codex_rollup_reconcile(
6092
+ conn, [row[:5] for row in rows], stats, attributions,
6093
+ keys=list(batch))
6094
+ if not admitted:
6095
+ conn.rollback()
6096
+ return 0
6097
+ _recompute_codex_rollups(
6098
+ conn, admitted, inputs_changed=set(),
6099
+ attribution_cache=attributions)
6100
+ after = dict(conn.execute(
6101
+ "SELECT conversation_key, render_revision "
6102
+ "FROM main.codex_conversation_rollups "
6103
+ f"WHERE conversation_key IN ({','.join('?' * len(admitted))})",
6104
+ admitted).fetchall())
6105
+ conn.commit()
6106
+ except BaseException:
6107
+ if conn.in_transaction:
6108
+ conn.rollback()
6109
+ raise
6110
+ _wal_checkpoint.after_commit(conn)
6111
+ return sum(1 for key in admitted if before.get(key) != after.get(key))
6112
+
6113
+
5449
6114
  def _replay_codex_normalization(
5450
6115
  conn: sqlite3.Connection,
5451
6116
  *,
@@ -5715,8 +6380,11 @@ def _codex_identity_has_rows(
5715
6380
  "WHERE path = ? AND source_root_key IS ? LIMIT 1",
5716
6381
  "SELECT 1 FROM codex_session_entries "
5717
6382
  "WHERE source_path = ? AND source_root_key IS ? LIMIT 1",
6383
+ # #971: ``+`` pins the quota probe to the UNIQUE (source, source_path,
6384
+ # ...) autoindex, as in _delete_codex_file_derived_rows; on the root
6385
+ # index an absent identity walked every quota row of the root.
5718
6386
  "SELECT 1 FROM quota_window_snapshots WHERE source = 'codex' "
5719
- "AND source_path = ? AND source_root_key IS ? LIMIT 1",
6387
+ "AND source_path = ? AND +source_root_key IS ? LIMIT 1",
5720
6388
  "SELECT 1 FROM codex_conversation_threads "
5721
6389
  "WHERE source_path = ? AND source_root_key IS ? LIMIT 1",
5722
6390
  )
@@ -6350,9 +7018,14 @@ def _release_cache_store_locks(token) -> None:
6350
7018
  def _drain_backoff(adm) -> None:
6351
7019
  """After a refused admission: wait out every fresh request; when none was
6352
7020
  fresh (the gate or a store lock was busy), sleep one poll step so a
6353
- waiting worker never spins on the gate."""
7021
+ waiting worker never spins on the gate. The step is short when this
7022
+ process observed a fresh Claude waiter within the last
7023
+ ``CLAUDE_WAITER_RECENT_SECONDS`` (#1055 spec 4.3: the waiter can vanish
7024
+ between the refusal and this check); the hint selects the interval only."""
6354
7025
  if adm.fresh_requests(_cctally_core.APP_DIR):
6355
7026
  adm.wait_until_no_fresh_requests(_cctally_core.APP_DIR)
7027
+ elif adm.claude_waiter_recently_observed(_cctally_core.APP_DIR):
7028
+ adm._sleep(adm.SHORT_POLL_SECONDS)
6356
7029
  else:
6357
7030
  adm._sleep(adm.POLL_SECONDS)
6358
7031
 
@@ -7479,6 +8152,10 @@ _CODEX_W11_EAGER_PREPARE_FOR_TESTS = False
7479
8152
  _CODEX_W11_TRACE = None
7480
8153
  #: Test seam: called immediately before each W11 commit (crash injection).
7481
8154
  _CODEX_BATCH_PRECOMMIT_HOOK = None
8155
+ #: Test seam (#982 R1): called inside the locked Codex sync immediately before
8156
+ #: its own store-lock acquisition, i.e. after a heal unit's ``admit()`` has
8157
+ #: returned and before the sync's ``_after_locks`` recheck.
8158
+ _CODEX_PRE_STORE_LOCK_HOOK = None
7482
8159
  _CODEX_W11_FILE_SAVEPOINT = "SAVEPOINT codex_w11_file"
7483
8160
  _CODEX_W11_FILE_RELEASE = "RELEASE SAVEPOINT codex_w11_file"
7484
8161
  _CODEX_W11_FILE_ROLLBACK_TO = "ROLLBACK TO SAVEPOINT codex_w11_file"
@@ -7646,9 +8323,14 @@ def _write_codex_file_batch(
7646
8323
  container: "CodexContainerAcceptance | None" = None,
7647
8324
  barrier: "CodexBarrierRead | None" = None,
7648
8325
  commit: bool = True,
8326
+ thread_guard: "frozenset[str]" = frozenset(),
7649
8327
  ) -> int:
7650
8328
  """Write one fully-buffered Codex file atomically and return entry changes.
7651
8329
 
8330
+ ``thread_guard`` (#982 spec 4.3) is the fenced heal population, passed by
8331
+ a sync without the heal executor's share: a thread row whose current
8332
+ ``source_path`` is a population file is left for the executor's walk.
8333
+
7652
8334
  ``commit=False`` (#901 W11) leaves the caller's transaction open: an
7653
8335
  unbudgeted sync applies each file inside its own savepoint and commits the
7654
8336
  batch once.
@@ -7732,6 +8414,19 @@ def _write_codex_file_batch(
7732
8414
  VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
7733
8415
  quota_rows,
7734
8416
  )
8417
+ if thread_rows and thread_guard:
8418
+ # The upsert overwrites every metadata column, so a conversation's row
8419
+ # is its last ingested contributor's. While the heal holds its
8420
+ # population, a row a population file owns is the single walk's to
8421
+ # replace (the timeline in which this write came before the walk).
8422
+ kept = []
8423
+ for row in thread_rows:
8424
+ current = conn.execute(
8425
+ "SELECT source_path FROM codex_conversation_threads "
8426
+ "WHERE conversation_key=?", (row[0],)).fetchone()
8427
+ if current is None or current[0] not in thread_guard:
8428
+ kept.append(row)
8429
+ thread_rows = kept
7735
8430
  if thread_rows:
7736
8431
  conn.executemany(
7737
8432
  """INSERT INTO codex_conversation_threads
@@ -7929,6 +8624,13 @@ class IngestStats:
7929
8624
  assistant_lines_skipped: int = 0
7930
8625
  skip_reasons: dict = field(default_factory=dict)
7931
8626
  full_walk_complete: bool = False
8627
+ # #916: an exhaustive walk that visited every on-disk source cleanly and
8628
+ # whose only defect was orphan rows (tracked transcripts no longer on
8629
+ # disk). It wrote the residual token instead of the walk-complete marker,
8630
+ # so ``full_walk_complete`` stays False; the accounting frontier accepts
8631
+ # either (`_lib_ingest_frontier.provider_sync_certifiable`).
8632
+ full_walk_residual_only: bool = False
8633
+ orphan_residual_count: int = 0
7932
8634
  maintenance_failed: bool = False
7933
8635
  # #944 spec 5.5 step 5: files whose descriptor no longer matched the
7934
8636
  # frozen stat (replaced between stat and open); retried next sync.
@@ -8172,18 +8874,38 @@ def _targeted_has_pending_global_work(conn) -> bool:
8172
8874
  return row is not None
8173
8875
 
8174
8876
 
8175
- def _acquire_cache_flock(lock_fh, *, timeout):
8877
+ def _acquire_cache_flock(lock_fh, *, timeout, poll: float = 0.2,
8878
+ deadline: "float | None" = None):
8176
8879
  """Acquire the exclusive cache flock on ``lock_fh``.
8177
8880
 
8178
- ``timeout is None`` -> a single non-blocking attempt (today's behavior
8179
- for routine auto-syncs): returns False immediately on contention.
8180
- ``timeout > 0`` -> retry ``LOCK_NB`` every ~0.2s until acquired or the
8181
- deadline elapses. Returns True iff the lock is held on return.
8881
+ ``timeout is None`` (or ``0``) -> a single non-blocking attempt (today's
8882
+ behavior for routine auto-syncs): returns False immediately on
8883
+ contention. ``timeout > 0`` -> retry ``LOCK_NB`` every ``poll`` seconds
8884
+ until acquired or the deadline elapses; the first attempt always happens.
8885
+ Returns True iff the lock is held on return.
8886
+
8887
+ An explicit absolute ``deadline`` (``time.monotonic()`` seconds; #1055
8888
+ spec 4.5) replaces ``timeout`` and has its own rule: an attempt is made
8889
+ only while ``monotonic() < deadline``, and each sleep is clamped to the
8890
+ time remaining, so an expired deadline makes no attempt at all.
8182
8891
 
8183
8892
  A retry-with-sleep loop, NOT a SIGALRM-based blocking LOCK_EX: the
8184
8893
  dashboard runs its sync on a background thread, where Python signals
8185
8894
  never fire, so an alarm timeout would silently never trip.
8186
8895
  """
8896
+ if deadline is not None:
8897
+ while True:
8898
+ if deadline - time.monotonic() <= 0:
8899
+ return False
8900
+ try:
8901
+ fcntl.flock(lock_fh, fcntl.LOCK_EX | fcntl.LOCK_NB)
8902
+ return True
8903
+ except BlockingIOError:
8904
+ pass
8905
+ remaining = deadline - time.monotonic()
8906
+ if remaining <= 0:
8907
+ return False
8908
+ time.sleep(min(poll, remaining))
8187
8909
  deadline = None if timeout is None else (time.monotonic() + timeout)
8188
8910
  while True:
8189
8911
  try:
@@ -8192,7 +8914,38 @@ def _acquire_cache_flock(lock_fh, *, timeout):
8192
8914
  except BlockingIOError:
8193
8915
  if deadline is None or time.monotonic() >= deadline:
8194
8916
  return False
8195
- time.sleep(0.2)
8917
+ time.sleep(poll)
8918
+
8919
+
8920
+ def _wait_behind_drain_unit(lock_fh, wait_seconds: float, owner: dict) -> str:
8921
+ """The Claude hook's waiting path after its first refusal (#1055 4.4).
8922
+
8923
+ Waits only when both the Codex provider lock and the drain gate are busy
8924
+ (best-effort evidence of an admitted drain unit), as the one registered
8925
+ Claude waiter, until one absolute deadline. Puts the waiter in
8926
+ ``owner["waiter"]`` as soon as it exists, so the caller's cleanup
8927
+ releases it on every path. Returns the ``drain_wait`` outcome; the lock
8928
+ is held iff it is ``acquired``.
8929
+ """
8930
+ import _lib_codex_drain_admission as adm
8931
+ deadline = time.monotonic() + wait_seconds
8932
+ if not adm.lock_busy(_cctally_core.CACHE_LOCK_CODEX_PATH):
8933
+ return "not_drain" # Claude against Claude, or another writer
8934
+ if not adm.lock_busy(_cctally_core.APP_DIR / adm.GATE_FILE_NAME):
8935
+ return "not_drain" # a Codex hook or an unadmitted Codex sync
8936
+ waiter = adm.ClaudeWaiter.register(
8937
+ _cctally_core.APP_DIR,
8938
+ deadline_ns=adm._monotonic_ns()
8939
+ + round((deadline - time.monotonic()) * 1e9))
8940
+ if waiter is None:
8941
+ return "busy_waiter"
8942
+ owner["waiter"] = waiter
8943
+ if not _acquire_cache_flock(
8944
+ lock_fh, timeout=None, poll=adm.CLAUDE_HOOK_DRAIN_POLL_SECONDS,
8945
+ deadline=deadline):
8946
+ return "timeout"
8947
+ waiter.mark_holding()
8948
+ return "acquired"
8196
8949
 
8197
8950
 
8198
8951
  def _prune_orphaned_cache_entries(conn, *, lock_timeout=None):
@@ -8679,11 +9432,21 @@ def sync_cache(
8679
9432
  rebuild: bool = False,
8680
9433
  only_paths: "set[str] | None" = None,
8681
9434
  lock_timeout: "float | None" = None,
9435
+ drain_wait_seconds: "float | None" = None,
8682
9436
  ) -> IngestStats:
8683
9437
  """Read-through delta ingest. Acquires an exclusive fcntl.flock; if
8684
9438
  another process holds it, returns immediately with lock_contended=True
8685
9439
  and the caller should proceed with whatever data is already cached.
8686
9440
 
9441
+ ``drain_wait_seconds`` (#1055; only the hook-tick Claude leg passes it,
9442
+ and only on its first invocation) applies when ``lock_timeout`` is None:
9443
+ after the first refusal, a hook that sees an admitted Codex drain unit
9444
+ may wait that long as the one Claude waiter the drain workers yield to.
9445
+ It must not exceed ``max(T, CLAUDE_WAITER_HOLD_ALLOWANCE_SECONDS)``: the
9446
+ drains treat a record ending further out as not fresh, so a larger
9447
+ allowance would poll while the drains never yield. Only the hook leg
9448
+ passes it, with ``CLAUDE_HOOK_DRAIN_WAIT_SECONDS``.
9449
+
8687
9450
  When `rebuild=True`, clears the cached rows AFTER acquiring the lock
8688
9451
  so a lost race does not wipe a cache another process is actively
8689
9452
  populating. If the lock is contended on a rebuild, the cache is left
@@ -8699,10 +9462,25 @@ def sync_cache(
8699
9462
  # verdict. Settled by the clean end-of-walk write; every other exit
8700
9463
  # withdraws a present marker in ``finally``.
8701
9464
  walk_marker_owed = False
9465
+ # #1055: the Claude waiter, once registered; released after the writer
9466
+ # unlock on every exit path (the outermost ``finally``).
9467
+ drain_waiter: dict = {}
8702
9468
  try:
8703
9469
  with _perf.phase("flock") as _p_flock:
8704
9470
  _acquired = _acquire_cache_flock(lock_fh, timeout=lock_timeout)
8705
- _p_flock.set_meta(contended=not _acquired)
9471
+ _drain_wait, _waited = "none", 0.0
9472
+ if (not _acquired and lock_timeout is None
9473
+ and drain_wait_seconds is not None
9474
+ and drain_wait_seconds > 0):
9475
+ _refused_at = time.monotonic()
9476
+ try:
9477
+ _drain_wait = _wait_behind_drain_unit(
9478
+ lock_fh, drain_wait_seconds, drain_waiter)
9479
+ finally:
9480
+ _waited = time.monotonic() - _refused_at
9481
+ _acquired = _drain_wait == "acquired"
9482
+ _p_flock.set_meta(contended=not _acquired, drain_wait=_drain_wait,
9483
+ drain_wait_seconds=round(_waited, 3))
8706
9484
  if not _acquired:
8707
9485
  eprint("[cache] sync already in progress; using existing cache")
8708
9486
  stats.lock_contended = True
@@ -8895,8 +9673,9 @@ def sync_cache(
8895
9673
  # Clear the walk-complete sentinel atomically with the wipe
8896
9674
  # (cctally-dev#93, D5/D2): a stale "complete" marker must never
8897
9675
  # survive a destructive rebuild. The end-of-loop write below
8898
- # re-establishes it only after this rebuild's clean walk.
8899
- conn.execute("DELETE FROM cache_meta WHERE key='claude_ingest_walk_complete'")
9676
+ # re-establishes it only after this rebuild's clean walk. #916:
9677
+ # the residual token goes in the same statement.
9678
+ _clear_claude_walk_verdicts(conn)
8900
9679
  # Issue #139: a rebuild walks every file from offset 0, so the
8901
9680
  # per-file fused walk below repopulates the whole message
8902
9681
  # index — that satisfies any deferred existing-install backfill.
@@ -9219,12 +9998,24 @@ def sync_cache(
9219
9998
  # one exists, so a repeated orphaned sync doesn't churn a no-op write
9220
9999
  # txn every tick); merely withholding THIS run's end-of-loop rewrite
9221
10000
  # is not enough, since a stale marker from a previous sync would
9222
- # otherwise survive and keep vouching for completeness. Setting
9223
- # walk_clean=False additionally suppresses the end-of-loop rewrite so
9224
- # the marker stays absent for this run. With the marker gone the
9225
- # upgrade gate DEFERs the 008/009/010 recomputes (rather than
9226
- # certifying aggregates that still include data from files no longer
9227
- # on disk). The safe CLEANUP lives OUT of sync_cache, in
10001
+ # otherwise survive and keep vouching for completeness. The orphan
10002
+ # set is kept in ``residual_orphans`` and the end-of-loop block
10003
+ # withholds the marker for this run whenever it is non-empty. With the
10004
+ # marker gone the upgrade gate DEFERs the 008/009/010 recomputes
10005
+ # (rather than certifying aggregates that still include data from
10006
+ # files no longer on disk).
10007
+ #
10008
+ # #916: orphans no longer set walk_clean=False. walk_clean describes
10009
+ # the on-disk sources only, so a walk whose ONLY defect is orphan rows
10010
+ # ends "residual-only" and writes the
10011
+ # `claude_ingest_walk_orphan_residual` token instead of the marker.
10012
+ # The token is the accounting frontier's completeness signal and
10013
+ # nobody else's: the walk never reads or writes the orphan rows, so
10014
+ # every on-disk source's bytes are still reflected in the cache, which
10015
+ # is all the frontier certifies. Without it a residual the prune gates
10016
+ # refuse kept the dashboard re-walking every transcript on every tick
10017
+ # until a rebuild. The marker keeps its D5a meaning unchanged.
10018
+ # The safe CLEANUP lives OUT of sync_cache, in
9228
10019
  # _prune_orphaned_cache_entries — it re-derives the safe orphan set
9229
10020
  # independently and removes the full derived surface under three
9230
10021
  # gates: A (session-id not shared by a survivor), B (coverage — every
@@ -9251,6 +10042,7 @@ def sync_cache(
9251
10042
  # file for an orphan and nuke the walk-complete marker. Skip it entirely
9252
10043
  # for targeted: the live-tail fast path never prunes orphans (the full
9253
10044
  # background sync owns that).
10045
+ residual_orphans: "tuple[str, ...]" = ()
9254
10046
  if not targeted:
9255
10047
  global _LAST_WARNED_ORPHAN_SET
9256
10048
  on_disk_paths = {str(jp) for jp in paths}
@@ -9283,7 +10075,10 @@ def sync_cache(
9283
10075
  "DELETE FROM cache_meta WHERE key='claude_ingest_walk_complete'"
9284
10076
  )
9285
10077
  conn.commit()
9286
- walk_clean = False # orphaned rows -> cache doesn't mirror disk (D5a)
10078
+ # Orphaned rows -> the cache doesn't mirror disk (D5a): the
10079
+ # end-of-loop block withholds the marker (#916: and writes the
10080
+ # residual token when the on-disk walk was clean).
10081
+ residual_orphans = tuple(sorted(orphaned_tracked_paths))
9287
10082
  else:
9288
10083
  # No orphans this walk: clear the throttle memory so a LATER,
9289
10084
  # distinct orphan episode (even one recreated at the same paths)
@@ -9363,7 +10158,8 @@ def sync_cache(
9363
10158
  # full-reset (cctally-dev#93, D5/D2): the cache is being wiped, so
9364
10159
  # any "complete" marker is now stale. The end-of-loop write below
9365
10160
  # re-establishes it only after this run's clean re-ingest walk.
9366
- conn.execute("DELETE FROM cache_meta WHERE key='claude_ingest_walk_complete'")
10161
+ # #916: the residual token goes in the same statement.
10162
+ _clear_claude_walk_verdicts(conn)
9367
10163
  # Crash-safety: also clear session_files's size/offset tracking
9368
10164
  # so a partial-state recovery on the NEXT sync forces every
9369
10165
  # file's per-file branch to take the fresh-ingest path. Without
@@ -9413,15 +10209,19 @@ def sync_cache(
9413
10209
  # "walk" phase (never per-row — Section 2 rule: volume is a count, not
9414
10210
  # N timed phases). Opened via the context-manager protocol so the hot
9415
10211
  # loop body below is not reindented; counts recorded after the loop.
9416
- # #195: is the cache-write-split re-walk armed? Computed ONCE per
9417
- # sync_cache call, before the file loop, so every file in this walk uses
9418
- # one statement. Cache migration 030 sets this flag and zeroes the
9419
- # per-file cursors; the end-of-walk block below clears it after a clean,
9420
- # non-targeted full walk. While armed, the chained-conflict variant is
9421
- # used so a replayed NULL-key row updates in place instead of raising
9422
- # IntegrityError and rolling back its whole file. Scoped to THIS flag
9423
- # (not marker-absence) so migration 020's loud duplicate-physical-key
9424
- # backstop stays intact on every other ingest path.
10212
+ # #195: is the cache-write-split re-walk armed? Read ONCE per
10213
+ # sync_cache call, before the file loop. Cache migration 030 sets this
10214
+ # flag and resets the per-file cursors to -1; the end-of-walk block
10215
+ # below clears it after a non-targeted full walk that is clean on disk,
10216
+ # either fully clean or residual-only (#916). While armed, every file
10217
+ # uses the chained-conflict variant so a replayed NULL-key row updates
10218
+ # in place instead of raising IntegrityError and rolling back its whole
10219
+ # file. #916: the statement is chosen PER FILE (``use_rewalk`` in the
10220
+ # loop): a file whose own cursor still reads -1 takes the chained
10221
+ # variant even after the flag retired. Scoped to the flag and to -1
10222
+ # cursors (not marker-absence) so migration 020's loud
10223
+ # duplicate-physical-key backstop stays intact on every other ingest
10224
+ # path.
9425
10225
  rewalk_armed = conn.execute(
9426
10226
  "SELECT 1 FROM cache_meta WHERE key=?",
9427
10227
  (_cctally_db_sib.CACHE_CREATION_SPLIT_REWALK_KEY,),
@@ -9663,13 +10463,24 @@ def sync_cache(
9663
10463
  # collides on idx_entries_physical and would roll back the
9664
10464
  # whole per-file transaction. Steady state keeps the
9665
10465
  # single-target SQL and its LOUD physical-key backstop.
10466
+ #
10467
+ # #916: the obligation is also carried per file. Migration
10468
+ # 030 is the only producer of a negative ``size_bytes``,
10469
+ # so a ``-1`` cursor is a file not yet re-walked since the
10470
+ # reset, still owed the chained clause even after a
10471
+ # residual-only walk retired the global flag (an orphan
10472
+ # whose transcript reappears). Read from THIS file's own
10473
+ # ``prev`` row: the ``prev_size`` local is unbound for a
10474
+ # new file and stale for a file after a ``-1`` one.
10475
+ use_rewalk = rewalk_armed or (
10476
+ prev is not None and prev[0] < 0)
9666
10477
  if attribution_binds is not None:
9667
10478
  _sql = (SESSION_ENTRY_UPSERT_SQL_REWALK_LINKED
9668
- if rewalk_armed
10479
+ if use_rewalk
9669
10480
  else SESSION_ENTRY_UPSERT_SQL_LINKED)
9670
10481
  else:
9671
10482
  _sql = (SESSION_ENTRY_UPSERT_SQL_REWALK
9672
- if rewalk_armed else SESSION_ENTRY_UPSERT_SQL)
10483
+ if use_rewalk else SESSION_ENTRY_UPSERT_SQL)
9673
10484
  conn.executemany(_sql, stamped_rows)
9674
10485
  stats.rows_changed += conn.total_changes - before
9675
10486
  # Conversation message ingest (Plan 1). Lands in the SAME
@@ -9851,7 +10662,12 @@ def sync_cache(
9851
10662
  # writes nothing after the first; the value is therefore the instant
9852
10663
  # the current run of complete walks began. Any exit that does not reach
9853
10664
  # here withdraws the marker (``walk_marker_owed`` in ``finally``).
9854
- if walk_clean and applied_at_start and not targeted:
10665
+ #
10666
+ # #916: a walk that is clean on disk but still tracks orphans
10667
+ # (``residual_orphans``) writes the residual token instead. The two
10668
+ # are mutually exclusive: the marker branch deletes the token, and the
10669
+ # orphan leg already deleted the marker before a token is written.
10670
+ if walk_clean and applied_at_start and not targeted and not residual_orphans:
9855
10671
  conn.execute(
9856
10672
  "INSERT INTO cache_meta(key, value) "
9857
10673
  "VALUES('claude_ingest_walk_complete', ?) "
@@ -9866,9 +10682,53 @@ def sync_cache(
9866
10682
  "DELETE FROM cache_meta WHERE key=?",
9867
10683
  (_cctally_db_sib.CACHE_CREATION_SPLIT_REWALK_KEY,),
9868
10684
  )
10685
+ # #916: the residual cleared (pruned, rebuilt or restored); the
10686
+ # marker now vouches, so the token goes in the same transaction.
10687
+ conn.execute(
10688
+ "DELETE FROM cache_meta WHERE key=?",
10689
+ (_ingest_frontier.CLAUDE_WALK_ORPHAN_RESIDUAL_KEY,),
10690
+ )
9869
10691
  conn.commit()
9870
10692
  stats.full_walk_complete = True
9871
10693
  walk_marker_owed = False
10694
+ elif walk_clean and applied_at_start and not targeted:
10695
+ # #916: residual-only. Every on-disk source was visited cleanly,
10696
+ # so the split re-walk flag retires here too: the only files still
10697
+ # owed their re-walk are absent orphans, and each one's own ``-1``
10698
+ # cursor re-selects the chained clause if it reappears. The marker
10699
+ # stays absent (D5a), and ``full_walk_complete`` stays False.
10700
+ conn.execute(
10701
+ "DELETE FROM cache_meta WHERE key=?",
10702
+ (_cctally_db_sib.CACHE_CREATION_SPLIT_REWALK_KEY,),
10703
+ )
10704
+ # #901 W8: an unchanged orphan set writes nothing. ``at`` is
10705
+ # excluded from the comparison, so it records when the current
10706
+ # set was first seen; the value is debug-only and no reader acts
10707
+ # on it.
10708
+ token = {
10709
+ "orphans": len(residual_orphans),
10710
+ "fingerprint": hashlib.sha256(
10711
+ "\n".join(residual_orphans).encode("utf-8")).hexdigest(),
10712
+ }
10713
+ row = conn.execute(
10714
+ "SELECT value FROM cache_meta WHERE key=?",
10715
+ (_ingest_frontier.CLAUDE_WALK_ORPHAN_RESIDUAL_KEY,),
10716
+ ).fetchone()
10717
+ try:
10718
+ held = json.loads(row[0]) if row is not None else None
10719
+ except (TypeError, ValueError):
10720
+ held = None
10721
+ if not (isinstance(held, dict)
10722
+ and held.get("orphans") == token["orphans"]
10723
+ and held.get("fingerprint") == token["fingerprint"]):
10724
+ token["at"] = dt.datetime.now(dt.timezone.utc).isoformat()
10725
+ _set_cache_meta(
10726
+ conn, _ingest_frontier.CLAUDE_WALK_ORPHAN_RESIDUAL_KEY,
10727
+ json.dumps(token))
10728
+ conn.commit()
10729
+ stats.full_walk_residual_only = True
10730
+ stats.orphan_residual_count = len(residual_orphans)
10731
+ walk_marker_owed = False
9872
10732
  # #279 S2 F1: rolling parse-health record. Anomaly-delta-gated so
9873
10733
  # steady-state (incl. targeted live-tail) syncs stay zero-write;
9874
10734
  # targeted syncs still accumulate — they ingest real new bytes.
@@ -9892,15 +10752,29 @@ def sync_cache(
9892
10752
  # stalling the lock under heavy-reader contention.
9893
10753
  _maybe_truncate_wal(conn, _cctally_core.CACHE_DB_PATH)
9894
10754
  finally:
9895
- if walk_marker_owed:
9896
- _withdraw_cache_meta_marker(conn, "claude_ingest_walk_complete")
9897
- # #901 §5.3c (W9): the end of a cache sync is a checkpoint boundary.
9898
- _wal_checkpoint.at_boundary(conn)
10755
+ # #1055 spec 4.5: nested so a cleanup failure still unlocks the
10756
+ # writer, and the Claude waiter is released only after that unlock.
9899
10757
  try:
9900
- fcntl.flock(lock_fh, fcntl.LOCK_UN)
9901
- except OSError:
9902
- pass
9903
- lock_fh.close()
10758
+ try:
10759
+ if walk_marker_owed:
10760
+ _withdraw_cache_meta_marker(
10761
+ conn, "claude_ingest_walk_complete")
10762
+ # #916: an unsettled walk cannot vouch for residual-only
10763
+ # either.
10764
+ _withdraw_cache_meta_marker(
10765
+ conn, _ingest_frontier.CLAUDE_WALK_ORPHAN_RESIDUAL_KEY)
10766
+ # #901 §5.3c (W9): the end of a cache sync is a checkpoint
10767
+ # boundary.
10768
+ _wal_checkpoint.at_boundary(conn)
10769
+ finally:
10770
+ try:
10771
+ fcntl.flock(lock_fh, fcntl.LOCK_UN)
10772
+ except OSError:
10773
+ pass
10774
+ lock_fh.close()
10775
+ finally:
10776
+ if drain_waiter.get("waiter") is not None:
10777
+ drain_waiter["waiter"].release()
9904
10778
  return stats
9905
10779
 
9906
10780
 
@@ -11301,7 +12175,9 @@ def _record_codex_container_status(conn, stats, *, census_complete: bool,
11301
12175
  prior = decoded if isinstance(decoded, dict) else {}
11302
12176
  except (TypeError, ValueError):
11303
12177
  prior = {}
11304
- observed = {k: int(stats.container_skipped.get(k, 0))
12178
+ # #1052 spec 5.3: every skip the pass saw, its unit phase's included.
12179
+ seen_skipped = stats.all_container_skipped()
12180
+ observed = {k: int(seen_skipped.get(k, 0))
11305
12181
  for k in _CONTAINER_SKIP_KEYS}
11306
12182
  # D3: verification debt is pending work only. A terminal `unverified`
11307
12183
  # container is reported (and warned about) on its own and never feeds V
@@ -11356,7 +12232,7 @@ def _record_codex_container_status(conn, stats, *, census_complete: bool,
11356
12232
  # ingestion observation does not pretend that an idle walk probed Python.
11357
12233
  new_capability_observation = bool(
11358
12234
  stats.files_processed or stats.container_pending
11359
- or any(stats.container_skipped.values()) or stats.rebuild_refused)
12235
+ or any(seen_skipped.values()) or stats.rebuild_refused)
11360
12236
  decoder = prior.get("decoder")
11361
12237
  decoder_observed_at = prior.get("decoderObservedAt", prior.get("observedAt"))
11362
12238
  if new_capability_observation or decoder not in (
@@ -11892,9 +12768,57 @@ class CodexIngestStats:
11892
12768
  barrier_deferred_targets: dict = field(default_factory=dict)
11893
12769
  barrier_published_paths: set = field(default_factory=set)
11894
12770
  barrier_retry_causes: set = field(default_factory=set)
12771
+ # #1052 spec 5.4: the conversation pass's terminal deferrals. A unit whose
12772
+ # contributor carries the conversations store's terminal `unverified`
12773
+ # verdict, and none of whose files is blocked, publishes nothing and is
12774
+ # not retried. ``barrier_keys_terminal`` counts its keys (they stay in
12775
+ # ``barrier_keys_deferred`` too) and ``barrier_terminal_keys`` maps each
12776
+ # terminal file to the keys it holds back (a residual field the
12777
+ # transcript worker does not serialize). ``barrier_container_skipped``
12778
+ # counts, by reason, the unit-phase skips of paths phase 1 did not skip:
12779
+ # ``container_skipped`` is phase 1's census only.
12780
+ barrier_keys_terminal: int = 0
12781
+ barrier_terminal_keys: dict = field(default_factory=dict)
12782
+ barrier_container_skipped: dict = field(default_factory=dict)
11895
12783
  # The status writer already observes capability for unresolved work.
11896
12784
  # Explicit adapters reuse it; unchanged accepted generations never probe.
11897
12785
  container_decoder: "str | None" = None
12786
+ # #982 spec 4.1 steps 4 and 7: a heal walk piece's own report. The files
12787
+ # it ingested (or reset), whether its ``_ingest_limit`` stopped it before
12788
+ # the next such file, the census files it examined successfully
12789
+ # (ingested, reset or found unchanged; never a failed, torn-deferred,
12790
+ # held or U6-skipped file), its discovery set (the census when it is a
12791
+ # walk's first piece), whether it left no census file unwalked, and
12792
+ # whether it made the transition (deleted the heal record) in its
12793
+ # end-of-walk transaction.
12794
+ heal_counted_paths: set = field(default_factory=set)
12795
+ heal_limit_stopped: bool = False
12796
+ heal_examined: "frozenset[str]" = frozenset()
12797
+ heal_discovered: "frozenset[str] | None" = None
12798
+ heal_walk_complete: bool = False
12799
+ heal_transitioned: bool = False
12800
+ # Revision 7: the committed cursor row of every census file this piece
12801
+ # visited (read after its last commit, under its locks), and the visited
12802
+ # files of earlier pieces whose rows changed before the walk's last piece.
12803
+ heal_visit_rows: dict = field(default_factory=dict)
12804
+ heal_visit_changed: "frozenset[str]" = frozenset()
12805
+ # #982 spec 4.4: a budgeted call declined `replay_pending` while a replay
12806
+ # heal record exists (the store is cleared and partly refilled), so the
12807
+ # hook must not run its projection or budget leg over it. A budgeted call
12808
+ # that met contention sets it from an unlocked read of the same record
12809
+ # (`_codex_replay_refill_unlocked`), and its hook publishes no request.
12810
+ replay_refill: bool = False
12811
+ # #982 spec 4.5a (Generation): an executor unit whose pass generation is
12812
+ # not the heal record's (or that found a fresh clear only a new pass's
12813
+ # start unit may perform) wrote nothing; the executor ends its pass
12814
+ # `restart`.
12815
+ heal_generation_stale: bool = False
12816
+ # #972 §5.4: conversations whose rollup the full pass's reconcile
12817
+ # rewrote (thread-derived fields drifted from the current answer).
12818
+ rollups_reattributed: int = 0
12819
+ # #972 §5.8 rule 4: rollups the reconcile skipped because deriving their
12820
+ # expected fields raised a non-database error (an unresolvable cwd).
12821
+ rollups_reconcile_skipped: int = 0
11898
12822
 
11899
12823
  @property
11900
12824
  def targeted_clean(self) -> bool:
@@ -11906,7 +12830,29 @@ class CodexIngestStats:
11906
12830
  and self.files_failed == 0
11907
12831
  and not self.container_pending
11908
12832
  and not self.barrier_retry
11909
- and not any(self.container_skipped.values()))
12833
+ and not any(self.all_container_skipped().values()))
12834
+
12835
+ def all_container_skipped(self) -> "dict[str, int]":
12836
+ """Every container skip this pass saw, by reason (#1052 spec 5.3):
12837
+ phase 1's census plus the unit phase's skips of other paths. The two
12838
+ are path-disjoint by construction, so the sum counts each path once.
12839
+ """
12840
+ merged = dict(self.container_skipped)
12841
+ for reason, count in self.barrier_container_skipped.items():
12842
+ merged[reason] = merged.get(reason, 0) + count
12843
+ return merged
12844
+
12845
+
12846
+ def _print_codex_done_line(stats: CodexIngestStats) -> None:
12847
+ """`cache-sync`'s Codex summary line (the first leg's and the heal's)."""
12848
+ eprint(
12849
+ f"[cache-sync] codex done: {stats.files_processed} processed, "
12850
+ f"{stats.files_skipped_unchanged} skipped, "
12851
+ f"{stats.files_reset_truncated} reset, "
12852
+ f"{stats.rows_changed} rows changed, "
12853
+ f"{stats.lines_malformed} malformed, "
12854
+ f"{stats.token_events_skipped} drift-skipped"
12855
+ )
11910
12856
 
11911
12857
 
11912
12858
  def _progress_codex_stderr(stats: CodexIngestStats, *, force: bool = False) -> None:
@@ -12113,7 +13059,7 @@ def _record_codex_replay_deferral(conn: sqlite3.Connection) -> None:
12113
13059
  raise
12114
13060
 
12115
13061
 
12116
- def _defer_codex_replay_drain() -> str:
13062
+ def _defer_codex_replay_drain(*, short: bool = False) -> str:
12117
13063
  """Hand the declined byte-zero replay to a detached worker (public #5).
12118
13064
 
12119
13065
  Returns ``"spawned"``, ``"throttled"`` or ``"failed"`` — for the lifecycle
@@ -12128,6 +13074,10 @@ def _defer_codex_replay_drain() -> str:
12128
13074
  re-read everything", and slicing that corrupts account attribution — so
12129
13075
  there is nothing to fall back to. The record written at the decline site is
12130
13076
  what makes a drain that never succeeds diagnosable.
13077
+
13078
+ ``short`` (#982 spec 4.5a): a heal pass that ended ``restart`` asks for
13079
+ the short retry throttle even when no heal record exists (a replay
13080
+ re-armed after the pass's transition leaves only the marker).
12131
13081
  """
12132
13082
  marker = _cctally_core.APP_DIR / CODEX_REPLAY_DRAIN_MARKER_NAME
12133
13083
  try:
@@ -12135,6 +13085,20 @@ def _defer_codex_replay_drain() -> str:
12135
13085
  except OSError:
12136
13086
  age = None
12137
13087
  if age is not None and 0 <= age < CODEX_REPLAY_DRAIN_SPAWN_THROTTLE_SECONDS:
13088
+ # #982 spec 4.5a: every path that requests this worker comes through
13089
+ # here, so the heal record's short retry throttle is applied once,
13090
+ # for all of them. Probed only inside the window it can change.
13091
+ if (age < CODEX_HEAL_RETRY_THROTTLE_SECONDS
13092
+ or not (short or _codex_heal_record_present())):
13093
+ return "throttled"
13094
+ if _codex_heal_walk_running():
13095
+ # A heal pass is running: hooks request the worker on every tick
13096
+ # while a record exists, and a worker spawned now would only run the
13097
+ # provenance, verification, reset, membership and seal drains beside
13098
+ # the executor. The pass requests the worker itself, after releasing
13099
+ # this lock, when it ends `contended` or `degraded`; on `restart` its
13100
+ # callers re-run it inline. The marker is not stamped, so the first
13101
+ # request after the pass is not throttled by this one.
12138
13102
  return "throttled"
12139
13103
  try:
12140
13104
  _cctally_core.APP_DIR.mkdir(parents=True, exist_ok=True)
@@ -12145,6 +13109,51 @@ def _defer_codex_replay_drain() -> str:
12145
13109
  else "failed")
12146
13110
 
12147
13111
 
13112
+ def _codex_heal_walk_running() -> bool:
13113
+ """Whether a heal pass holds ``codex-heal-walk.lock``: a non-blocking
13114
+ exclusive probe on a separate open file description, released at once.
13115
+ Never creates the file; any other error answers False (spawn as before).
13116
+ """
13117
+ path = _cctally_core.APP_DIR / CODEX_HEAL_WALK_LOCK_NAME
13118
+ try:
13119
+ fd = os.open(path, os.O_RDWR)
13120
+ except OSError:
13121
+ return False
13122
+ try:
13123
+ try:
13124
+ fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
13125
+ except BlockingIOError:
13126
+ return True
13127
+ except OSError:
13128
+ return False
13129
+ fcntl.flock(fd, fcntl.LOCK_UN)
13130
+ return False
13131
+ finally:
13132
+ os.close(fd)
13133
+
13134
+
13135
+ def _codex_heal_record_present() -> bool:
13136
+ """Whether cache.db holds a well-formed heal record, read through a raw
13137
+ ``mode=rw`` connection (no schema apply, migration or store lock; never
13138
+ creates the file). Anything unreadable answers False, which keeps the
13139
+ hourly throttle."""
13140
+ path = pathlib.Path(_cctally_core.CACHE_DB_PATH)
13141
+ try:
13142
+ if not path.is_file():
13143
+ return False
13144
+ conn = sqlite3.connect(f"{path.resolve().as_uri()}?mode=rw",
13145
+ uri=True, timeout=1.0)
13146
+ except (OSError, sqlite3.Error):
13147
+ return False
13148
+ try:
13149
+ record = load_codex_heal_record(conn)
13150
+ except sqlite3.Error:
13151
+ return False
13152
+ finally:
13153
+ conn.close()
13154
+ return record is not None and not record.get("malformed")
13155
+
13156
+
12148
13157
  class ResetDrainResult(NamedTuple):
12149
13158
  outcome: str
12150
13159
  reset: int
@@ -12338,14 +13347,33 @@ def _cmd_codex_replay_drain_body(args) -> int:
12338
13347
  conn = open_cache_db()
12339
13348
  try:
12340
13349
  stats = CodexIngestStats()
12341
- if ((history.outcome == "settled" and history.candidates)
12342
- or conn.execute("SELECT 1 FROM cache_meta WHERE key=? LIMIT 1",
12343
- (CODEX_REPLAY_FROM_ZERO_KEY,)).fetchone()):
12344
- stats = sync_codex_cache(
12345
- conn, maintenance="caller",
12346
- lock_timeout=CODEX_REPLAY_DRAIN_LOCK_TIMEOUT_SECONDS)
13350
+ replay_armed = conn.execute(
13351
+ "SELECT 1 FROM cache_meta WHERE key=? LIMIT 1",
13352
+ (CODEX_REPLAY_FROM_ZERO_KEY,)).fetchone() is not None
13353
+ heal_record = load_codex_heal_record(conn)
12347
13354
  finally:
12348
13355
  conn.close()
13356
+ # #982 spec 4.6: the byte-zero replay and the history heal that
13357
+ # follows a settled census both run as admitted per-file units, never
13358
+ # as one locked walk; a record left by a dead executor resumes from
13359
+ # its phase. The executor picks replay mode itself when the marker or
13360
+ # a replay record exists.
13361
+ heal = HealResult("none", None, 0, 0, 0, 0, 0)
13362
+ if (replay_armed or heal_record is not None
13363
+ or (history.outcome == "settled" and history.candidates)):
13364
+ heal = run_codex_admitted_heal(
13365
+ wait_for_requests=True,
13366
+ population=(None if heal_record is not None or replay_armed
13367
+ else _codex_history_candidates()))
13368
+ if heal.outcome in ("replay_due", "restart"):
13369
+ # Spec 4.5a: a replay armed during the history heal ended
13370
+ # that pass before its fenced walk; this pass replays.
13371
+ heal = run_codex_admitted_heal(wait_for_requests=True)
13372
+ if heal.outcome == "contended":
13373
+ stats = CodexIngestStats(lock_contended=True)
13374
+ elif heal.stats is not None:
13375
+ # Either mode: the heal's own counts, summed over its units.
13376
+ stats = heal.stats
12349
13377
  if stats.rebuild_refused:
12350
13378
  _log("refused", f"rebuild_refused={stats.rebuild_refused}")
12351
13379
  return 0
@@ -12379,7 +13407,9 @@ def _cmd_codex_replay_drain_body(args) -> int:
12379
13407
  f"reset_done={reset_result.reset} reset_unchanged={reset_result.unchanged} "
12380
13408
  f"barrier_proven={scan.proven} barrier_sealed={seal.sealed} "
12381
13409
  f"barrier_remaining={seal.remaining} history={history.outcome} "
12382
- f"history_candidates={history.candidates} history_traced={history.traced}")
13410
+ f"history_candidates={history.candidates} history_traced={history.traced} "
13411
+ f"heal={heal.outcome} heal_units={heal.units} "
13412
+ f"heal_degraded={heal.degraded}")
12383
13413
  return 0
12384
13414
 
12385
13415
 
@@ -12483,6 +13513,15 @@ def _sync_codex_cache_locked(
12483
13513
  _on_first_file_rollback: Callable[[], None] | None = None,
12484
13514
  _on_file_committed: Callable[[str], None] | None = None,
12485
13515
  _on_file_prepared: Callable[[str], None] | None = None,
13516
+ _walk_share: "_CodexWalkShare | None" = None,
13517
+ _ingest_limit: "int | None" = None,
13518
+ _heal_census: "frozenset[str] | None" = None,
13519
+ _heal_visited: "frozenset[str]" = frozenset(),
13520
+ _heal_visited_rows: "dict[str, tuple | None] | None" = None,
13521
+ _defer_finalization: bool = False,
13522
+ _heal_transition: bool = False,
13523
+ _replay_stage: "str | None" = None,
13524
+ _heal_generation: "str | None" = None,
12486
13525
  ) -> CodexIngestStats:
12487
13526
  """Read-through delta ingest of ~/.codex/sessions/**/*.jsonl.
12488
13527
 
@@ -12520,6 +13559,66 @@ def _sync_codex_cache_locked(
12520
13559
  duplicated cost — measured at roughly half of every growing turn. Every
12521
13560
  other caller keeps ``"auto"``, because the sync-internal reconcile is what
12522
13561
  keeps the projection current for the dashboard and ``cache-sync``.
13562
+
13563
+ The private ``_walk_share`` … ``_heal_transition`` arguments belong to the
13564
+ heal executor (#982 spec 4.1–4.3, ``run_codex_admitted_heal``):
13565
+
13566
+ * ``_walk_share``: this call is an executor unit. Its resolver and ledger
13567
+ path set come from the share (bound under the locks, sealed after the
13568
+ last commit), and the heal fence's population hold and thread-row guard
13569
+ do not apply to it. Every other call holds the population.
13570
+ * ``_ingest_limit``: this call is one piece of a heal walk. After that
13571
+ many files were ingested or reset, the walk stops before the next file
13572
+ with bytes to ingest or reset; it and the rest stay unwalked, as at a
13573
+ budget stop (no deadline, no ``delta_append``), so the piece does not
13574
+ certify. A file that fails does not count.
13575
+ * ``_heal_census`` / ``_heal_visited``: the walk's census (the first
13576
+ piece's discovery set, ``stats.heal_discovered``; ``None`` on the first
13577
+ piece) and the census files earlier pieces examined successfully
13578
+ (``stats.heal_examined``). A piece skips visited files and files
13579
+ outside the census without examining them, counting them as walked
13580
+ (``files_skipped_unchanged``), exactly as the single walk treats a file
13581
+ it already visited or one created after its discovery.
13582
+ ``stats.heal_walk_complete`` says the piece left no census file
13583
+ unwalked.
13584
+ * ``_defer_finalization``: the walk's own settlement runs only the
13585
+ ``expand`` and ``model`` stages and the change-log prune stays owed.
13586
+ * ``_heal_visited_rows``: each visited file's committed cursor row as
13587
+ recorded when a piece visited it (``stats.heal_visit_rows``). The piece
13588
+ that completes the walk re-reads them under its own locks (revision
13589
+ 7): a visited file whose row another writer changed since its visit
13590
+ still counts as walked, but stays in the backlog, makes the barrier
13591
+ census ``0`` and withholds the full-walk certificate
13592
+ (``stats.heal_visit_changed``); it is not re-examined.
13593
+ * ``_heal_transition``: the piece that completes the walk deletes a
13594
+ history heal record in its end-of-walk transaction, whether or not it
13595
+ failed a file or deferred one for a torn ``auth.json``: such a file
13596
+ stays owed exactly as the single walk leaves it
13597
+ (``stats.heal_transitioned``). A replay record is ended by the
13598
+ unchanged consumption block instead: deleted with the marker, or moved
13599
+ to phase ``certify`` when the replay is blocked.
13600
+ * ``_replay_stage="start"`` (#982 spec 4.5): the replay's start unit runs
13601
+ the byte-zero replay's preamble (D1 snapshot, preflight refusals, the
13602
+ clear, which writes the replay heal record, and the authoritative
13603
+ rehydration, which marks it ``rehydrated``) and returns before the
13604
+ walk. With a replay record already present nothing is cleared again,
13605
+ unless a later arming site marked it ``reclear``: the start unit of a
13606
+ new pass is the only executor unit that performs that fresh clear.
13607
+ * ``_heal_generation`` (spec 4.5a): the ``gen`` of the record the unit's
13608
+ pass started with, carried by every executor unit but the start unit.
13609
+ Under the writer locks a unit whose generation is not the record's, or
13610
+ that would have to perform a fresh clear (``reclear``, or an armed
13611
+ marker without a replay record), writes nothing and reports
13612
+ ``stats.heal_generation_stale``; the transition and the consumption
13613
+ block act only when the generations match.
13614
+
13615
+ A replay heal record (spec 4.5) changes every other writer: the D1 test
13616
+ and the compressed-history ``known=`` read its persisted ``known`` set,
13617
+ nothing re-clears, a record not yet ``rehydrated`` is rehydrated first,
13618
+ a targeted call that is not an executor unit and every budgeted call
13619
+ decline ``replay_pending`` (the budgeted one records the deferral and
13620
+ sets ``stats.replay_refill``), and an unbudgeted ordinary call completes
13621
+ the remaining refill as an ordinary walk.
12523
13622
  """
12524
13623
  if quota_reconcile not in ("auto", "defer"):
12525
13624
  raise ValueError(
@@ -12575,7 +13674,14 @@ def _sync_codex_cache_locked(
12575
13674
  rebuild_prune_scope: "_CodexPruneScope | None" = None
12576
13675
  ordinary_prune_scope: "_CodexPruneScope | None" = None
12577
13676
  raised = False
13677
+ # #982: True once this call bound the heal executor's share under its
13678
+ # locks; only such a call may seal (or must invalidate) it.
13679
+ share_bound = False
12578
13680
  try:
13681
+ if _CODEX_PRE_STORE_LOCK_HOOK is not None:
13682
+ # Test seam (#982 R1): after admission returned, immediately
13683
+ # before this sync's own store-lock acquisition.
13684
+ _CODEX_PRE_STORE_LOCK_HOOK()
12579
13685
  with _perf.phase("flock") as _p_flock:
12580
13686
  acquired = acquire_cache_writer_flocks(
12581
13687
  _cctally_core.CACHE_LOCK_PATH,
@@ -12588,6 +13694,11 @@ def _sync_codex_cache_locked(
12588
13694
  if acquired is None:
12589
13695
  eprint("[codex-cache] sync already in progress; using existing cache")
12590
13696
  stats.lock_contended = True
13697
+ if deadline is not None:
13698
+ # #982 spec 4.4: tell the hook whether this tick would have
13699
+ # declined as a replay refill, so it publishes no waiting
13700
+ # request that the refill units could only wait out.
13701
+ stats.replay_refill = _codex_replay_refill_unlocked(conn)
12591
13702
  return stats
12592
13703
  if _after_locks is not None and _after_locks():
12593
13704
  stats.lock_contended = stats.drain_yielded = True
@@ -12641,7 +13752,80 @@ def _sync_codex_cache_locked(
12641
13752
  "SELECT 1 FROM cache_meta WHERE key=?",
12642
13753
  (CODEX_REPLAY_FROM_ZERO_KEY,),
12643
13754
  ).fetchone() is not None
12644
- if replay_pending and (targeted or deadline is not None):
13755
+ if _replay_stage not in (None, "start"):
13756
+ raise ValueError(
13757
+ "sync_codex_cache: _replay_stage must be None or 'start'")
13758
+ if _replay_stage is not None and (
13759
+ targeted or rebuild or deadline is not None):
13760
+ raise ValueError(
13761
+ "sync_codex_cache: _replay_stage is an unbudgeted whole-tree "
13762
+ "start unit")
13763
+ if _heal_generation is not None and (
13764
+ _walk_share is None or _replay_stage is not None or rebuild):
13765
+ raise ValueError(
13766
+ "sync_codex_cache: _heal_generation belongs to an executor "
13767
+ "unit that is not the start unit")
13768
+ # #982 spec 4.5: a replay heal record means the clear already
13769
+ # happened (its start unit committed). Every writer then refills
13770
+ # with the persisted D1 set instead of clearing again; only the
13771
+ # executor's units refill out of the single walk's order.
13772
+ heal_record = load_codex_heal_record(conn)
13773
+ replay_record = (heal_record if heal_record is not None
13774
+ and heal_record.get("mode") == "replay" else None)
13775
+ replay_active = replay_pending or replay_record is not None
13776
+ if _replay_stage is not None and not replay_active:
13777
+ # Nothing to start: another writer consumed the replay.
13778
+ return stats
13779
+ # #982 spec 4.5a (Generation): every executor unit but the start unit
13780
+ # belongs to the pass whose record generation it carries. A record
13781
+ # rewritten since (a fresh clear writes a new `gen`), a `reclear`
13782
+ # mark, or an armed marker without a replay record (a fresh clear
13783
+ # only a new pass's start unit may perform) ends that pass: this unit
13784
+ # writes nothing -- no ingest, clear, consumption, transition or
13785
+ # settlement, and no withdrawal of the full-walk certificate -- so no
13786
+ # piece ever skips a file it visited before a clear it did not see.
13787
+ if (_walk_share is not None and _replay_stage is None
13788
+ and _codex_heal_generation_stale(
13789
+ heal_record, replay_pending, _heal_generation)):
13790
+ stats.heal_generation_stale = True
13791
+ walk_marker_owed = False
13792
+ return stats
13793
+ # #982 spec 4.5a: a replay record re-armed by a later arming site
13794
+ # (`reclear`) is cleared again like an armed marker; a malformed one
13795
+ # (its D1 set or rehydration flag unreadable) declines every writer,
13796
+ # because clearing would recapture D1 from a partly refilled store.
13797
+ # The payload (population and D1 set) is parsed only by a call that
13798
+ # will use it, once, or taken from the executor's share. A `reclear`
13799
+ # mark acts only with the marker its arming site wrote in the same
13800
+ # transaction (`_codex_replay_reclear_due`); one whose marker is gone
13801
+ # (only a damaged store has it) is a record that no longer asks for a
13802
+ # replay, refilled with and retired like any other.
13803
+ replay_reclear = _codex_replay_reclear_due(replay_record,
13804
+ replay_pending)
13805
+ replay_payload: "CodexHealPayload | None" = None
13806
+ if replay_record is not None and not replay_record.get("malformed") \
13807
+ and deadline is None and not (targeted and _walk_share is None):
13808
+ replay_payload = (
13809
+ _walk_share.heal_payload(conn, replay_record)
13810
+ if _walk_share is not None
13811
+ else load_codex_heal_payload(conn, replay_record))
13812
+ replay_malformed = replay_record is not None and (
13813
+ bool(replay_record.get("malformed"))
13814
+ or (deadline is None and not (targeted and _walk_share is None)
13815
+ and replay_payload is None))
13816
+ refill_record = (replay_record if replay_record is not None
13817
+ and not replay_reclear and not replay_malformed
13818
+ else None)
13819
+ if replay_active and (
13820
+ # Spec 4.5a (revision 10): every writer, an explicit
13821
+ # `cache-sync --rebuild` included, fails closed on a malformed
13822
+ # record. Its persisted D1 set may be the only evidence for a
13823
+ # rollout not yet refilled, and a fresh capture cannot recover
13824
+ # it, so no clear may run over it.
13825
+ replay_malformed
13826
+ or deadline is not None
13827
+ or (targeted and (_walk_share is None
13828
+ or refill_record is None))):
12645
13829
  # A live-tail tick must DEFER, never raise through the
12646
13830
  # `targeted and rebuild` guard below.
12647
13831
  #
@@ -12668,13 +13852,54 @@ def _sync_codex_cache_locked(
12668
13852
  # and Codex ingest froze permanently. So the budgeted decline
12669
13853
  # RECORDS itself (doctor reads the record) and hands the unbudgeted
12670
13854
  # drain to a detached worker.
13855
+ #
13856
+ # #982 spec 4.5: with a replay heal record (the clear committed)
13857
+ # a targeted call proceeds only as an executor unit, and a
13858
+ # budgeted one still declines: ingesting outside the executor's
13859
+ # order would change thread rows and anchors relative to the
13860
+ # single walk. `replay_refill` tells the hook that the store is
13861
+ # cleared and partly refilled (no projection, no budget leg): a
13862
+ # record in phase `units`, or one too malformed to tell. A
13863
+ # blocked replay's `certify` record ends the fence, and its hooks
13864
+ # project and run the budget leg as for a blocked replay today.
12671
13865
  if deadline is not None:
12672
13866
  _record_codex_replay_deferral(conn)
13867
+ stats.replay_refill = replay_record is not None and (
13868
+ bool(replay_record.get("malformed"))
13869
+ or replay_record.get("phase") == "units")
12673
13870
  stats.deferred_reason = "replay_pending"
12674
13871
  return stats
12675
13872
  # Both incompatible combinations already returned at the decline above,
12676
- # so the marker-OR'd rebuild needs no second guard.
12677
- rebuild = rebuild or replay_pending
13873
+ # so the marker-OR'd rebuild needs no second guard. A replay whose
13874
+ # start unit committed (a replay record) never clears again: the
13875
+ # caller refills with the record's persisted D1 set. An explicit
13876
+ # caller rebuild still clears (it is its own single walk) and keeps
13877
+ # the persisted set in its snapshot (a malformed record declined
13878
+ # above, whatever the caller asked).
13879
+ rebuild = rebuild or (replay_pending and (replay_record is None
13880
+ or replay_reclear))
13881
+ replay_refill = refill_record is not None and not rebuild
13882
+ if _walk_share is not None and deadline is not None:
13883
+ raise ValueError(
13884
+ "sync_codex_cache: _walk_share is an unbudgeted heal unit")
13885
+ # #982 spec 4.3: a history heal record in phase `units` is the fence.
13886
+ # A sync without the executor's share holds its population (skips
13887
+ # those files, exactly as unsettled history is held) and requests the
13888
+ # worker, which resumes a dead executor from the record. A rebuild is
13889
+ # itself the single walk the heal stands in for: it ingests every
13890
+ # file and retires the record in its clear transaction below.
13891
+ heal_history = (heal_record is not None
13892
+ and heal_record.get("mode") == "history")
13893
+ heal_population: "frozenset[str]" = frozenset()
13894
+ if (heal_history and heal_record.get("phase") == "units"
13895
+ and _walk_share is None and not rebuild):
13896
+ # The one parse of the payload in this call. Unreadable holds
13897
+ # nothing more (the fence itself still stands, fail closed).
13898
+ history_payload = load_codex_heal_payload(conn, heal_record)
13899
+ if history_payload is not None:
13900
+ heal_population = history_payload.population
13901
+ stats.drain_requested = tuple(sorted(
13902
+ set(stats.drain_requested) | {CODEX_REPLAY_DRAIN_COMMAND}))
12678
13903
  # #927 spec §4.2 B4: the ordinary prune's protection needs every
12679
13904
  # retained identity at a discovered path the scope refuses, including
12680
13905
  # path-level refusal under a recognized root and child-only
@@ -12715,7 +13940,10 @@ def _sync_codex_cache_locked(
12715
13940
  # design rejects, and it is what breaks acceptance criterion 4. Captured
12716
13941
  # BEFORE the clear because the clear is what erases the evidence. A
12717
13942
  # rollout first seen DURING a rebuild is absent here and takes a normal
12718
- # first-ingest decision.
13943
+ # first-ingest decision. The clear below persists this set in its own
13944
+ # transaction for EVERY clear (the replay record's `known`, #982 4.5;
13945
+ # an explicit rebuild arms the replay there, #1051), so an interrupt
13946
+ # between the clear's commit and the walk's completion never loses it.
12719
13947
  # Keyed on the DURABLE file identity, never on `path`. `path` holds the
12720
13948
  # first configured candidate spelling, which is unstable across
12721
13949
  # `$CODEX_HOME` reordering and symlink respelling (review F12) — a
@@ -12736,6 +13964,20 @@ def _sync_codex_cache_locked(
12736
13964
  except (ValueError, TypeError, OSError):
12737
13965
  continue
12738
13966
  rebuild_known_identities |= _codex_history_identities(conn)
13967
+ if replay_payload is not None:
13968
+ # #982 spec 4.5: an explicit rebuild (or a re-armed replay's
13969
+ # fresh clear, spec 4.5a) over a refill in progress keeps
13970
+ # every identity the replay's own snapshot captured before its
13971
+ # clear (the cursors it erased).
13972
+ rebuild_known_identities |= replay_payload.known
13973
+ elif replay_refill:
13974
+ # #982 spec 4.5: the snapshot the replay's start unit captured
13975
+ # before its clear, persisted in the record. Never recaptured: the
13976
+ # cursors it was taken from are gone. Shared, never copied: a
13977
+ # refill only reads it.
13978
+ rebuild_known_identities = replay_payload.known
13979
+ # Which reads use the D1 snapshot: a clearing rebuild's and a refill's.
13980
+ d1_snapshot = rebuild or replay_refill
12739
13981
  if rebuild and not targeted:
12740
13982
  # #485: rebuild used to clear first and discover second. With an
12741
13983
  # empty/missing/misconfigured root that made the explicit repair
@@ -12748,12 +13990,30 @@ def _sync_codex_cache_locked(
12748
13990
  preflight_sources, _preflight_root_keys = (
12749
13991
  _collect_retained_codex_paths_and_roots(conn)
12750
13992
  )
13993
+ preflight_roots = _codex_provider_roots()
13994
+ # #983 spec 5.2: a pre-#294 S1 row carries no source_root_key. The
13995
+ # snapshot above skipped its cursor, and the partition would never
13996
+ # check the configured root that holds it, so resolve each one to
13997
+ # its owning root before either guard reads it. A discovered file
13998
+ # joins the D1 snapshot here, before the clear and the walk.
13999
+ if any(root_key is None for _path, root_key in preflight_sources):
14000
+ discovered_by_identity = {
14001
+ str(item.physical_path): item for item in preflight_files}
14002
+ resolved_sources: list[tuple[str, str | None]] = []
14003
+ for _path, root_key in preflight_sources:
14004
+ if root_key is None:
14005
+ root_key, owner_file = _codex_legacy_null_root_owner(
14006
+ _path, discovered_by_identity, preflight_roots)
14007
+ if owner_file is not None:
14008
+ rebuild_known_identities.add(
14009
+ codex_file_identity(owner_file))
14010
+ resolved_sources.append((_path, root_key))
14011
+ preflight_sources = resolved_sources
12751
14012
  preflight_source_root_keys = {
12752
14013
  root_key
12753
14014
  for _path, root_key in preflight_sources
12754
14015
  if root_key is not None
12755
14016
  }
12756
- preflight_roots = _codex_provider_roots()
12757
14017
  (
12758
14018
  _safe_sources,
12759
14019
  refused_sources,
@@ -12797,12 +14057,45 @@ def _sync_codex_cache_locked(
12797
14057
  if rebuild:
12798
14058
  # Clear INSIDE the lock — see sync_cache() for the full
12799
14059
  # rationale. Done before the existing SELECT so delta
12800
- # detection sees an empty baseline.
14060
+ # detection sees an empty baseline. The clear, the full
14061
+ # finalization debt, the replay marker and the replay record
14062
+ # with the D1 set commit together in this one transaction
14063
+ # (#1051): there is no intermediate commit.
12801
14064
  if _clear_codex_derived_rows(conn):
12802
14065
  _bump_codex_physical_mutation_seq(conn)
12803
14066
  # #980 D3: full re-derivation restores the unattributed state, so
12804
14067
  # every root's whole range is owed, in the wipe's own transaction.
12805
14068
  add_codex_finalization_debt(conn, full=True)
14069
+ if heal_history:
14070
+ # #982: this walk re-reads every rollout in discovery order.
14071
+ _delete_codex_heal_record(conn)
14072
+ if not replay_pending:
14073
+ # #1051: every clear runs as a byte-zero replay, an explicit
14074
+ # `cache-sync --rebuild` included. Arming in the clear's own
14075
+ # transaction is what persists the D1 snapshot below: an
14076
+ # explicit rebuild interrupted after this commit and before
14077
+ # its walk completes leaves the marker and the replay record,
14078
+ # so every retry refills with the persisted `known` instead of
14079
+ # sending pre-mechanism history to the live `auth.json`. The
14080
+ # predicate is marker absence, not `not replay_active`, so a
14081
+ # replay record that lost its marker gets it back too. From
14082
+ # here on this call runs the replay's lifecycle: the
14083
+ # rehydration marks the record, a clean complete walk consumes
14084
+ # both, a blocked one moves the record to `certify`.
14085
+ arm_codex_replay_from_zero(conn)
14086
+ replay_pending = True
14087
+ replay_active = True
14088
+ if replay_active:
14089
+ # #982 spec 4.5: the clear's own transaction writes the replay
14090
+ # heal record with the D1 snapshot, so no later writer clears
14091
+ # again or recaptures it from the cursors this just erased.
14092
+ # The rehydration's transaction below marks it `rehydrated`.
14093
+ _store_codex_heal_record(
14094
+ conn, {"mode": "replay", "phase": "units",
14095
+ "rehydrated": False},
14096
+ population=[str(item.source_path)
14097
+ for item in preflight_files],
14098
+ known=rebuild_known_identities)
12806
14099
  conn.commit()
12807
14100
  eprint("[cache-sync] rebuild: cleared Codex cached entries")
12808
14101
  # #416 spec §3.4: rehydrate the attribution map from the journal BEFORE
@@ -12847,8 +14140,16 @@ def _sync_codex_cache_locked(
12847
14140
  except sqlite3.DatabaseError:
12848
14141
  _cursor_row = None
12849
14142
  _cursor_text = _cursor_row[0] if _cursor_row else None
14143
+ # #982 spec 4.5: a replay record whose rehydration never committed (a
14144
+ # crash between the clear's commit and the rehydration's) is
14145
+ # rehydrated authoritatively first, under these locks, before any
14146
+ # rollout is read; the same transaction marks it `rehydrated`.
14147
+ mark_rehydrated = replay_active and (
14148
+ rebuild or (refill_record is not None
14149
+ and not refill_record.get("rehydrated")))
14150
+ authoritative_rehydration = bool(rebuild) or mark_rehydrated
12850
14151
  _since = None
12851
- if _cursor_text and not rebuild:
14152
+ if _cursor_text and not authoritative_rehydration:
12852
14153
  _seg, _sep, _off = str(_cursor_text).rpartition(":")
12853
14154
  if _seg and _off.isdigit():
12854
14155
  _since = (_seg, int(_off))
@@ -12869,10 +14170,10 @@ def _sync_codex_cache_locked(
12869
14170
  # `database is locked` trigger #297 documents.
12870
14171
  _rehydration = _jr.rehydrate_codex_journal_families(
12871
14172
  conn,
12872
- authoritative=bool(rebuild),
14173
+ authoritative=authoritative_rehydration,
12873
14174
  file_account_since=_since,
12874
14175
  window_attribution_since=(
12875
- None if rebuild
14176
+ None if authoritative_rehydration
12876
14177
  else load_codex_window_attribution_cursor(conn)),
12877
14178
  caller="sync_codex_cache(rebuild=True)",
12878
14179
  )
@@ -12903,7 +14204,15 @@ def _sync_codex_cache_locked(
12903
14204
  # without the caller having to model that function's write
12904
14205
  # conditions. Getting this wrong strands an open transaction across
12905
14206
  # the whole walk.
12906
- if rebuild or _new_cursor != _cursor_text or conn.in_transaction:
14207
+ if mark_rehydrated:
14208
+ rehydrated_record = load_codex_heal_record(conn)
14209
+ if (rehydrated_record is not None
14210
+ and rehydrated_record.get("mode") == "replay"
14211
+ and not rehydrated_record.get("malformed")):
14212
+ rehydrated_record["rehydrated"] = True
14213
+ _store_codex_heal_fence(conn, rehydrated_record)
14214
+ if (authoritative_rehydration or _new_cursor != _cursor_text
14215
+ or conn.in_transaction):
12907
14216
  conn.commit()
12908
14217
  # Both reports come AFTER the commit (closeout review C5): the
12909
14218
  # `except` below rolls back, so anything printed before it would
@@ -12930,6 +14239,10 @@ def _sync_codex_cache_locked(
12930
14239
  "[cache-sync] could not rehydrate Codex attribution decisions "
12931
14240
  f"or window attributions: {exc}; deferring the Codex walk")
12932
14241
  return stats
14242
+ if _replay_stage == "start":
14243
+ # #982 spec 4.5: the start unit ends with the rehydration; the
14244
+ # executor's units refill in the single walk's order.
14245
+ return stats
12933
14246
 
12934
14247
  # #980 D2: a budgeted call settles the finalization debt it inherited
12935
14248
  # BEFORE walking, under the same admission rules, so older debt cannot
@@ -13225,8 +14538,14 @@ def _sync_codex_cache_locked(
13225
14538
  # #980 F3: a budgeted walk bounds the resolver's group seed read with
13226
14539
  # its walk deadline (late-bound clock, so the walk's test seam drives
13227
14540
  # it); an unbudgeted call is unchanged.
13228
- anchor_resolver = CodexResetAnchorResolver(
13229
- conn, deadline=deadline, clock=lambda: _walk_clock())
14541
+ if _walk_share is not None:
14542
+ # #982 spec 4.2: the single walk's own resolver, shared across the
14543
+ # heal's units; kept only while no other writer mutated quota.
14544
+ anchor_resolver = _walk_share.bind_walk(conn)
14545
+ share_bound = True
14546
+ else:
14547
+ anchor_resolver = CodexResetAnchorResolver(
14548
+ conn, deadline=deadline, clock=lambda: _walk_clock())
13230
14549
  # #901 §5.3c (W11, Q14): every call without the hook's budget prepares
13231
14550
  # its delta with no write transaction open and applies it in one
13232
14551
  # transaction, each file in its own savepoint, at most
@@ -13246,6 +14565,16 @@ def _sync_codex_cache_locked(
13246
14565
  # Whole-transaction losses per file, so a persistent one ends.
13247
14566
  w11_losses: "dict[str, int]" = {}
13248
14567
  walk_queue = collections.deque(enumerate(walk))
14568
+ # #982 spec 4.1 steps 4 and 7: a heal walk piece's census (its own
14569
+ # discovery set on the first piece) and the census files it found
14570
+ # unchanged; ``None`` outside a heal walk.
14571
+ heal_census: "frozenset[str] | None" = None
14572
+ heal_unchanged: "set[str]" = set()
14573
+ if _ingest_limit is not None:
14574
+ stats.heal_discovered = frozenset(
14575
+ str(item.source_path) for item in files)
14576
+ heal_census = (stats.heal_discovered if _heal_census is None
14577
+ else frozenset(_heal_census))
13249
14578
 
13250
14579
  def _w11_batch_bytes() -> int:
13251
14580
  return sum(item.new_input for item in w11_batch)
@@ -13495,7 +14824,9 @@ def _sync_codex_cache_locked(
13495
14824
  strike_marks: list[tuple[str, int]] = []
13496
14825
  barrier_holds: list = []
13497
14826
  barrier_touched: set[str] = set()
13498
- history = _CodexHistoryContext(conn)
14827
+ history = _CodexHistoryContext(
14828
+ conn, ledger_source=(None if _walk_share is None
14829
+ else _walk_share.ledger_paths))
13499
14830
  for _walk_index, discovered in _walk_items():
13500
14831
  # The budget is checked BEFORE a file is opened, so a tick either
13501
14832
  # commits a file whole (or to a recorded partial offset) or does not
@@ -13506,12 +14837,28 @@ def _sync_codex_cache_locked(
13506
14837
  break
13507
14838
  jp = discovered.source_path
13508
14839
  path_str = str(jp)
14840
+ if heal_census is not None and (
14841
+ path_str in _heal_visited or path_str not in heal_census):
14842
+ # #982 spec 4.1: an earlier piece of this walk examined the
14843
+ # file (bytes appended since belong to later syncs), or it was
14844
+ # created after the census. Either way the single walk would
14845
+ # not read it again, so it is walked, never examined.
14846
+ stats.files_skipped_unchanged += 1
14847
+ continue
13509
14848
  if path_str in protected_paths:
13510
14849
  # The file is still visible, but its configured root did not
13511
14850
  # yield recognizable Codex data. Treat that as unknown
13512
14851
  # filesystem state: neither truncation/requalification reset
13513
14852
  # nor append ingestion may replace the retained generation.
13514
14853
  continue
14854
+ if path_str in heal_population:
14855
+ # #982 spec 4.3: only the heal executor ingests population
14856
+ # files, in the single walk's order; held like unsettled
14857
+ # history (the worker was requested above).
14858
+ stats.container_reads_held += 1
14859
+ stats.container_skipped["verification-pending"] = (
14860
+ stats.container_skipped.get("verification-pending", 0) + 1)
14861
+ continue
13515
14862
  selected = _select_codex_container(jp)
13516
14863
  if selected is None:
13517
14864
  eprint(f"[codex-cache] stat failed for {jp}: no container")
@@ -13541,7 +14888,7 @@ def _sync_codex_cache_locked(
13541
14888
  # included, is classified like compressed history.
13542
14889
  verdict = history.classify(
13543
14890
  conn, codex_file_identity(discovered), path_str,
13544
- known=rebuild_known_identities if rebuild else (),
14891
+ known=rebuild_known_identities if d1_snapshot else (),
13545
14892
  archive=selected.location == _rc.ARCHIVE)
13546
14893
  if verdict == "unknown":
13547
14894
  # Untraced compressed history waits for the off-hook
@@ -13721,6 +15068,8 @@ def _sync_codex_cache_locked(
13721
15068
  and size == prev_size
13722
15069
  ):
13723
15070
  stats.files_skipped_unchanged += 1
15071
+ if heal_census is not None:
15072
+ heal_unchanged.add(path_str)
13724
15073
  continue
13725
15074
  elif (
13726
15075
  not requalified and not replaced and not incomplete
@@ -13746,6 +15095,32 @@ def _sync_codex_cache_locked(
13746
15095
  initial_total_tokens = 0
13747
15096
  prev_total_tokens = None
13748
15097
 
15098
+ # #982 spec 4.1 steps 4 and 7: a heal walk piece ingests (or
15099
+ # resets) at most `_ingest_limit` files. Reaching the next file
15100
+ # with bytes to ingest or reset stops the piece BEFORE it is read:
15101
+ # it and the rest stay unwalked, as at a budget stop, and the
15102
+ # piece does not certify.
15103
+ if (_ingest_limit is not None
15104
+ and path_str not in stats.heal_counted_paths
15105
+ and len(stats.heal_counted_paths) >= _ingest_limit):
15106
+ requeue = (_w11_flush(final=False) if w11 and w11_batch
15107
+ else [])
15108
+ # W11 counts a file when it is prepared; one whose batch then
15109
+ # failed ingested nothing, so it does not count (the next
15110
+ # piece re-examines it) and this piece goes on.
15111
+ stats.heal_counted_paths &= set(committed_state)
15112
+ if len(stats.heal_counted_paths) < _ingest_limit:
15113
+ if requeue:
15114
+ walk_queue.extendleft(
15115
+ reversed(requeue + [(_walk_index, discovered)]))
15116
+ continue
15117
+ else:
15118
+ stats.heal_limit_stopped = True
15119
+ owed = ({_walk_index}
15120
+ | {index for index, _item in requeue}
15121
+ | {index for index, _item in walk_queue})
15122
+ unwalked = [walk[index] for index in sorted(owed)]
15123
+ break
13749
15124
  # #901 §5.3c (W11): a file's new input is known before it is read.
13750
15125
  # Preparation stops before the file that would take the batch past
13751
15126
  # either bound; that batch is applied and committed, and
@@ -13870,7 +15245,7 @@ def _sync_codex_cache_locked(
13870
15245
  # an ascending list, so the appended low offset would then shadow
13871
15246
  # the real decision for every later byte in the file.
13872
15247
  file_account_key = None
13873
- elif rebuild and file_identity in rebuild_known_identities:
15248
+ elif d1_snapshot and file_identity in rebuild_known_identities:
13874
15249
  # Spec D1: history that was never durably stamped becomes
13875
15250
  # unattributed; nothing is inferred. A rebuild re-reads bytes
13876
15251
  # that were ingested BEFORE this mechanism existed, and reading
@@ -14463,6 +15838,7 @@ def _sync_codex_cache_locked(
14463
15838
  inode=st.st_ino,
14464
15839
  container=container_acceptance,
14465
15840
  barrier=barrier_read,
15841
+ thread_guard=heal_population,
14466
15842
  ),
14467
15843
  merges=anchor_resolver.take_pending_merges(),
14468
15844
  reset_counted=bool(truncated or requalified),
@@ -14477,6 +15853,8 @@ def _sync_codex_cache_locked(
14477
15853
  skip_reasons=dict(iter_state.skip_reasons),
14478
15854
  ))
14479
15855
  _codex_w11_trace("prepare_end", path_str, new_input)
15856
+ if _ingest_limit is not None:
15857
+ stats.heal_counted_paths.add(path_str)
14480
15858
  # Private test seam: after a file is prepared and before the
14481
15859
  # next file's preparation, which under W11 is where a later
14482
15860
  # target's turn begins (its commit comes with its batch's).
@@ -14535,6 +15913,7 @@ def _sync_codex_cache_locked(
14535
15913
  inode=st.st_ino,
14536
15914
  container=container_acceptance,
14537
15915
  barrier=barrier_read,
15916
+ thread_guard=heal_population,
14538
15917
  )
14539
15918
  except sqlite3.DatabaseError as exc:
14540
15919
  conn.rollback()
@@ -14567,6 +15946,8 @@ def _sync_codex_cache_locked(
14567
15946
  anchor_resolver.discard_uncommitted_file()
14568
15947
  continue
14569
15948
  anchor_resolver.mark_file_committed()
15949
+ if _ingest_limit is not None:
15950
+ stats.heal_counted_paths.add(path_str)
14570
15951
  # #901 §5.3c (W9): the size trigger is evaluated after each commit.
14571
15952
  _wal_checkpoint.after_commit(conn)
14572
15953
  committed_state[path_str] = (
@@ -14625,6 +16006,24 @@ def _sync_codex_cache_locked(
14625
16006
  # points: the pre-open deadline check fills `unwalked`, while a stop
14626
16007
  # INSIDE a file only sets the flag.
14627
16008
  walk_complete = not stats.budget_exhausted and not unwalked
16009
+ # #982 spec 4.1 steps 4 and 7 (revision 7): the piece that completes
16010
+ # a heal walk certifies a file an earlier piece visited only while its
16011
+ # committed cursor row still equals the one recorded at that visit.
16012
+ # A file another writer changed since (a hook that ingested part of an
16013
+ # append and stopped at its budget, say) still counts as walked, but
16014
+ # stays in this piece's backlog, makes the barrier census 0 and
16015
+ # withholds the full-walk certificate, exactly as a single walk
16016
+ # followed by that writer would leave them; it is not re-examined.
16017
+ visit_changed: "list[CodexDiscoveredFile]" = []
16018
+ if heal_census is not None and walk_complete and _heal_visited_rows:
16019
+ current_rows = _load_codex_session_files_rows(
16020
+ conn, sorted(_heal_visited_rows))
16021
+ changed = frozenset(
16022
+ path for path, row in _heal_visited_rows.items()
16023
+ if current_rows.get(path) != row)
16024
+ stats.heal_visit_changed = changed
16025
+ visit_changed = [item for item in files
16026
+ if str(item.source_path) in changed]
14628
16027
  _p_walk.set_count(stats.files_processed)
14629
16028
  _p_walk.set_meta(skipped=stats.files_skipped_unchanged,
14630
16029
  rows=stats.rows_changed)
@@ -14671,7 +16070,8 @@ def _sync_codex_cache_locked(
14671
16070
  else (unwalked[0] if unwalked else None))
14672
16071
  backlog_files, backlog_bytes = _codex_backlog_after(
14673
16072
  partial_files + unwalked
14674
- + [item.discovered for item in withdrawn],
16073
+ + [item.discovered for item in withdrawn]
16074
+ + visit_changed,
14675
16075
  existing, committed_state)
14676
16076
  stats.backlog_files = backlog_files
14677
16077
  stats.backlog_bytes = backlog_bytes
@@ -14750,8 +16150,32 @@ def _sync_codex_cache_locked(
14750
16150
  elif walk_complete:
14751
16151
  conn.execute("DELETE FROM cache_meta WHERE key = ?",
14752
16152
  ("codex_torn_auth_deferred",))
16153
+ # #982 spec 4.1 step 5: the transition. The fenced walk's
16154
+ # piece that leaves no census file unwalked ends a history
16155
+ # heal's fence here, in its own end-of-walk transaction
16156
+ # (before its settlement, which the executor defers to the
16157
+ # finalization units anyway), whether or not it failed a file
16158
+ # or deferred one for a torn `auth.json`: such a file stays
16159
+ # owed exactly as the single walk leaves it, and the census
16160
+ # below, set from this piece's counters, keeps it blocking
16161
+ # new seals. (A replay heal keeps the consumption conditions
16162
+ # of the block after this one.) A run that later withdraws
16163
+ # its last batch repeats this step with `walk_complete` False
16164
+ # after the rollback undid the delete.
16165
+ if _heal_transition:
16166
+ stats.heal_transitioned = False
16167
+ if walk_complete:
16168
+ transition_record = load_codex_heal_record(conn)
16169
+ if (transition_record is not None
16170
+ and transition_record.get("mode") == "history"
16171
+ and transition_record.get("gen")
16172
+ == _heal_generation):
16173
+ _delete_codex_heal_record(conn)
16174
+ stats.heal_transitioned = True
14753
16175
  # Consume the byte-zero replay marker only after a clean full walk,
14754
- # and only when THIS call observed it. A contended call returned
16176
+ # and only when THIS call observed it or armed it itself (#1051:
16177
+ # every clear arms the marker in its own transaction, so an
16178
+ # explicit rebuild consumes what it armed). A contended call returned
14755
16179
  # long before here, and a walk that failed or deferred a file leaves
14756
16180
  # the marker standing — a surviving marker is what makes the repair
14757
16181
  # retry on the next sync, and it is also what keeps
@@ -14769,6 +16193,19 @@ def _sync_codex_cache_locked(
14769
16193
  # file. It is defense in depth today — a budgeted tick declines a
14770
16194
  # pending replay outright above — but the two clears must keep the
14771
16195
  # same shape.
16196
+ # #982 spec 4.5a (Generation): an executor walk consumes the
16197
+ # marker, ends a replay record or moves it to `certify` only
16198
+ # while the record is still its pass's, re-read here under the
16199
+ # same locks (the preamble already returned otherwise).
16200
+ heal_generation_ok = True
16201
+ if _walk_share is not None:
16202
+ heal_generation_ok = not _codex_heal_generation_stale(
16203
+ load_codex_heal_record(conn),
16204
+ conn.execute(
16205
+ "SELECT 1 FROM cache_meta WHERE key=? LIMIT 1",
16206
+ (CODEX_REPLAY_FROM_ZERO_KEY,)).fetchone()
16207
+ is not None,
16208
+ _heal_generation)
14772
16209
  if (
14773
16210
  walk_complete
14774
16211
  and stats.files_failed == 0
@@ -14786,17 +16223,26 @@ def _sync_codex_cache_locked(
14786
16223
  )
14787
16224
  ):
14788
16225
  _clear_codex_prune_refusal(conn)
14789
- if replay_pending:
16226
+ if replay_pending and heal_generation_ok:
14790
16227
  conn.execute("DELETE FROM cache_meta WHERE key = ?",
14791
16228
  (CODEX_REPLAY_FROM_ZERO_KEY,))
14792
- conn.execute("DELETE FROM cache_meta WHERE key = ?",
14793
- (CODEX_REPLAY_BLOCKED_KEY,))
14794
- # The budgeted-decline record is cleared by the same walk, for
14795
- # the same reason: an unbudgeted walk reaching here is exactly
14796
- # the caller whose absence the record was reporting.
14797
- conn.execute("DELETE FROM cache_meta WHERE key = ?",
14798
- (CODEX_REPLAY_DEFERRED_KEY,))
14799
- elif replay_pending and walk_complete:
16229
+ # #982 spec 4.1 step 5: a replay heal record ends with
16230
+ # the marker, in the same transaction.
16231
+ consumed_record = load_codex_heal_record(conn)
16232
+ if (heal_generation_ok and consumed_record is not None
16233
+ and consumed_record.get("mode") == "replay"):
16234
+ _delete_codex_heal_record(conn)
16235
+ stats.heal_transitioned = _heal_transition
16236
+ if heal_generation_ok:
16237
+ conn.execute("DELETE FROM cache_meta WHERE key = ?",
16238
+ (CODEX_REPLAY_BLOCKED_KEY,))
16239
+ # The budgeted-decline record is cleared by the same
16240
+ # walk, for the same reason: an unbudgeted walk
16241
+ # reaching here is exactly the caller whose absence
16242
+ # the record was reporting.
16243
+ conn.execute("DELETE FROM cache_meta WHERE key = ?",
16244
+ (CODEX_REPLAY_DEFERRED_KEY,))
16245
+ elif replay_pending and walk_complete and heal_generation_ok:
14800
16246
  # A full walk ran and could NOT consume the marker, so the
14801
16247
  # replay — and with it every Codex transcript ingest, which
14802
16248
  # defers behind this marker — is stalled rather than merely
@@ -14808,6 +16254,15 @@ def _sync_codex_cache_locked(
14808
16254
  "files_failed": stats.files_failed,
14809
16255
  "files_deferred_torn": stats.files_deferred_torn,
14810
16256
  }, sort_keys=True))
16257
+ # #982 spec 4.1 step 5: a blocked replay ends its fence
16258
+ # (phase `certify`) and keeps `known`; nothing re-clears.
16259
+ blocked_record = load_codex_heal_record(conn)
16260
+ if (blocked_record is not None
16261
+ and blocked_record.get("mode") == "replay"
16262
+ and blocked_record.get("phase") == "units"):
16263
+ blocked_record["phase"] = "certify"
16264
+ _store_codex_heal_fence(conn, blocked_record)
16265
+ stats.heal_transitioned = _heal_transition
14811
16266
  conn.execute(_CODEX_FINALIZATION_RELEASE)
14812
16267
  # #980 D3/D4: settle the persisted finalization debt — the debt
14813
16268
  # this call's batches wrote and any debt inherited — in order:
@@ -14837,15 +16292,21 @@ def _sync_codex_cache_locked(
14837
16292
  # a later call (or the executor) revisits it; a budgeted call
14838
16293
  # admits a unit only while the budget leaves room, and a unit that
14839
16294
  # reaches the deadline rolls back and is never reported as a zero.
16295
+ settle_stages = (
16296
+ {"stages": _CODEX_HEAL_FENCED_STAGES} if _defer_finalization
16297
+ else {})
14840
16298
  outcome = _codex_settle_finalization(
14841
16299
  conn, deadline=budget_deadline, clock=_finalization_clock,
14842
- core_model=True, strike_state=strike_state)
16300
+ core_model=True, strike_state=strike_state, **settle_stages)
14843
16301
  for message in outcome.errors:
14844
16302
  eprint(message)
14845
16303
  if (outcome.model_failed or outcome.adoption_failed
14846
16304
  or outcome.reconcile_failed):
14847
16305
  stats.maintenance_failed = True
14848
16306
  if outcome.cancelled or outcome.deferred_to_worker or (
16307
+ # #982: a fenced or deferred settlement leaves debt owed.
16308
+ outcome.fenced or (
16309
+ _defer_finalization and not outcome.completed)) or (
14849
16310
  budget_deadline is not None and not outcome.completed):
14850
16311
  # Cancelled, handed to the worker, or a unit the remaining
14851
16312
  # budget could not admit: the persisted debt stays owed.
@@ -14870,7 +16331,11 @@ def _sync_codex_cache_locked(
14870
16331
  # prunes; when the prune is due it records it as owed maintenance
14871
16332
  # for the drain executor instead.
14872
16333
  with _codex_finalization_stage(conn):
14873
- if deadline is None:
16334
+ # #982 spec 4.3: during the heal fence (and in a heal round
16335
+ # that defers its settlement) a due prune stays owed for the
16336
+ # executor instead.
16337
+ if (deadline is None and not _defer_finalization
16338
+ and not _codex_heal_fence_active(conn)):
14874
16339
  _prune_codex_accounting_change_log(conn)
14875
16340
  elif _codex_change_log_prune_due(conn):
14876
16341
  record_codex_owed_maintenance(conn, change_log_prune=True)
@@ -14889,6 +16354,13 @@ def _sync_codex_cache_locked(
14889
16354
  stats.maintenance_failed = False
14890
16355
  finalization_notes.clear()
14891
16356
  _finalization_steps(False, w11_withdrawn)
16357
+ if heal_census is not None:
16358
+ # #982 spec 4.1: committed and unchanged census files are
16359
+ # visited; a withdrawn batch's files are not (`committed_state`
16360
+ # forgot them), so the next piece examines them again.
16361
+ stats.heal_examined = frozenset(
16362
+ (heal_unchanged | set(committed_state)) & heal_census)
16363
+ stats.heal_walk_complete = walk_complete and not w11_lost
14892
16364
  # Projection intentionally runs only after this function releases the
14893
16365
  # Codex cache flock in ``finally`` below. cache.db and stats.db are not
14894
16366
  # cross-database atomic: after this committed ingest, a projection
@@ -14975,6 +16447,7 @@ def _sync_codex_cache_locked(
14975
16447
  and not stats.files_deferred_reset
14976
16448
  and not stats.container_pending
14977
16449
  and not stats.container_reads_held
16450
+ and not stats.heal_visit_changed
14978
16451
  and (
14979
16452
  stats.files_processed + stats.files_skipped_unchanged
14980
16453
  + sum(stats.container_skipped.values())
@@ -15016,6 +16489,7 @@ def _sync_codex_cache_locked(
15016
16489
  barrier_census = bool(
15017
16490
  walk_complete and stats.files_failed == 0
15018
16491
  and stats.files_deferred_torn == 0 and not w11_lost
16492
+ and not stats.heal_visit_changed
15019
16493
  and stats.files_processed + stats.files_skipped_unchanged
15020
16494
  + sum(stats.container_skipped.values())
15021
16495
  + stats.container_pending == stats.files_total)
@@ -15083,6 +16557,11 @@ def _sync_codex_cache_locked(
15083
16557
  # one recorded the drain as owed above.
15084
16558
  if deadline is None:
15085
16559
  _maybe_truncate_wal(conn, _cctally_core.CACHE_DB_PATH)
16560
+ if heal_census is not None and stats.heal_examined:
16561
+ # Revision 7: what this piece visited, as committed, read after
16562
+ # its last write and before its locks are released.
16563
+ stats.heal_visit_rows = _load_codex_session_files_rows(
16564
+ conn, sorted(stats.heal_examined))
15086
16565
  except BaseException:
15087
16566
  raised = True
15088
16567
  raise
@@ -15126,7 +16605,16 @@ def _sync_codex_cache_locked(
15126
16605
  raise
15127
16606
  if read_policy is not None and raised:
15128
16607
  read_policy.invalidate("cache_unavailable")
16608
+ if share_bound and not raised:
16609
+ # #982 spec 4.2: after this unit's last commit, before its
16610
+ # locks are released: the next unit reuses the memo only if
16611
+ # nobody mutated quota in between.
16612
+ _walk_share.seal(conn)
16613
+ share_bound = False
15129
16614
  finally:
16615
+ if share_bound:
16616
+ # An exception (or a failure above): the next unit reseeds.
16617
+ _walk_share.invalidate()
15130
16618
  # #981 R4: released on every path, whatever the pin raised.
15131
16619
  release_cache_writer_flocks(held_writer_flocks)
15132
16620
 
@@ -15342,9 +16830,299 @@ class FinalizationOutcome:
15342
16830
  restored: int = 0
15343
16831
  readopted: int = 0
15344
16832
  deferred_to_worker: bool = False
16833
+ #: #982 spec 4.3: the heal fence restricted this settlement to the
16834
+ #: ``expand`` and ``model`` stages; callers treat it like a deferred unit.
16835
+ fenced: bool = False
15345
16836
  errors: list = field(default_factory=list)
15346
16837
 
15347
16838
 
16839
+ # --------------------------------------------------------------------------
16840
+ # #982 / #975 — the Codex heal record and its fence (spec 4.3)
16841
+ # --------------------------------------------------------------------------
16842
+ #
16843
+ # The heal record is two ``cache_meta`` keys written and retired together:
16844
+ #
16845
+ # * ``codex_heal_walk``, the small FENCE key ``{"v", "gen", "mode", "phase"}``
16846
+ # plus, in replay mode, ``"rehydrated"`` and, once a later arming site
16847
+ # re-arms the replay, ``"reclear"``. Every fence check reads only this key.
16848
+ # * ``codex_heal_walk_payload``, the history-sized half ``{"v", "gen",
16849
+ # "population"}`` plus, in replay mode, ``"known"`` (the D1 snapshot). It is
16850
+ # parsed at most once per sync call, or once per executor pass (the walk
16851
+ # share hands it to the units), and bound to the fence key by ``gen``.
16852
+ #
16853
+ # Both keys are written once by the heal executor's start unit (or the
16854
+ # replay's clear) in one transaction and deleted together once by the
16855
+ # transition (the first fenced certification round that walks clean and
16856
+ # complete); they are never rewritten per unit. Only the fence key changes in
16857
+ # between: the rehydration sets ``rehydrated``, a blocked replay moves it to
16858
+ # phase ``certify``, and ``arm_codex_replay_from_zero`` sets ``reclear``.
16859
+ # While the fence key holds phase ``units`` (the fence):
16860
+ #
16861
+ # * only the executor's units (syncs carrying its ``_walk_share``) ingest a
16862
+ # population file; every other sync holds them and requests the worker;
16863
+ # * every one-way stamping path is restricted: the settlement chokepoint runs
16864
+ # only ``expand`` and ``model`` and reports ``fenced``, and the two stamping
16865
+ # kernels stamp nothing (or raise ``CodexHealFenced`` when strict);
16866
+ # * a non-executor thread-row upsert never replaces a population file's row;
16867
+ # * the accounting change-log prune deletes nothing and stays owed.
16868
+
16869
+ CODEX_HEAL_WALK_KEY = "codex_heal_walk"
16870
+ CODEX_HEAL_PAYLOAD_KEY = "codex_heal_walk_payload"
16871
+ CODEX_HEAL_WALK_LOCK_NAME = "codex-heal-walk.lock"
16872
+ CODEX_HEAL_FENCED_TOKEN = "deferred_heal"
16873
+ _CODEX_HEAL_RECORD_VERSION = 1
16874
+ _CODEX_HEAL_FENCED_STAGES = ("expand", "model")
16875
+
16876
+
16877
+ class CodexHealFenced(sqlite3.DatabaseError):
16878
+ """A strict stamping kernel refused while the heal fence holds.
16879
+
16880
+ A ``sqlite3.DatabaseError`` whose text is the status token
16881
+ ``deferred_heal``, mirroring ``deferred_model``: ``account attribute``'s
16882
+ all-or-nothing apply rolls its cache transaction back and reaches its
16883
+ existing ``recordedPending`` outcome, and a rerun after the heal
16884
+ completes it."""
16885
+
16886
+
16887
+ class CodexHealPayload(NamedTuple):
16888
+ """The history-sized half of a heal record, parsed once."""
16889
+
16890
+ gen: str
16891
+ population: "frozenset[str]"
16892
+ known: "frozenset[str] | None" # replay mode only (the D1 snapshot)
16893
+
16894
+
16895
+ def _codex_heal_meta_value(conn: sqlite3.Connection, key: str):
16896
+ try:
16897
+ row = conn.execute("SELECT value FROM cache_meta WHERE key=?",
16898
+ (key,)).fetchone()
16899
+ except sqlite3.OperationalError as exc:
16900
+ if "no such table" in str(exc):
16901
+ return None
16902
+ raise
16903
+ return None if row is None else row[0]
16904
+
16905
+
16906
+ def load_codex_heal_record(conn: sqlite3.Connection) -> "dict | None":
16907
+ """The heal record's small fence key, or None. Never the payload.
16908
+
16909
+ Unreadable fails closed. A record nobody can parse is a fence over an
16910
+ unknown population marked ``malformed``: in history mode (the next
16911
+ executor's transition removes it), or in replay mode when the record
16912
+ says so or the byte-zero replay marker is armed, because a replay record
16913
+ that lost its D1 set must never be cleared again with a recaptured one
16914
+ (spec 4.5a): writers decline and the executor ends ``degraded``."""
16915
+ text = _codex_heal_meta_value(conn, CODEX_HEAL_WALK_KEY)
16916
+ if text is None:
16917
+ return None
16918
+ try:
16919
+ record = json.loads(str(text))
16920
+ except (TypeError, ValueError):
16921
+ record = None
16922
+ if (isinstance(record, dict)
16923
+ and record.get("v") == _CODEX_HEAL_RECORD_VERSION
16924
+ and isinstance(record.get("gen"), str)
16925
+ and record.get("mode") in ("history", "replay")
16926
+ and record.get("phase") in ("units", "certify")
16927
+ and isinstance(record.get("reclear", False), bool)
16928
+ and (record.get("mode") != "replay"
16929
+ or isinstance(record.get("rehydrated"), bool))):
16930
+ return record
16931
+ replay = (isinstance(record, dict) and record.get("mode") == "replay")
16932
+ if not replay:
16933
+ try:
16934
+ replay = conn.execute(
16935
+ "SELECT 1 FROM cache_meta WHERE key=? LIMIT 1",
16936
+ (CODEX_REPLAY_FROM_ZERO_KEY,)).fetchone() is not None
16937
+ except sqlite3.OperationalError:
16938
+ replay = False
16939
+ return {"v": _CODEX_HEAL_RECORD_VERSION, "gen": None,
16940
+ "mode": "replay" if replay else "history", "phase": "units",
16941
+ "malformed": True}
16942
+
16943
+
16944
+ def _codex_replay_refill_unlocked(conn: sqlite3.Connection) -> bool:
16945
+ """Would a budgeted sync decline as a replay refill (#982 spec 4.4)?
16946
+
16947
+ Read without the store locks by a budgeted call that met contention: a
16948
+ replay heal record in phase ``units``, or one too malformed to tell, is
16949
+ the state in which no budgeted tick ingests, projects or evaluates a
16950
+ budget (the predicate the decline sets ``replay_refill`` from). A waiting
16951
+ request could then only make the refill units idle for its lifetime.
16952
+ A marker without a record (the start unit has not committed) and a
16953
+ blocked replay's ``certify`` record answer False: their hooks still
16954
+ project, so their requests are kept. Unreadable answers False, so the
16955
+ hook publishes its request, as today."""
16956
+ try:
16957
+ record = load_codex_heal_record(conn)
16958
+ except sqlite3.Error:
16959
+ return False
16960
+ return (record is not None and record.get("mode") == "replay"
16961
+ and (bool(record.get("malformed"))
16962
+ or record.get("phase") == "units"))
16963
+
16964
+
16965
+ def load_codex_heal_payload(
16966
+ conn: sqlite3.Connection,
16967
+ record: "dict | None") -> "CodexHealPayload | None":
16968
+ """Parse the record's payload: the population and, in replay mode, the
16969
+ D1 set. None when the record is malformed or the payload is missing,
16970
+ malformed or another record's (its ``gen`` differs); callers fail closed
16971
+ (a history fence over an empty population; a replay that declines)."""
16972
+ if record is None or record.get("malformed"):
16973
+ return None
16974
+ text = _codex_heal_meta_value(conn, CODEX_HEAL_PAYLOAD_KEY)
16975
+ if text is None:
16976
+ return None
16977
+ try:
16978
+ raw = json.loads(str(text))
16979
+ except (TypeError, ValueError):
16980
+ return None
16981
+ if not (isinstance(raw, dict)
16982
+ and raw.get("v") == _CODEX_HEAL_RECORD_VERSION
16983
+ and raw.get("gen") == record.get("gen")):
16984
+ return None
16985
+ population = raw.get("population")
16986
+ if not (isinstance(population, list)
16987
+ and all(isinstance(p, str) for p in population)):
16988
+ return None
16989
+ known = None
16990
+ if record.get("mode") == "replay":
16991
+ known = raw.get("known")
16992
+ if not (isinstance(known, list)
16993
+ and all(isinstance(k, str) for k in known)):
16994
+ return None
16995
+ known = frozenset(known)
16996
+ return CodexHealPayload(str(raw["gen"]), frozenset(population), known)
16997
+
16998
+
16999
+ def _codex_heal_fence_active(conn: sqlite3.Connection) -> bool:
17000
+ """The fence: a heal record in phase ``units``."""
17001
+ record = load_codex_heal_record(conn)
17002
+ return record is not None and record.get("phase") == "units"
17003
+
17004
+
17005
+ def _codex_heal_fence_json(record: dict) -> str:
17006
+ fence = {key: value for key, value in record.items()
17007
+ if key not in ("malformed", "population", "known")}
17008
+ return json.dumps(fence, sort_keys=True, separators=(",", ":"))
17009
+
17010
+
17011
+ def _store_codex_heal_record(conn: sqlite3.Connection, record: dict, *,
17012
+ population, known=None) -> dict:
17013
+ """Write a NEW record, both keys under a fresh ``gen``, on the caller's
17014
+ transaction (the history start unit, the replay clear). Returns the
17015
+ fence key as written."""
17016
+ fence = {key: value for key, value in record.items()
17017
+ if key not in ("malformed", "population", "known", "reclear")}
17018
+ fence["v"] = _CODEX_HEAL_RECORD_VERSION
17019
+ fence["gen"] = uuid.uuid4().hex
17020
+ payload = {"v": _CODEX_HEAL_RECORD_VERSION, "gen": fence["gen"],
17021
+ "population": sorted(set(str(p) for p in population))}
17022
+ if fence.get("mode") == "replay":
17023
+ payload["known"] = sorted(set(known or ()))
17024
+ conn.execute(
17025
+ "INSERT OR REPLACE INTO cache_meta(key, value) VALUES (?, ?)",
17026
+ (CODEX_HEAL_PAYLOAD_KEY,
17027
+ json.dumps(payload, sort_keys=True, separators=(",", ":"))))
17028
+ conn.execute(
17029
+ "INSERT OR REPLACE INTO cache_meta(key, value) VALUES (?, ?)",
17030
+ (CODEX_HEAL_WALK_KEY, _codex_heal_fence_json(fence)))
17031
+ return fence
17032
+
17033
+
17034
+ def _store_codex_heal_fence(conn: sqlite3.Connection, record: dict) -> None:
17035
+ """Rewrite only the fence key of an existing record (its ``gen`` and
17036
+ payload unchanged): ``rehydrated``, phase ``certify``, ``reclear``."""
17037
+ conn.execute(
17038
+ "INSERT OR REPLACE INTO cache_meta(key, value) VALUES (?, ?)",
17039
+ (CODEX_HEAL_WALK_KEY, _codex_heal_fence_json(record)))
17040
+
17041
+
17042
+ def _delete_codex_heal_record(conn: sqlite3.Connection) -> None:
17043
+ conn.execute("DELETE FROM cache_meta WHERE key IN (?, ?)",
17044
+ (CODEX_HEAL_WALK_KEY, CODEX_HEAL_PAYLOAD_KEY))
17045
+
17046
+
17047
+ def arm_codex_replay_from_zero(conn: sqlite3.Connection) -> None:
17048
+ """Arm the guarded byte-zero Codex replay, on the caller's transaction.
17049
+
17050
+ Every arming site goes through here (spec 4.5a). Without a replay heal
17051
+ record this writes ``CODEX_REPLAY_FROM_ZERO_KEY`` and nothing else. With
17052
+ one (a refill in progress or a blocked replay's ``certify`` record) the
17053
+ clear already happened, so a bare marker would only be refilled and the
17054
+ repair it was armed for would never re-derive the rows already refilled.
17055
+ The record is marked ``reclear`` instead: the next writer clears again
17056
+ exactly as an armed marker does today, with the fresh D1 capture united
17057
+ with the record's persisted ``known`` (a rollout not yet refilled keeps
17058
+ its pre-mechanism identity, I7), and writes a new record. A malformed
17059
+ record is left as it is: it already fails closed.
17060
+ """
17061
+ _set_cache_meta(conn, CODEX_REPLAY_FROM_ZERO_KEY, "1")
17062
+ record = load_codex_heal_record(conn)
17063
+ if (record is not None and record.get("mode") == "replay"
17064
+ and not record.get("malformed") and not record.get("reclear")):
17065
+ record["reclear"] = True
17066
+ _store_codex_heal_fence(conn, record)
17067
+
17068
+
17069
+ def _codex_replay_reclear_due(record: "dict | None",
17070
+ replay_armed: bool) -> bool:
17071
+ """Whether a replay record's ``reclear`` mark asks for a fresh clear.
17072
+
17073
+ ``arm_codex_replay_from_zero`` writes the mark with the replay marker in
17074
+ one transaction, and the consumption block deletes the marker with the
17075
+ record, so the mark acts only while its marker is armed. A mark whose
17076
+ marker is gone (only a damaged ``cache_meta`` has one) asks for nothing:
17077
+ every writer treats the record as one that no longer needs a replay,
17078
+ refills with its persisted D1 set and retires it, and none clears."""
17079
+ return bool(replay_armed and record is not None
17080
+ and record.get("mode") == "replay" and record.get("reclear"))
17081
+
17082
+
17083
+ def _codex_heal_generation_stale(record: "dict | None", replay_armed: bool,
17084
+ generation: "str | None") -> bool:
17085
+ """#982 spec 4.5a (Generation): whether an executor unit of the pass
17086
+ that started with record generation ``generation`` must write nothing.
17087
+
17088
+ True when the store now asks for a fresh clear, which only the start
17089
+ unit of a new pass (or an unbudgeted ordinary sync outside any pass) may
17090
+ perform: the replay marker is armed without a replay record, or the
17091
+ replay record is marked ``reclear`` with that marker armed
17092
+ (``_codex_replay_reclear_due``). Also true when the record is not
17093
+ the pass's (another ``gen``). An unreadable record has no ``gen``, so it
17094
+ is the pass's only when the pass started from it (a malformed history
17095
+ record, which that pass's transition removes; a malformed replay record
17096
+ still declines every writer below). A missing record with no armed
17097
+ marker is not stale: the pass's transition (or another writer's
17098
+ complete walk) ended it, and the finalization units and the closing walk
17099
+ run after that."""
17100
+ if replay_armed and (record is None or record.get("mode") != "replay"):
17101
+ return True
17102
+ if record is None:
17103
+ return False
17104
+ return (_codex_replay_reclear_due(record, replay_armed)
17105
+ or record.get("gen") != generation)
17106
+
17107
+
17108
+ def _codex_heal_fenced_kernel(conn: sqlite3.Connection, strict: bool) -> bool:
17109
+ """The stamping kernels' own check (spec 4.3): True means "stamp nothing";
17110
+ strict callers get ``CodexHealFenced`` instead."""
17111
+ try:
17112
+ fenced = _codex_heal_fence_active(conn)
17113
+ except sqlite3.DatabaseError as exc:
17114
+ if (isinstance(exc, sqlite3.OperationalError)
17115
+ and _is_sqlite_interrupt(exc)):
17116
+ # A cancellation, never a swallowable read error (D4).
17117
+ raise CodexFinalizationCancelled("interrupt") from exc
17118
+ if strict:
17119
+ raise
17120
+ return True
17121
+ if fenced and strict:
17122
+ raise CodexHealFenced(CODEX_HEAL_FENCED_TOKEN)
17123
+ return fenced
17124
+
17125
+
15348
17126
  def _codex_malformed_debt() -> CodexFinalizationDebt:
15349
17127
  return CodexFinalizationDebt(
15350
17128
  full=True, model=True, attribution=True, malformed=True)
@@ -15993,6 +17771,13 @@ def _codex_settle_finalization(
15993
17771
  """
15994
17772
  outcome = FinalizationOutcome()
15995
17773
  budgeted = deadline is not None
17774
+ if _codex_heal_fence_active(conn):
17775
+ # #982 spec 4.3: while a heal holds its population, adoption and
17776
+ # reconciliation (one-way NULL -> account stamps) would decide from a
17777
+ # partly ingested store. Expansion and model resolution are safe:
17778
+ # model resolution reads only the observation's own rollout.
17779
+ stages = tuple(s for s in stages if s in _CODEX_HEAL_FENCED_STAGES)
17780
+ outcome.fenced = True
15996
17781
  if strike_state is None:
15997
17782
  # One call, one strike: a caller settling in several steps (the sync's
15998
17783
  # pre-walk settle and its finalization) shares one state instead.
@@ -16359,6 +18144,9 @@ def apply_codex_window_spend_adoption(
16359
18144
  high = max(span[1] for span in spans.values())
16360
18145
  reset_bounds = (low - _CODEX_ACCOUNT_WEEK, high + _CODEX_ACCOUNT_WEEK)
16361
18146
  candidate_bounds = (low - _CODEX_ACCOUNT_WEEK, high)
18147
+ # #982 spec 4.3: no one-way stamp while a heal holds its population.
18148
+ if _codex_heal_fenced_kernel(conn, strict):
18149
+ return 0
16362
18150
 
16363
18151
  from _cctally_quota import load_codex_quota_observations
16364
18152
 
@@ -16501,6 +18289,59 @@ def _codex_stored_identity_path(
16501
18289
  return _codex_identity_path(selected, _codex_root_for_logical(logical, roots))
16502
18290
 
16503
18291
 
18292
+ def _codex_legacy_null_root_owner(
18293
+ path: str,
18294
+ discovered_by_identity: "dict[str, CodexDiscoveredFile]",
18295
+ roots: "list[CodexProviderRoot]",
18296
+ ) -> "tuple[str | None, CodexDiscoveredFile | None]":
18297
+ """#983 spec 5.2: ``(root_key, discovered_file | None)`` for a stored path
18298
+ whose row carries no ``source_root_key`` (pre-#294 S1), or ``(None, None)``.
18299
+
18300
+ Ownership follows discovery's own answer, never literal containment of the
18301
+ stored spelling, because a symlinked spelling, overlapping roots and the
18302
+ #970 archive release all defeat that. In order:
18303
+
18304
+ 1. the stored identity names a discovered file: that file's root, and the
18305
+ file itself so the rebuild's D1 snapshot gains exactly the identity the
18306
+ walk computes for it;
18307
+ 2. otherwise the first root in configured order whose CANONICALIZED walk
18308
+ or archive scope contains the identity (``_codex_scope_root`` would
18309
+ compare the configured spelling, which misses a symlinked root);
18310
+ 3. otherwise a configured root ``_codex_provider_roots`` omitted (an
18311
+ unmounted volume, a missing directory), keyed as ``_codex_prune_scope``
18312
+ keys it, so the scope refuses it as configured but unrecognizable;
18313
+ 4. otherwise no owner: the path is outside every configured root and stays
18314
+ removable, as a root-keyed row whose root left the configured set is.
18315
+ """
18316
+ try:
18317
+ phys = _codex_stored_identity_path(path, roots)
18318
+ except (OSError, TypeError, ValueError):
18319
+ return None, None
18320
+ discovered = discovered_by_identity.get(str(phys))
18321
+ if discovered is not None:
18322
+ return discovered.source_root_key, discovered
18323
+ for root in roots:
18324
+ scopes = [root.walk_root]
18325
+ if root.archive_root is not None:
18326
+ scopes.append(root.archive_root)
18327
+ for scope in scopes:
18328
+ try:
18329
+ if phys.is_relative_to(_canonical_codex_path(scope)):
18330
+ return root.source_root_key, None
18331
+ except (OSError, TypeError, ValueError):
18332
+ continue
18333
+ walked = {root.source_root_key for root in roots}
18334
+ for raw_root in _cctally()._codex_home_roots():
18335
+ try:
18336
+ canonical = _canonical_codex_path(raw_root)
18337
+ key = source_root_key(str(canonical))
18338
+ if key not in walked and phys.is_relative_to(canonical):
18339
+ return key, None
18340
+ except (OSError, TypeError, ValueError):
18341
+ continue
18342
+ return None, None
18343
+
18344
+
16504
18345
 
16505
18346
  def codex_file_key_for_entry_path(
16506
18347
  source_root_key: str, source_path: str, *,
@@ -16620,9 +18461,9 @@ class _CodexHistoryContext:
16620
18461
  """One walk's provenance state, read once before the walk loop."""
16621
18462
 
16622
18463
  __slots__ = ("payload", "traced", "settle_on_finish", "ledger_paths",
16623
- "consulted", "archives_covered")
18464
+ "consulted", "archives_covered", "ledger_source")
16624
18465
 
16625
- def __init__(self, conn):
18466
+ def __init__(self, conn, *, ledger_source=None):
16626
18467
  import _cctally_journal as _jr
16627
18468
  self.payload = _codex_history_marker(conn)
16628
18469
  self.archives_covered = _codex_history_marker_covers_archives(
@@ -16635,6 +18476,9 @@ class _CodexHistoryContext:
16635
18476
  and _jr.journal_high_water() is None)
16636
18477
  self.ledger_paths = None
16637
18478
  self.consulted = False
18479
+ # #982 spec 4.2: a heal unit reads the ledger path set through its
18480
+ # share (one full read per pass, then only new rows).
18481
+ self.ledger_source = ledger_source
16638
18482
 
16639
18483
  def classify(self, conn, identity: str, path: str, *, known=(),
16640
18484
  archive: bool = False) -> str:
@@ -16657,7 +18501,9 @@ class _CodexHistoryContext:
16657
18501
  self.consulted = True
16658
18502
  return "history"
16659
18503
  if self.ledger_paths is None:
16660
- self.ledger_paths = _codex_ledger_paths(conn)
18504
+ self.ledger_paths = (_codex_ledger_paths(conn)
18505
+ if self.ledger_source is None
18506
+ else self.ledger_source(conn))
16661
18507
  if _codex_has_history_provenance(conn, identity, path,
16662
18508
  ledger_paths=self.ledger_paths):
16663
18509
  return "history"
@@ -16721,6 +18567,40 @@ def _codex_history_candidates(*, archive_only: bool = False) -> "list[str]":
16721
18567
  conn.close()
16722
18568
 
16723
18569
 
18570
+ def _codex_provenance_identity_write(
18571
+ share: _CodexWalkShare, path: str, apply, *, wait_for_requests: bool):
18572
+ """One provenance identity's admitted transaction over ``share``.
18573
+
18574
+ ``apply(conn, anchors)`` receives the identity's resolver, built from
18575
+ copies of the share's committed-evidence base (#975, spec 4.2), and
18576
+ returns whether it applied. After the commit, still under the writer
18577
+ locks, the base absorbs that identity's stored rows and the token is
18578
+ sealed. A refusal, an exception or a rollback adds nothing and drops the
18579
+ base, so the next identity seeds from the committed store.
18580
+ """
18581
+ applied: list = []
18582
+
18583
+ def run(conn):
18584
+ result = apply(conn, share.identity_resolver(conn))
18585
+ applied.append(bool(result))
18586
+ return result
18587
+
18588
+ def absorb(conn) -> None:
18589
+ if applied and applied[0]:
18590
+ share.absorb_identity(conn, path)
18591
+ share.seal(conn)
18592
+
18593
+ try:
18594
+ admitted, result = _codex_admitted_cache_write(
18595
+ run, wait_for_requests=wait_for_requests, after_commit=absorb)
18596
+ except BaseException:
18597
+ share.invalidate()
18598
+ raise
18599
+ if not admitted or not result:
18600
+ share.invalidate()
18601
+ return admitted, result
18602
+
18603
+
16724
18604
  def run_codex_history_provenance_drain(
16725
18605
  *, wait_for_requests: bool = True) -> ProvenanceDrainResult:
16726
18606
  """Settle provenance and retained quota before historical raw ingestion.
@@ -16753,6 +18633,9 @@ def run_codex_history_provenance_drain(
16753
18633
  for path, item in prepared.items()}
16754
18634
  traced, high_water, quotas, decisions = _jr._codex_journal_history_evidence(
16755
18635
  candidates, identities=identities.values(), capture=True)
18636
+ # #975 RC2: one committed-evidence seed for the whole pass, so each
18637
+ # anchor group is read once rather than once per identity.
18638
+ share = _CodexWalkShare()
16756
18639
  for path in candidates:
16757
18640
  item, identity = prepared[path], identities[path]
16758
18641
  # Codex legacy normalization is independent of Claude's cutover
@@ -16761,7 +18644,7 @@ def run_codex_history_provenance_drain(
16761
18644
  records = list(_jr._decoded_quota_stream(quotas.get(path, ()), None))
16762
18645
  file_ops = decisions.get(identity, ())
16763
18646
 
16764
- def apply(conn, path=path, item=item, identity=identity,
18647
+ def apply(conn, anchors, path=path, item=item, identity=identity,
16765
18648
  records=records, file_ops=file_ops):
16766
18649
  live = _qualify_codex_targets({path})
16767
18650
  if (len(live) != 1
@@ -16774,7 +18657,8 @@ def run_codex_history_provenance_drain(
16774
18657
  return False # another writer moved the captured population
16775
18658
  before = conn.total_changes
16776
18659
  _jr._apply_file_account_records(conn, file_ops)
16777
- _jr._apply_quota_records(conn, records, quiet=True)
18660
+ _jr._apply_quota_records(conn, records, quiet=True,
18661
+ anchors=anchors)
16778
18662
  if conn.total_changes != before:
16779
18663
  # Preserve triggers/change ledgers; no partial recovery
16780
18664
  # mints journal coverage or leaves old coverage standing.
@@ -16782,8 +18666,8 @@ def run_codex_history_provenance_drain(
16782
18666
  _bump_codex_physical_mutation_seq(conn)
16783
18667
  return True
16784
18668
 
16785
- admitted, applied = _codex_admitted_cache_write(
16786
- apply, wait_for_requests=wait_for_requests)
18669
+ admitted, applied = _codex_provenance_identity_write(
18670
+ share, path, apply, wait_for_requests=wait_for_requests)
16787
18671
  if not admitted or not applied:
16788
18672
  return ProvenanceDrainResult("yielded", len(candidates), len(traced))
16789
18673
  # A changed inventory is retried from one new captured census; never
@@ -16819,6 +18703,511 @@ def run_codex_history_provenance_drain(
16819
18703
  _release_worker_lock(worker)
16820
18704
 
16821
18705
 
18706
+ def _codex_replay_heal_due(conn: sqlite3.Connection) -> bool:
18707
+ """An armed byte-zero replay or a replay heal record (#982 spec 4.6)."""
18708
+ if conn.execute("SELECT 1 FROM cache_meta WHERE key=? LIMIT 1",
18709
+ (CODEX_REPLAY_FROM_ZERO_KEY,)).fetchone() is not None:
18710
+ return True
18711
+ record = load_codex_heal_record(conn)
18712
+ return record is not None and record.get("mode") == "replay"
18713
+
18714
+
18715
+ class HealResult(NamedTuple):
18716
+ """What one ``run_codex_admitted_heal`` pass did (#982 spec 4.1)."""
18717
+
18718
+ outcome: str # "done", "busy", "yielded", "contended", "degraded",
18719
+ # "refused", "replay_due", "restart" or "none"
18720
+ mode: "str | None"
18721
+ units: int # admitted targeted units: population and owed pass
18722
+ owed_units: int # of them, the owed pass's
18723
+ degraded: int # population files still owed after their attempts (U6)
18724
+ finalization_units: int
18725
+ certification_rounds: int # fenced and closing full rounds
18726
+ #: The last admitted sync's stats (the start unit's when it refused, the
18727
+ #: last walk piece's otherwise) with every work counter of the summary
18728
+ #: line (`_CODEX_HEAL_SUMMED_COUNTERS`) summed over every unit and piece
18729
+ #: of the pass, except the skipped count: the files the pass only found
18730
+ #: unchanged, each once. The failed and torn-deferred counts stay the
18731
+ #: last piece's. Each file is reported once, as the single walk does.
18732
+ #: ``None`` when no unit ran.
18733
+ stats: "CodexIngestStats | None" = None
18734
+
18735
+
18736
+ #: The work counters a heal pass reports summed over its units and pieces.
18737
+ _CODEX_HEAL_SUMMED_COUNTERS = (
18738
+ "files_processed", "files_skipped_unchanged", "files_reset_truncated",
18739
+ "rows_changed", "lines_malformed", "token_events_skipped")
18740
+
18741
+
18742
+ class _CodexHealYield(Exception):
18743
+ """A fresh hook request made a non-waiting executor give up its pass."""
18744
+
18745
+
18746
+ class _CodexHealContended(Exception):
18747
+ """Another writer held the store locks for longer than the replay worker
18748
+ has always waited for them (``CODEX_REPLAY_DRAIN_LOCK_TIMEOUT_SECONDS``)."""
18749
+
18750
+
18751
+ class _CodexHealRestart(Exception):
18752
+ """A unit found its pass generation stale (spec 4.5a): the record was
18753
+ rewritten, or a fresh clear only a new pass's start unit may perform is
18754
+ due. The unit wrote nothing; the pass ends ``restart``."""
18755
+
18756
+
18757
+ def _codex_heal_owed_files(
18758
+ conn: sqlite3.Connection,
18759
+ files: "list[CodexDiscoveredFile]") -> "list[CodexDiscoveredFile]":
18760
+ """The discovered files whose bytes ``_codex_backlog_after`` still owes:
18761
+ no cursor, an incomplete cursor, or a size beyond the cursor."""
18762
+ existing = _load_codex_session_files_rows(
18763
+ conn, [str(item.source_path) for item in files])
18764
+ return [item for item in files
18765
+ if _codex_backlog_after([item], existing)[0]]
18766
+
18767
+
18768
+ def run_codex_admitted_heal(
18769
+ *, wait_for_requests: bool = True,
18770
+ population: "list[str] | None" = None) -> HealResult:
18771
+ """Run a Codex history heal as admitted per-file units (#982 spec 4.1).
18772
+
18773
+ Single-flight through ``codex-heal-walk.lock`` (busy returns ``busy``).
18774
+ No writer lock is held between units, and every unit is admitted the way
18775
+ ``_apply_codex_verdict`` admits a verdict: the store is opened before
18776
+ admission, ``admit`` takes only the gate (the sync owns its locks, tried
18777
+ without waiting), and the sync rechecks for a fresh request once it holds
18778
+ them (``_after_locks``). A contended or rechecked unit changed nothing;
18779
+ with ``wait_for_requests`` the executor waits out every fresh request and
18780
+ retries the same unit, otherwise it returns ``yielded``. Store locks that
18781
+ another writer holds continuously for ``CODEX_REPLAY_DRAIN_LOCK_TIMEOUT_SECONDS``
18782
+ (the replay worker's lock wait) end the pass ``contended``.
18783
+
18784
+ History mode runs when ``population`` (the census a settled provenance
18785
+ pass just produced) is given or a history heal record exists, in order:
18786
+
18787
+ 1. the start unit writes the record (phase ``units``: the fence);
18788
+ 2. one targeted unit per population file, in discovery order;
18789
+ 3. the owed pass: every discovered file still owed, at most twice each;
18790
+ 4. the fenced walk: one full walk in admitted pieces (``_ingest_limit=1``,
18791
+ ``_heal_census``, ``_heal_visited``, settlement deferred), each
18792
+ ingesting or resetting at most one file, until a piece leaves no
18793
+ census file unwalked; that piece makes the transition, deleting the
18794
+ record, whether or not a file failed (it stays owed);
18795
+ 5. the #980 executor's unbudgeted finalization units until the debt is
18796
+ empty (a failed or idle unit hands the rest to ``_codex-prune-drain``);
18797
+ 6. the closing walk: a second walk in pieces, with its own census and
18798
+ visited set and with ordinary settlement and sealing; its last piece
18799
+ certifies the walk.
18800
+
18801
+ A piece that neither completes its walk nor adds a census file to the
18802
+ visited set (only an existing refusal such as ``quota_index_pending``
18803
+ does) ends the pass as ``degraded``: the record stays, and the worker is
18804
+ requested again. Every other piece but the last adds a visited file, so a
18805
+ walk takes at most one piece more than its census has files.
18806
+
18807
+ Replay mode (spec 4.5) runs when ``CODEX_REPLAY_FROM_ZERO_KEY`` or a
18808
+ replay record exists, whatever ``population`` says. Its start unit
18809
+ (``_replay_stage="start"``) captures the D1 snapshot, clears and writes
18810
+ the record (population: every discovered file), then rehydrates; a
18811
+ resumed pass whose record is not yet ``rehydrated`` reruns it, which
18812
+ rehydrates without clearing again. A refused start (``rebuild_refused``
18813
+ or a prune refusal) clears nothing and returns ``refused``. Steps 2-6
18814
+ follow exactly as in history mode, except that the fenced walk's last
18815
+ piece ends the record through the unchanged consumption block: deleted
18816
+ with the marker, or moved to phase ``certify`` when the replay is
18817
+ blocked (a failed or torn-deferred file). A ``certify`` record resumes at
18818
+ step 5: its fence is over, and the closing walk retries the blocked file
18819
+ and consumes the marker once it walks clean.
18820
+
18821
+ Generation (spec 4.5a): every unit after the start unit carries the
18822
+ ``gen`` of the record the pass started with (``_heal_generation``), and
18823
+ each finalization unit checks it under its store locks too. A unit that
18824
+ finds another ``gen``, a ``reclear`` mark or an armed marker without a
18825
+ replay record writes nothing; the pass then ends ``restart``, discarding
18826
+ its census, visited set and cached payload, and requests the worker under
18827
+ the short retry throttle. Only the next pass's start unit performs the
18828
+ fresh clear, so no piece skips a file it visited before that clear.
18829
+ """
18830
+ import _lib_codex_drain_admission as adm
18831
+ worker = _worker_lock(CODEX_HEAL_WALK_LOCK_NAME)
18832
+ if worker is None:
18833
+ return HealResult("busy", None, 0, 0, 0, 0, 0)
18834
+ counts = {"units": 0, "owed": 0, "degraded": 0, "final": 0, "rounds": 0}
18835
+ summed = collections.Counter()
18836
+ last: dict = {"stats": None, "mode": "history"}
18837
+ # The summary reports each file at most once per pass, as the single
18838
+ # walk does: a file any unit processed (or reset) counts as processed
18839
+ # only, and one the pass only found unchanged counts as skipped once,
18840
+ # however many walks re-find it. A piece's skips of visited and
18841
+ # out-of-census files stay in its own `files_skipped_unchanged` (the
18842
+ # barrier census sum needs them) and never reach the pass's count.
18843
+ pass_files: "dict[str, set[str]]" = {"processed": set(), "unchanged": set()}
18844
+ # A worker requested by this pass (`contended`, `degraded`) is spawned
18845
+ # after the heal lock is released: `_defer_codex_replay_drain` spawns
18846
+ # nothing while a pass holds it.
18847
+ worker_owed = {"replay": False}
18848
+
18849
+ def request_worker() -> None:
18850
+ worker_owed["replay"] = True
18851
+
18852
+ def count_pass_files(only_paths, stats: CodexIngestStats) -> None:
18853
+ if only_paths is not None:
18854
+ if stats.files_processed or stats.files_reset_truncated:
18855
+ pass_files["processed"].update(only_paths)
18856
+ elif stats.files_skipped_unchanged:
18857
+ pass_files["unchanged"].update(only_paths)
18858
+ return
18859
+ counted = set(stats.heal_counted_paths)
18860
+ pass_files["processed"] |= counted
18861
+ pass_files["unchanged"] |= set(stats.heal_examined) - counted
18862
+
18863
+ def result(outcome: str, mode: "str | None" = None) -> HealResult:
18864
+ stats = last["stats"]
18865
+ if stats is not None:
18866
+ stats = _dc_replace(stats, **{
18867
+ **{name: summed[name] for name in _CODEX_HEAL_SUMMED_COUNTERS},
18868
+ "files_skipped_unchanged": len(
18869
+ pass_files["unchanged"] - pass_files["processed"])})
18870
+ return HealResult(outcome, last["mode"] if mode is None else mode,
18871
+ counts["units"], counts["owed"],
18872
+ counts["degraded"], counts["final"],
18873
+ counts["rounds"], stats)
18874
+
18875
+ share = _CodexWalkShare()
18876
+ # The generation of the record this pass works on (spec 4.5a), set once
18877
+ # the start unit (or the probe of a resumed record) has it.
18878
+ pass_state: dict = {"gen": None}
18879
+
18880
+ def recheck() -> bool:
18881
+ return bool(adm.fresh_requests(_cctally_core.APP_DIR))
18882
+
18883
+ def admitted_sync(kind: str, **kwargs) -> CodexIngestStats:
18884
+ # The store locks held by another writer (not a hook request, which
18885
+ # expires) are waited for as long as the replay worker's single sync
18886
+ # always waited for them, then the pass gives up as `contended`.
18887
+ contended_since: "float | None" = None
18888
+ while True:
18889
+ stats = None
18890
+ conn = open_cache_db()
18891
+ try:
18892
+ admission = adm.admit(_cctally_core.APP_DIR,
18893
+ lambda: "sync-owns-locks",
18894
+ lambda token: None)
18895
+ if admission.admitted:
18896
+ try:
18897
+ with _perf.phase("codex_heal_unit") as _p_unit:
18898
+ _p_unit.set_meta(kind=kind)
18899
+ if kind != "start":
18900
+ kwargs["_heal_generation"] = pass_state["gen"]
18901
+ stats = sync_codex_cache(
18902
+ conn, maintenance="caller", lock_timeout=0,
18903
+ _walk_share=share, _after_locks=recheck,
18904
+ **kwargs)
18905
+ finally:
18906
+ adm.finish(admission, lambda token: None)
18907
+ finally:
18908
+ conn.close()
18909
+ if stats is not None and stats.heal_generation_stale:
18910
+ raise _CodexHealRestart()
18911
+ if stats is not None and not stats.lock_contended:
18912
+ last["stats"] = stats
18913
+ for name in _CODEX_HEAL_SUMMED_COUNTERS:
18914
+ summed[name] += int(getattr(stats, name) or 0)
18915
+ count_pass_files(kwargs.get("only_paths"), stats)
18916
+ return stats
18917
+ if not wait_for_requests:
18918
+ raise _CodexHealYield()
18919
+ if (stats is not None and not stats.drain_yielded
18920
+ and not adm.fresh_requests(_cctally_core.APP_DIR)):
18921
+ now = time.monotonic()
18922
+ if contended_since is None:
18923
+ contended_since = now
18924
+ if now - contended_since >= CODEX_REPLAY_DRAIN_LOCK_TIMEOUT_SECONDS:
18925
+ raise _CodexHealContended()
18926
+ else:
18927
+ # An admission refusal, a drain yield, or a raw refusal met
18928
+ # while a request (a Codex hook's, or the Claude waiter,
18929
+ # #1055 spec 4.3) is fresh: a deliberate wait, not continuous
18930
+ # contention.
18931
+ contended_since = None
18932
+ _drain_backoff(adm)
18933
+
18934
+ def run_finalization_units() -> None:
18935
+ """Step 5: unbudgeted finalization units until the debt is empty.
18936
+ Store locks another writer holds continuously are waited for as long
18937
+ as every other unit waits for them, then the pass is `contended`."""
18938
+ contended_since: "float | None" = None
18939
+ while True:
18940
+ conn = open_cache_db()
18941
+ try:
18942
+ if load_codex_finalization_debt(conn).empty:
18943
+ break
18944
+ conn.rollback()
18945
+ admission = adm.admit(_cctally_core.APP_DIR, _cache_store_locks,
18946
+ _release_cache_store_locks)
18947
+ if not admission.admitted:
18948
+ if not wait_for_requests:
18949
+ raise _CodexHealYield()
18950
+ if adm.fresh_requests(_cctally_core.APP_DIR):
18951
+ contended_since = None
18952
+ else:
18953
+ now = time.monotonic()
18954
+ if contended_since is None:
18955
+ contended_since = now
18956
+ if (now - contended_since
18957
+ >= CODEX_REPLAY_DRAIN_LOCK_TIMEOUT_SECONDS):
18958
+ raise _CodexHealContended()
18959
+ _drain_backoff(adm)
18960
+ continue
18961
+ contended_since = None
18962
+ try:
18963
+ # Spec 4.5a: under the store locks, the unit belongs to
18964
+ # this pass's record generation or writes nothing.
18965
+ if _codex_heal_generation_stale(
18966
+ load_codex_heal_record(conn),
18967
+ conn.execute(
18968
+ "SELECT 1 FROM cache_meta WHERE key=? LIMIT 1",
18969
+ (CODEX_REPLAY_FROM_ZERO_KEY,)).fetchone()
18970
+ is not None,
18971
+ pass_state["gen"]):
18972
+ raise _CodexHealRestart()
18973
+ with _perf.phase("codex_heal_unit") as _p_unit:
18974
+ _p_unit.set_meta(kind="finalization")
18975
+ ok, model_cleared = _run_codex_executor_unit(
18976
+ conn, "finalization")
18977
+ except BaseException:
18978
+ conn.rollback()
18979
+ raise
18980
+ finally:
18981
+ adm.finish(admission, _release_cache_store_locks)
18982
+ finally:
18983
+ conn.close()
18984
+ if model_cleared:
18985
+ try:
18986
+ from _cctally_quota import _defer_codex_quota_verification
18987
+ _defer_codex_quota_verification()
18988
+ except Exception:
18989
+ pass
18990
+ if not ok:
18991
+ # The prune drain's executor owns this debt with its
18992
+ # `blocked` rule; the closing round's settlement retries too.
18993
+ _request_codex_drain_worker(CODEX_PRUNE_DRAIN_COMMAND)
18994
+ break
18995
+ counts["final"] += 1
18996
+
18997
+ def probe_state() -> "tuple[dict | None, bool]":
18998
+ """The record's fence key and whether the replay marker is armed."""
18999
+ conn = open_cache_db()
19000
+ try:
19001
+ return (load_codex_heal_record(conn), conn.execute(
19002
+ "SELECT 1 FROM cache_meta WHERE key=? LIMIT 1",
19003
+ (CODEX_REPLAY_FROM_ZERO_KEY,)).fetchone() is not None)
19004
+ finally:
19005
+ conn.close()
19006
+
19007
+ try:
19008
+ record, replay = probe_state()
19009
+ replay_mode = replay or (record is not None
19010
+ and record.get("mode") == "replay")
19011
+ if replay_mode:
19012
+ last["mode"] = "replay"
19013
+ if (record is not None and record.get("mode") == "replay"
19014
+ and record.get("malformed")):
19015
+ # Spec 4.5a: a replay record whose D1 set cannot be read is
19016
+ # never cleared again (every writer declines); nothing here
19017
+ # can make progress without it.
19018
+ return result("degraded")
19019
+ # 1. The replay's start unit: the clear (which writes the record)
19020
+ # and the rehydration. Rerun on a resumed record that never
19021
+ # rehydrated; it clears nothing once the record exists, unless a
19022
+ # later arming site re-armed it (`reclear`, spec 4.5a).
19023
+ if (record is None or record.get("mode") != "replay"
19024
+ or not record.get("rehydrated")
19025
+ or _codex_replay_reclear_due(record, replay)):
19026
+ start_stats = admitted_sync("start", _replay_stage="start")
19027
+ record, replay = probe_state()
19028
+ if record is None or record.get("mode") != "replay":
19029
+ if not replay:
19030
+ return result("none") # consumed by another writer
19031
+ if start_stats.rebuild_refused or start_stats.prune_refused:
19032
+ return result("refused")
19033
+ request_worker()
19034
+ return result("degraded")
19035
+ if record.get("malformed"):
19036
+ return result("degraded")
19037
+ if _codex_replay_reclear_due(record, replay):
19038
+ # The fresh clear did not run (refused or deferred).
19039
+ if start_stats.rebuild_refused or start_stats.prune_refused:
19040
+ return result("refused")
19041
+ request_worker()
19042
+ return result("degraded")
19043
+ if not record.get("rehydrated"):
19044
+ # The rehydration failed and rolled back; a later
19045
+ # worker reruns it before any rollout is read.
19046
+ request_worker()
19047
+ return result("degraded")
19048
+ else:
19049
+ if record is None and not population:
19050
+ return result("none", None)
19051
+ if record is not None and record.get("phase") != "units":
19052
+ return result("none")
19053
+ if record is not None:
19054
+ pass_state["gen"] = record.get("gen")
19055
+
19056
+ # 1. The start unit.
19057
+ if record is None:
19058
+ fresh_population = sorted(set(str(p) for p in population))
19059
+
19060
+ def start(conn):
19061
+ current = load_codex_heal_record(conn)
19062
+ if current is not None:
19063
+ return current
19064
+ written = _store_codex_heal_record(
19065
+ conn, {"mode": "history", "phase": "units"},
19066
+ population=fresh_population)
19067
+ share.remember_heal_payload(CodexHealPayload(
19068
+ written["gen"], frozenset(fresh_population), None))
19069
+ return written
19070
+
19071
+ admitted, record = _codex_admitted_cache_write(
19072
+ start, wait_for_requests=wait_for_requests,
19073
+ contention_bound=CODEX_REPLAY_DRAIN_LOCK_TIMEOUT_SECONDS)
19074
+ if not admitted:
19075
+ return result("yielded")
19076
+ if record.get("mode") != "history" or record.get("phase") != "units":
19077
+ return result("none", record.get("mode"))
19078
+ pass_state["gen"] = record.get("gen")
19079
+
19080
+ def walk_in_pieces(kind: str, **kwargs) -> bool:
19081
+ """Steps 4 and 7: one full walk in admitted pieces. True when a
19082
+ piece left no census file unwalked; False when a piece neither
19083
+ completed the walk nor visited a file (no progress). Each
19084
+ visited file's committed row is kept for the last piece's
19085
+ revalidation (revision 7)."""
19086
+ census: "frozenset[str] | None" = None
19087
+ visited: "dict[str, tuple | None]" = {}
19088
+ while True:
19089
+ stats = admitted_sync(
19090
+ kind, _ingest_limit=1, _heal_census=census,
19091
+ _heal_visited=frozenset(visited),
19092
+ _heal_visited_rows=dict(visited), **kwargs)
19093
+ counts["rounds"] += 1
19094
+ if census is None and stats.heal_discovered is not None:
19095
+ census = stats.heal_discovered
19096
+ added = set(stats.heal_examined) - set(visited)
19097
+ for path in added:
19098
+ visited[path] = stats.heal_visit_rows.get(path)
19099
+ if stats.heal_walk_complete:
19100
+ return True
19101
+ if not added:
19102
+ return False
19103
+
19104
+ if record.get("phase") == "units":
19105
+ # The pass's one parse of the population (and, in replay mode,
19106
+ # the D1 set the units then take from the share).
19107
+ conn = open_cache_db()
19108
+ try:
19109
+ payload = share.heal_payload(conn, record)
19110
+ finally:
19111
+ conn.close()
19112
+ if payload is None and last["mode"] == "replay":
19113
+ return result("degraded") # unreadable D1 set: fail closed
19114
+ members = (frozenset() if payload is None
19115
+ else payload.population)
19116
+
19117
+ # 2. Population units, in the single walk's (discovery) order.
19118
+ attempts: "collections.Counter[str]" = collections.Counter()
19119
+ for item in _discover_codex_files_with_roots():
19120
+ path = str(item.source_path)
19121
+ if path not in members:
19122
+ continue
19123
+ attempts[path] += 1
19124
+ admitted_sync("population", only_paths={path})
19125
+ counts["units"] += 1
19126
+
19127
+ # 3. The owed pass: appearances after the census, tails another
19128
+ # writer left, and population files a targeted unit declined.
19129
+ while True:
19130
+ files = _discover_codex_files_with_roots()
19131
+ conn = open_cache_db()
19132
+ try:
19133
+ owed = _codex_heal_owed_files(conn, files)
19134
+ finally:
19135
+ conn.close()
19136
+ todo = [item for item in owed
19137
+ if attempts[str(item.source_path)] < 2]
19138
+ if not todo:
19139
+ # Only population files: a file that appeared after the
19140
+ # census is a later sync's, as for the single walk.
19141
+ counts["degraded"] = sum(
19142
+ 1 for item in owed if str(item.source_path) in members)
19143
+ break
19144
+ for item in todo:
19145
+ path = str(item.source_path)
19146
+ attempts[path] += 1
19147
+ admitted_sync("owed", only_paths={path})
19148
+ counts["units"] += 1
19149
+ counts["owed"] += 1
19150
+
19151
+ # Spec 4.5a: a replay armed meanwhile (or re-armed over this
19152
+ # replay) makes every piece below clear and refill one file;
19153
+ # end the pass instead, without a transition. The next pass
19154
+ # starts in replay mode, whose clear retires a history record.
19155
+ current, armed = probe_state()
19156
+ if ((last["mode"] == "history" and armed)
19157
+ or _codex_replay_reclear_due(current, armed)):
19158
+ return result("replay_due")
19159
+
19160
+ # 4. The fenced walk, whose last piece makes the transition
19161
+ # (replay: the unchanged consumption block deletes the record
19162
+ # with the marker, or moves it to `certify` when blocked).
19163
+ walk_in_pieces("certification", _defer_finalization=True,
19164
+ _heal_transition=True)
19165
+ conn = open_cache_db()
19166
+ try:
19167
+ current = load_codex_heal_record(conn)
19168
+ finally:
19169
+ conn.close()
19170
+ if current is not None and current.get("phase") == "units":
19171
+ # No progress (or a complete walk that could not end the
19172
+ # record): the fence stays, and every hook keeps requesting
19173
+ # the worker while the record exists. Without a record (or
19174
+ # with a `certify` one), the last piece made the transition
19175
+ # or another walk (a rebuild) retired it meanwhile.
19176
+ request_worker()
19177
+ return result("degraded")
19178
+
19179
+ # 5. Finalization units of the #980 executor, now unfenced.
19180
+ run_finalization_units()
19181
+
19182
+ # 6. The closing walk: ordinary settlement; it certifies the walk.
19183
+ # The fence is gone, so a refusal here is the ordinary syncs' to
19184
+ # retry (the refusing piece requested its own worker).
19185
+ if not walk_in_pieces("closing"):
19186
+ return result("degraded")
19187
+ return result("done")
19188
+ except _CodexHealYield:
19189
+ return result("yielded")
19190
+ except _CodexHealContended:
19191
+ # The record (and with it the fence) stays: request the worker, which
19192
+ # the record's short retry throttle lets through within minutes.
19193
+ request_worker()
19194
+ return result("contended")
19195
+ except _CodexHealRestart:
19196
+ # Spec 4.5a: the census and visited set died with the walk's frame;
19197
+ # the cached payload is another generation's. The next pass starts
19198
+ # over from its own start unit (the fresh clear, when one is due).
19199
+ share.forget_heal_payload()
19200
+ # Spawns nothing while this pass holds the heal lock (every caller
19201
+ # re-runs the executor inline on `restart`), which keeps a re-run and
19202
+ # a spawned worker from racing for the lock.
19203
+ _defer_codex_replay_drain(short=True)
19204
+ return result("restart")
19205
+ finally:
19206
+ _release_worker_lock(worker)
19207
+ if worker_owed["replay"]:
19208
+ _request_codex_drain_worker(CODEX_REPLAY_DRAIN_COMMAND)
19209
+
19210
+
16822
19211
  class _CodexFileBaselineResolver:
16823
19212
  """Per-row ``codex_file_accounts`` baselines, memoised per file.
16824
19213
 
@@ -16916,6 +19305,10 @@ def reconcile_codex_window_attribution_spend(
16916
19305
  """
16917
19306
  from _cctally_quota import resolve_codex_window_attributions_with_evidence
16918
19307
 
19308
+ # #982 spec 4.3: `account attribute` calls this directly, so the kernel
19309
+ # checks the heal fence itself (strict: `CodexHealFenced`).
19310
+ if _codex_heal_fenced_kernel(conn, strict):
19311
+ return (0, 0)
16919
19312
  try:
16920
19313
  with _codex_cancellable_read(conn, deadline, clock):
16921
19314
  active = load_active_window_attributions(conn)
@@ -19147,12 +21540,36 @@ def scope_conversations_db_to_account(
19147
21540
  f"""
19148
21541
  CREATE TEMP VIEW conversation_messages AS
19149
21542
  SELECT * FROM main.conversation_messages WHERE {stored_predicate};
21543
+ -- #1062: a title is in scope iff (a) its own file's message walk has
21544
+ -- committed past the title's line, (b) that file still holds a
21545
+ -- retained in-scope message of the title's session, (c) no retained
21546
+ -- message of the session, in any file, is outside the scope, and
21547
+ -- (d) no Claude conversation rebuild is in progress (its message
21548
+ -- table may be partial). (b) reads the scoped TEMP view; (a), (c)
21549
+ -- and (d) read main. The fragment's unqualified account_key binds to
21550
+ -- the innermost FROM (main.conversation_messages m). The base rowid
21551
+ -- is projected as `rowid` so the title FTS joins resolve on a scoped
21552
+ -- connection and return the base row (C1).
19150
21553
  CREATE TEMP VIEW conversation_ai_titles AS
19151
- SELECT t.* FROM main.conversation_ai_titles t
21554
+ SELECT t.rowid AS rowid, t.* FROM main.conversation_ai_titles t
19152
21555
  WHERE EXISTS (
21556
+ SELECT 1 FROM main.conversation_source_files f
21557
+ WHERE f.path = t.source_path
21558
+ AND f.last_byte_offset > t.byte_offset
21559
+ )
21560
+ AND EXISTS (
19153
21561
  SELECT 1 FROM conversation_messages m
19154
- WHERE m.source_path=t.source_path
19155
- AND m.byte_offset=t.byte_offset
21562
+ WHERE m.session_id = t.session_id
21563
+ AND m.source_path = t.source_path
21564
+ )
21565
+ AND NOT EXISTS (
21566
+ SELECT 1 FROM main.conversation_messages m
21567
+ WHERE m.session_id = t.session_id
21568
+ AND NOT ({stored_predicate})
21569
+ )
21570
+ AND NOT EXISTS (
21571
+ SELECT 1 FROM main.cache_meta
21572
+ WHERE key = 'conversation_rebuild_claude_pending'
19156
21573
  );
19157
21574
  CREATE TEMP VIEW conversation_file_touches AS
19158
21575
  SELECT t.* FROM main.conversation_file_touches t
@@ -21262,6 +23679,19 @@ def _write_claude_prepared_file(
21262
23679
  and the scoped ``_fill_file_touches`` never commits."""
21263
23680
  path_str = prepared.path
21264
23681
  sessions: set[str] = set()
23682
+ # #1062 §4.2: a rebuild writes titles to staging and leaves the live table
23683
+ # to the publish (#752) — except for a file replayed under a NEW
23684
+ # incarnation. `prepared.truncated` means exactly that (#1062 F8): a
23685
+ # replacement, a truncation below the cursor, or missing identity
23686
+ # evidence; a continuous file that merely shrank (a trimmed unfinished
23687
+ # tail) is not truncated and keeps its committed rows. A new incarnation's
23688
+ # live rows get what an incremental sync of the file would leave: the
23689
+ # path's rows deleted and its replayed titles re-upserted, owner-restricted,
23690
+ # here in the file's savepoint, so they commit with its cursor. Without it
23691
+ # the fold carries a title the file has lost into the new generation, and
23692
+ # after an interrupted rebuild no later run detects the replacement again.
23693
+ refresh_live = (prepared.truncated
23694
+ and title_table != "conversation_ai_titles")
21265
23695
  if prepared.truncated:
21266
23696
  sessions.update(
21267
23697
  row[0]
@@ -21284,6 +23714,11 @@ def _write_claude_prepared_file(
21284
23714
  f"DELETE FROM {title_table} WHERE source_path=?",
21285
23715
  (path_str,),
21286
23716
  )
23717
+ if refresh_live:
23718
+ conn.execute(
23719
+ "DELETE FROM conversation_ai_titles WHERE source_path=?",
23720
+ (path_str,),
23721
+ )
21287
23722
  if prepared.conv_rows:
21288
23723
  conn.executemany(_CONV_ACCOUNT_INSERT_SQL, prepared.conv_rows)
21289
23724
  _fill_file_touches(
@@ -21303,6 +23738,9 @@ def _write_claude_prepared_file(
21303
23738
  )
21304
23739
  if prepared.ai_rows:
21305
23740
  conn.executemany(_ai_title_upsert_sql(title_table), prepared.ai_rows)
23741
+ if refresh_live:
23742
+ conn.executemany(
23743
+ _ai_title_upsert_sql(owner_only=True), prepared.ai_rows)
21306
23744
  size, mtime_ns, final_offset, device_id, inode, incarnation_id, prefix = (
21307
23745
  prepared.cursor)
21308
23746
  conn.execute(
@@ -21355,6 +23793,7 @@ def _claude_batch_committed(conn, released, sessions, stats, touched_sessions,
21355
23793
  _CONVERSATION_RESIDUAL_FIELDS = frozenset({
21356
23794
  "failed_paths", "barrier_deferred_keys", "barrier_deferred_targets",
21357
23795
  "barrier_published_paths", "barrier_retry_causes",
23796
+ "barrier_terminal_keys", # #1052
21358
23797
  })
21359
23798
 
21360
23799
 
@@ -21647,9 +24086,11 @@ def sync_claude_conversations(
21647
24086
  # nothing, so every one of its records is first observed now and takes
21648
24087
  # the active identity rather than being read as historical-and-unknown.
21649
24088
  high_water: "dict[tuple[str, str], int]" = {}
21650
- # #752: a rebuild writes its titles and rollup into staging and does
21651
- # not touch the live tables until the publish, so readers keep seeing
21652
- # the previous complete generation for the whole replay.
24089
+ # #752: a rebuild writes its titles and rollup into staging and
24090
+ # otherwise leaves the live tables to the publish, so readers keep
24091
+ # seeing the previous complete generation for the replay. The one
24092
+ # exception is #1062 §4.2: a file replayed under a NEW incarnation has
24093
+ # its live title rows replaced, owner-restricted, at its batch commit.
21653
24094
  title_table = "conversation_ai_titles"
21654
24095
  rollup_table = "conversation_sessions"
21655
24096
  if rebuild:
@@ -21751,6 +24192,11 @@ def sync_claude_conversations(
21751
24192
  stats.files_skipped_unchanged += 1
21752
24193
  _report_conversation_progress(progress, "ingest", stats)
21753
24194
  continue
24195
+ # The raw shrink decides only the targeted deferral below,
24196
+ # which runs before the incarnation resolver and so cannot yet
24197
+ # tell a truncation below the cursor from a trimmed unfinished
24198
+ # tail; deferring both to a full sync is conservative. Past the
24199
+ # resolver, `truncated` is redefined as "a new incarnation".
21754
24200
  truncated = prev is not None and size < prev[0]
21755
24201
  if targeted and truncated:
21756
24202
  # #939 D1: every exit resolves the open batch.
@@ -21784,13 +24230,20 @@ def sync_claude_conversations(
21784
24230
  start_offset = incarnation.start_offset
21785
24231
  file_high_water = high_water.get(
21786
24232
  (path_str, incarnation.incarnation_id), 0)
21787
- if incarnation.is_new:
21788
- # A new incarnation forces byte-zero treatment: the
21789
- # bytes before the cursor are not the bytes the cursor
21790
- # described, so resuming from it would skip real records
21791
- # and stamp the rest against an identity that never held
21792
- # them.
21793
- truncated = prev is not None
24233
+ # A new incarnation forces byte-zero treatment: the
24234
+ # bytes before the cursor are not the bytes the cursor
24235
+ # described, so resuming from it would skip real records
24236
+ # and stamp the rest against an identity that never held
24237
+ # them. A CONTINUOUS incarnation is never a truncation,
24238
+ # even when the file shrank (#1062 F8): a pass that ended
24239
+ # on an unfinished last line records a size past its
24240
+ # cursor, and trimming that line shrinks the file without
24241
+ # moving a committed byte. Its committed rows stay; the
24242
+ # walk resumes from the cursor and the cursor row records
24243
+ # the smaller size. Treating that shrink as a truncation
24244
+ # deleted every row of the path and re-inserted only the
24245
+ # tail.
24246
+ truncated = incarnation.is_new and prev is not None
21794
24247
  # #939 D1: admission is decided BEFORE the parse. The
21795
24248
  # read limit is the size the incarnation decision saw
21796
24249
  # (the same fstat recorded as the change detector
@@ -22125,6 +24578,32 @@ def _cache_side_replay_pending(conn: sqlite3.Connection) -> bool:
22125
24578
  return False
22126
24579
 
22127
24580
 
24581
+ def _cache_side_history_heal_pending(conn: sqlite3.Connection) -> bool:
24582
+ """Whether cache.db holds a history heal fence (#982 spec 4.3, R10).
24583
+
24584
+ Per-file heal units commit one population file at a time, and a
24585
+ cursorless contributor's membership is unknown until its own unit reads
24586
+ it, so the barrier cannot hold a conversation whose other contributor is
24587
+ still to come. The pass therefore publishes nothing while the fence holds;
24588
+ the heal's transition ends it once every population file is accounted.
24589
+ Qualified through ``cache_db`` like ``_cache_side_replay_pending``, and a
24590
+ connection without the attachment reports False.
24591
+ """
24592
+ try:
24593
+ row = conn.execute(
24594
+ "SELECT value FROM cache_db.cache_meta WHERE key=?",
24595
+ (CODEX_HEAL_WALK_KEY,)).fetchone()
24596
+ except sqlite3.OperationalError:
24597
+ return False
24598
+ if row is None:
24599
+ return False
24600
+ try:
24601
+ record = json.loads(str(row[0]))
24602
+ except (TypeError, ValueError):
24603
+ return True # malformed fails closed, as the fence
24604
+ return not (isinstance(record, dict) and record.get("phase") != "units")
24605
+
24606
+
22128
24607
  def _clear_codex_conversation_store(conn: sqlite3.Connection) -> None:
22129
24608
  """Clear only the re-derivable Codex transcript families."""
22130
24609
  conn.execute("DELETE FROM codex_conversation_events")
@@ -22201,6 +24680,75 @@ def _codex_conversation_cursor_row(conn, path: str):
22201
24680
  return None if row is None else tuple(row[1:])
22202
24681
 
22203
24682
 
24683
+ def _codex_conversation_accepted(row) -> _AcceptedContainer:
24684
+ """The container evidence of a conversations cursor row (``row[1:]``)."""
24685
+ return _accepted_container(row, ext=14, mtime=1, device=12, inode=13, size=0)
24686
+
24687
+
24688
+ def _codex_cursor_unverified(row) -> bool:
24689
+ """A conversations cursor row whose state is explicitly ``unverified``."""
24690
+ return row is not None and _codex_conversation_accepted(row).state == "unverified"
24691
+
24692
+
24693
+ _CURSOR_UNREAD = object()
24694
+
24695
+
24696
+ def _classify_codex_unit_file(conn, discovered, *, executor_paths,
24697
+ protected_paths, requal_held, unit, targeted,
24698
+ cursor=_CURSOR_UNREAD) -> str:
24699
+ """``"blocked"``, ``"terminal"`` or ``"quiet"`` for one unit file (#1052
24700
+ spec 5.1), by the accessors and in the order of the conversation loop's
24701
+ own checks. Never decodes: a cursorless compressed container, whose
24702
+ decision would validate it, is blocked before the decision is reached,
24703
+ and an executor path, the only other decoding entrance, is blocked first.
24704
+
24705
+ Terminal is the conversations store's terminal verdict exactly: an
24706
+ explicitly ``unverified`` cursor whose decision is the quiet
24707
+ ``skip/verification-unverified``. The decision maps every non-accepted,
24708
+ non-pending state to that reason, so the reason alone is not enough.
24709
+
24710
+ Quiet is a ``read`` decision the loop would not refuse before reading a
24711
+ byte of it. The loop refuses two such files, and so does this (#1052
24712
+ review F2): in a targeted pass, a retained cursor the file no longer
24713
+ continues (truncated, replaced, or under another root), and, in ``unit``,
24714
+ a certificate that names another generation of the file. ``cursor`` is
24715
+ the row the caller already read, if any."""
24716
+ path_str = str(discovered.source_path)
24717
+ if path_str in protected_paths or path_str in executor_paths:
24718
+ return "blocked"
24719
+ selected = _select_codex_container(discovered.source_path)
24720
+ if selected is None:
24721
+ return "blocked"
24722
+ prev = (_codex_conversation_cursor_row(conn, path_str)
24723
+ if cursor is _CURSOR_UNREAD else cursor)
24724
+ if requal_held and _codex_held_by_requalification(
24725
+ requal_held, frozenset(), discovered, selected.location,
24726
+ first_sight=prev is None):
24727
+ return "blocked"
24728
+ if prev is None and selected.kind == _rc.ZSTD:
24729
+ return "blocked"
24730
+ decision = _codex_container_decision(
24731
+ selected, None if prev is None else _codex_conversation_accepted(prev),
24732
+ executor_path=False, should_stop=None)
24733
+ if decision.action == "read":
24734
+ fp = selected.fingerprint
24735
+ size = int(decision.size)
24736
+ if targeted and prev is not None and (
24737
+ size < prev[0] or prev[3] != discovered.source_root_key
24738
+ or _ingest_frontier.source_identity_replaced(
24739
+ prev[12], prev[13], fp.device, fp.inode)):
24740
+ return "blocked"
24741
+ if unit is not None and unit.cap_for(path_str, _rc.generation_token(
24742
+ fp, size if selected.kind == _rc.ZSTD else None)) is None:
24743
+ return "blocked"
24744
+ return "quiet"
24745
+ if (decision.action == "skip"
24746
+ and decision.reason == "verification-unverified"
24747
+ and _codex_cursor_unverified(prev)):
24748
+ return "terminal"
24749
+ return "blocked"
24750
+
24751
+
22204
24752
  def _codex_conversation_population_subset(conn, keys) -> "set[str]":
22205
24753
  found: set[str] = set()
22206
24754
  for chunk in _codex_barrier_chunks({k for k in keys if k}):
@@ -22291,15 +24839,24 @@ class _CodexPublicationWork:
22291
24839
  files as ``(discovered, unit)`` and ``(None, unit)`` (the loop commits the
22292
24840
  unit). Planning captures one read snapshot between the two phases and
22293
24841
  releases it before any publication transaction begins.
24842
+
24843
+ #1052 spec 5.1: each planned unit is classified before any of its files
24844
+ is yielded. A terminal deferral (a terminal contributor and no blocked
24845
+ file) yields nothing, so nothing of it is read, decoded or published.
22294
24846
  """
22295
24847
 
22296
- def __init__(self, conn, files, *, only_paths, stats):
24848
+ def __init__(self, conn, files, *, only_paths, stats,
24849
+ executor_paths=frozenset(), protected_paths=frozenset(),
24850
+ requal_held=None):
22297
24851
  self.conn = conn
22298
24852
  self.files = list(files)
22299
24853
  self.files_by_path = {str(d.source_path): d for d in self.files}
22300
24854
  self.only_paths = (None if only_paths is None
22301
24855
  else {str(path) for path in only_paths})
22302
24856
  self.stats = stats
24857
+ self.executor_paths = frozenset(executor_paths)
24858
+ self.protected_paths = frozenset(protected_paths)
24859
+ self.requal_held = requal_held or {}
22303
24860
  self.population_present = conn.execute(
22304
24861
  "SELECT 1 FROM cache_db.codex_barrier_population LIMIT 1"
22305
24862
  ).fetchone() is not None
@@ -22314,10 +24871,49 @@ class _CodexPublicationWork:
22314
24871
  yield None, None
22315
24872
  self.units = _plan_codex_publication_units(self.conn, self)
22316
24873
  for unit in self.units:
24874
+ terminal = self._terminal_paths(unit)
24875
+ if terminal:
24876
+ # #1052 spec 5.2: deferred, never retried. A retry flag raised
24877
+ # elsewhere is neither set nor cleared here.
24878
+ unit.failed = True
24879
+ self.stats.barrier_keys_deferred += len(unit.keys)
24880
+ self.stats.barrier_keys_terminal += len(unit.keys)
24881
+ named = self.stats.barrier_terminal_keys
24882
+ for path in terminal:
24883
+ named[path] = named.get(path, frozenset()) | frozenset(unit.keys)
24884
+ continue
22317
24885
  for discovered in unit.files:
22318
24886
  yield discovered, unit
22319
24887
  yield None, unit
22320
24888
 
24889
+ def _terminal_paths(self, unit) -> "tuple[str, ...]":
24890
+ """The terminal files of a terminal deferral; ``()`` for every other
24891
+ unit, which the loop then treats exactly as before (#1052 spec 5.1).
24892
+
24893
+ A unit none of whose files has an ``unverified`` conversations cursor
24894
+ is not classified further. Otherwise one blocked file makes the whole
24895
+ unit an ordinary one, so its own refusal (G10, pending verification,
24896
+ a protected or executor path, an unread new identity) still reaches
24897
+ the loop's retryable failure."""
24898
+ cursors = [
24899
+ (discovered, _codex_conversation_cursor_row(
24900
+ self.conn, str(discovered.source_path)))
24901
+ for discovered in unit.files]
24902
+ if not any(_codex_cursor_unverified(row) for _d, row in cursors):
24903
+ return ()
24904
+ terminal = []
24905
+ for discovered, row in cursors:
24906
+ verdict = _classify_codex_unit_file(
24907
+ self.conn, discovered, executor_paths=self.executor_paths,
24908
+ protected_paths=self.protected_paths,
24909
+ requal_held=self.requal_held, unit=unit,
24910
+ targeted=self.only_paths is not None, cursor=row)
24911
+ if verdict == "blocked":
24912
+ return ()
24913
+ if verdict == "terminal":
24914
+ terminal.append(str(discovered.source_path))
24915
+ return tuple(terminal)
24916
+
22321
24917
  def _defer(self, discovered, keys, *, generation, size) -> bool:
22322
24918
  path = str(discovered.source_path)
22323
24919
  prior = self.deferred.get(path)
@@ -22792,6 +25388,38 @@ def _rollback_codex_file_savepoint(conn: sqlite3.Connection) -> bool:
22792
25388
  return False
22793
25389
 
22794
25390
 
25391
+ class _CodexConversationCensus:
25392
+ """One conversation pass's container skips and held reads (#1052 5.3).
25393
+
25394
+ The census is phase 1's: its skips are ``container_skipped``. A
25395
+ unit-phase skip of a path phase 1 did not skip is counted once in
25396
+ ``barrier_container_skipped`` instead, so certification never counts one
25397
+ file twice. ``container_reads_held`` counts distinct logical paths across
25398
+ both phases."""
25399
+
25400
+ def __init__(self, stats):
25401
+ self.stats = stats
25402
+ self.skipped_phase1: "set[str]" = set()
25403
+ self.skipped_units: "set[str]" = set()
25404
+ self.held: "set[str]" = set()
25405
+
25406
+ def skip(self, path: str, reason: str, *, unit_phase: bool) -> None:
25407
+ stats = self.stats
25408
+ if not unit_phase:
25409
+ self.skipped_phase1.add(path)
25410
+ stats.container_skipped[reason] = (
25411
+ stats.container_skipped.get(reason, 0) + 1)
25412
+ elif path not in self.skipped_phase1 and path not in self.skipped_units:
25413
+ self.skipped_units.add(path)
25414
+ stats.barrier_container_skipped[reason] = (
25415
+ stats.barrier_container_skipped.get(reason, 0) + 1)
25416
+
25417
+ def hold(self, path: str) -> None:
25418
+ if path not in self.held:
25419
+ self.held.add(path)
25420
+ self.stats.container_reads_held += 1
25421
+
25422
+
22795
25423
  def _sync_codex_conversations_locked(
22796
25424
  conn: sqlite3.Connection,
22797
25425
  *,
@@ -22879,6 +25507,9 @@ def _sync_codex_conversations_locked(
22879
25507
  if _cache_side_replay_pending(conn):
22880
25508
  stats.deferred_reason = "cache_replay_pending"
22881
25509
  return stats
25510
+ if _cache_side_history_heal_pending(conn):
25511
+ stats.deferred_reason = "cache_heal_pending"
25512
+ return stats
22882
25513
  # #968 spec 5.11: the barrier extends this admission point. Its state
22883
25514
  # or attachment missing fails closed rather than reading as "no
22884
25515
  # population".
@@ -23078,7 +25709,10 @@ def _sync_codex_conversations_locked(
23078
25709
  location_marks: "list[tuple[str, _rc.SelectedContainer, _AcceptedContainer]]" = []
23079
25710
  batch = _CodexConversationBatch()
23080
25711
  work = _CodexPublicationWork(
23081
- conn, files, only_paths=only_paths, stats=stats)
25712
+ conn, files, only_paths=only_paths, stats=stats,
25713
+ executor_paths=executor_paths, protected_paths=protected_paths,
25714
+ requal_held=requal_held)
25715
+ census = _CodexConversationCensus(stats)
23082
25716
  for discovered, unit in work:
23083
25717
  if discovered is None:
23084
25718
  if unit is None: # the phase boundary
@@ -23118,7 +25752,7 @@ def _sync_codex_conversations_locked(
23118
25752
  requal_held, frozenset(), discovered, selected.location,
23119
25753
  first_sight=prev is None):
23120
25754
  # G10: held until both stores retire the direct-era spelling.
23121
- stats.container_reads_held += 1
25755
+ census.hold(path_str)
23122
25756
  stats.drain_requested = tuple(sorted(
23123
25757
  set(stats.drain_requested) | {CODEX_PRUNE_DRAIN_COMMAND}))
23124
25758
  if unit is not None:
@@ -23127,8 +25761,8 @@ def _sync_codex_conversations_locked(
23127
25761
  continue
23128
25762
  _fp = selected.fingerprint
23129
25763
  st = _CodexContainerStat(_fp.size, _fp.mtime_ns, _fp.device, _fp.inode)
23130
- accepted = (None if prev is None else _accepted_container(
23131
- prev, ext=14, mtime=1, device=12, inode=13, size=0))
25764
+ accepted = (None if prev is None
25765
+ else _codex_conversation_accepted(prev))
23132
25766
  container_decision = _codex_container_decision(
23133
25767
  selected, accepted, executor_path=path_str in executor_paths, should_stop=None)
23134
25768
  if container_decision.action == "adopt-location":
@@ -23145,12 +25779,15 @@ def _sync_codex_conversations_locked(
23145
25779
  continue
23146
25780
  if container_decision.action == "skip":
23147
25781
  reason = container_decision.reason
23148
- stats.container_skipped[reason] = stats.container_skipped.get(reason, 0) + 1
25782
+ census.skip(path_str, reason, unit_phase=unit is not None)
23149
25783
  if selected.kind == _rc.ZSTD and prev is None:
23150
- stats.container_reads_held += 1
25784
+ census.hold(path_str)
23151
25785
  if unit is not None:
23152
25786
  # A failed publication is still due, even when its failure
23153
25787
  # was an unreadable skip rather than pending verification.
25788
+ # #1052: a unit reaching a terminal contributor here also
25789
+ # carries a blocked file (or its cursor changed after
25790
+ # classification), so it stays retryable.
23154
25791
  batch = _fail_codex_publication_unit(
23155
25792
  conn, batch, stats, retry=True)
23156
25793
  _report_conversation_progress(progress, "ingest", stats)
@@ -23357,9 +25994,9 @@ def _sync_codex_conversations_locked(
23357
25994
  raise _rc.ContainerError("unstable-stat")
23358
25995
  except _rc.ContainerError as exc:
23359
25996
  reason = _container_skip_reason(exc.category)
23360
- stats.container_skipped[reason] = stats.container_skipped.get(reason, 0) + 1
25997
+ census.skip(path_str, reason, unit_phase=unit is not None)
23361
25998
  if selected.kind == _rc.ZSTD and prev is None:
23362
- stats.container_reads_held += 1
25999
+ census.hold(path_str)
23363
26000
  if unit is not None:
23364
26001
  batch = _fail_codex_publication_unit(conn, batch, stats, retry=True)
23365
26002
  _report_conversation_progress(progress, "ingest", stats)
@@ -23564,6 +26201,11 @@ def _sync_codex_conversations_locked(
23564
26201
  _commit_codex_conversation_batch(conn, batch, stats, progress)
23565
26202
  batch = _CodexConversationBatch()
23566
26203
  _commit_codex_conversation_batch(conn, batch, stats, progress)
26204
+ if only_paths is None:
26205
+ # #972 §5.4: the rollups the walk did not rewrite still follow the
26206
+ # current thread metadata and filesystem. Failed files do not stop
26207
+ # it; a targeted pass never runs it.
26208
+ _reconcile_codex_rollup_attribution(conn, stats)
23567
26209
  if (work.seal_seq is not None and work.backlog_complete
23568
26210
  and not stats.barrier_retry):
23569
26211
  # Every certificate sealed up to this token was considered. Units
@@ -23581,7 +26223,8 @@ def _sync_codex_conversations_locked(
23581
26223
  conn, "codex_conversation_source_files", path, adopted, snap)
23582
26224
  conn.commit()
23583
26225
  if (stats.container_pending
23584
- or stats.container_skipped.get("verification-pending", 0)):
26226
+ or stats.container_skipped.get("verification-pending", 0)
26227
+ or stats.barrier_container_skipped.get("verification-pending", 0)):
23585
26228
  stats.drain_requested = ("_codex-replay-drain",)
23586
26229
  run_codex_find_projection_backfill(conn)
23587
26230
  _report_conversation_progress(progress, "finalize", stats)
@@ -23626,7 +26269,7 @@ def _sync_codex_conversations_locked(
23626
26269
  and not stats.prune_refused
23627
26270
  and not stats.files_deferred_torn
23628
26271
  and not stats.container_pending
23629
- and not any(stats.container_skipped.values())
26272
+ and not any(stats.all_container_skipped().values())
23630
26273
  and stats.files_processed + stats.files_skipped_unchanged
23631
26274
  == stats.files_total),
23632
26275
  cursor_table="codex_conversation_source_files")
@@ -23805,7 +26448,9 @@ def _run_transcript_rebuild_worker(
23805
26448
  emit({
23806
26449
  "event": "result",
23807
26450
  "stats": {
23808
- name: value for name, value in asdict(stats).items()
26451
+ name: (sorted(value) if isinstance(value, (set, frozenset))
26452
+ else value)
26453
+ for name, value in asdict(stats).items()
23809
26454
  if name not in _CONVERSATION_RESIDUAL_FIELDS
23810
26455
  },
23811
26456
  "statsType": type(stats).__name__,
@@ -24136,6 +26781,21 @@ def cmd_cache_sync(args: argparse.Namespace) -> int:
24136
26781
  def _sync_codex_leg(
24137
26782
  active_conn: sqlite3.Connection,
24138
26783
  ) -> CodexIngestStats:
26784
+ if not args.rebuild and _codex_replay_heal_due(active_conn):
26785
+ # #982 spec 4.6: an armed byte-zero replay (or one a dead
26786
+ # executor left) clears and refills through the admitted heal
26787
+ # executor, before provenance runs, as the first sync did.
26788
+ with _perf.phase("codex_heal") as _p_heal:
26789
+ heal = run_codex_admitted_heal(wait_for_requests=True)
26790
+ if heal.outcome in ("replay_due", "restart"):
26791
+ # Spec 4.5a: re-armed during the pass (or a unit
26792
+ # found its generation stale); replay again.
26793
+ heal = run_codex_admitted_heal(wait_for_requests=True)
26794
+ _p_heal.set_meta(outcome=heal.outcome, units=heal.units,
26795
+ mode=heal.mode)
26796
+ if heal.outcome in ("busy", "contended"):
26797
+ return CodexIngestStats(lock_contended=True)
26798
+ return heal.stats or CodexIngestStats()
24139
26799
  with _perf.phase("sync_codex_cache"):
24140
26800
  return sync_codex_cache(
24141
26801
  active_conn,
@@ -24233,14 +26893,7 @@ def cmd_cache_sync(args: argparse.Namespace) -> int:
24233
26893
  )
24234
26894
  contended = True
24235
26895
  elif not stats.lock_contended:
24236
- eprint(
24237
- f"[cache-sync] codex done: {stats.files_processed} processed, "
24238
- f"{stats.files_skipped_unchanged} skipped, "
24239
- f"{stats.files_reset_truncated} reset, "
24240
- f"{stats.rows_changed} rows changed, "
24241
- f"{stats.lines_malformed} malformed, "
24242
- f"{stats.token_events_skipped} drift-skipped"
24243
- )
26896
+ _print_codex_done_line(stats)
24244
26897
  # #416 review B4: emitted on EVERY branch (including a contended or
24245
26898
  # incomplete rebuild) — a deferral means Codex spend and quota stopped
24246
26899
  # updating, which the "done" line's zeroes look identical to. Exit code
@@ -24256,14 +26909,41 @@ def cmd_cache_sync(args: argparse.Namespace) -> int:
24256
26909
  codex_stats = stats
24257
26910
  if not stats.lock_contended:
24258
26911
  history = run_codex_history_provenance_drain(wait_for_requests=True)
24259
- if history.outcome == "settled" and history.candidates:
24260
- # Same accounting path and lock policy, without repeating the
24261
- # rebuild clear: the now-settled historical inputs can be read.
24262
- codex_stats = sync_codex_cache(
24263
- conn, progress=_progress_codex_stderr, lock_timeout=lt,
24264
- maintenance="caller")
24265
- if codex_stats.lock_contended:
26912
+ heal_record = load_codex_heal_record(conn)
26913
+ # A replay the first leg left blocked (or unfinished) keeps the
26914
+ # executor in replay mode; the history heal waits for it.
26915
+ if not _codex_replay_heal_due(conn) and (
26916
+ (history.outcome == "settled" and history.candidates)
26917
+ or (heal_record is not None
26918
+ and heal_record.get("mode") == "history")):
26919
+ # #982 spec 4.6: the now-settled historical inputs are read by
26920
+ # the admitted heal executor, one file per unit, in place of a
26921
+ # second whole-tree sync under one writer-lock hold.
26922
+ with _perf.phase("codex_heal") as _p_heal:
26923
+ heal = run_codex_admitted_heal(
26924
+ wait_for_requests=True,
26925
+ population=(None if heal_record is not None
26926
+ else _codex_history_candidates()))
26927
+ if heal.outcome in ("replay_due", "restart"):
26928
+ # Spec 4.5a: a replay armed during this heal ended
26929
+ # its pass before the fenced walk; replay now, as the
26930
+ # first leg would have.
26931
+ heal = run_codex_admitted_heal(wait_for_requests=True)
26932
+ _p_heal.set_meta(outcome=heal.outcome, units=heal.units)
26933
+ if heal.stats is not None and heal.stats.rebuild_refused:
26934
+ # The inline re-run's replay was refused by the #485/#968
26935
+ # preflight: reported exactly as the first leg reports a
26936
+ # refused replay (every marker was kept).
26937
+ conn.close()
26938
+ eprint(CODEX_REBUILD_REFUSED_MESSAGE)
26939
+ _p_root.__exit__(None, None, None)
26940
+ return 3
26941
+ if heal.outcome in ("busy", "contended"):
26942
+ codex_stats = CodexIngestStats(lock_contended=True)
24266
26943
  contended = True
26944
+ elif heal.stats is not None:
26945
+ # The heal's summary, once, in the first leg's format.
26946
+ _print_codex_done_line(heal.stats)
24267
26947
  if not codex_stats.lock_contended:
24268
26948
  run_codex_barrier_membership_scan(wait_for_requests=True)
24269
26949
  run_codex_barrier_seal_drain(wait_for_requests=True)