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.
- package/CHANGELOG.md +37 -0
- package/README.md +7 -3
- package/bin/_cctally_cache.py +2873 -193
- package/bin/_cctally_completion.py +106 -33
- package/bin/_cctally_core.py +69 -4
- package/bin/_cctally_dashboard.py +188 -13
- package/bin/_cctally_dashboard_conversation.py +5 -1
- package/bin/_cctally_dashboard_envelope.py +50 -19
- package/bin/_cctally_dashboard_sources.py +23 -1
- package/bin/_cctally_db.py +193 -153
- package/bin/_cctally_diagnosis_sources.py +63 -7
- package/bin/_cctally_doctor.py +865 -93
- package/bin/_cctally_five_hour.py +67 -50
- package/bin/_cctally_forecast.py +35 -13
- package/bin/_cctally_journal.py +186 -18
- package/bin/_cctally_milestone_history.py +859 -62
- package/bin/_cctally_oauth_samples.py +286 -15
- package/bin/_cctally_parser.py +2 -2
- package/bin/_cctally_quota.py +104 -12
- package/bin/_cctally_record.py +153 -29
- package/bin/_cctally_rederive.py +608 -68
- package/bin/_cctally_statusline.py +108 -81
- package/bin/_cctally_store.py +179 -38
- package/bin/_cctally_tui.py +892 -181
- package/bin/_cctally_update.py +32 -13
- package/bin/_lib_codex_conversation_query.py +52 -14
- package/bin/_lib_codex_drain_admission.py +224 -6
- package/bin/_lib_codex_metadata.py +39 -0
- package/bin/_lib_conversation_anon.py +15 -8
- package/bin/_lib_conversation_dispatch.py +7 -0
- package/bin/_lib_conversation_query.py +289 -70
- package/bin/_lib_dashboard_sources.py +37 -1
- package/bin/_lib_detector_access_registry.py +497 -26
- package/bin/_lib_doctor.py +85 -20
- package/bin/_lib_doctor_scan_index.py +321 -0
- package/bin/_lib_ingest_frontier.py +20 -1
- package/bin/_lib_oauth_samples.py +23 -7
- package/bin/_lib_pricing.py +146 -16
- package/bin/_lib_quota.py +19 -0
- package/bin/_lib_rederive.py +11 -6
- package/bin/_lib_retained_records.py +516 -0
- package/bin/_lib_snapshot_cache.py +10 -0
- package/bin/_lib_stale_proof.py +272 -43
- package/bin/_lib_stats_content_revision.py +831 -0
- package/bin/_lib_write_io.py +11 -4
- package/bin/cctally +4 -1
- package/dashboard/static/assets/ConversationsView-D4Iv4ku0.js +72 -0
- package/dashboard/static/assets/{DoctorModal-B0DnU9d9.js → DoctorModal-D4QSq6uz.js} +1 -1
- package/dashboard/static/assets/ModalRoot-C9F5dhZi.js +1 -0
- package/dashboard/static/assets/{ProjectsDrillPanel-DSxXQSa-.js → ProjectsDrillPanel-WxAdRDMt.js} +1 -1
- package/dashboard/static/assets/{SourceDetailModal-BleDpQqG.js → SourceDetailModal-NyyEfXmc.js} +1 -1
- package/dashboard/static/assets/{UpdateModal-CMqaAkSK.js → UpdateModal-CFLq9pT1.js} +1 -1
- package/dashboard/static/assets/{index-BjQV-0-D.js → index-BKTWH9JE.js} +2 -2
- package/dashboard/static/assets/{outlineNavigation-CO-VWBm7.js → outlineNavigation-BnGUwCS3.js} +3 -3
- package/dashboard/static/dashboard.html +1 -1
- package/package.json +4 -1
- package/dashboard/static/assets/ConversationsView-oqNsWzWV.js +0 -72
- package/dashboard/static/assets/ModalRoot-CYua2wP_.js +0 -1
package/bin/_cctally_cache.py
CHANGED
|
@@ -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"
|
|
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)
|
|
295
|
-
a
|
|
296
|
-
|
|
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
|
-
|
|
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
|
|
1087
|
-
git
|
|
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
|
|
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(
|
|
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 = ?" +
|
|
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.
|
|
4974
|
-
# only
|
|
4975
|
-
#
|
|
4976
|
-
#
|
|
4977
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
5380
|
-
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
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
|
|
8179
|
-
for routine auto-syncs): returns False immediately on
|
|
8180
|
-
``timeout > 0`` -> retry ``LOCK_NB`` every
|
|
8181
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
9223
|
-
#
|
|
9224
|
-
# the marker
|
|
9225
|
-
# upgrade gate DEFERs the 008/009/010 recomputes
|
|
9226
|
-
# certifying aggregates that still include data from
|
|
9227
|
-
# on disk).
|
|
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
|
-
|
|
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
|
-
|
|
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?
|
|
9417
|
-
# sync_cache call, before the file loop
|
|
9418
|
-
#
|
|
9419
|
-
#
|
|
9420
|
-
#
|
|
9421
|
-
#
|
|
9422
|
-
# IntegrityError and rolling back its whole
|
|
9423
|
-
#
|
|
9424
|
-
#
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
9896
|
-
|
|
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
|
-
|
|
9901
|
-
|
|
9902
|
-
|
|
9903
|
-
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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
|
-
|
|
12342
|
-
|
|
12343
|
-
|
|
12344
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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=
|
|
14173
|
+
authoritative=authoritative_rehydration,
|
|
12873
14174
|
file_account_since=_since,
|
|
12874
14175
|
window_attribution_since=(
|
|
12875
|
-
None if
|
|
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
|
|
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
|
-
|
|
13229
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
14793
|
-
|
|
14794
|
-
|
|
14795
|
-
|
|
14796
|
-
|
|
14797
|
-
|
|
14798
|
-
|
|
14799
|
-
|
|
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
|
-
|
|
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 =
|
|
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.
|
|
19155
|
-
AND m.
|
|
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
|
|
21651
|
-
#
|
|
21652
|
-
# the previous complete generation for the
|
|
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
|
-
|
|
21788
|
-
|
|
21789
|
-
|
|
21790
|
-
|
|
21791
|
-
|
|
21792
|
-
|
|
21793
|
-
|
|
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
|
-
|
|
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
|
|
23131
|
-
|
|
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
|
-
|
|
25782
|
+
census.skip(path_str, reason, unit_phase=unit is not None)
|
|
23149
25783
|
if selected.kind == _rc.ZSTD and prev is None:
|
|
23150
|
-
|
|
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
|
-
|
|
25997
|
+
census.skip(path_str, reason, unit_phase=unit is not None)
|
|
23361
25998
|
if selected.kind == _rc.ZSTD and prev is None:
|
|
23362
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
24260
|
-
|
|
24261
|
-
|
|
24262
|
-
|
|
24263
|
-
|
|
24264
|
-
|
|
24265
|
-
|
|
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)
|