diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/reconcile.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/reconcile.rs index 5b6b4b758e..c7aa4b14a4 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/reconcile.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/reconcile.rs @@ -1520,34 +1520,32 @@ impl CodeIndexWorktreeSchedulerV1 { }; // Equal metadata is not currency. A quiet Noop is only honest when // this pass decoded the generation and re-derived every sealed digest. - // A dirty remount (hints, config drift, or a moved frontier) must still - // seat from the durable pointer without joining the publication decode - // barrier, activation may already own that flight. - let frontier_sweep = self.retained_frontier_stat_sweep(&pointer); + // Otherwise return None so the caller runs the content proof now; + // a later wake is not a substitute, because this empty-slot seat is + // the first branch and would keep swallowing the pass. let content_matches = decoded.as_ref().is_some_and(|generation| { - frontier_sweep.as_ref().is_some_and(|sweep| { - sweep.content_matches( - &self.project_root, - &SourceContentManifestV1::for_snapshot(generation.snapshot()), - &self.shutting_down, - ) - }) + self.retained_frontier_stat_sweep(&pointer) + .is_some_and(|sweep| { + sweep.content_matches( + &self.project_root, + &SourceContentManifestV1::for_snapshot(generation.snapshot()), + &self.shutting_down, + ) + }) }); + if !retained_empty_seat_settles_source(decoded.is_some(), content_matches) { + return Ok(None); + } + let Some(generation) = decoded else { + return Ok(None); + }; let dirty = { let hints = self .hints .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); hints.overflow || !hints.paths.is_empty() - } || configuration_changed - || frontier_sweep.is_none() - || (decoded.is_some() && !content_matches); - if !dirty && !retained_empty_seat_settles_source(decoded.is_some(), content_matches) { - // Quiet + undecoded (or unproven) must not settle: a later wake is - // not a substitute, because this empty-slot seat is the first - // branch and would keep swallowing the content-proof pass. - return Ok(None); - } + } || configuration_changed; if dirty { self.request_background_reconcile(); } @@ -1555,13 +1553,8 @@ impl CodeIndexWorktreeSchedulerV1 { // `load_active_shared` here parked remount on the publication // barrier while activation owned it, so the seated event never // published and the dirty successor extract never started. - let snapshot_content_identity = if let Some(generation) = decoded { - self.adopt_ignored_source_roster(&generation); - generation.snapshot().content_identity.clone() - } else { - ContentDigest::new(pointer.snapshot_content_identity.clone()) - .map_err(|error| CodeIndexSchedulerErrorV1::Identity(error.to_string()))? - }; + self.adopt_ignored_source_roster(&generation); + let snapshot_content_identity = generation.snapshot().content_identity.clone(); self.latest_content_identity = Some(snapshot_content_identity.clone()); Ok(Some(CodeIndexReconcileOutcomeV1::Noop( CodeIndexNoopEvidenceV1 { @@ -1592,13 +1585,6 @@ impl CodeIndexWorktreeSchedulerV1 { .filter(|sweep| witness.stat_signature == sweep.signature) } - #[cfg(test)] - pub fn seat_retained_generation_on_empty_serving_for_test( - &mut self, - ) -> Result, CodeIndexSchedulerErrorV1> { - self.seat_retained_generation_on_empty_serving() - } - /// Verify an unchanged retained text generation without decoding the full /// graph-bearing generation. /// @@ -3794,9 +3780,8 @@ fn changed_paths_between_trees( /// A quiet empty-slot seat may end the pass only when the generation was /// decoded and its sealed file digests still match the bytes on disk. /// -/// Equal stat metadata with no decoded generation is not currency. Dirty -/// remount seating bypasses this gate and may emit a Noop from the durable -/// pointer without joining the publication decode barrier. +/// Equal stat metadata with no decoded generation is not currency. Callers +/// that treat `false` as a settled Noop will skip the content proof. pub(crate) fn retained_empty_seat_settles_source( generation_decoded: bool, content_matches: bool, 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 8f18393c32..668576b8d5 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 @@ -277,6 +277,7 @@ impl CodeIndexSchedulerRegistryV1 { generation_recovery, build_progress, hints, + pending_wake, source_freshness, graph_activation_enabled, ) = { @@ -294,6 +295,7 @@ impl CodeIndexSchedulerRegistryV1 { Arc::clone(&worktree.generation_recovery), Arc::clone(&worktree.build_progress), Arc::clone(&worktree.hints), + Arc::clone(&worktree.pending_wake), worktree.source_freshness.clone(), worktree.graph_activation.policy().is_enabled(), ) @@ -316,12 +318,13 @@ impl CodeIndexSchedulerRegistryV1 { } progress }); - // `Verifying` / `Refreshing` name an executing source proof or - // rebuild pass. A bare pending wake is only a scheduled follow-up; - // counting it here flipped Fresh→Verifying between consecutive - // status reads after a settled seat (registry publication feeds). - // The pass guard (`reconcile_in_progress`) is the durable signal. - let refresh_in_flight = reconcile_in_progress.load(Ordering::Acquire) != 0; + let refresh_in_flight = reconcile_in_progress.load(Ordering::Acquire) != 0 + || pending_wake + .state + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .micros + != 0; let source_change_pending = source_freshness.source_change_pending(); let parked = convergence_park .read() diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/tests/reconcile.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/tests/reconcile.rs index e8e9f56a14..fe2022e54d 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/tests/reconcile.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/tests/reconcile.rs @@ -923,61 +923,6 @@ async fn restart_remount_seats_the_retained_generation_before_a_dirty_rebuild() restarted.shutdown().await; } -/// Dirty remount seating must not park on the publication decode barrier -/// while holding the scheduler lock. Activation may already own that cache; -/// joining it left remount warming with no seated generation. -#[tokio::test] -async fn dirty_retained_seat_does_not_join_the_publication_decode_cache() { - let fixture = GitFixture::new(ALPHA_LIB_V1); - let store = TempDir::new().expect("store root"); - let registry = CodeIndexSchedulerRegistryV1::new(1); - registry - .mount_worktree( - test_project_id(), - fixture.path(), - store.path().to_path_buf(), - ) - .await - .expect("mount worktree"); - wait_for_live_complete_generation(®istry, fixture.path()).await; - - fixture.edit("src/lib.rs", "pub fn alpha() -> u32 { 2 }\n"); - let scheduler = registry - .scheduler_handle(fixture.path()) - .await - .expect("scheduler handle"); - let held_decode = scheduler - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) - .hold_active_decode(); - let seating = scheduler.clone(); - let outcome = tokio::time::timeout( - Duration::from_secs(1), - tokio::task::spawn_blocking(move || { - seating - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) - .seat_retained_generation_on_empty_serving_for_test() - }), - ) - .await - .expect("dirty retained seat must not wait for the decode cache") - .expect("seat task") - .expect("seat result"); - assert!( - matches!(outcome, Some(CodeIndexReconcileOutcomeV1::Noop(_))), - "dirty remount must still emit a retained-seat Noop without decoding" - ); - assert_eq!( - held_decode.waiter_count(), - 0, - "retained seating must not join the publication decode flight" - ); - - drop(held_decode); - registry.shutdown().await; -} - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn scheduler_notifications_remain_nonblocking_while_reconcile_is_busy() { let fixture = GitFixture::new(ALPHA_LIB_V1); @@ -5228,6 +5173,10 @@ async fn concurrent_query_admissions_claim_one_pending_wake_before_worker_coales let fixture = GitFixture::new(&[("src/main.rs", "fn main() {}\n")]); let store = TempDir::new().expect("store root"); let (registry, scope) = mounted_core_query_worktree_with_one_permit(&fixture, &store).await; + // Clone backfill owns the same coalesced pending-wake slot. This test is + // about simultaneous query admissions, so finish that independent + // production journey before establishing the empty-slot precondition. + drain_clone_backfill(®istry, fixture.path()).await; let admission = registry .background_reconcile_admission() .acquire_owned()