fix(reindex): isolate inactive candidate durable writes - #3885
Conversation
Owned inactive generations previously reopened source and user tiers as writable through candidate read-through symlinks. The production bootstrap correctly rejected the layout, and bypassing that check would have let candidate replay mutate the frozen phase-2 source. Add an explicit owned-candidate mode that writes only index.db, validates every read-through target, opens durable tiers read-only, refuses blob publication, and requires current parser census plus persisted authority before allocating or accepting a candidate. Ref polylogue-inactive-candidate-durable-barrier.
Problem Inactive candidates could reach live ops telemetry, convergence debt, and several inherited blob and user-tier mutation APIs through read-through links. Candidate allocation also preceded complete frozen-source authority. What changed Require current parser and settled authority evidence before allocating a generation. Validate split-root ownership against generation metadata, block every blob and user mutation surface, and suppress inactive-candidate writes to the live ops ledger while retaining active-generation telemetry. Verification Focused candidate barrier tests pass 6/6. The wider affected batch passed 162 tests; its 15 failures exactly match the frozen current-master full-suite ledger. Ruff and strict mypy pass for all touched source and test files. Ref polylogue-inactive-candidate-durable-barrier. Co-Authored-By: Codex <noreply@openai.com>
Problem The writer-module policy discovered the new frozen-candidate classifier as a source/index mutation entrypoint but its declared inventory did not name it. What changed Add the classifier to the revision-governance writer inventory so the policy continues to audit its twin-tier reach. Verification `devtools verify layering --json` reports zero violations. Ref polylogue-inactive-candidate-durable-barrier. Co-Authored-By: Codex <noreply@openai.com>
Problem The inactive-generation telemetry guard applied only when the sampler inferred ops.db from the candidate index path. A caller supplying an explicit archive root could bypass the same durable boundary. What changed Resolve and classify the connected index before either ops path is selected. The candidate canary now exercises both inferred and explicit telemetry routes and proves live ops.db bytes remain unchanged. Verification Strict mypy and Ruff pass. The candidate barrier and active FTS convergence suites pass 10/10. Ref polylogue-inactive-candidate-durable-barrier. Co-Authored-By: Codex <noreply@openai.com>
Re-derive current byte and membership authority before creating an inactive generation or rebuild transaction. Add a non-repairing generation-store mode so candidate validation cannot rewrite a missing or poisoned active-pointer anchor. Prove refusal leaves pointer, generation, transaction, and durable-tier evidence unchanged. Ref polylogue-inactive-candidate-durable-barrier Co-Authored-By: Codex <noreply@openai.com>
Bind frozen source validation to the canonical active index selected by the owned archive identity. Keep the durable archive root for source, user, and blob reads while ignoring missing or poisoned generation-member anchors without repairing them. Ref polylogue-inactive-candidate-durable-barrier Co-Authored-By: Codex <noreply@openai.com>
Run the same current byte and membership authority validation inside the daemon bulk resolver after ownership and provenance checks, before constructing the generation store or creating a transaction. Update daemon fixtures to represent a phase-2-complete source and prove missing or poisoned anchors remain untouched on refusal. Ref polylogue-inactive-candidate-durable-barrier Co-Authored-By: Codex <noreply@openai.com>
Keep daemon adapters out of source-layer internals by exposing the frozen-source admission boundary from the maintenance operation layer. The wrapper retains the exact owned active-index identity and leaves generation allocation ordering unchanged. Ref polylogue-inactive-candidate-durable-barrier Co-Authored-By: Codex <noreply@openai.com>
A resumable rebuild that failed provenance admission marked its transaction stale through an IndexGenerationStore constructor that also repaired missing or poisoned active-pointer anchors. That lifecycle-only failure path must not alter active archive authority. Construct the store with anchor repair disabled and cover both missing and poisoned anchors on the real operation-id route while preserving stale transaction evidence. Ref polylogue-inactive-candidate-durable-barrier.
Inactive admission trusted a parser-census status without comparing its persisted logical-key identity to a fresh current-parser derivation. Candidate transaction setup also repaired missing or poisoned active-pointer anchors before any promotion boundary. Decode and normalize every current parser census key set, compare parseable raws against fresh parser identity, and construct candidate admission stores without pointer repair. Promotion-authorized offline requests retain their explicit repair authority. Ref polylogue-inactive-candidate-durable-barrier.
Frozen authority receipts can contain either transitional Provider prefixes or public Origin prefixes depending on the acquisition route. Comparing either raw spelling to freshly parsed public identity rejected valid Codex and ChatGPT archives. Normalize both persisted aliases to Origin while preserving the native id, reject unknown or collapsing key sets, and compare that canonical identity to the current parser result. Ref polylogue-inactive-candidate-durable-barrier.
Parser receipts were compared to fresh parses only for standalone payloads. Extra durable membership bindings and typed continuation rows could therefore carry identity outside that comparison and survive frozen-source admission. Require each canonical census key set to equal the durable typed and membership authority bindings before replay. Add real-route regressions for an extra membership, a poisoned typed append receipt, and jointly poisoned durable plus parser identity. Ref polylogue-inactive-candidate-durable-barrier.
Typed append rows cannot be parsed independently, so jointly poisoned durable and census keys could evade fresh full-row identity checks. Canary failure cleanup also constructed a generation store with pointer repair enabled. Require each append key to match its linked predecessor and baseline authority, whose full rows remain fresh-parser validated. Keep canary discard bookkeeping from repairing missing or poisoned active anchors. Ref polylogue-inactive-candidate-durable-barrier.
A self-linked or incomplete APPEND row could make its poisoned durable and census identity agree without reaching any independently parseable authority. String equality alone could not establish that the continuation belonged to a real full source. Expand selected validation through revision links, require byte-proven acyclic predecessor chains that terminate at a distinct parseable FULL baseline, and enforce one canonical identity across the chain. Ref polylogue-inactive-candidate-durable-barrier.
Record the exact implementation head, focused verification, quick-gate receipt, and independent no-blockers verdict. Production candidate execution and downstream phases remain open under their existing Beads.
📝 WalkthroughWalkthroughInactive rebuild candidates now validate frozen source authority, avoid active-pointer repair, and restrict durable-tier mutations. Frozen replay skips source writes while validating authority. Tests cover write barriers, admission failures, pointer preservation, and authority drift. ChangesInactive candidate durability
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant Rebuild
participant FrozenBackfill
participant ArchiveStore
participant CandidateIndex
Rebuild->>FrozenBackfill: validate frozen source evidence
FrozenBackfill->>ArchiveStore: verify durable authority and census
ArchiveStore-->>FrozenBackfill: validation result
FrozenBackfill->>CandidateIndex: replay index changes only
CandidateIndex-->>Rebuild: candidate readiness
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@polylogue/sources/revision_backfill.py`:
- Around line 897-916: Add brief comments next to the two logical-key
derivations in the surrounding backfill function, clarifying that
parsed_logical_keys use origin-prefixed keys to match recorded_logical_keys,
while membership_candidates uses provider-prefixed keys to match
raw_session_memberships.logical_source_key. Keep both derivations unchanged.
- Around line 84-107: Explicitly close the SQLite connections used by
_expand_frozen_revision_link_selection and all four source.db connections in
require_current_parser_source_census, using contextlib.closing or equivalent
finally-based cleanup while preserving the existing transaction context
behavior.
In `@polylogue/storage/sqlite/archive_tiers/archive.py`:
- Around line 2178-2200: In _preacquire_attachment_blobs, remove the
hashlib.file_digest read and stored_hash comparison for attachment blobs. Retain
the blob_path existence and size validation, then record the attachment as
acquired and continue; rely on rebuild_source_evidence_snapshot for
content-identity verification.
In `@polylogue/storage/sqlite/archive_tiers/revision_governance.py`:
- Around line 2525-2528: Move the frozen-candidate check before the
terminal_raw_ids loop at
polylogue/storage/sqlite/archive_tiers/revision_governance.py:2525-2528 so the
function returns before calling raw_revision_descriptor for any raw ID. In the
corresponding flow at
polylogue/storage/sqlite/archive_tiers/revision_governance.py:2994-3008, return
immediately after require_frozen_membership_authority succeeds, preventing
raw_membership_census from being repeated per raw.
- Around line 209-215: Declare the _inactive_candidate_durable_read_only boolean
member on the RawRevisionGovernanceHost Protocol alongside the other private
members used by this module, so host implementations are required to expose the
guard flag and type checking can detect omissions.
In `@tests/unit/maintenance/test_inactive_candidate_durable_barrier.py`:
- Around line 600-610: Update initialize_active_archive_root and its
durable-file validation path through initialize_archive_database so unsafe tier
files such as symlinks or non-files raise DurableChangeTrainError instead of
RuntimeError, using an error message that matches “unsafe file” and preserving
the existing rejection behavior.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 9926611c-af9a-4606-a7ea-f2d80ed2e7b4
📒 Files selected for processing (15)
.beads/issues.jsonldocs/plans/layering.yamlpolylogue/daemon/bulk_rebuild.pypolylogue/maintenance/rebuild_index.pypolylogue/maintenance/reindex_canary.pypolylogue/sources/revision_backfill.pypolylogue/storage/fts/drift_sampling.pypolylogue/storage/index_generation.pypolylogue/storage/repair.pypolylogue/storage/sqlite/archive_tiers/archive.pypolylogue/storage/sqlite/archive_tiers/revision_governance.pytests/unit/daemon/test_bulk_rebuild.pytests/unit/maintenance/test_inactive_candidate_durable_barrier.pytests/unit/maintenance/test_rebuild_index_provenance_gate.pytests/unit/maintenance/test_reindex_canary.py
| if self._inactive_candidate_durable_read_only: | ||
| if attachment.inline_bytes is not None: | ||
| hash_hex = hashlib.sha256(attachment.inline_bytes).hexdigest() | ||
| size = len(attachment.inline_bytes) | ||
| elif attachment.precomputed_blob is not None: | ||
| hash_hex, size = attachment.precomputed_blob | ||
| else: | ||
| continue | ||
| blob_path = self._blob_publisher.blob_path(hash_hex) | ||
| if not blob_path.is_file() or blob_path.stat().st_size != size: | ||
| raise InactiveCandidateDurableWriteError( | ||
| "inactive candidate requires attachment bytes to be present in the frozen blob namespace: " | ||
| f"{hash_hex}" | ||
| ) | ||
| with blob_path.open("rb") as handle: | ||
| stored_hash = hashlib.file_digest(handle, "sha256").hexdigest() | ||
| if stored_hash != hash_hex: | ||
| raise InactiveCandidateDurableWriteError( | ||
| "inactive candidate found attachment bytes that do not match the frozen blob identity: " | ||
| f"{hash_hex}" | ||
| ) | ||
| acquired[id(attachment)] = (bytes.fromhex(hash_hex), size, "acquired") | ||
| continue |
There was a problem hiding this comment.
🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Check where blob content identity is already verified during a rebuild.
set -euo pipefail
rg -nP -C 5 '\bdef verify\s*\(' polylogue/storage/blob_store.py
rg -nP -C 3 'blob_store\.verify|file_digest' --type=pyRepository: Sinity/polylogue
Length of output: 616
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== files =="
fd -a 'archive\.py|blob_store\.py$' . | sed 's#^\./##'
echo "== archive relevant outline =="
ast-grep outline polylogue/storage/sqlite/archive_tiers/archive.py --match Archive --view expanded || true
echo "== archive imports and verify context =="
sed -n '1,120p' polylogue/storage/sqlite/archive_tiers/archive.py
echo "== around preacquire =="
rg -n "_preacquire_attachment_blobs|inactive_candidate_durable_read_only|rebuild_source_evidence_snapshot|verify" polylogue/storage/sqlite/archive_tiers/archive.py -C 8
echo "== blob_store relevant =="
sed -n '250,350p' polylogue/storage/blob_store.pyRepository: Sinity/polylogue
Length of output: 40640
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== rewrite_raw_evidence and rebuild calls =="
rg -n "rebuild_source_evidence_snapshot|raw_replay|preacquire_attachment_blobs|_preacquire_attachment_blobs|_raw_revision_replay_plan|raw_revision_material|verify\\(" polylogue/storage/sqlite/archive_tiers/archive.py -C 8
echo "== rebuild source snapshot outline/context =="
sed -n '11700,12280p' polylogue/storage/sqlite/archive_tiers/archive.py
echo "== tests around attachment preacquire =="
rg -n "_preacquire_attachment_blobs|inactive_candidate_durable_read_only|attachment" tests/unit/storage/test_archive_tiers_archive.py -C 5
echo "== tests around rebuild/verify =="
rg -n "rebuild_source_evidence_snapshot|verify\\(" tests/unit/storage/test_archive_tiers_archive.py -C 6Repository: Sinity/polylogue
Length of output: 18868
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== full _preacquire_attachment_blobs =="
sed -n '2162,2218p' polylogue/storage/sqlite/archive_tiers/archive.py
echo "== write_raw_and_parsed and payload blob paths =="
rg -n "def write_raw_and_parsed|write_raw_payload|write_hook_event|write_parsed_for_retained_raw|write_parsed|verify|blob_store|file_digest|precompute|precomputed" polylogue/storage/sqlite/archive_tiers/archive.py -C 12
echo "== raw revision material / replay plan helpers =="
rg -n "def open_raw_revision_material|def raw_revision_material|def raw_revision_replay_plan|def raw_revision_rebuild_selection|raw_revision_replay_plan\\(|raw_revision_rebuild_selection\\(" polylogue -g '*.py' -C 8
echo "== all verify usages outside tests if any =="
rg -n "\bverify\s*\(|file_digest" polylogue -g '*.py' -C 4 | rg -v 'tests/|\.pyc' || trueRepository: Sinity/polylogue
Length of output: 47915
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== revision backfill archive opening and selected reconstruction flow =="
sed -n '980,1060p' polylogue/sources/revision_backfill.py
sed -n '1100,1220p' polylogue/sources/revision_backfill.py
sed -n '1480,1565p' polylogue/sources/revision_backfill.py
echo "== active writer lease / write_parsed calls in revision backfill =="
rg -n "open_frozen_source_validation|archive.open\\(|write_parsed\\(|_preacquire_attachment_blobs|selected_raw_ids|accepted_raw|write_hook_event|write_raw_blob_ref|apply_raw_revision_replay" polylogue/sources/revision_backfill.py -C 6
echo "== write hook event implementation =="
rg -n "def _write_hook_event|def write_hook_event|def write_raw_payload|def write_raw_blob_ref" polylogue/storage/sqlite/archive_tiers source_write.py -C 10
echo "== source_write relevant functions =="
sed -n '1,180p' polylogue/storage/sqlite/archive_tiers/source_write.pyRepository: Sinity/polylogue
Length of output: 45947
Drop the attachment blob end-to-end digest from _preacquire_attachment_blobs.
_preacquire_attachment_blobs already checks the frozen blob path and size before this loop. Re-hashing every attachment blob on every accepted raw also duplicates the existing frozen raw payload verification in rebuild_source_evidence_snapshot. Keep the existence and size check here; let the raw payload verification cover attachment blob content identity.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@polylogue/storage/sqlite/archive_tiers/archive.py` around lines 2178 - 2200,
In _preacquire_attachment_blobs, remove the hashlib.file_digest read and
stored_hash comparison for attachment blobs. Retain the blob_path existence and
size validation, then record the attachment as acquired and continue; rely on
rebuild_source_evidence_snapshot for content-identity verification.
| def test_active_bootstrap_still_rejects_candidate_durable_symlinks( | ||
| tmp_path: Path, | ||
| monkeypatch: pytest.MonkeyPatch, | ||
| ) -> None: | ||
| root = tmp_path / "archive" | ||
| _prepare_frozen_source(root, monkeypatch) | ||
| generation_store = IndexGenerationStore.for_archive_root(root) | ||
| generation = generation_store.create(source_snapshot=source_revision_snapshot(root)) | ||
|
|
||
| with pytest.raises(DurableChangeTrainError, match="unsafe file"): | ||
| initialize_active_archive_root(Path(generation.index_path).parent) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Locate initialize_active_archive_root and the refusal it raises.
set -euo pipefail
rg -nP -A 40 'def initialize_active_archive_root\s*\(' polylogue/storage/sqlite/archive_tiers/bootstrap.py
rg -nP -C 3 'DurableChangeTrainError\(' --type=py
rg -nP -C 2 'unsafe file' --type=pyRepository: Sinity/polylogue
Length of output: 2285
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== locate files =="
git ls-files | rg '(^|/)(bootstrap\.py|test_inactive_candidate_durable_barrier\.py)$' || true
echo
echo "== bootstrap relevant sections =="
wc -l polylogue/storage/sqlite/archive_tiers/bootstrap.py
rg -n -C 6 'candidate|symlink|safe existing|unsafe|PermissionError|DurableChangeTrainError|RuntimeError|has_bootstrap_marker|has_pending_bootstrap|fresh_durable_bootstrap' polylogue/storage/sqlite/archive_tiers/bootstrap.py
echo
echo "== test section =="
wc -l tests/unit/maintenance/test_inactive_candidate_durable_barrier.py
sed -n '540,630p' tests/unit/maintenance/test_inactive_candidate_durable_barrier.py | cat -n
echo
echo "== durable change train error definition/usages =="
rg -n 'class DurableChangeTrainError|DurableChangeTrainError' --type=py .Repository: Sinity/polylogue
Length of output: 50372
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== test file imports and helper =="
sed -n '1,80p' tests/unit/maintenance/test_inactive_candidate_durable_barrier.py | cat -n
sed -n '1,180p' tests/unit/maintenance/test_inactive_candidate_durable_barrier.py | rg -n -C 4 '_prepare_frozen_source|DurableChangeTrainError|initialize_active_archive_root' || true
echo
echo "== bootstrap import/order around entrypoint =="
sed -n '1,40p' polylogue/storage/sqlite/archive_tiers/bootstrap.py | cat -n
sed -n '230,370p' polylogue/storage/sqlite/archive_tiers/bootstrap.py | cat -n
echo
echo "== deterministic source-text verifier =="
python3 - <<'PY'
from pathlib import Path
import re
boot = Path('polylogue/storage/sqlite/archive_tiers/bootstrap.py').read_text()
test = Path('tests/unit/maintenance/test_inactive_candidate_durable_barrier.py').read_text()
patterns = {
'test_awaited_type': 'DurableChangeTrainError',
'test_awaited_message': 'match="unsafe file"',
'entrypoint': re.compile(r'def initialize_active_archive_root\s*\(', re.S),
'runtime_unsafe_file_text': re.compile(r'raise RuntimeError\([\s\S]*?safe existing file[\s\S]*?\)', re.S),
'dCTE_in_bootstrap': re.compile(r'DurableChangeTrainError', re.S),
'match_expr_for_test': re.compile(r'pytest\.raises\s*\(\s*([A-Za-z0-9_]+)[\s\S]*?match=([^,\)]+)', re.S),
}
print("bootstrap contains DurableChangeTrainError:", bool(patterns['dCTE_in_bootstrap'].search(boot)))
print("bootstrap entrypoint found:", bool(patterns['entrypoint'].search(boot)))
print("runtime unsafe existing file raise found:", bool(patterns['runtime_unsafe_file_text'].search(boot)))
print("test imported/annotated exception:", patterns['test_awaited_type'] in test)
print("test contains unsafe file matcher:", patterns['test_awaited_message'] in test)
m = patterns['match_expr_for_test'].search(test)
print("parsed pytest.raises exception/message:", (m.group(1).strip(), m.group(2).strip()) if m else None)
# Check control flow path: is archive tier spec for one durable tier, and does code call initialize_archive_database after link check.
if any(t in test for t in ['source.db']) or any(line in Path('tests/unit/maintenance/test_inactive_candidate_durable_barrier.py').read_text() for line in ['source.db', '.durable']):
print("test likely creates durable archive files.")
PY
echo
echo "== targeted test helper =="
python3 - <<'PY'
from pathlib import Path
text = Path('tests/unit/maintenance/test_inactive_candidate_durable_barrier.py').read_text()
start = text.index('def _prepare_frozen_source')
next_def = re.search(r'\ndef \w', text[start+1:])
if next_def:
print(text[start:start+next_def.start()+1])
else:
print(text[start:])
PYRepository: Sinity/polylogue
Length of output: 15700
Raise the asserted exception for unsafe durable bootstrap files.
initialize_active_archive_root() validates each archive tier file through initialize_archive_database(), which raises RuntimeError("durable tier is not a safe existing file; refusing runtime initialization: ...") for symlinks/non-files. Update the bootstrap check to raise DurableChangeTrainError with a message matching "unsafe file" so this test can catch the existing candidate durable symlink case.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/unit/maintenance/test_inactive_candidate_durable_barrier.py` around
lines 600 - 610, Update initialize_active_archive_root and its durable-file
validation path through initialize_archive_database so unsafe tier files such as
symlinks or non-files raise DurableChangeTrainError instead of RuntimeError,
using an error message that matches “unsafe file” and preserving the existing
rejection behavior.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6bf48c08eb
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| validate_frozen_source_authority( | ||
| root, | ||
| active_index_path=canonical_active_index_path(owned.location), | ||
| ) |
There was a problem hiding this comment.
Validate only the bounded rebuild page on resume
For a resumable full-source rebuild containing more raws than raw_batch_size, this call omits selected_raw_ids, so validate_frozen_source_authority() reaches _load_frozen_revision_evidence() and reparses every parseable raw before next_raw_page() selects the bounded page. The daemon path performs the same archive-wide validation during transaction resolution and then repeats it here, meaning each page can parse the entire corpus twice and an N-row rebuild with B-row pages approaches O(N²/B) parsing; on the multi-GiB archives this path targets, bounded passes can become impractical. Perform the full admission once and validate only the selected page/cohort on resumed passes, or persist reusable validation evidence.
Useful? React with 👍 / 👎.
| c.raw_id IS NULL OR c.status != 'complete' | ||
| OR m.raw_id IS NULL OR m.decision IS NULL | ||
| OR m.decision IN ('ambiguous', 'deferred') |
There was a problem hiding this comment.
Treat terminal non-session census as resolved
When phase 2 parses a retained raw into zero sessions, replace_raw_membership_census() records status='non_session' with no membership rows, and _record_raw_authority_parser_census() explicitly treats that status as complete. If the historical raw remains revision_authority='quarantined', this predicate nevertheless flags it because the status is not complete and m.raw_id is null. A legitimate settings/config/sidecar artifact therefore makes every full candidate rebuild fail permanently with FrozenSourceRemediationRequiredError, even though no membership can or should be created for it; exclude terminal non_session census rows from unresolved authority debt.
Useful? React with 👍 / 👎.
| validate_frozen_source_authority( | ||
| root, | ||
| active_index_path=canonical_active_index_path(location), | ||
| ) |
There was a problem hiding this comment.
Allow frozen admission when the active index is absent
When rebuilding after a lost or explicitly reset index.db, canonical_active_index_path(location) names a path that does not exist, but frozen admission passes it to ArchiveStore.open_frozen_source_validation(), which opens the index with SQLite mode=ro before any candidate is allocated. The rebuild therefore fails with an open error instead of recreating the derived tier, despite missing_index_raw_ids() and rebuild_schema_currency_preflight() explicitly treating a missing index as a supported rebuild case. Frozen source validation must use a source-only path or tolerate an absent/corrupt active read model rather than requiring the tier being rebuilt.
AGENTS.md reference: AGENTS.md:L193-L201
Useful? React with 👍 / 👎.
Close read-only source census connections explicitly and document the intentional Provider-to-Origin normalization boundary. Expose frozen candidate mode through the governance protocol and stop source-tier finalization work once frozen authority has already been proven. Focused candidate, provenance, and revision-backfill verification passes with 130 tests.
Declare the Drive revision adapter as a normal mutable governance host so the frozen-candidate protocol remains explicit and structurally type-safe.
Validate the full frozen source once when a resumable transaction is created, then revalidate only each selected authority component on resumed pages. Treat terminal non-session census rows as resolved and let source-only admission operate when the derived index is missing or unreadable. Regression coverage proves bounded resume, non-session authority, and missing-index recovery. The 133-test candidate/provenance/revision suite passes. The daemon suite has seven passes plus two failures already present in the exact current-master baseline.
Keep the resumed-page validation spy faithful to the production validator signature so strict type checking covers the regression harness.
Summary
Introduce an owned inactive-candidate access mode that permits index construction while preserving the frozen durable source, user database, blob namespace, active pointer, and live operations state. Ref
polylogue-inactive-candidate-durable-barrier.Problem
The current rebuild route represented an inactive generation with read-through symlinks, then initialized that generation as a normal writable archive. The durable change-train guard rejected the symlinked source tier. Bypassing that guard would have allowed candidate replay to mutate source, user, blob, and operations state during phase 3. Parser-census and persisted-authority decisions also needed to be validated before allocating a candidate generation.
Solution
Add explicit owned-inactive archive semantics, verify generation ownership and exact read-through targets, open durable tiers read-only, and refuse durable, blob, and live-operations mutations before side effects. Candidate admission now re-derives the frozen parser census and exact source authority before generation allocation. APPEND authority must form an acyclic predecessor chain terminating at a byte-proven FULL baseline under one canonical source key. Candidate, stale-retirement, canary-cleanup, and daemon routes no longer repair the active pointer unless promotion is explicitly authorized.
Production candidate construction, acceptance, promotion, restart, and postflight remain under their existing phase Beads. This PR supplies the safe mechanism only.
Verification
devtools test tests/unit/maintenance/test_inactive_candidate_durable_barrier.py tests/unit/maintenance/test_rebuild_index_provenance_gate.py tests/unit/sources/test_revision_backfill.py: 133 passed.devtools test tests/unit/maintenance/test_inactive_candidate_durable_barrier.py tests/unit/maintenance/test_reindex_canary.py -k 'inactive_candidate_durable_barrier or cleans_candidate_after_comparison_failure': 20 passed, 40 deselected.devtools test tests/unit/daemon/test_bulk_rebuild.py: 7 passed; the two failures are present in the exact current-master full-suite baseline under the same node IDs.devtools verify --quick: all 24 steps passed in run20260808T120215Z-quick-1146980-704f744aat head4d02ba7e83b75049e60c65f049aa78dfc73add53.92a9d4ac3a62e81a01d2ad832acd1060d5657c02.bdgraph policy still reports the inherited 220-item AC-less database. The committed branch snapshot contains the 218 structured contracts merged in chore(beads): add guarded acceptance contracts #3880; tracker database reconciliation remains separate release-authority work.