From 0fb77d7180ab58f68c708985817a65869ae4fb4a Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Fri, 2 Oct 2026 09:05:26 +0000 Subject: [PATCH] fix(code-index): replay the published generation over an older head e0fb1ebe82 (#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> --- .../code_index_scheduler/graph_activation.rs | 13 ++++++++ .../src/code_index_scheduler/registry.rs | 5 ++- .../code_index_scheduler/registry/mount.rs | 33 ++++++++++++++----- 3 files changed, 39 insertions(+), 12 deletions(-) diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/graph_activation.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/graph_activation.rs index 2d3d12a94b..1df0417d43 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/graph_activation.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/graph_activation.rs @@ -724,6 +724,19 @@ impl CodeIndexWorktreeSchedulerV1 { .sealed_replay_binding(generation_id) .map_err(|error| CodeIndexProductionErrorV1::Publication(error).into()) } + + /// Whether the durable publication pointer names `generation_id`. Graph heads + /// only follow the pointer, so a head on any other generation is older than it. + pub fn names_active_publication( + &self, + generation_id: &tracedecay_domain::CodeGenerationId, + ) -> Result { + Ok(self + .publication + .read_publication_pointer() + .map_err(CodeIndexProductionErrorV1::Publication)? + .is_some_and(|pointer| pointer.generation_id == generation_id.as_str())) + } } struct SchedulerGraphCancellation(Arc); diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry.rs index 11f02cd4b6..7a22c60023 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry.rs @@ -114,9 +114,8 @@ pub(crate) fn retained_noop_requires_follow_up_wake( serving_empty && !activation_deferred && consumed_external_arrival && source_is_noop } -/// The verified head belongs to a different generation than the retained -/// manifest this pass tried to recover. Replaying that manifest publishes a -/// second graph over the generation that owns the head. +/// The verified head generation differs from the retained manifest this pass +/// tried to recover. pub(crate) fn graph_head_belongs_to_another_generation(error: &CodeIndexSchedulerErrorV1) -> bool { error.activation_conflict_context().is_some_and(|context| { context.site == "code_graph.recover_verified_snapshot_from_head.generation" diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/mount.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/mount.rs index ecb3a38d09..2f4c8d1bf7 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/mount.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/mount.rs @@ -1936,17 +1936,20 @@ impl CodeIndexSchedulerRegistryV1 { let shutting_down = Arc::clone(&worker_shutting_down); let replay_passes = Arc::clone(&worker_reconcile_in_progress); let replay_binding = tokio::task::spawn_blocking(move || { - Self::lock_scheduler_for_graph_step( + let scheduler = Self::lock_scheduler_for_graph_step( &replay_scheduler, &shutting_down, &replay_passes, )? - .1 - .code_graph_replay_binding(&generation_id) + .1; + Ok::<_, CodeIndexSchedulerErrorV1>(( + scheduler.code_graph_replay_binding(&generation_id)?, + scheduler.names_active_publication(&generation_id)?, + )) }) .await; match replay_binding { - Ok(Ok(replay_binding)) => { + Ok(Ok((replay_binding, retained_names_active_publication))) => { match worker_graph_activation .recover_verified_head( &worker_project_id, @@ -1983,12 +1986,24 @@ impl CodeIndexSchedulerRegistryV1 { ); } Ok(false) => {} + Err(error) + if graph_head_belongs_to_another_generation(&error) + && retained_names_active_publication => + { + // The durable pointer names this manifest, so the head is + // older and this generation's graph publication stopped + // before seating; resume it from its sealed segments. + tracing::info!( + event = "code_index_graph_head_recovery_resumes_publication", + error = %error, + "durable publication names the retained manifest; resume \ + its interrupted graph publication" + ); + } Err(error) if graph_head_belongs_to_another_generation(&error) => { - // The head is a different generation. Falling - // through into a cold replay discards the - // in-flight successor that owns it. Leave - // that generation's own publish to seat its - // graph, and do not retry this manifest. + // A newer generation owns the durable pointer. Falling + // through into a cold replay discards the in-flight successor + // that owns the head; do not retry this manifest. prepare_graph = false; graph_head_conflict_generation = Some(recovered_generation); pass_waits_for_store_release = true;