Skip to content

fix(code-index): replay published generations over older heads - #2927

Merged
ScriptedAlchemy merged 1 commit into
masterfrom
fix/graph-activation-seat-regression
Oct 2, 2026
Merged

ScriptedAlchemy merged 1 commit into
masterfrom
fix/graph-activation-seat-regression

Conversation

@ScriptedAlchemy

@ScriptedAlchemy ScriptedAlchemy commented Oct 2, 2026 •

Copy link
Copy Markdown
Owner

On master, competing_reconcile_during_graph_activation_preserves_the_incumbent in daemon_suite fails every run. It times out after about 5.7 s with "serving generation ... did not seat: deadline has elapsed". The same failure shows up in the CI root-journeys lane.

The breaking commit is e0fb1eb, "fix(code-index): wait out a held store lock on follow-ups", which landed in merge 1db05cb (#2842). I bisected master's first-parent history. 60fce0d and c6c2359 pass. 1db05cb, 555db0e, 06edb2f, 3cd1bb9, aa6af67, 10e84bf and 952a2fa all fail the same way. The later code-index commits (#2840 layered refresh, #2910, the page-segment commits) are not involved, because the failure is already present before each of them.

Root cause. e0fb1eb added a verified-head recovery arm in registry/mount.rs. When the graph head belongs to a different generation than the retained manifest, the arm reads that as "an in-flight successor owns the head". It sets graph_head_conflict_generation, and every later retained pass of that manifest skips with graph_head_belongs_to_another_generation. In this test, a stale ignored-dependency admission loses to a competing reconcile. The publication pointer then names the competing generation 3, the retained manifest is generation 3, and the graph head is still the incumbent generation 1. The head is older, not newer, but the arm cannot tell the two cases apart. Generation 3 is never replayed and never seats. The debug log of the failing run shows code_index_graph_head_recovery_generation_conflict once, then graph_head_belongs_to_another_generation skips on every pass until the deadline.

The fix reads the durable publication pointer during recovery. Graph heads only follow the pointer, and the pointer only moves forward. So when the pointer names the retained manifest, a head on any other generation is older, and recovery falls through to the normal sealed replay. The #2797 case is unchanged: there the worker holds a stale retained owner while the pointer already names a newer successor, and the skip still applies. With the fix, the same test logs code_index_graph_head_recovery_resumes_publication and generation 3 seats.

This production change is the one in open PR #2897 (Devin, "resume a successor's graph seat after restart"), applied unchanged to graph_activation.rs, registry.rs and registry/mount.rs. So #2897 does fix this failure. Its restart test is the reason it has not landed yet, and that test is not included here. The hunks are identical, so #2897 should merge cleanly on top of this PR, or it can be reduced to its test. No test was changed or loosened here: the existing daemon_suite test is the deterministic regression check, failing before and passing after.

Verification, run on this branch:

  • cargo test -p tracedecay --features test-helpers --test daemon_suite competing_reconcile_during_graph_activation: passed 3 of 3 runs in a row (it failed on master).
  • cargo test -p tracedecay --features test-helpers --test daemon_suite code_index_ignored_dependencies_test: 21 passed, 0 failed.
  • cargo test -p tracedecay-code-index-runtime --lib: 569 passed, 0 failed, 1 ignored.
  • cargo clippy -p tracedecay-code-index-runtime --all-targets -- -D warnings: clean.
  • cargo fmt --check: clean.

Devin Review

Local verification (land-open-prs)

Verified on the branch merged with origin/master (fa11c68af4).

e0fb1eb (#2842) made retained graph-head recovery treat any head on
another generation as an in-flight successor that owns the head, and
stopped replaying that retained manifest for good. When the durable
publication pointer names the retained manifest, the head is older: the
manifest is the newest publication and its graph never seated. A stale
ignored-dependency admission that loses to a competing reconcile leaves
exactly that state, so the competing generation never served.

Recovery now reads the publication pointer and resumes the replay when
it names the retained manifest. The #2797 case, a stale retained owner
behind a newer pointer, keeps the skip. The production change is the
one in #2897, applied unchanged.

Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@changeset-bot

changeset-bot Bot commented Oct 2, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 0fb77d7

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 1 potential issue.

Devin Review

Comment on lines +1989 to +1992
Err(error)
if graph_head_belongs_to_another_generation(&error)
&& retained_names_active_publication =>
{

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.

🔴 Newer graph head overwritten by stale replay

If publication advances during recover_verified_head, retained_names_active_publication remains true for the old generation. Its sealed replay can then replace the newer graph head while the pointer names the newer generation.

Learn more

The worker reads the durable text publication pointer while holding the scheduler mutex, then releases it before awaiting graph-head recovery. Another publisher can commit a successor and seat its graph during that await. A generation mismatch is then interpreted using the obsolete pointer snapshot, and the retained generation reaches publish_sealed_graph, which publishes using the graph store's current head rather than requiring that this generation still owns the text pointer. The graph can therefore regress behind the active text publication.

Example: The pointer initially names generation G2 and the graph head names G1. While G2's head recovery is awaiting, another publisher advances both to G3. G2's recovery sees G3 but the captured boolean remains true; G2 publishes its graph over G3 instead of deferring to G3.

Recommended fix: Revalidate active publication ownership at the point of replay admission and fence the graph-head commit against subsequent pointer changes. A second unchecked pointer read before the asynchronous graph publish only narrows, rather than closes, this race.

Devin Review


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

@ScriptedAlchemy ScriptedAlchemy changed the title fix(code-index): replay the published generation over an older head fix(code-index): replay published generations over older heads Oct 2, 2026
@ScriptedAlchemy
ScriptedAlchemy merged commit 730026c into master Oct 2, 2026
19 of 26 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