From 5cf3482fa85c94309301e2fb7547ecc6b92f2d76 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 17 Sep 2026 21:50:12 +0000 Subject: [PATCH] fix(code-index): withhold current while the serving seat lags Status advertised the replacement text owner as current before the serving swap, so search kept answering the predecessor and bound a retryable graph generation mismatch. Terminal freshness now requires the advertised generation and the serving seat to be the same. Co-authored-by: Zack Jackson --- .../src/code_index_scheduler/registry.rs | 63 ++++++++++++++++++- .../registry/serving_readiness_tests.rs | 40 ++++++++++++ .../registry/serving_reads.rs | 8 ++- 3 files changed, 106 insertions(+), 5 deletions(-) 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 65041e909e..fc9c8db791 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 @@ -1116,11 +1116,15 @@ pub(super) fn dashboard_code_graph_serving( )) } -/// Whether status may report this worktree as terminal (`fresh` / `current`). +/// Whether the lane owners alone would let status report this worktree as +/// terminal (`fresh` / `current`). /// /// Refused graph activation remains terminal for text serving, preserving the /// existing status behavior; strict dogfood can distinguish it from Ready via -/// the separate typed projection. +/// the separate typed projection. Lane readiness is not seat identity: a +/// publication installs the replacement text owner before the serving swap +/// (`registry/mount.rs`), and search still answers the incumbent seat. +/// [`dashboard_terminal_status`] is what freshness reads use. fn dashboard_generation_is_ready( latest: Option<&LatestCompleteCodeIndexV1>, text_ready: bool, @@ -1134,6 +1138,61 @@ fn dashboard_generation_is_ready( } } +/// Whether the generation status advertises is the one search will serve. +/// +/// `advertised_text_generation_id` is `None` when no text owner is installed; +/// freshness then names the serving seat, so there is no split to refuse. +pub(super) fn serving_seat_matches_advertised_generation( + serving_generation_id: Option<&str>, + advertised_text_generation_id: Option<&str>, +) -> bool { + match advertised_text_generation_id { + None => true, + Some(advertised) => serving_generation_id == Some(advertised), + } +} + +pub(super) fn serving_seat_matches_advertised_text_owner( + serving: Option<&LatestCompleteCodeIndexV1>, + text: Option<&LatestCodeTextGenerationV1>, +) -> bool { + serving_seat_matches_advertised_generation( + serving.map(|serving| serving.generation().manifest().generation_id.as_str()), + text.map(|text| text.metadata().manifest().generation_id.as_str()), + ) +} + +/// Terminal freshness: lane owners are ready and the serving seat is the +/// generation status will advertise. +pub(super) fn dashboard_terminal_status( + latest: Option<&LatestCompleteCodeIndexV1>, + text: Option<&LatestCodeTextGenerationV1>, + text_ready: bool, + graph_activation_enabled: bool, + code_graph_serving: &Option, +) -> bool { + terminal_status_from_lanes( + dashboard_generation_is_ready( + latest, + text_ready, + graph_activation_enabled, + code_graph_serving, + ), + serving_seat_matches_advertised_text_owner(latest, text), + ) +} + +/// Lane readiness and seat identity are one freshness verdict. +/// +/// The graph-rebuild receipt had ready lanes and a serving seat that still +/// held the predecessor. That combination is not terminal. +pub(super) fn terminal_status_from_lanes( + lanes_ready: bool, + serving_matches_advertised: bool, +) -> bool { + lanes_ready && serving_matches_advertised +} + fn dashboard_text_freshness_identity( latest: Option<&LatestCodeTextGenerationV1>, ) -> tracedecay_contracts::code_index_freshness::CodeIndexWorktreeFreshnessV1 { diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/serving_readiness_tests.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/serving_readiness_tests.rs index fee4829028..d47bf06e5f 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/serving_readiness_tests.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/serving_readiness_tests.rs @@ -13,6 +13,7 @@ use tracedecay_domain::ProjectId; use super::super::graph_activation::install_injected_activation_gate; use super::{ CodeIndexCadenceOutcomeV1, CodeIndexSchedulerRegistryV1, dashboard_generation_is_ready, + serving_seat_matches_advertised_generation, terminal_status_from_lanes, }; /// Failure bound on an owner pass finishing once the worker is parked. Nothing @@ -52,6 +53,45 @@ fn dashboard_ready_requires_text_and_graph_lane_owners() { } } +/// The graph-rebuild receipt (transport acceptance, both ~90s attempts): lane +/// owners were ready on the replacement text generation while search still +/// served the predecessor. Status must not call that split terminal. +#[test] +fn graph_rebuild_split_is_not_terminal_freshness() { + let lanes_ready = + dashboard_generation_is_ready(None, true, true, &Some(CodeGraphServingReadinessV1::Ready)); + assert!( + lanes_ready, + "the receipt's text owner and graph activation were ready" + ); + assert!( + !serving_seat_matches_advertised_generation( + Some("generation.predecessor"), + Some("generation.head"), + ), + "search served the predecessor while status advertised the head" + ); + assert!( + !serving_seat_matches_advertised_generation(None, Some("generation.head")), + "a text owner installed before the serving swap is not yet the served generation" + ); + assert!(serving_seat_matches_advertised_generation( + Some("generation.head"), + Some("generation.head"), + )); + assert!(serving_seat_matches_advertised_generation( + Some("generation.head"), + None, + )); + assert!(serving_seat_matches_advertised_generation(None, None)); + assert!( + !terminal_status_from_lanes(lanes_ready, false), + "ready lanes must not be terminal while search serves the predecessor" + ); + assert!(terminal_status_from_lanes(lanes_ready, true)); + assert!(!terminal_status_from_lanes(false, true)); +} + /// Park the background worker and wait out whatever pass is already in flight. /// /// The worker releases its admission permit after source reconciliation but diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/serving_reads.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/serving_reads.rs index a98a5f7613..6d6e174527 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/serving_reads.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/registry/serving_reads.rs @@ -20,7 +20,7 @@ use super::scope_identity::{latest_matches_scope_identity, text_matches_scope_id use super::{ CodeIndexMountedScopeV1, CodeIndexSchedulerRegistryV1, CodeIndexServingScopeV1, MountedCodeIndexWorktreeV1, PendingWakeClaimV1, ReadyProbeServingPartsV1, - dashboard_code_graph_serving, dashboard_freshness_identity, dashboard_generation_is_ready, + dashboard_code_graph_serving, dashboard_freshness_identity, dashboard_terminal_status, dashboard_text_freshness_identity, unique_mounted_for_scope, }; @@ -369,8 +369,9 @@ impl CodeIndexSchedulerRegistryV1 { &text.metadata().manifest().generation_id, ) }); - let ready = dashboard_generation_is_ready( + let ready = dashboard_terminal_status( latest.as_ref(), + text.as_ref(), text_ready, graph_activation_enabled, &code_graph_serving, @@ -443,8 +444,9 @@ impl CodeIndexSchedulerRegistryV1 { &text.metadata().manifest().generation_id, ) }); - let ready = dashboard_generation_is_ready( + let ready = dashboard_terminal_status( latest.as_ref(), + text.as_ref(), text_ready, graph_activation_enabled, &code_graph_serving,