Skip to content

fix(code-index): keep a commit that lands mid-reconcile visible - #4624

Merged
ScriptedAlchemy merged 1 commit into
masterfrom
fix/hook-branch-sealed-flake
Oct 30, 2026
Merged

ScriptedAlchemy merged 1 commit into
masterfrom
fix/hook-branch-sealed-flake

Conversation

@ScriptedAlchemy

@ScriptedAlchemy ScriptedAlchemy commented Oct 29, 2026 •

Copy link
Copy Markdown
Owner

Problem

hooks_lsp_suite's hook_branch_write_lands_in_a_sealed_single_store_generation failed on CI in two unrelated PRs: #4618, a dependency bump, and #4623. The error was code_index_activation_unavailable: code-index scheduler did not publish exact branch source. In the failure's observed state:

  • the serving generation is at refs/heads/feature/hook with revision: 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 in mark_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 answered Current.

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_reconciled takes 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.
  • A lost publication race (CompareAndSwap, PublicationConflict) is now retried. New classifier: CodeIndexSchedulerErrorV1::is_publication_race.
    • Post-projection verification wakes an overflow rescan.
    • The main reconcile loop takes the existing bounded capacity retry. Before, it dropped the 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.

  • With the fix, it passes.
  • With the old witness sampling (mutation check), it fails: "a commit that landed during the pass must read as moved".

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.
  • rustfmt: clean.

🤖 Generated with Claude Code


Devin Review

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>
@ScriptedAlchemy
ScriptedAlchemy enabled auto-merge (squash) October 29, 2026 21:57
@changeset-bot

changeset-bot Bot commented Oct 29, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 9c6e149

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 potential issues.

1 flag not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)

Devin Review

// authority; the headroom wake is its retry.
capacity_retry.record_progress();
} else if transient_capacity {
} else if transient_capacity || error.is_publication_race() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Devin Review


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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

@ScriptedAlchemy
ScriptedAlchemy merged commit 43d4b23 into master Oct 30, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant