From 2ca4ad95cc7504c4828ac044d697aaab22bb3c66 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 17 Sep 2026 09:35:21 +0000 Subject: [PATCH] fix(code-index): stabilize Fresh reads and dirty retained seats Dashboard freshness treated a bare pending wake as refresh_in_flight, so Fresh flipped to Verifying between settled status samples. Dirty remount seating required an already-decoded generation, so a held decode barrier returned None instead of a pointer Noop. Keep quiet undecoded seats from claiming currency while allowing dirty pointer Noops without joining the decode flight, and wait for owner quiescence before declaring ready. Co-authored-by: Zack Jackson --- .../src/code_index_scheduler/reconcile.rs | 52 +++++++++++-------- .../registry/serving_reads.rs | 15 +++--- .../src/code_index_scheduler/tests/mod.rs | 20 ++++++- 3 files changed, 55 insertions(+), 32 deletions(-) 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 f6b29a1565..0c339b4b7e 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,32 +1520,34 @@ 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. - // 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. + // 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); let content_matches = decoded.as_ref().is_some_and(|generation| { - self.retained_frontier_stat_sweep(&pointer) - .is_some_and(|sweep| { - sweep.content_matches( - &self.project_root, - &SourceContentManifestV1::for_snapshot(generation.snapshot()), - &self.shutting_down, - ) - }) + frontier_sweep.as_ref().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; + } || 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); + } if dirty { self.request_background_reconcile(); } @@ -1553,8 +1555,13 @@ 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. - self.adopt_ignored_source_roster(&generation); - let snapshot_content_identity = generation.snapshot().content_identity.clone(); + 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.latest_content_identity = Some(snapshot_content_identity.clone()); Ok(Some(CodeIndexReconcileOutcomeV1::Noop( CodeIndexNoopEvidenceV1 { @@ -3787,8 +3794,9 @@ 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. Callers -/// that treat `false` as a settled Noop will skip the content proof. +/// 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. 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 1d41d5560e..a98a5f7613 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,7 +277,6 @@ impl CodeIndexSchedulerRegistryV1 { generation_recovery, build_progress, hints, - pending_wake, source_freshness, graph_activation_enabled, ) = { @@ -295,7 +294,6 @@ 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(), ) @@ -318,13 +316,12 @@ impl CodeIndexSchedulerRegistryV1 { } progress }); - 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; + // `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 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/mod.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/tests/mod.rs index 845dfa9f10..a8cf5e9564 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/tests/mod.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/tests/mod.rs @@ -1509,7 +1509,25 @@ async fn wait_for_dashboard_ready(registry: &CodeIndexSchedulerRegistryV1, path: && freshness.coverage == tracedecay_contracts::code_index_freshness::CodeIndexFreshnessCoverageV1::Complete }); if ready { - break; + // Fresh is projected whenever refresh_in_flight is briefly + // false between owner passes. Join the seating pass and + // re-sample so ready is not a trough before Verifying. + wait_for_quiescent_owner_pass(registry, path).await; + let still_ready = registry + .dashboard_freshness(path) + .await + .is_some_and(|freshness| { + freshness.staleness_state + == Some( + tracedecay_contracts::code_index_freshness::CodeIndexStalenessStateV1::Fresh, + ) + && freshness.coverage + == tracedecay_contracts::code_index_freshness::CodeIndexFreshnessCoverageV1::Complete + }); + if still_ready && !registry.reconcile_in_progress_for_test(path).await { + break; + } + continue; } tokio::time::sleep(Duration::from_millis(2)).await; }