Repository navigation
fix(code-index): keep a commit that lands mid-reconcile visible - #4624
Conversation
hook_branch_write_lands_in_a_sealed_single_store_generation failed on CI for two unrelated PRs (#4618, #4623) with code_index_activation_unavailable: the branch publisher waited for a generation at the new HEAD that the scheduler never built. The reconcile pass sampled the git-metadata freshness witness in mark_reconciled, after capture, because capture can refresh .git/index. When a commit landed between capture and mark, the pass sealed a dirty snapshot with no source revision but recorded the post-commit HEAD as reconciled. A commit leaves worktree stats unchanged, so every later freshness probe answered Current and no pass ever built the new HEAD. mark_reconciled now takes the fingerprint sampled before the pass read any source. If HEAD or a ref moved since then, it records that earlier sample, so the next probe sees the move. The index stays out of the comparison because capture may refresh it on its own. The same run showed the sibling path: post-projection source verification found the moved source, its rebuild lost the publication compare-and-swap, and the worker only logged it. A lost publication race (CompareAndSwap, PublicationConflict) now wakes a rescan there and takes the bounded capacity retry in the main reconcile loop. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
There was a problem hiding this comment.
Devin Review found 2 potential issues.
1 flag not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)
| // authority; the headroom wake is its retry. | ||
| capacity_retry.record_progress(); | ||
| } else if transient_capacity { | ||
| } else if transient_capacity || error.is_publication_race() { |
There was a problem hiding this comment.
🟡 Publication retries repeat a stale conflict
When another publisher supersedes a retained text owner, is_publication_race retries using that same owner. Its generation ID still differs from the active pointer, so every retry fails and the source stays unindexed.
Learn more
The worker retains its text owner across passes, and reconcile_retained_text_generation_with rebuilds using that owner's metadata. When another publisher advances the pointer, finish_retained_reconcile rejects the old generation ID before publication. A delayed capacity wake does not replace the retained owner, so repeated passes hit the same conflict until the retry budget runs out. A later unrelated wake still uses that owner.
Example: Publisher B replaces generation A while this worker retains A's text owner. Each retry captures new source but rejects A against B's pointer instead of publishing a successor to B.
Recommended fix: On a PublicationConflict caused by a superseded retained owner, refresh or retire the retained owner before retrying, then reconcile against the current active pointer. Keep the existing bounded backoff for genuine compare-and-swap races.
Was this helpful? React with 👍 or 👎 to provide feedback.
| // bound to; a HEAD move under the same worktree is allowed and simply | ||
| // records a new source revision, so the served generation is never | ||
| // mis-attributed across identities. | ||
| let captured_under = identity::GitMetadataFingerprintV1::capture(&self.project_root); |
There was a problem hiding this comment.
🟡 Successful retry retains an old fingerprint
If a commit cancels the first capture, captured_under still names the old HEAD when the retry captures the new one. The completed generation then reports moved and triggers an unnecessary follow-up reconcile.
Learn more
The bounded retry loop starts below this sample. A source hint advances the epoch and can cancel its first capture; the next iteration creates a new control and captures the new commit. mark_reconciled still compares the original sample against the post-retry metadata, storing the old fingerprint even though the successful snapshot carries the new revision. Graph-disabled worktrees do not run the post-projection verification that could refresh this state.
Example: A pass samples HEAD A, a hook commits B and cancels the first capture, then retry captures and publishes B. A subsequent query sees the recorded A fingerprint, reports movement, and wakes another pass for B.
Recommended fix: Capture a fresh pre-source fingerprint for each retry, before that attempt resolves or captures the source; pass the successful attempt's fingerprint to mark_reconciled.
Was this helpful? React with 👍 or 👎 to provide feedback.
Problem
hooks_lsp_suite'shook_branch_write_lands_in_a_sealed_single_store_generationfailed on CI in two unrelated PRs: #4618, a dependency bump, and #4623. The error wascode_index_activation_unavailable: code-index scheduler did not publish exact branch source. In the failure's observed state:refs/heads/feature/hookwithrevision: null;staleness_state: fresh;rebuild_in_flight: false.The scheduler had sealed a dirty capture, believed it was current, and never built the new HEAD. The branch publisher waited out its 20 s idle timeout.
Root cause
The test writes
src/lib.rs, commits it, and then tracks the branch again. A background pass captured the worktree between the write and the commit. That dirty snapshot has no source revision. The pass then sampled the git-metadata freshness witness inmark_reconciled, after the commit. It samples that late on purpose, because capture can refresh.git/index. So the pass recorded the post-commit HEAD as reconciled. A commit leaves worktree stats unchanged, so every later freshness probe answeredCurrent.The same CI run shows a second gap. Post-projection source verification noticed the moved source. Its rebuild then lost the publication compare-and-swap ("the active generation changed before atomic publication"). The worker only logged a warning and scheduled nothing.
Fix
mark_reconciledtakes the fingerprint sampled before the pass read any source. If HEAD or a ref moved since then, it records the earlier sample, so the next probe sees the move. The index is left out of the comparison (GitMetadataFingerprintV1::same_refs_as), because capture may refresh it on its own.CompareAndSwap,PublicationConflict) is now retried. New classifier:CodeIndexSchedulerErrorV1::is_publication_race.Test
a_commit_during_a_pass_stays_visible_to_the_next_freshness_probe: the capture hook commits the dirty bytes between capture and seal.Verification
bazel test //crates/tracedecay-code-index-runtime:all //crates/tracedecay:hooks_lsp_suite: passes.bazel build --config=clippy //crates/tracedecay-code-index-runtime:all: clean.🤖 Generated with Claude Code