fix(embeddings): reconcile orphan rows across index rebuilds (#2755)#2755
Conversation
Problem: embeddings.db (message_embeddings_meta / message_embeddings vec0 / embedding_status) is not rebuilt in lockstep with index.db. A full re-ingest, ops reset --index, or a provider full-replace parse can leave embedding rows pointing at message/session identities that no longer exist in the rebuilt index. A live 2026-07-10 audit found 675,825 message_embeddings_meta rows vs 675,725 status-summed embedded messages: 11,348 orphan message ids and 6 orphan embedding_status rows, inflating coverage counters past 100%. Solution: add polylogue/storage/embeddings/reconcile.py (reconcile_embedding_orphans / inspect_embedding_orphans) — a bounded, resumable, idempotent reconciler over an attached index.db, guarded by: identity (NOT EXISTS join on message_id/session_id is the sole deletion trigger), content-hash (an identity-present row is never deleted for a stale content_hash — that's 0k6's re-embed territory), and quiet-window (skip rows embedded within the last 5 minutes, in case index materialization for a full-replace session is mid-write). Recomputes message_count_embedded for sessions with removed message rows. Wired into daemon convergence as periodic_embedding_orphan_reconcile_check (polylogue/daemon/embedding_backlog.py, 15 minute interval, 500-row bounded batches) alongside the existing embedding backlog drain loop, and exposed as the manual break-glass/inspect path via `polylogue ops maintenance embedding-orphan-reconcile` (--yes to apply, defaults to dry-run/inspect), mirroring the existing blob-reference-prune-orphans command shape. Verification: - devtools test tests/unit/storage/test_embedding_orphan_reconcile.py tests/unit/daemon/test_embedding_orphan_reconcile_daemon.py tests/unit/cli/test_archive_maintenance_cli.py -> 55 passed - mypy --strict on all touched files -> Success: no issues found in 8 source files - ruff check / format --check on all touched files -> clean - Manual end-to-end smoke against a real `polylogue demo seed` archive: deleted a live embedded message from index.db, confirmed `ops maintenance embedding-orphan-reconcile` (dry-run) reports the orphan, `--yes` removes exactly it and recounts message_count_embedded, and a follow-up dry-run reports a clean, idempotent archive - devtools render topology-projection && devtools render topology-status (new module regenerates docs/plans/topology-target.yaml + docs/topology-status.md per repo convention) Remaining: acceptance criteria call for "live inspect-before/after evidence reporting the 11,348/6 baseline and resulting exact counts" — this worktree has no access to the real user archive, so that run is a follow-up the operator (or a session with archive access) needs to execute via `polylogue ops maintenance embedding-orphan-reconcile --yes` before the bead is closed. Bead left open per instructions. Ref polylogue-1dk1 Co-Authored-By: Claude <noreply@anthropic.com>
Problem: orphan embedding deletion, vector recount, and status cleanup committed separately, while surviving sessions were not returned to the ambient embedding backlog. Manual apply could also race the daemon or an index generation replacement.\n\nMake the reconciliation pass one rollback-safe transaction, revalidate the active index identity before commit, and require daemon-coordinator or exclusive offline authority for mutation. Count meta-only and vec0-only populations independently and mark surviving affected sessions for reindex.\n\nAdd production-route tests for rollback and retry, bounded selector eligibility, daemon coordination, offline lease exclusion, index identity drift, and asymmetric embedding rows.\n\nRef polylogue-1dk1\n\nCo-Authored-By: Claude <noreply@anthropic.com>
Problem: orphan reconciliation could treat a schema-stale active index as deletion truth. The live archive currently has index v32 while the packaged schema is v34, so automatic daemon cleanup could remove embeddings before the authoritative rebuild completes.\n\nRequire apply mode to verify the attached index user_version matches the packaged index schema before beginning its transaction. Keep dry-run census available, and pin refusal through substrate, daemon, and CLI production routes with mutation-sensitive v32 regressions.\n\nRef polylogue-1dk1\n\nCo-Authored-By: Claude <noreply@anthropic.com>
Regenerate the topology projection and status after carrying the orphan reconciliation module onto current master.\n\nRef polylogue-1dk1\n\nCo-Authored-By: Claude <noreply@anthropic.com>
📝 WalkthroughWalkthroughAdds cross-tier orphan embedding reconciliation with bounded, transactional cleanup; exposes daemon and offline CLI execution paths; adds safety and behavior tests; and updates topology records. ChangesEmbedding orphan reconciliation
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Operator
participant MaintenanceCLI as maintenance embedding-orphan-reconcile
participant Reconcile as reconcile_embedding_orphans
participant Databases as index.db and embeddings.db
Operator->>MaintenanceCLI: invoke dry-run or --yes
MaintenanceCLI->>Reconcile: request inspection or mutation
Reconcile->>Databases: scan and validate orphan rows
Reconcile-->>MaintenanceCLI: return reconciliation report
MaintenanceCLI-->>Operator: render JSON or plain summary
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 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.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8d2c1f3d47
ℹ️ 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".
Problem: dry-run inspection counted orphan rows but rendered apply-only removal counters, telling operators it would remove zero rows even when candidates were present. Track the bounded candidate identity, metadata, vector, and status populations separately from rows actually removed. Render candidate counts in dry-run mode while preserving zero mutation counters, and cover both JSON and plain CLI routes against real SQLite/vec0 fixtures. Ref polylogue-1dk1 Co-Authored-By: Claude <noreply@anthropic.com>
Problem: omitting --max-count in apply mode passed None through the CLI, overriding the storage reconciler's 500-identity default and holding one unbounded writer transaction. Keep dry-run inspection unbounded, but use DEFAULT_MAX_COUNT for apply unless the operator explicitly supplies a limit. A production CLI regression seeds 501 candidates and proves the default pass removes exactly 500 and leaves resumable debt. Ref polylogue-1dk1 Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 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/cli/commands/maintenance.py`:
- Around line 47-52: Export DEFAULT_MAX_COUNT and DEFAULT_QUIET_WINDOW_MS from
polylogue.storage.embeddings alongside EmbeddingOrphanReconcileReport and
reconcile_embedding_orphans, including both the re-export and __all__ entries.
Update the maintenance CLI imports to use the package public API rather than
polylogue.storage.embeddings.reconcile.
- Around line 2268-2299: The authoritative reconcile path around
reconcile_embedding_orphans must catch its RuntimeError and present it as a
click.ClickException, preserving the original error as the cause. Keep the
existing RebuildLeaseUnavailableError handling intact and ensure schema-refusal
failures no longer expose a raw traceback to the CLI operator.
In `@tests/unit/daemon/test_embedding_orphan_reconcile_daemon.py`:
- Around line 104-113: Add a unit test for reconcile_embedding_orphans_once that
creates index.db while leaving embeddings.db absent, enables embeddings with a
configured voyage_api_key, and asserts the function returns None. Keep this
distinct from the existing missing-index test and from
reconcile_embedding_orphans’s empty-report behavior.
🪄 Autofix (Beta)
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
Run ID: eb388eb7-e96e-4d1d-88d5-9a6711119705
📒 Files selected for processing (10)
docs/plans/topology-target.yamldocs/topology-status.mdpolylogue/cli/commands/maintenance.pypolylogue/daemon/cli.pypolylogue/daemon/embedding_backlog.pypolylogue/storage/embeddings/__init__.pypolylogue/storage/embeddings/reconcile.pytests/unit/cli/test_archive_maintenance_cli.pytests/unit/daemon/test_embedding_orphan_reconcile_daemon.pytests/unit/storage/test_embedding_orphan_reconcile.py
| from polylogue.storage.embeddings.reconcile import ( | ||
| DEFAULT_MAX_COUNT, | ||
| DEFAULT_QUIET_WINDOW_MS, | ||
| EmbeddingOrphanReconcileReport, | ||
| reconcile_embedding_orphans, | ||
| ) |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Import through the package public API, not the internal submodule.
This surface reaches directly into polylogue.storage.embeddings.reconcile. The package __init__ already curates the public API (EmbeddingOrphanReconcileReport, reconcile_embedding_orphans, …); it just doesn't export DEFAULT_MAX_COUNT/DEFAULT_QUIET_WINDOW_MS, which is what forces the internal import here. Export those two constants and import from the package instead.
As per coding guidelines: "surfaces should remain leaf adapters and must not import substrate internals directly."
♻️ Proposed fix (CLI + package exports)
In polylogue/cli/commands/maintenance.py:
-from polylogue.storage.embeddings.reconcile import (
- DEFAULT_MAX_COUNT,
- DEFAULT_QUIET_WINDOW_MS,
- EmbeddingOrphanReconcileReport,
- reconcile_embedding_orphans,
-)
+from polylogue.storage.embeddings import (
+ DEFAULT_MAX_COUNT,
+ DEFAULT_QUIET_WINDOW_MS,
+ EmbeddingOrphanReconcileReport,
+ reconcile_embedding_orphans,
+)In polylogue/storage/embeddings/__init__.py, add the constants to the re-export and __all__:
from polylogue.storage.embeddings.reconcile import (
+ DEFAULT_MAX_COUNT,
+ DEFAULT_QUIET_WINDOW_MS,
EmbeddingOrphanReconcileReport,
EmbeddingOrphanSample,
inspect_embedding_orphans,
reconcile_embedding_orphans,
) __all__ = [
+ "DEFAULT_MAX_COUNT",
+ "DEFAULT_QUIET_WINDOW_MS",
"EmbedSessionOutcome",
"EmbedSingleStatus",
"EmbeddingOrphanReconcileReport",📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| from polylogue.storage.embeddings.reconcile import ( | |
| DEFAULT_MAX_COUNT, | |
| DEFAULT_QUIET_WINDOW_MS, | |
| EmbeddingOrphanReconcileReport, | |
| reconcile_embedding_orphans, | |
| ) | |
| from polylogue.storage.embeddings import ( | |
| DEFAULT_MAX_COUNT, | |
| DEFAULT_QUIET_WINDOW_MS, | |
| EmbeddingOrphanReconcileReport, | |
| reconcile_embedding_orphans, | |
| ) |
🤖 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/cli/commands/maintenance.py` around lines 47 - 52, Export
DEFAULT_MAX_COUNT and DEFAULT_QUIET_WINDOW_MS from polylogue.storage.embeddings
alongside EmbeddingOrphanReconcileReport and reconcile_embedding_orphans,
including both the re-export and __all__ entries. Update the maintenance CLI
imports to use the package public API rather than
polylogue.storage.embeddings.reconcile.
Source: Coding guidelines
| if yes: | ||
| from polylogue.maintenance.offline_guard import running_daemon_pid | ||
| from polylogue.storage.index_generation import RebuildLease, RebuildLeaseUnavailableError | ||
|
|
||
| active_config = Config(archive_root=root, render_root=render_root(), sources=[], db_path=index_db) | ||
| try: | ||
| with RebuildLease(root): | ||
| daemon_pid = running_daemon_pid(active_config) | ||
| if daemon_pid is not None: | ||
| raise click.ClickException( | ||
| f"embedding orphan reconcile refused while polylogued PID {daemon_pid} is running" | ||
| ) | ||
| report = reconcile_embedding_orphans( | ||
| index_db, | ||
| embeddings_db, | ||
| dry_run=False, | ||
| max_count=DEFAULT_MAX_COUNT if max_count is None else max_count, | ||
| sample_size=sample_limit, | ||
| quiet_window_ms=quiet_window_seconds * 1000, | ||
| mutation_authority="offline-exclusive", | ||
| ) | ||
| except RebuildLeaseUnavailableError as exc: | ||
| raise click.ClickException(str(exc)) from exc | ||
| else: | ||
| report = reconcile_embedding_orphans( | ||
| index_db, | ||
| embeddings_db, | ||
| dry_run=True, | ||
| max_count=max_count, | ||
| sample_size=sample_limit, | ||
| quiet_window_ms=quiet_window_seconds * 1000, | ||
| ) |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Map the target file and find the relevant command and tests.
ast-grep outline polylogue/cli/commands/maintenance.py --view expanded || true
printf '\n--- matches ---\n'
rg -n "reconcile_embedding_orphans|RebuildLeaseUnavailableError|ClickException|stale|schema|apply_refuses_stale_index_schema|offline-exclusive" polylogue/cli/commands/maintenance.py polylogue -g '*.py'Repository: Sinity/polylogue
Length of output: 50373
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the implementation around the command and the tests.
sed -n '2240,2315p' polylogue/cli/commands/maintenance.py
printf '\n--- test references ---\n'
rg -n "test_apply_refuses_stale_index_schema|stale index schema|authoritative-schema|offline-exclusive|reconcile_embedding_orphans" polylogue -g '*test*.py' -g '*tests*.py'Repository: Sinity/polylogue
Length of output: 3152
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate the implementation and any tests that reference the stale/authoritative-schema refusal path.
rg -n "def reconcile_embedding_orphans|RuntimeError|stale.*schema|authoritative.*schema|schema refusal|RebuildLeaseUnavailableError|mutation_authority" polylogue -g '*.py' -A 6 -B 6
printf '\n--- tests ---\n'
rg -n "apply_refuses_stale_index_schema|stale index schema|authoritative schema|reconcile_embedding_orphans|mutation_authority" polylogue -g '*test*.py' -g '*tests*.py' -A 4 -B 4Repository: Sinity/polylogue
Length of output: 50373
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Find the actual implementation and tests for the embedding orphan reconcile path.
rg -n "def reconcile_embedding_orphans|class EmbeddingOrphanReconcile|stale.*schema|authoritative.*schema|mutation_authority|apply_refuses_stale_index_schema" polylogue -g '*.py' -A 8 -B 8 --max-count 80Repository: Sinity/polylogue
Length of output: 13542
Wrap the authoritative-schema refusal in a ClickException. reconcile_embedding_orphans() raises a raw RuntimeError when the active index schema is not authoritative, and this CLI path lets it bubble to the operator as a traceback.
🤖 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/cli/commands/maintenance.py` around lines 2268 - 2299, The
authoritative reconcile path around reconcile_embedding_orphans must catch its
RuntimeError and present it as a click.ClickException, preserving the original
error as the cause. Keep the existing RebuildLeaseUnavailableError handling
intact and ensure schema-refusal failures no longer expose a raw traceback to
the CLI operator.
| def test_reconcile_embedding_orphans_once_noop_when_index_missing(tmp_path: Path) -> None: | ||
| missing_index_db = tmp_path / "index.db" # never created | ||
|
|
||
| with patch("polylogue.daemon.convergence_stages.load_polylogue_config") as mock_cfg: | ||
| mock_cfg.return_value.embedding_enabled = True | ||
| mock_cfg.return_value.voyage_api_key = "test-key" | ||
| result = reconcile_embedding_orphans_once(missing_index_db) | ||
|
|
||
| assert result is None | ||
|
|
There was a problem hiding this comment.
🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win
Add coverage for the "embeddings.db missing" daemon gating branch.
Tests cover config-disabled and index-missing no-ops, but not reconcile_embedding_orphans_once's own embeddings_db.exists() check (embedding_backlog.py's line ~130-131), which returns None — distinct from reconcile_embedding_orphans's own handling of a missing embeddings.db (which returns an empty report rather than None). A test with index.db present but no embeddings.db would close this gap.
🤖 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/daemon/test_embedding_orphan_reconcile_daemon.py` around lines 104
- 113, Add a unit test for reconcile_embedding_orphans_once that creates
index.db while leaving embeddings.db absent, enables embeddings with a
configured voyage_api_key, and asserts the function returns None. Keep this
distinct from the existing missing-index test and from
reconcile_embedding_orphans’s empty-report behavior.
…2757) ## Summary Add the typed-annotation foundation on current master: closed schema declarations, an explicit registry, a schema-enforced assertion writer, and type-correct predicates over JSON annotation values. This fresh branch replaces unmerged PRs #2752 and #2756 without rewriting their published history and is based on master `7fd5b6bb9` after PR #2755. Ref polylogue-rxdo.7 ## Problem External labels currently degrade into untyped JSON blobs. The archive cannot prove that a label was written under the active schema definition, and query predicates cannot reliably distinguish JSON strings, numbers, booleans, and null. The earlier branches predated merged topology changes; a fresh branch is required to preserve public history and regenerate derived surfaces against current master. ## Solution - Add closed `AnnotationField` and `AnnotationSchema` models plus an explicit `AnnotationSchemaRegistry` in `polylogue/annotations/schema.py`. - Validate all writes against the exact active registered schema, rejecting unknown, draft, deprecated, drifted, and non-finite values. Runtime declarations reject unknown closed-vocabulary values and non-finite bounds; arbitrarily large finite JSON integers do not pass through float coercion. - Route validated external-agent rows through the existing assertion write chokepoint, preserving candidate/non-injected safety semantics. - Add `value.<path>` equality and range predicates for assertion queries. SQL guards JSON type before comparison, so strings, numbers, booleans, null, and missing paths remain distinct; equality alternation preserves each scalar's identity and numeric ranges reject non-finite right-hand sides. - Semantically carry forward the latest `polylogue-rxdo.7` record plus open children `.7.1` and `.7.2`, preserving unrelated master Beads records. - Regenerate topology projection/status against merged PR #2755 master. ## Acceptance criteria Satisfied in this foundation phase: - closed typed field declarations and finite numeric values/bounds - exact active registered-schema enforcement at the public writer - candidate/non-injected writes through the production assertion chokepoint - typed JSON scalar equality and numeric range predicates - real archive write/query/judge tests Explicitly deferred: - `polylogue-rxdo.7.1`: durable `annotation_schemas` and `annotation_batches`, immutable fingerprints, a concrete delegation-discourse schema, and queryable batch provenance - `polylogue-rxdo.7.2`: bounded JSONL import, live target/evidence-span validation, CLI/MCP contracts, disagreement/adjudication roundtrips, and rendering - `polylogue-kmts`: structural-target joins without row multiplication The parent bead remains open; neither child is optional for its full acceptance criteria. No prior PR or bead changes state through this PR body. ## Verification - consolidated fresh-master regression selector over schema closure, finite values/bounds, scalar identity/alternation, and public-writer rejection -> `23 passed, 14733 deselected in 25.18s` - `devtools verify --quick` with the regenerated/merged working tree -> all 15 steps exited 0 in 47.63s, including mypy, generated surfaces, topology, layering, schema roundtrip, and safety gates - pre-push `devtools verify --quick` at `c7d1349c6` -> all 15 steps exited 0 in 23.53s - `devtools render topology-projection && devtools render topology-status` -> `917 rows`, `764 stable`, `6 TBD` - `.agent/scripts/bd-graph-lint` -> `dup_labels=0`, `inversions=0`; two inherited missing-AC records (`polylogue-2ilz`, `polylogue-nu2h`) are unchanged and outside this diff GitHub-hosted jobs may still fail before startup while the repository account is billing-locked; that state produces no job logs or substantive CI result. Anti-vacuity: the focused tests exercise the real SQLite assertion writer, typed SQL lowering, archive query readback, and candidate judgment. Removing the active-registry check, finite guards, JSON-type equality guard, or candidate chokepoint causes the corresponding production-route regression to fail. The equality fixture places `true` beside numeric `1`, and `false` beside numeric `0`, so SQLite coercion cannot pass unnoticed. ## Adversarial review notes The independent audit of PR #2756 found four real gaps: boolean/number equality coercion, missing runtime enforcement of closed declaration vocabularies, accepted non-finite bounds/equality literals, and `OverflowError` on huge finite integer validation. Logical commit `ea1e75faf` carries the repair on current master with mutation-sensitive regressions. The replacement diff was then rechecked after merged PR #2755 and regenerated without carrying stale topology bytes. --------- Co-authored-by: Claude <noreply@anthropic.com>
Summary
Adds bounded, resumable reconciliation for orphan rows in
embeddings.db, runs it through daemon convergence, and exposes a dry-run-first maintenance command. Mutation is atomic and writer-coordinated, preserves surviving sessions for re-embedding, accounts for asymmetric meta/vec0 populations, and refuses to use a schema-stale active index as deletion truth. Dry-run output reports the bounded meta/vector/status candidate set separately from rows actually removed.Problem
embeddings.dbis not rebuilt in lockstep withindex.db. Full replacement or index rebuild can leave metadata, vec0, and status rows whose message/session identities no longer exist, inflating storage and coverage counters. A 2026-07-12 read-only census of the current active generation found 741,267 metadata rows, including 22,442 identities absent from the active index, plus 17,231 status rows with 303 absent-session rows.That active index is schema v32 while the packaged index schema is v34 and daemon ingest is still running. Those counts are diagnostic only: the active generation is not yet authoritative deletion truth, so destructive cleanup must wait for the rebuild.
This replaces conflicting PR #2749 from fresh master after PR #2754 changed generated topology.
Solution
polylogue/storage/embeddings/reconcile.pywith oneBEGIN IMMEDIATEtransaction covering meta/vec0 deletion, surviving-session recount +needs_reindex = 1, and absent-session status cleanup. Exceptions roll back the whole pass and retry remains idempotent.polylogue-0k6.PRAGMA idx.user_versionequals packagedINDEX_SCHEMA_VERSION. Dry-run inspection remains available on mismatched generations; daemon and CLI mutation refuse the live v32-vs-v34 state.DaemonWriteCoordinator; require the CLI--yespath to hold the exclusive offline rebuild lease and reject a running daemon.--max-countis supplied.polylogue ops maintenance embedding-orphan-reconcilefor inspect/break-glass operation.Acceptance-criteria disposition
polylogue-b5l; this slice refuses schema-stale generations now.polylogue-0k6; this reconciler deliberately preserves identity-present rows.Verification
The focused run exercises real SQLite/vec0 rows, the production pending-session selector, the daemon coordinator route, and plain/JSON CLI routes. The rollback test fails if the transaction boundary is removed; the schema/path guards and offline lease tests fail if their production checks are removed. The manual-bound regression seeds 501 real candidates and requires the default apply pass to remove exactly 500 while leaving resumable debt.
Adversarial review notes
ba5d8820badds separate bounded candidate counters and production plain/JSON CLI regressions.None. Commita1df0b4abrestores the 500-identity apply default and adds a 501-candidate production-route regression; the review thread is answered and resolved.No destructive live apply was run. Apply and bead closure remain deferred until the active index is rebuilt from v32 to packaged v34 and the resulting before/after census is recorded.
Ref polylogue-1dk1
Co-Authored-By: Claude noreply@anthropic.com
Summary by CodeRabbit