Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
740052a
fix(sessions): refuse an authoritative zero from a partial generation
ScriptedAlchemy Sep 19, 2026
5e03dfa
fix(daemon): compare version identity per semver build rules
ScriptedAlchemy Sep 19, 2026
cc10394
fix(sessions): retire refused refresh progress instead of retrying
ScriptedAlchemy Sep 19, 2026
8b75523
perf(code-index): scan clone postings once per resumed page
ScriptedAlchemy Sep 19, 2026
e2384db
fix(sessions): retire only refused progress, not a cancelled pass
ScriptedAlchemy Sep 19, 2026
f2ab808
perf(code-index): index clone postings by occurrence for resume replay
ScriptedAlchemy Sep 19, 2026
f77e763
test(global-db): gate analytics append on abandonment, not poll
ScriptedAlchemy Sep 19, 2026
53d7f9d
fix(retention): skip an artifact reclaimed during the scan
ScriptedAlchemy Sep 19, 2026
b130bce
test(daemon): defer the retention plan while the store is busy
ScriptedAlchemy Sep 19, 2026
63c0784
fix(mcp): attribute risky sites by byte span, not line
ScriptedAlchemy Sep 19, 2026
6d8ef97
test(application): keep git auto-maintenance out of the fixture
ScriptedAlchemy Sep 19, 2026
b05cab2
fix(daemon): mount the published branch worktree's query authority
ScriptedAlchemy Sep 19, 2026
2579de9
fix(code-index): idle means the pass tail already ran
cursoragent Sep 19, 2026
de5f920
fix(mcp): attribute field sites by byte span, not line
ScriptedAlchemy Sep 19, 2026
aab865a
test(runtime): settle the worker before sampling elapsed freshness
ScriptedAlchemy Sep 19, 2026
15f25b3
test(cli): keep the hotpath metrics port out of the quiet-pipeline test
ScriptedAlchemy Sep 19, 2026
b7be3fa
Merge remote-tracking branch 'origin/fix/master-ci-green-3' into curs…
ScriptedAlchemy Sep 19, 2026
c534d03
fix(code-index): keep admission ahead of the publication gate
ScriptedAlchemy Sep 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions crates/tracedecay-application/src/git_intelligence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1762,6 +1762,14 @@ mod tests {
"user.email=fixture@example.com",
"-c",
"commit.gpgsign=false",
// `git commit` spawns a detached `git maintenance run --auto`
// that holds `.git/objects/maintenance.lock` after the commit
// returns; the byte-identical snapshot must not see it appear
// or vanish between its two walks.
"-c",
"maintenance.auto=false",
"-c",
"gc.auto=0",
])
.args(args)
.current_dir(self.path())
Expand Down
4 changes: 4 additions & 0 deletions crates/tracedecay-cli/tests/core_cli_suite/cli_boundary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ fn shipped_binary_stops_quietly_when_a_pipeline_reader_exits() {
let output = Command::new("sh")
.args(["-c", r#""$TRACEDECAY_BIN" tool | head -n 4"#])
.env("TRACEDECAY_BIN", env!("CARGO_BIN_EXE_tracedecay"))
// A hotpath-enabled binary binds its metrics port on start; when a
// sibling test's daemon already holds it, the bind failure lands on
// stderr and breaks the quiet-pipeline assertion below.
.env("HOTPATH_METRICS_SERVER_OFF", "true")
.output()
.expect("tracedecay tool pipeline should run");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -997,6 +997,52 @@ fn text_artifact_retention_collects_staging_database_sidecars_with_their_owner()
);
}

/// The inventory scans the artifact root without the generation-store lock, so
/// the text-artifact builder can retire a `.staging` family between the
/// directory listing and the stat. A vanished entry is already reclaimed and
/// must leave the plan intact rather than failing it with a storage error.
#[test]
fn text_artifact_inventory_skips_an_entry_reclaimed_during_the_scan() {
let store = tempfile::TempDir::new().expect("artifact store");
let artifacts_root = code_text_artifacts_root(store.path());
std::fs::create_dir_all(&artifacts_root).expect("create artifact root");
let staging_family = ["a", "b", "c"]
.into_iter()
.map(|seed| {
let path = artifacts_root.join(format!(".text-artifact-{}.staging", seed.repeat(64)));
std::fs::write(&path, b"staging").expect("write staging evidence");
path
})
.collect::<Vec<_>>();

// The scan probes cancellation once on entry and once per directory entry,
// before it takes that entry. Retiring from the third probe on leaves the
// listing already taken and one entry already inspected, so every further
// name the scan holds names a file that is gone from disk.
let probes = std::sync::atomic::AtomicUsize::new(0);
let retire_during_the_scan = || {
if probes.fetch_add(1, std::sync::atomic::Ordering::Relaxed) >= 2 {
for path in &staging_family {
let _ = std::fs::remove_file(path);
}
}
false
};

let inventory = plan_collectable_text_artifacts_cancellable(
store.path(),
None,
GenerationDigestVerificationV1::Full,
&retire_during_the_scan,
)
.expect("an entry reclaimed mid-scan leaves the store plannable");
assert!(
inventory.candidates.len() < staging_family.len(),
"an entry that vanished before its stat is reclaimed, not planned: {:?}",
inventory.candidates
);
}

#[test]
fn applied_retention_refuses_a_busy_generation_store_and_retries() {
let (store, _) = fixture_store(2);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,22 @@ pub(super) fn plan_collectable_text_artifacts_cancellable(
)
})?;
let path = entry.path();
let metadata = std::fs::symlink_metadata(&path).map_err(storage)?;
// This inventory reads the artifact root without the generation-store
// lock, so an entry the listing just named can already be gone: the
// text-artifact builder retires a `.staging` family (the staging
// database and its `-journal`/`-wal`/`-shm` sidecars) under that lock
// while this scan runs. A vanished entry is reclaimed, which is what
// this inventory would have planned anyway, so it is not a candidate
// and not a failure. Failing the plan here turned every publish that
// raced a maintenance tick into a loud `retention_plan_failed` pass
// (master run 35422072661, `Storage("No such file or directory")`).
// A completed artifact the durable index *references* is verified
// above, before this scan, and stays fail-closed if it disappears.
let metadata = match std::fs::symlink_metadata(&path) {
Ok(metadata) => metadata,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
Err(error) => return Err(storage(error)),
};
if !metadata.file_type().is_file() {
return Err(CodeGenerationRetentionErrorV1::UnsafeState(format!(
"code text artifact inventory path '{}' is not a regular file",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2109,6 +2109,21 @@ impl CodeIndexSchedulerRegistryV1 {
}
}

/// Stamp a continuation while `reconcile_in_progress` still reports this pass.
///
/// Callers that already released the worker's pass guard use this so a
/// reader waiting for the counter to hit zero cannot observe an empty
/// slot and then lose to `BusyFollowUp`. The stamp is the idle boundary;
/// the guard lives only for the note.
fn note_visible_worker_continuation(
passes: &Arc<AtomicUsize>,
pending_wake: &PendingWakeV1,
wake: &tokio::sync::Notify,
) {
let _visible = super::ReconcilePassGuard::enter(passes);
Self::note_worker_continuation(pending_wake, wake);
Comment on lines +2123 to +2124

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep the pass visible until late continuation decisions

When the worker has dropped its main pass guard before optional graph work (for example at mount.rs:1233-1235), a caller can acquire the admission permit, observe both reconcile_in_progress == 0 and an empty pending slot, and conclude that the owner is settled before this helper is invoked later by graph preparation or serving-swap logic. Entering a guard only for the duration of note_worker_continuation cannot repair that earlier quiet window: the caller may already have returned before the temporary guard is created, after which this code stamps a new BusyFollowUp. The pass must remain visible, or the continuation must be reserved, from before the unguarded tail begins rather than only while the note is written.

Useful? React with 👍 / 👎.

}

/// Claim the pending wake as one reconcile's arrival, at the instant the
/// scheduler dequeues it.
///
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1033,10 +1033,16 @@ impl CodeIndexSchedulerRegistryV1 {
}
// Source reconciliation is complete: release the background
// admission permit before HeadOpening / graph work so sibling
// stores can start. Keep `reconcile_pass` through text
// seating, dropping it made `reconcile_in_progress` lie while
// this worker still owned graph try_lock, which deadlocked
// tests that hold the scheduler mutex and wait for that flag.
// stores can start. The permit is never re-acquired inside
// this pass: `_build_publication` is held for the rest of the
// iteration, and `run_ignored_dependency_admission` takes the
// admission *before* that same gate, so waiting on admission
// here would invert that order (see
// `background_worker_waits_for_global_admission_before_publication_gate`).
// Keep `reconcile_pass` through text seating, dropping it
// made `reconcile_in_progress` lie while this worker still
// owned graph try_lock, which deadlocked tests that hold the
// scheduler mutex and wait for that flag.
drop(_background_reconcile_admission);
// A publication must first reopen its own lightweight text
// owner: publication moved the durable pointer, so the prior
Expand Down Expand Up @@ -1110,6 +1116,12 @@ impl CodeIndexSchedulerRegistryV1 {
&& !graph_activation_deferred
&& let Some(text) = graph_text.clone()
{
// `reconcile_pass` is held across this projection, so
// the pointer rename is inside the pass a reader
// samples. Taking the admission permit back here
// instead would deadlock against an
// ignored-dependency owner that already holds it and
// is waiting for `_build_publication`.
let projection = tokio::spawn(Self::drive_text_projection(
text,
Arc::clone(&worker_shutting_down),
Expand Down Expand Up @@ -1168,7 +1180,21 @@ impl CodeIndexSchedulerRegistryV1 {
// A successor-only retained projection holds no pass guard of
// its own; keeping the worker's guard through graph seat would
// report rebuild_in_flight for clone backfill that is not
// exact/lexical work.
// exact/lexical work. Stamp the continuation this projection
// already owes before that drop: the slot, not a later note,
// is what an idle reader observes.
if let Some(outcome) = published_text_projection_outcome.as_ref() {
let schedule_continuation = match outcome {
PublishedTextProjectionOutcomeV1::Finished => graph_text
.as_ref()
.is_some_and(LatestCodeTextGenerationV1::text_projection_needs_work),
PublishedTextProjectionOutcomeV1::Unfinished => true,
PublishedTextProjectionOutcomeV1::Shutdown => false,
};
if schedule_continuation {
Self::note_worker_continuation(&worker_pending_wake, &worker_wake);
}
}
if retained_text_projection.is_none() || retained_projection_successor_only {
drop(reconcile_pass.take());
}
Expand Down Expand Up @@ -1296,6 +1322,14 @@ impl CodeIndexSchedulerRegistryV1 {
.filter(|retained| retained.uses_partitioned_manifest())
.cloned()
{
// Every outcome of this attempt schedules one successor.
// Stamp it before the recovery await, while the pass is
// visible, so the wait cannot be sampled as an idle slot.
Self::note_visible_worker_continuation(
&worker_reconcile_in_progress,
&worker_pending_wake,
&worker_wake,
);
retained_graph_head_recovery_attempted = true;
let generation_id = retained.metadata().manifest().generation_id.clone();
let replay_scheduler = Arc::clone(&worker_scheduler);
Expand Down Expand Up @@ -1400,8 +1434,8 @@ impl CodeIndexSchedulerRegistryV1 {
// all and never published the successor generation. The
// `retained_graph_head_recovery_attempted` guard above is
// now false for every later pass, so this cannot spin
// another retained-recovery Noop.
Self::note_worker_continuation(&worker_pending_wake, &worker_wake);
// another retained-recovery Noop. The successor was
// stamped before this await.
}
// A recovered revision-7 verified head already serves its
// native graph from the retained text owner, and that owner
Expand Down Expand Up @@ -1458,6 +1492,8 @@ impl CodeIndexSchedulerRegistryV1 {
let graph_text = graph_text.clone();
let shutting_down = Arc::clone(&worker_shutting_down);
let prepare_passes = Arc::clone(&worker_reconcile_in_progress);
let prepare_pending_wake = Arc::clone(&worker_pending_wake);
let prepare_wake = Arc::clone(&worker_wake);
match hotpath::future!(
tokio::task::spawn_blocking(move || {
let decoder = Self::lock_scheduler_for_graph_step(
Expand Down Expand Up @@ -1513,6 +1549,18 @@ impl CodeIndexSchedulerRegistryV1 {
)?
.1
.take_ignored_roster_refusal_rebuild();
if roster_refusal_rebuild {
// One pass, claimed from the scheduler, so
// a refusal that keeps reproducing cannot
// spin this worker. Stamp before this
// closure drops the step guard: the result
// is observed only after the slot is set.
Self::note_visible_worker_continuation(
&prepare_passes,
&prepare_pending_wake,
&prepare_wake,
);
}
let replay_binding = match latest.as_ref() {
Some(latest) => Some(
Self::lock_scheduler_for_graph_step(
Expand Down Expand Up @@ -1545,15 +1593,6 @@ impl CodeIndexSchedulerRegistryV1 {
the sealed generation cannot seat"
);
}
if roster_refusal_rebuild {
// One pass, claimed from the scheduler, so
// a refusal that keeps reproducing cannot
// spin this worker.
Self::note_worker_continuation(
&worker_pending_wake,
&worker_wake,
);
}
Ok((outcome, latest, replay_binding))
}
Ok(Err(error)) => {
Expand Down Expand Up @@ -1714,7 +1753,14 @@ impl CodeIndexSchedulerRegistryV1 {
.as_ref()
.is_some_and(LatestCodeTextGenerationV1::text_projection_needs_work)
{
Self::note_worker_continuation(&worker_pending_wake, &worker_wake);
// Already stamped before optional graph. Re-enter
// the pass so a reader that cleared the slot
// during graph still cannot sample the stamp.
Self::note_visible_worker_continuation(
&worker_reconcile_in_progress,
&worker_pending_wake,
&worker_wake,
);
}
// Large text projections can outlive the bounded
// source proof established before publication. The
Expand Down Expand Up @@ -1784,7 +1830,11 @@ impl CodeIndexSchedulerRegistryV1 {
"the publication's text owner did not finish its projection; \
the sealed generation stays unseated until it does"
);
Self::note_worker_continuation(&worker_pending_wake, &worker_wake);
Self::note_visible_worker_continuation(
&worker_reconcile_in_progress,
&worker_pending_wake,
&worker_wake,
);
}
}
// Keep the pass lifetime around the post-projection source
Expand Down Expand Up @@ -1965,7 +2015,11 @@ impl CodeIndexSchedulerRegistryV1 {
if text_latest.text_projection_needs_work()
&& !text_latest.query_owners_are_ready()
{
Self::note_worker_continuation(&worker_pending_wake, &worker_wake);
Self::note_visible_worker_continuation(
&worker_reconcile_in_progress,
&worker_pending_wake,
&worker_wake,
);
}
}
Ok(Err(error)) => {
Expand Down Expand Up @@ -1994,7 +2048,27 @@ impl CodeIndexSchedulerRegistryV1 {
}
// The source proof and serving witness are now published as
// one lifecycle. Optional receipts do not keep source
// verification in flight.
// verification in flight. A clone-backfill continuation this
// pass already knows about is stamped first, so the drop is
// not an empty slot.
if clone_backfill_waiting_for_source
&& matches!(
&result,
Ok((Ok(CodeIndexReconcileOutcomeV1::Noop(_)), _, _))
)
&& worker_text_generation
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.is_some()
&& worker_source_freshness
.ready_without_stat(&worker_project_root, &worker_shutting_down)
{
Self::note_visible_worker_continuation(
&worker_reconcile_in_progress,
&worker_pending_wake,
&worker_wake,
);
}
drop(reconcile_pass.take());
if let Ok((Ok(outcome), _, _)) = &result {
// A pass that ran to a terminal outcome proves neither the
Expand Down Expand Up @@ -2046,12 +2120,8 @@ impl CodeIndexSchedulerRegistryV1 {
);
}
worker_serving_generation_changed.send_replace(());
// The retained slice was checked before reconciliation
// renewed this proof. Preserve its wake now that source
// is current, without requiring another query arrival.
if clone_backfill_waiting_for_source {
Self::note_worker_continuation(&worker_pending_wake, &worker_wake);
}
// The clone-backfill continuation was stamped before
// this pass dropped `reconcile_in_progress`.
}
} else {
// Surface bounded non-terminal failure without new project-path data.
Expand Down Expand Up @@ -2269,7 +2339,6 @@ impl CodeIndexSchedulerRegistryV1 {
PublishedTextProjectionOutcomeV1::Unfinished
}
};
drop(reconcile_pass.take());
match outcome {
PublishedTextProjectionOutcomeV1::Finished
if !retained_head_recovered_without_complete_replay
Expand Down Expand Up @@ -2316,6 +2385,9 @@ impl CodeIndexSchedulerRegistryV1 {
Self::note_worker_continuation(&worker_pending_wake, &worker_wake);
}
}
// The continuation is already in the slot. Dropping here
// is the first moment this pass looks idle.
drop(reconcile_pass.take());
}
if worker_shutting_down.load(Ordering::Acquire) {
tracing::info!(
Expand Down
Loading
Loading