Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 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
aecc70b
fix(code-index): stop continuation receipts looking like probe wakes
cursoragent Sep 19, 2026
b05cab2
fix(daemon): mount the published branch worktree's query authority
ScriptedAlchemy 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
52eb712
Merge remote-tracking branch 'origin/fix/master-ci-green-3' into curs…
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 @@ -1266,11 +1266,17 @@ enum ColdMountAdmissionV1 {

/// One exact worktree's pending worker wake. `micros == 0` means no pending
/// arrival, and every nonzero arrival is held by one nonzero owner token.
///
/// `attributable` is false for a worker-owned continuation: the slot stays
/// nonzero so freshness still sees the follow-up, but the instant is not an
/// external wake. Publishing it as [`CodeIndexArrivalV1::Observed`] fabricated
/// an event-to-ready receipt for a pass nobody requested.
struct PendingWakeStateV1 {
micros: u64,
trigger: u64,
owner: u64,
next_owner: u64,
attributable: bool,
}

/// The single synchronization authority for one worktree's coalesced wake.
Expand Down Expand Up @@ -1348,6 +1354,7 @@ impl Default for PendingWakeStateV1 {
trigger: 0,
owner: 0,
next_owner: 1,
attributable: false,
}
}
}
Expand Down Expand Up @@ -1385,6 +1392,7 @@ impl PendingWakeClaimV1 {
let claimed_micros = u64::try_from(now_micros().0).unwrap_or(u64::MAX);
let owner = state.next_owner();
state.micros = claimed_micros;
state.attributable = true;
state.owner = owner;
drop(state);
Some(Self {
Expand Down Expand Up @@ -1426,6 +1434,7 @@ impl Drop for PendingWakeClaimV1 {
state.micros = 0;
state.trigger = 0;
state.owner = 0;
state.attributable = false;
}
}
}
Expand Down Expand Up @@ -1878,6 +1887,7 @@ impl CodeIndexSchedulerRegistryV1 {
pending_wake.micros = 0;
pending_wake.owner = 0;
pending_wake.trigger = 0;
pending_wake.attributable = false;
}
}
}
Expand Down Expand Up @@ -2065,9 +2075,13 @@ impl CodeIndexSchedulerRegistryV1 {
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
state.owner = state.next_owner();
if state.micros == 0 {
// A worker continuation occupies the slot without an external instant.
// This wake is the arrival; keep an already-observed one so a later
// stamp cannot shorten the wait that wake already took.
if state.micros == 0 || !state.attributable {
state.micros = wake_micros;
}
state.attributable = true;
state.trigger = Self::pack_trigger(trigger);
drop(state);
wake.notify_one();
Expand All @@ -2085,28 +2099,47 @@ impl CodeIndexSchedulerRegistryV1 {
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if state.micros != 0 {
// An unattributable continuation is not an arrival. Upgrade it: the
// caller that just proved work is the event the receipt must name.
if state.micros != 0 && state.attributable {
return false;
}
state.owner = state.next_owner();
state.micros = wake_micros;
state.attributable = true;
state.trigger = Self::pack_trigger(trigger);
drop(state);
wake.notify_one();
true
}

/// Queue worker-owned continuation work through the same pending-arrival
/// authority as external wakes. This keeps readiness truthful while the
/// continuation waits for shared admission; a bare `Notify` permit is not
/// observable by freshness readers.
/// Queue worker-owned continuation work so freshness still sees it.
///
/// A bare `Notify` permit is not observable by freshness readers, so the
/// pending slot stays nonzero. That slot is not an external arrival: the
/// worker decided to continue work an earlier wake already claimed.
/// Stamping a wall-clock instant here made the follow-up pass publish an
/// event-to-ready receipt, and a suppressed freshness probe that raced the
/// stamp was charged with it.
fn note_worker_continuation(pending_wake: &PendingWakeV1, wake: &tokio::sync::Notify) {
if !Self::note_wake_if_idle(pending_wake, wake, CodeIndexCadenceTriggerV1::BusyFollowUp) {
// This pass may have consumed the permit for an arrival it has not
// claimed yet. Keep that observable arrival and replenish its
// coalesced permit so the continuation cannot sleep behind it.
let mut state = pending_wake
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if state.micros != 0 {
// An arrival is already queued, or a continuation already occupies
// the slot. Replenish the coalesced permit so the worker cannot
// sleep behind work it has not claimed.
drop(state);
wake.notify_one();
return;
}
state.owner = state.next_owner();
state.micros = u64::try_from(now_micros().0).unwrap_or(u64::MAX);
state.attributable = false;
state.trigger = Self::pack_trigger(CodeIndexCadenceTriggerV1::BusyFollowUp);
drop(state);
wake.notify_one();
}

/// Claim the pending wake as one reconcile's arrival, at the instant the
Expand All @@ -2120,20 +2153,27 @@ impl CodeIndexSchedulerRegistryV1 {
pending_wake: &PendingWakeV1,
default_trigger: CodeIndexCadenceTriggerV1,
) -> (CodeIndexArrivalV1, CodeIndexCadenceTriggerV1) {
let (wake_micros, packed_trigger) = {
let (wake_micros, packed_trigger, attributable) = {
let mut state = pending_wake
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let wake_micros = state.micros;
let packed_trigger = state.trigger;
let attributable = state.attributable;
state.micros = 0;
state.trigger = 0;
state.owner = 0;
(wake_micros, packed_trigger)
state.attributable = false;
(wake_micros, packed_trigger, attributable)
};
if wake_micros == 0 {
return (CodeIndexArrivalV1::Unavailable, default_trigger);
if wake_micros == 0 || !attributable {
let trigger = if wake_micros == 0 {
default_trigger
} else {
Self::unpack_trigger(packed_trigger)
};
return (CodeIndexArrivalV1::Unavailable, trigger);
}
let trigger = Self::unpack_trigger(packed_trigger);
match i64::try_from(wake_micros) {
Expand Down Expand Up @@ -2163,12 +2203,14 @@ impl CodeIndexSchedulerRegistryV1 {
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
// A wake that arrived while this pass ran is newer, so the restored
// arrival remains the earliest and stays authoritative.
if state.micros != 0 && state.micros <= wake_micros {
// arrival remains the earliest and stays authoritative. A continuation
// occupying the slot is not an arrival and must not hide this one.
if state.attributable && state.micros != 0 && state.micros <= wake_micros {
return;
}
state.owner = state.next_owner();
state.micros = wake_micros;
state.attributable = true;
state.trigger = Self::pack_trigger(trigger);
}

Expand Down Expand Up @@ -3542,7 +3584,9 @@ mod feedback_document_path_tests {

#[cfg(test)]
mod text_slice_fairness_tests {
use super::{CodeIndexCadenceTriggerV1, CodeIndexSchedulerRegistryV1, PendingWakeV1};
use super::{
CodeIndexArrivalV1, CodeIndexCadenceTriggerV1, CodeIndexSchedulerRegistryV1, PendingWakeV1,
};

#[test]
fn pending_reconcile_is_serviced_between_bounded_text_slices() {
Expand Down Expand Up @@ -3572,6 +3616,58 @@ mod text_slice_fairness_tests {
"text continuation resumes only after reconcile claims the pending arrival"
);
}

#[test]
fn worker_continuation_stays_pending_without_an_observed_arrival() {
let pending = PendingWakeV1::default();
let wake = tokio::sync::Notify::new();
CodeIndexSchedulerRegistryV1::note_worker_continuation(&pending, &wake);
assert!(
pending.has_pending_arrival(),
"freshness must still see the continuation while it waits"
);

let (arrival, trigger) = CodeIndexSchedulerRegistryV1::take_pending_arrival(
&pending,
CodeIndexCadenceTriggerV1::Mount,
);
assert_eq!(
arrival,
CodeIndexArrivalV1::Unavailable,
"a worker continuation is not an external wake and must not publish \
an event-to-ready sample"
);
assert_eq!(trigger, CodeIndexCadenceTriggerV1::BusyFollowUp);
assert!(
!pending.has_pending_arrival(),
"claiming the continuation clears the slot"
);
}

#[test]
fn an_external_wake_replaces_an_unattributable_continuation() {
let pending = PendingWakeV1::default();
let wake = tokio::sync::Notify::new();
CodeIndexSchedulerRegistryV1::note_worker_continuation(&pending, &wake);
assert!(
CodeIndexSchedulerRegistryV1::note_wake_if_idle(
&pending,
&wake,
CodeIndexCadenceTriggerV1::QueryAdmission,
),
"a real wake must replace the continuation placeholder"
);

let (arrival, trigger) = CodeIndexSchedulerRegistryV1::take_pending_arrival(
&pending,
CodeIndexCadenceTriggerV1::Mount,
);
assert!(
matches!(arrival, CodeIndexArrivalV1::Observed { wake_micros } if wake_micros > 1),
"the receipt names the external wake, not the continuation slot: {arrival:?}"
);
assert_eq!(trigger, CodeIndexCadenceTriggerV1::QueryAdmission);
}
}

#[cfg(test)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4022,7 +4022,9 @@ async fn diagnostics_change_generation_advances_for_out_of_band_git_drift() {
async fn elapsed_freshness_window_alone_does_not_make_dashboard_state_stale() {
let fixture = GitFixture::new(&[("src/main.rs", "fn main() {}\n")]);
let store = TempDir::new().expect("store root");
let registry = CodeIndexSchedulerRegistryV1::new(1);
// Single-permit admission: holding it below parks the background worker,
// which the host's default bound cannot do.
let registry = CodeIndexSchedulerRegistryV1::with_background_reconcile_permits(1, 1);
registry
.mount_worktree(
test_project_id(),
Expand All @@ -4033,18 +4035,33 @@ async fn elapsed_freshness_window_alone_does_not_make_dashboard_state_stale() {
.expect("mount daemon-owned scheduler");
wait_for_initial_generation(&registry, fixture.path()).await;
wait_for_dashboard_ready(&registry, fixture.path()).await;
// The mount leaves clone backfill behind, and the wakes that drain it
// leave a banked permit whose no-op pass projects `Verifying` instead of
// `Fresh` (CI run 35425541839). Settle the mount-era chain, hold the
// admission so no pass can start under the sample, and prove the
// pending-wake slot stays empty, exactly as the text-progress test does.
drain_clone_backfill(&registry, fixture.path()).await;
settled_owner_with_idle_admission(&registry, fixture.path()).await;
let _quiet_owner = quiesced_background_reconcile_admission(&registry, fixture.path()).await;
let canonical = fixture.path().canonicalize().expect("canonical fixture");
{
let scope = {
let mounted = registry.mounted.lock().await;
mounted
.get(&canonical)
.expect("mounted worktree")
let worktree = mounted.get(&canonical).expect("mounted worktree");
worktree
.scheduler
.lock()
.expect("scheduler")
.policy
.staleness_threshold = Duration::ZERO;
}
tracedecay_contracts::ResolvedScope::new(
test_project_id(),
worktree.repository_id.clone(),
worktree.worktree_id.clone(),
None,
)
.expect("resolved scope")
};
clear_pending_wake_until_quiet(&registry, &scope).await;

let projected = registry
.dashboard_freshness(fixture.path())
Expand Down
Loading