From 12cda586f92f418f8089d6b9c98c6f822e26f8f5 Mon Sep 17 00:00:00 2001 From: Zack Jackson <25274700+ScriptedAlchemy@users.noreply.github.com> Date: Sun, 8 Nov 2026 04:25:28 -0800 Subject: [PATCH 1/9] perf(sessions): avoid extra scans of settled Codex buckets --- .../src/runtime/hosts/codex.rs | 22 ++++++++++--- .../src/runtime/hosts/codex/tests.rs | 31 ++++++++++++++++++- 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/crates/tracedecay-sessions/src/runtime/hosts/codex.rs b/crates/tracedecay-sessions/src/runtime/hosts/codex.rs index c75fba704f2..ad0edc58859 100644 --- a/crates/tracedecay-sessions/src/runtime/hosts/codex.rs +++ b/crates/tracedecay-sessions/src/runtime/hosts/codex.rs @@ -1056,7 +1056,7 @@ struct CodexDirectoryResume { #[derive(Clone, Copy)] struct CodexDirectoryWitness { stat: [u8; 32], - entries: [u8; 32], + entries: Option<[u8; 32]>, } #[derive(Clone)] @@ -2578,11 +2578,18 @@ fn codex_directory_changed( let Some(stored) = stored else { return Ok(true); }; - // An unsettled stat proves nothing either way; the entry names do. + // A settled stat is sufficient to test unchanged history without + // rescanning every entry in its directory merely to retain a witness. if current.settled { return Ok(current.stat != stored.stat); } - Ok(directory_entry_fingerprint(path)? != stored.entries) + // An older settled directory did not need an entry-name witness. + // If its stat becomes unsettled, conservatively revalidate it rather + // than assuming that newly added rollouts were already discovered. + let Some(entries) = stored.entries else { + return Ok(true); + }; + Ok(directory_entry_fingerprint(path)? != entries) } fn codex_directory_witness(path: &Path) -> TranscriptIngestResult> { @@ -2591,7 +2598,14 @@ fn codex_directory_witness(path: &Path) -> TranscriptIngestResult Date: Sun, 8 Nov 2026 04:51:28 -0800 Subject: [PATCH 2/9] perf(sessions): short-circuit changed Codex bucket identity --- .../src/runtime/hosts/codex.rs | 11 +++++-- .../src/runtime/hosts/codex/tests.rs | 32 ++++++++++++++++++- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/crates/tracedecay-sessions/src/runtime/hosts/codex.rs b/crates/tracedecay-sessions/src/runtime/hosts/codex.rs index ad0edc58859..113eedb0d50 100644 --- a/crates/tracedecay-sessions/src/runtime/hosts/codex.rs +++ b/crates/tracedecay-sessions/src/runtime/hosts/codex.rs @@ -2578,10 +2578,15 @@ fn codex_directory_changed( let Some(stored) = stored else { return Ok(true); }; - // A settled stat is sufficient to test unchanged history without - // rescanning every entry in its directory merely to retain a witness. + // A different native identity proves the directory changed, even while + // the filesystem change clock is unsettled. Do not enumerate and sort its + // entries (or miss a metadata-only change with identical entry names). + if current.stat != stored.stat { + return Ok(true); + } + // A settled, identical stat proves no directory mutation. if current.settled { - return Ok(current.stat != stored.stat); + return Ok(false); } // An older settled directory did not need an entry-name witness. // If its stat becomes unsettled, conservatively revalidate it rather diff --git a/crates/tracedecay-sessions/src/runtime/hosts/codex/tests.rs b/crates/tracedecay-sessions/src/runtime/hosts/codex/tests.rs index d06c4a5467b..4ee7dac0190 100644 --- a/crates/tracedecay-sessions/src/runtime/hosts/codex/tests.rs +++ b/crates/tracedecay-sessions/src/runtime/hosts/codex/tests.rs @@ -1232,7 +1232,8 @@ mod recent_first_discovery_tests { CodexIndexedPath, CodexReplayIndex, EXACT_HOOK_DISCOVERY_UNITS_PER_CALL, IDLE_FULL_VALIDATION_CYCLES, MAX_EXACT_HOOK_SESSION_REQUESTS, MAX_EXACT_HOOK_SOURCE_AUTHORITIES, MAX_SCAN_DEPTH, codex_directory_changed, - codex_directory_witness, directory_stat_witness, indexed_replay_pass, + codex_directory_witness, directory_entry_fingerprint, directory_stat_witness, + indexed_replay_pass, replay_index_entries_visited_for_test, reset_replay_index_entries_visited_for_test, }; use crate::runtime::ingest::quarantine::SourceSweep; @@ -2617,6 +2618,35 @@ mod recent_first_discovery_tests { ); } + #[test] + #[cfg(unix)] + fn unsettled_bucket_native_change_invalidates_even_with_same_entry_names() { + let temp = TempDir::new().unwrap(); + let bucket = temp.path().join(".codex/sessions/2026/11/08"); + std::fs::create_dir_all(&bucket).unwrap(); + std::fs::write(bucket.join("rollout-existing.jsonl"), b"{}\n").unwrap(); + + // A recently changed bucket uses an entry-name witness when the + // clock is still coarse. Model a different previously observed stat + // (such as a directory metadata update) with identical entry names. + let mut stored = codex_directory_witness(&bucket).unwrap().unwrap(); + assert!( + !directory_stat_witness(&bucket).unwrap().unwrap().settled, + "this regression requires the unsettled-clock branch" + ); + assert!(stored.entries.is_some()); + assert_eq!( + stored.entries, + Some(directory_entry_fingerprint(&bucket).unwrap()) + ); + stored.stat[0] ^= 1; + + assert!( + codex_directory_changed(&bucket, Some(stored)).unwrap(), + "a changed native identity must invalidate even when the entry-name fingerprint is unchanged" + ); + } + #[test] #[cfg(unix)] fn settled_codex_bucket_skips_name_scans_and_detects_new_rollouts() { From d936b861b16c36ef0ee7df29283efb00dd0e2ea0 Mon Sep 17 00:00:00 2001 From: Zack Jackson <25274700+ScriptedAlchemy@users.noreply.github.com> Date: Sun, 8 Nov 2026 05:51:34 -0800 Subject: [PATCH 3/9] style(sessions): apply Bazel rustfmt on Codex regression --- crates/tracedecay-sessions/src/runtime/hosts/codex/tests.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/tracedecay-sessions/src/runtime/hosts/codex/tests.rs b/crates/tracedecay-sessions/src/runtime/hosts/codex/tests.rs index 4ee7dac0190..fdb938a22ac 100644 --- a/crates/tracedecay-sessions/src/runtime/hosts/codex/tests.rs +++ b/crates/tracedecay-sessions/src/runtime/hosts/codex/tests.rs @@ -1233,8 +1233,8 @@ mod recent_first_discovery_tests { IDLE_FULL_VALIDATION_CYCLES, MAX_EXACT_HOOK_SESSION_REQUESTS, MAX_EXACT_HOOK_SOURCE_AUTHORITIES, MAX_SCAN_DEPTH, codex_directory_changed, codex_directory_witness, directory_entry_fingerprint, directory_stat_witness, - indexed_replay_pass, - replay_index_entries_visited_for_test, reset_replay_index_entries_visited_for_test, + indexed_replay_pass, replay_index_entries_visited_for_test, + reset_replay_index_entries_visited_for_test, }; use crate::runtime::ingest::quarantine::SourceSweep; use crate::runtime::jsonl_observation_admission::{ From 6346d7ba0f0b3f44e90a6b4b57c113cd34aaa214 Mon Sep 17 00:00:00 2001 From: Zack Jackson <25274700+ScriptedAlchemy@users.noreply.github.com> Date: Sun, 8 Nov 2026 07:02:39 -0800 Subject: [PATCH 4/9] fix(sessions): stabilize unsettled Codex directory probes Refs #4756. Keep the native directory change-time in the negative metadata witness rather than a fresh per-sample Unvouched nonce. An unsettled equal stat still requires a name fingerprint; the first settled probe forces revalidation of an unsettled witness. Add deterministic regression for repeated filesystem probes. --- .../src/runtime/hosts/codex.rs | 26 +++++++++- .../src/runtime/hosts/codex/tests.rs | 48 ++++++++++++++++++- 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/crates/tracedecay-sessions/src/runtime/hosts/codex.rs b/crates/tracedecay-sessions/src/runtime/hosts/codex.rs index 113eedb0d50..a226f8f4841 100644 --- a/crates/tracedecay-sessions/src/runtime/hosts/codex.rs +++ b/crates/tracedecay-sessions/src/runtime/hosts/codex.rs @@ -2630,11 +2630,35 @@ fn directory_stat_witness(path: &Path) -> TranscriptIngestResult TranscriptIngestResult<[u8; 32]> { + let stat_change = match changed { + ChangeStamp::Unvouched(_) => { + let native_change_time = RewriteWitness::NATIVE + .change_time_nanos(metadata) + .map_or(0, |time| u128::from_le_bytes(time.to_le_bytes())); + ChangeStamp::Unvouched(native_change_time) + } + settled => settled, + }; + codex_corpus_identity(path, metadata, stat_change) +} + fn directory_entry_fingerprint(path: &Path) -> TranscriptIngestResult<[u8; 32]> { let listed = std::fs::read_dir(path).map_err(|source| TranscriptIngestError::ScanIo { operation: "fingerprint Codex transcript directory", diff --git a/crates/tracedecay-sessions/src/runtime/hosts/codex/tests.rs b/crates/tracedecay-sessions/src/runtime/hosts/codex/tests.rs index fdb938a22ac..35624a34416 100644 --- a/crates/tracedecay-sessions/src/runtime/hosts/codex/tests.rs +++ b/crates/tracedecay-sessions/src/runtime/hosts/codex/tests.rs @@ -1218,6 +1218,7 @@ mod recent_first_discovery_tests { use serde_json::json; use tempfile::TempDir; use tracedecay_domain::ProjectId; + use tracedecay_private_fs::ChangeStamp; use tracedecay_runtime_core::native_runtime::{ LONG_POLL, NativeTask, NativeTaskOutcome, block_on, long_native_polls, }; @@ -1232,7 +1233,8 @@ mod recent_first_discovery_tests { CodexIndexedPath, CodexReplayIndex, EXACT_HOOK_DISCOVERY_UNITS_PER_CALL, IDLE_FULL_VALIDATION_CYCLES, MAX_EXACT_HOOK_SESSION_REQUESTS, MAX_EXACT_HOOK_SOURCE_AUTHORITIES, MAX_SCAN_DEPTH, codex_directory_changed, - codex_directory_witness, directory_entry_fingerprint, directory_stat_witness, + codex_directory_stat_identity, codex_directory_witness, directory_entry_fingerprint, + directory_stat_witness, indexed_replay_pass, replay_index_entries_visited_for_test, reset_replay_index_entries_visited_for_test, }; @@ -2618,6 +2620,50 @@ mod recent_first_discovery_tests { ); } + #[test] + #[cfg(unix)] + fn unsettled_directory_stat_does_not_change_with_each_sample() { + let temp = TempDir::new().unwrap(); + let bucket = temp.path().join(".codex/sessions/2026/11/08"); + std::fs::create_dir_all(&bucket).unwrap(); + std::fs::write(bucket.join("rollout-existing.jsonl"), b"{}\\n").unwrap(); + let metadata = std::fs::metadata(&bucket).unwrap(); + + // A native Unvouched stamp is deliberately unique on each read. + // That is correct for file-content cursors, but must not invalidate + // the directory scan merely because the clock has not settled yet. + let first_sample = + crate::runtime::hosts::codex::codex_corpus_identity( + &bucket, + &metadata, + ChangeStamp::Unvouched(1), + ) + .unwrap(); + let next_sample = + crate::runtime::hosts::codex::codex_corpus_identity( + &bucket, + &metadata, + ChangeStamp::Unvouched(2), + ) + .unwrap(); + assert_ne!(first_sample, next_sample); + + let first = + codex_directory_stat_identity(&bucket, &metadata, ChangeStamp::Unvouched(1)).unwrap(); + let second = + codex_directory_stat_identity(&bucket, &metadata, ChangeStamp::Unvouched(2)).unwrap(); + assert_eq!( + first, second, + "identical native directory metadata must survive repeated unsettled probes" + ); + + // Once the clock settles, the identity must change so that a prior + // coarse-window entry-name witness cannot certify an unprobed write. + let settled = + codex_directory_stat_identity(&bucket, &metadata, ChangeStamp::Settled(0)).unwrap(); + assert_ne!(first, settled); + } + #[test] #[cfg(unix)] fn unsettled_bucket_native_change_invalidates_even_with_same_entry_names() { From 12a0e8245fca273126e1d637f2f70967a511b6a7 Mon Sep 17 00:00:00 2001 From: Zack Jackson <25274700+ScriptedAlchemy@users.noreply.github.com> Date: Sun, 8 Nov 2026 07:04:19 -0800 Subject: [PATCH 5/9] test(sessions): tighten unsettled Codex witness regression Refs #4756. Exercise the raw-vs-directory identity distinction on a real filesystem bucket and use a valid JSONL fixture. --- .../src/runtime/hosts/codex/tests.rs | 42 +++++++------------ 1 file changed, 16 insertions(+), 26 deletions(-) diff --git a/crates/tracedecay-sessions/src/runtime/hosts/codex/tests.rs b/crates/tracedecay-sessions/src/runtime/hosts/codex/tests.rs index 35624a34416..a676df79ba3 100644 --- a/crates/tracedecay-sessions/src/runtime/hosts/codex/tests.rs +++ b/crates/tracedecay-sessions/src/runtime/hosts/codex/tests.rs @@ -1232,8 +1232,9 @@ mod recent_first_discovery_tests { CodexDiscoverySourceKey, CodexDiscoveryState, CodexExactSessionPathAuthority, CodexIndexedPath, CodexReplayIndex, EXACT_HOOK_DISCOVERY_UNITS_PER_CALL, IDLE_FULL_VALIDATION_CYCLES, MAX_EXACT_HOOK_SESSION_REQUESTS, - MAX_EXACT_HOOK_SOURCE_AUTHORITIES, MAX_SCAN_DEPTH, codex_directory_changed, - codex_directory_stat_identity, codex_directory_witness, directory_entry_fingerprint, + MAX_EXACT_HOOK_SOURCE_AUTHORITIES, MAX_SCAN_DEPTH, codex_corpus_identity, + codex_directory_changed, codex_directory_stat_identity, codex_directory_witness, + directory_entry_fingerprint, directory_stat_witness, indexed_replay_pass, replay_index_entries_visited_for_test, reset_replay_index_entries_visited_for_test, @@ -2626,39 +2627,28 @@ mod recent_first_discovery_tests { let temp = TempDir::new().unwrap(); let bucket = temp.path().join(".codex/sessions/2026/11/08"); std::fs::create_dir_all(&bucket).unwrap(); - std::fs::write(bucket.join("rollout-existing.jsonl"), b"{}\\n").unwrap(); + std::fs::write(bucket.join("rollout-existing.jsonl"), b"{}\n").unwrap(); let metadata = std::fs::metadata(&bucket).unwrap(); + let first_stamp = ChangeStamp::Unvouched(1); + let next_stamp = ChangeStamp::Unvouched(2); - // A native Unvouched stamp is deliberately unique on each read. - // That is correct for file-content cursors, but must not invalidate - // the directory scan merely because the clock has not settled yet. - let first_sample = - crate::runtime::hosts::codex::codex_corpus_identity( - &bucket, - &metadata, - ChangeStamp::Unvouched(1), - ) - .unwrap(); - let next_sample = - crate::runtime::hosts::codex::codex_corpus_identity( - &bucket, - &metadata, - ChangeStamp::Unvouched(2), - ) - .unwrap(); + // Each native Unvouched stamp is deliberately different. The raw + // file-content identity must not accidentally become stable. + let first_sample = codex_corpus_identity(&bucket, &metadata, first_stamp).unwrap(); + let next_sample = codex_corpus_identity(&bucket, &metadata, next_stamp).unwrap(); assert_ne!(first_sample, next_sample); - let first = - codex_directory_stat_identity(&bucket, &metadata, ChangeStamp::Unvouched(1)).unwrap(); - let second = - codex_directory_stat_identity(&bucket, &metadata, ChangeStamp::Unvouched(2)).unwrap(); + // Directory change detection, unlike file-content authority, must + // not force a fresh traversal on every sample of identical metadata. + let first = codex_directory_stat_identity(&bucket, &metadata, first_stamp).unwrap(); + let second = codex_directory_stat_identity(&bucket, &metadata, next_stamp).unwrap(); assert_eq!( first, second, "identical native directory metadata must survive repeated unsettled probes" ); - // Once the clock settles, the identity must change so that a prior - // coarse-window entry-name witness cannot certify an unprobed write. + // The settled transition still invalidates an unsettled snapshot: + // a prior coarse-window name check cannot vouch for a later write. let settled = codex_directory_stat_identity(&bucket, &metadata, ChangeStamp::Settled(0)).unwrap(); assert_ne!(first, settled); From 1b7fdd794470a9086dc4b7337c8f8289c7173676 Mon Sep 17 00:00:00 2001 From: Zack Jackson <25274700+ScriptedAlchemy@users.noreply.github.com> Date: Sun, 8 Nov 2026 08:18:38 -0800 Subject: [PATCH 6/9] style(sessions): apply exact hosted rustfmt import layout --- crates/tracedecay-sessions/src/runtime/hosts/codex/tests.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/crates/tracedecay-sessions/src/runtime/hosts/codex/tests.rs b/crates/tracedecay-sessions/src/runtime/hosts/codex/tests.rs index a676df79ba3..c26c094c0a4 100644 --- a/crates/tracedecay-sessions/src/runtime/hosts/codex/tests.rs +++ b/crates/tracedecay-sessions/src/runtime/hosts/codex/tests.rs @@ -1234,10 +1234,8 @@ mod recent_first_discovery_tests { IDLE_FULL_VALIDATION_CYCLES, MAX_EXACT_HOOK_SESSION_REQUESTS, MAX_EXACT_HOOK_SOURCE_AUTHORITIES, MAX_SCAN_DEPTH, codex_corpus_identity, codex_directory_changed, codex_directory_stat_identity, codex_directory_witness, - directory_entry_fingerprint, - directory_stat_witness, - indexed_replay_pass, replay_index_entries_visited_for_test, - reset_replay_index_entries_visited_for_test, + directory_entry_fingerprint, directory_stat_witness, indexed_replay_pass, + replay_index_entries_visited_for_test, reset_replay_index_entries_visited_for_test, }; use crate::runtime::ingest::quarantine::SourceSweep; use crate::runtime::jsonl_observation_admission::{ From 841d2af846cb228e05088fa63e0cb380912b7a75 Mon Sep 17 00:00:00 2001 From: Zack Jackson <25274700+ScriptedAlchemy@users.noreply.github.com> Date: Sun, 8 Nov 2026 08:50:10 -0800 Subject: [PATCH 7/9] perf(sessions): skip duplicate hot Codex directory probes Refs #4756. The rotating idle probe can revisit the same hot buckets already checked in the same pass. Advance the rotation without a second stat or unsettled directory fingerprint; preserve urgent hot change detection and the complete rotation cycle. Add a filesystem regression that detects a new rollout when rotation starts on the hot prefix. --- .../src/runtime/hosts/codex.rs | 6 ++ .../src/runtime/hosts/codex/tests.rs | 56 +++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/crates/tracedecay-sessions/src/runtime/hosts/codex.rs b/crates/tracedecay-sessions/src/runtime/hosts/codex.rs index a226f8f4841..2ab2ff738ac 100644 --- a/crates/tracedecay-sessions/src/runtime/hosts/codex.rs +++ b/crates/tracedecay-sessions/src/runtime/hosts/codex.rs @@ -1735,6 +1735,12 @@ impl CodexSource { } else { index + 1 }; + // The hot prefix was already checked earlier in this tick. + // Advance the rotating cursor through it without repeating + // directory stats or unsettled entry-name fingerprint scans. + if index < hot { + continue; + } if let Some(directory) = idle.directories.get(index) { if codex_directory_changed(&directory.path, directory.identity)? { changed = true; diff --git a/crates/tracedecay-sessions/src/runtime/hosts/codex/tests.rs b/crates/tracedecay-sessions/src/runtime/hosts/codex/tests.rs index c26c094c0a4..771f91636db 100644 --- a/crates/tracedecay-sessions/src/runtime/hosts/codex/tests.rs +++ b/crates/tracedecay-sessions/src/runtime/hosts/codex/tests.rs @@ -2619,6 +2619,62 @@ mod recent_first_discovery_tests { ); } + #[test] + fn hot_codex_directory_is_detected_even_when_rotation_starts_on_hot_prefix() { + let temp = TempDir::new().unwrap(); + let home = temp.path(); + let newest = write_dated_rollout(home, ("2026", "08", "29"), "current"); + write_dated_rollout(home, ("2026", "08", "01"), "older"); + let source = CodexSource::with_home(home); + let bounds = TranscriptDiscoveryBounds::from_discovered_units(8); + let mut state = CodexDiscoveryState::default(); + let mut frontier = CodexDiscoveryFrontier::initial(); + let mut admitted = BTreeSet::new(); + for _ in 0..64 { + let pass = retained_pass(&source, &mut state, bounds, frontier); + admitted.extend(pass.report.paths); + frontier = pass.next_frontier; + if frontier.is_complete() { + break; + } + } + assert!(frontier.is_complete(), "the initial corpus must converge"); + assert!(admitted.contains(&newest)); + + let idle = state.idle.as_mut().expect("completed idle authority"); + let hot_index = idle + .directories + .iter() + .position(|directory| directory.path == newest.parent().unwrap()) + .expect("newest bucket must be retained"); + assert!( + hot_index < super::super::IDLE_HOT_DIRECTORIES, + "the newly changed bucket must be in the hot prefix" + ); + idle.next = hot_index; + + let added = newest.parent().unwrap().join("rollout-after-idle.jsonl"); + std::fs::write(&added, b"{}").unwrap(); + spin_until_jsonl_change_settled(&added); + let detection = retained_pass(&source, &mut state, bounds, frontier); + assert!( + !detection.next_frontier.is_complete(), + "the hot probe must invalidate a changed bucket before rotation skips its duplicate" + ); + + frontier = detection.next_frontier; + let mut found = detection.report.paths.contains(&added); + for _ in 0..64 { + let pass = retained_pass(&source, &mut state, bounds, frontier); + found |= pass.report.paths.contains(&added); + frontier = pass.next_frontier; + if found { + break; + } + } + assert!(found, "the changed hot bucket must deliver the new rollout"); + } + #[test] #[cfg(unix)] fn unsettled_directory_stat_does_not_change_with_each_sample() { From e85d4600145fb19ad5b1c94492774368e8bffa4c Mon Sep 17 00:00:00 2001 From: Zack Jackson <25274700+ScriptedAlchemy@users.noreply.github.com> Date: Sun, 8 Nov 2026 01:52:34 -0800 Subject: [PATCH 8/9] test(automation): bind scheduler fixtures to backend identities Repair three hosted automation_runner_test failures by supplying the configured executable identity required by the production prefilter. Use the same identity for the prefilter and retained runner, preserving manual-schedule, exact-ledger, live-lock and lock-race assertions. Add a production-prefilter regression for unconfigured refusal, deduplication and same-occurrence recovery after binding, with zero backend calls and no effectful terminal. Do not relax backend admission, change timeouts, or alter the graph publication changes. Refs #4532 --- .../tests/automation_runner_test/jobs.rs | 124 ++++++++++++++++-- 1 file changed, 110 insertions(+), 14 deletions(-) diff --git a/crates/tracedecay/tests/automation_runner_test/jobs.rs b/crates/tracedecay/tests/automation_runner_test/jobs.rs index 740283d56e8..7342786381b 100644 --- a/crates/tracedecay/tests/automation_runner_test/jobs.rs +++ b/crates/tracedecay/tests/automation_runner_test/jobs.rs @@ -86,6 +86,7 @@ fn enabled_job_config() -> AutomationSettingsV1 { struct ContentBackend { calls: AtomicUsize, content: &'static str, + executable: Option, } impl ContentBackend { @@ -93,9 +94,15 @@ impl ContentBackend { Self { calls: AtomicUsize::new(0), content, + executable: None, } } + fn with_executable(mut self, executable: &Path) -> Self { + self.executable = Some(executable.to_path_buf()); + self + } + fn calls(&self) -> usize { self.calls.load(Ordering::SeqCst) } @@ -126,7 +133,7 @@ impl AgentTaskBackend for ContentBackend { } fn executable(&self) -> Option<&std::path::Path> { - None + self.executable.as_deref() } } @@ -988,6 +995,10 @@ async fn scheduler_trigger_skips_repeat_and_respects_lock_discipline() { let mut job = sample_job("sched-job"); job.schedule = Some("manual".to_string()); let config = enabled_job_config(); + // Reach the schedule gate with a bound identity, not the earlier + // backend-unconfigured refusal. This gate never invokes the backend. + let executable = tempfile::NamedTempFile::new().unwrap(); + let backend = ContentBackend::new("unused").with_executable(executable.path()); // Manual schedule: every scheduler tick of the same occurrence reaches // the prefilter, which records the skip once. @@ -996,7 +1007,7 @@ async fn scheduler_trigger_skips_repeat_and_respects_lock_discipline() { let run = evaluate_and_record_scheduler_skip( &dashboard_root, &config, - None, + backend.executable(), &job, "sched-occurrence", None, @@ -1019,7 +1030,7 @@ async fn scheduler_trigger_skips_repeat_and_respects_lock_discipline() { let repeated = evaluate_and_record_scheduler_skip( &dashboard_root, &config, - None, + backend.executable(), &job, "sched-occurrence", None, @@ -1031,6 +1042,76 @@ async fn scheduler_trigger_skips_repeat_and_respects_lock_discipline() { repeated.ledger_record, records[0], "an immediate repeat skip must return the exact durable prior row" ); + assert_eq!(backend.calls(), 0); +} + +#[tokio::test] +async fn scheduler_prefilter_rejects_unconfigured_backend_and_recovers_without_effect() { + let temp = tempdir().unwrap(); + let dashboard_root = temp.path().join("dashboard"); + let job = sample_job("backend-recovery"); + let config = enabled_job_config(); + let occurrence = "backend-recovery-occurrence"; + let backend = ContentBackend::new("unused"); + + let first = evaluate_and_record_scheduler_skip( + &dashboard_root, + &config, + backend.executable(), + &job, + occurrence, + None, + ) + .await + .unwrap() + .expect("a due job still requires a configured backend"); + assert_eq!(first.report["reason"], json!("backend_unconfigured")); + assert_eq!(first.ledger_record.status, AutomationRunStatus::Skipped); + assert_ne!(first.run_id, occurrence); + let repeated = evaluate_and_record_scheduler_skip( + &dashboard_root, + &config, + backend.executable(), + &job, + occurrence, + None, + ) + .await + .unwrap() + .unwrap(); + assert_eq!(repeated.ledger_record, first.ledger_record); + + let executable = tempfile::NamedTempFile::new().unwrap(); + let backend = backend.with_executable(executable.path()); + assert!( + evaluate_and_record_scheduler_skip( + &dashboard_root, + &config, + backend.executable(), + &job, + occurrence, + None, + ) + .await + .unwrap() + .is_none(), + "binding the backend must admit the same due occurrence without cooldown" + ); + assert_eq!(backend.calls(), 0); + assert_eq!( + load_run_records(&dashboard_root, 10).await.unwrap(), + vec![first.ledger_record], + "advisory admission must not append an effectful terminal" + ); + assert!( + tracedecay_automation_runtime::automation::run_ledger::find_run_record_exact_bounded( + &dashboard_root, + occurrence, + ) + .await + .unwrap() + .is_none() + ); } #[tokio::test] @@ -1206,15 +1287,22 @@ async fn scheduler_prefilter_live_lock_wins_over_not_due_summary() { let config = enabled_job_config(); let occurrence = "locked-prefilter-occurrence"; - let prefilter = - evaluate_and_record_scheduler_skip(&dashboard_root, &config, None, &job, occurrence, None) - .await - .unwrap() - .expect("the live job lock must produce a diagnostic"); + let executable = tempfile::NamedTempFile::new().unwrap(); + let backend = ContentBackend::new("unused").with_executable(executable.path()); + let prefilter = evaluate_and_record_scheduler_skip( + &dashboard_root, + &config, + backend.executable(), + &job, + occurrence, + None, + ) + .await + .unwrap() + .expect("the live job lock must produce a diagnostic"); assert_eq!(prefilter.report["reason"], json!("scheduler_lock_active")); assert_ne!(prefilter.run_id, occurrence); - let backend = ContentBackend::new("unused"); let admitted_job = tracedecay_automation_runtime::automation::jobs::admit_job(&job).unwrap(); let retained = run_user_job_with_backend_for_retained_settlement( &dashboard_root, @@ -1262,12 +1350,21 @@ async fn retained_scheduler_runner_reacquires_after_due_prefilter() { job.schedule = Some("every 1h".to_owned()); let config = enabled_job_config(); let occurrence = "prefilter-gap-occurrence"; + let executable = tempfile::NamedTempFile::new().unwrap(); + let backend = ContentBackend::new("unused").with_executable(executable.path()); assert!( - evaluate_and_record_scheduler_skip(&dashboard_root, &config, None, &job, occurrence, None) - .await - .unwrap() - .is_none(), + evaluate_and_record_scheduler_skip( + &dashboard_root, + &config, + backend.executable(), + &job, + occurrence, + None, + ) + .await + .unwrap() + .is_none(), "a new interval job is advisory-due before effect admission" ); let _raced_in = @@ -1276,7 +1373,6 @@ async fn retained_scheduler_runner_reacquires_after_due_prefilter() { .unwrap() .expect("the advisory prefilter must release its reservation lock"); - let backend = ContentBackend::new("unused"); let admitted_job = tracedecay_automation_runtime::automation::jobs::admit_job(&job).unwrap(); let retained = run_user_job_with_backend_for_retained_settlement( &dashboard_root, From bdbc26668a6153683e775b507055f4cb04de9cef Mon Sep 17 00:00:00 2001 From: Zack Jackson <25274700+ScriptedAlchemy@users.noreply.github.com> Date: Sun, 8 Nov 2026 13:59:23 -0800 Subject: [PATCH 9/9] test(git): wait for Git status admission in parity journey --- .../runtime_surface_acceptance.rs | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/crates/tracedecay/tests/runtime_acceptance_suite/runtime_surface_acceptance.rs b/crates/tracedecay/tests/runtime_acceptance_suite/runtime_surface_acceptance.rs index f962270f60c..bd145446f44 100644 --- a/crates/tracedecay/tests/runtime_acceptance_suite/runtime_surface_acceptance.rs +++ b/crates/tracedecay/tests/runtime_acceptance_suite/runtime_surface_acceptance.rs @@ -340,10 +340,6 @@ async fn git_runtime_fixture() -> RuntimeFixture { ) .expect("write staged Git change"); git(&project, &["add", "src/main.rs"]); - // Git reads answer a typed retryable `mounting` problem until project open - // installs the owner's authority; the journeys start from a served project. - await_published_code_index(environment.home(), &project); - let handshake = crate::common::handshake_for_current_client( environment.profile(), Some(project.clone()), @@ -1945,19 +1941,19 @@ async fn production_primitive_code_routes_have_cli_mcp_http_parity() { #[tokio::test(flavor = "multi_thread")] async fn git_preview_and_apply_have_real_cli_mcp_runtime_parity() { let fixture = git_runtime_fixture().await; - let status = resolve_mcp_application_surface( + let status = admitted_mcp_invocation( + &fixture.client, ApplicationSurfaceOperation::GitStatus, - RequestId::new("request.git-parity.status").expect("status request id"), - parse_application_surface_request( - ApplicationSurfaceOperation::GitStatus, - serde_json::json!({}), - ) - .expect("Git status request"), - RequestedOutputFormat::Json, - Some(&fixture.client), + "request.git-parity.status", + || { + parse_application_surface_request( + ApplicationSurfaceOperation::GitStatus, + serde_json::json!({}), + ) + .expect("Git status request") + }, ) - .await - .expect("Git status dispatch"); + .await; let scope = status .result .as_ref()