From c4af9ae22a8535f031b92e3a8c3705e9bde5c8c1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 Oct 2026 03:59:10 +0000 Subject: [PATCH 1/7] chore: record implementation plan From 4b5dc79bc2eb81043903829e3e374f20d3405e2e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 Oct 2026 04:08:01 +0000 Subject: [PATCH 2/7] build(ci): check Windows workspace with Bazel Co-authored-by: ScriptedAlchemy <25274700+ScriptedAlchemy@users.noreply.github.com> --- .github/workflows/ci.yml | 43 +++++++++++++--------------------------- 1 file changed, 14 insertions(+), 29 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 77c8ed4556..45fcbf95d3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -529,12 +529,17 @@ jobs: run: test "${{ needs.linux-test.result }}" = "success" windows-test: - name: Test Windows + name: Windows Bazel check needs: scope-gate - if: ${{ needs.scope-gate.outputs.run-os == 'true' }} + if: >- + ${{ needs.scope-gate.outputs.run-heavy == 'true' && + (github.event_name != 'workflow_dispatch' || + needs.scope-gate.outputs.run-os == 'true') }} runs-on: windows-latest timeout-minutes: 330 steps: + # Linux builds do not enable cfg(windows). Compile every Bazel target on + # native Windows for ready PRs and pushes; full tests remain dispatch-only. # Full history: the search-quality workload fixture pins a # `source_repository_commit`, and `validate_source_bindings` resolves that # object out of this checkout. @@ -552,9 +557,13 @@ jobs: with: cache-key: ci-windows-test - # `bazel test` builds every target in the pattern, so tests start - # while the rest of the workspace still links. - - name: Build and test the workspace + - name: Build and type-check the Windows workspace + if: ${{ github.event_name != 'workflow_dispatch' }} + shell: bash + run: bazel build --config=ci //... + + - name: Build and test the Windows workspace + if: ${{ github.event_name == 'workflow_dispatch' && needs.scope-gate.outputs.run-os == 'true' }} shell: bash run: bazel test --config=ci //... @@ -604,30 +613,6 @@ jobs: - name: Run blocking Clippy policy run: bazel build --config=ci --config=clippy //... - # Master pushes run no Windows lane, so `cfg(windows)` code otherwise - # first compiles in the release build. Bazel has no MSVC toolchain on - # Linux, so this cross-target type check stays on Cargo: `cargo check` - # needs no linker, and mingw supplies the C compiler the native build - # scripts run. macOS has no equivalent: its build scripts need the Apple - # SDK headers. - - name: Install pinned toolchain and Windows check target - run: | - rustup toolchain install - rustup target add x86_64-pc-windows-gnu - - - uses: ./.github/actions/setup-pnpm - - - name: Install the Windows C cross-compiler - run: | - sudo apt-get update - sudo apt-get install -y --no-install-recommends gcc-mingw-w64-x86-64 - - - name: Check the Windows target - run: >- - cargo check --workspace --all-targets --locked - --target x86_64-pc-windows-gnu - --features tracedecay/test-transport,tracedecay/test-helpers,tracedecay-cli/test-transport - - name: Save compiled Bazel actions if: always() && !cancelled() && github.ref == 'refs/heads/master' && steps.bazel.outputs.disk-cache-key != '' uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 From ef22accd002e174f4fbce4f382a77c64ad2af79d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 Oct 2026 04:09:16 +0000 Subject: [PATCH 3/7] build(ci): run Windows Bazel check on draft PRs Co-authored-by: ScriptedAlchemy <25274700+ScriptedAlchemy@users.noreply.github.com> --- .github/workflows/ci.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 45fcbf95d3..cb3d8c396a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -532,14 +532,15 @@ jobs: name: Windows Bazel check needs: scope-gate if: >- - ${{ needs.scope-gate.outputs.run-heavy == 'true' && + ${{ github.event_name == 'pull_request' || + (needs.scope-gate.outputs.run-heavy == 'true' && (github.event_name != 'workflow_dispatch' || - needs.scope-gate.outputs.run-os == 'true') }} + needs.scope-gate.outputs.run-os == 'true')) }} runs-on: windows-latest timeout-minutes: 330 steps: # Linux builds do not enable cfg(windows). Compile every Bazel target on - # native Windows for ready PRs and pushes; full tests remain dispatch-only. + # native Windows for every PR and push; full tests remain dispatch-only. # Full history: the search-quality workload fixture pins a # `source_repository_commit`, and `validate_source_bindings` resolves that # object out of this checkout. From d74895de64b25807fa60133db62a9f68ec9a6245 Mon Sep 17 00:00:00 2001 From: Zack Jackson <25274700+ScriptedAlchemy@users.noreply.github.com> Date: Tue, 20 Oct 2026 23:55:37 -0700 Subject: [PATCH 4/7] fix(ci): keep Windows builds behind the heavy scope gate --- .github/workflows/ci.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0c4c4267d5..1b9db573a6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -559,15 +559,14 @@ jobs: name: Windows Bazel check needs: scope-gate if: >- - ${{ github.event_name == 'pull_request' || - (needs.scope-gate.outputs.run-heavy == 'true' && + ${{ needs.scope-gate.outputs.run-heavy == 'true' && (github.event_name != 'workflow_dispatch' || - needs.scope-gate.outputs.run-os == 'true')) }} + needs.scope-gate.outputs.run-os == 'true') }} runs-on: windows-latest timeout-minutes: 330 steps: # Linux builds do not enable cfg(windows). Compile every Bazel target on - # native Windows for every PR and push; full tests remain dispatch-only. + # native Windows for ready PRs and pushes; full tests remain dispatch-only. # Full history: the search-quality workload fixture pins a # `source_repository_commit`, and `validate_source_bindings` resolves that # object out of this checkout. From ff2342a038399b237bc2a7d4457ca010ed053ccd Mon Sep 17 00:00:00 2001 From: Zack Jackson <25274700+ScriptedAlchemy@users.noreply.github.com> Date: Wed, 21 Oct 2026 01:07:45 -0700 Subject: [PATCH 5/7] fix(memory): correct native index build admission --- .../src/native_integration/journey_tests.rs | 45 ++++- .../src/code_index_scheduler.rs | 3 +- .../src/code_index_scheduler/serving.rs | 156 ++++++------------ .../tests/code_index_scheduler/serving.rs | 39 ----- .../src/resident_memory.rs | 66 +++++++- .../src/resident_memory/tests.rs | 127 ++++++++++++++ 6 files changed, 288 insertions(+), 148 deletions(-) diff --git a/crates/tracedecay-agent-hosts/src/native_integration/journey_tests.rs b/crates/tracedecay-agent-hosts/src/native_integration/journey_tests.rs index 7a624dba01..1e2788add4 100644 --- a/crates/tracedecay-agent-hosts/src/native_integration/journey_tests.rs +++ b/crates/tracedecay-agent-hosts/src/native_integration/journey_tests.rs @@ -495,17 +495,56 @@ async fn mount( .mount_worktree(project_id.clone(), &repository_root, index_store) .await .expect("mount canonical code-index scheduler"); - tokio::time::timeout(Duration::from_secs(5), async { + let memory = schedulers.process_resident_memory(); + let mut last_observation = "no owner observation completed".to_owned(); + let initial_generation = tokio::time::timeout(Duration::from_secs(5), async { + let owner_activity = schedulers.subscribe_owner_activity(&repository_root).await; while schedulers .latest_generation_id(&repository_root) .await .is_none() { + let parked = schedulers + .convergence_park(&repository_root) + .await + .map(|parked| { + ( + parked.reason.chars().take(200).collect::(), + parked.blocked_reason, + parked.retries_on_wake, + parked.observed_passes, + ) + }); + let activity = owner_activity.as_ref().map(|activity| { + ( + activity.worker_phase(), + activity.passes(), + activity.wake_pending(), + ) + }); + let pressure = memory.pressure().state(); + let ledger = memory.snapshot(); + last_observation = format!( + "parked={parked:?}; activity={activity:?}; pressure={pressure:?}; \ + ledger_used_bytes={}; ledger_limit_bytes={}; ledger_charge_count={}; \ + process_shared_charge_count={}", + ledger.used_bytes, + ledger.limit_bytes, + ledger.charges.len(), + ledger.process_shared_charges.len() + ); tokio::time::sleep(Duration::from_millis(10)).await; } }) - .await - .expect("initial code-index generation seat"); + .await; + if let Err(error) = initial_generation { + // The bounded observation was captured inside the original deadline; + // reporting starts no new reads, admission, or readiness wait. + // A failed fixture still owns the worker it mounted. Signal its + // canonical cancellation before unwinding out of the temporary root. + schedulers.cancel(); + panic!("initial code-index generation seat: {error}; {last_observation}"); + } let analysis = Arc::new(DaemonNativeIntegrationAnalysisV1::new( schedulers, analysis_scope, diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler.rs index baca65a33f..a325e59813 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler.rs @@ -300,8 +300,7 @@ pub use serving::{ use serving::{ CodeIndexCommittedProgressSampleV1, CodeTextProjectionSlotV1, map_sealed_page_source_error, sha256_private_file_and_size, text_artifact_admitted_build_budget, - text_artifact_builder_budget, text_artifact_resident_memory_charge, - text_artifact_source_batch_limits, + text_artifact_builder_budget, text_artifact_source_batch_limits, }; #[cfg(test)] use tracedecay_query::retrieval::ports::TEXT_ARTIFACT_PAGE_CHUNKS_V1; diff --git a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/serving.rs b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/serving.rs index de14bd949d..e6a5be1c4a 100644 --- a/crates/tracedecay-code-index-runtime/src/code_index_scheduler/serving.rs +++ b/crates/tracedecay-code-index-runtime/src/code_index_scheduler/serving.rs @@ -5,7 +5,6 @@ mod family_report; use std::{ collections::{BTreeSet, VecDeque}, io::Read, - num::NonZeroU64, path::{Path, PathBuf}, sync::{ Arc, Condvar, Mutex, MutexGuard, OnceLock, PoisonError, RwLock, @@ -947,29 +946,6 @@ pub struct DaemonCodeTextArtifactStoreV1 { worktree_id: WorktreeId, } -/// The bytes one artifact admission reserves: its ceiling plus the band above -/// the admission watermark, so the reserve call enforces that watermark -/// against both the ledger and the measured resident set. The band is not -/// memory the artifact owns and is released right after the reserve; -/// retaining it in every overlapping build would charge the same -/// process-wide margin repeatedly. The process's live set is not charged: -/// the reserve already compares the request with the measured resident set, -/// which contains it. -pub(super) fn text_artifact_resident_memory_charge( - requested: NonZeroU64, - watermark_headroom: u64, -) -> Result { - requested - .get() - .checked_add(watermark_headroom) - .and_then(NonZeroU64::new) - .ok_or_else(|| { - RetrievalPortError::Contract( - "text-artifact resident-memory accounting overflowed".to_owned(), - ) - }) -} - pub(super) fn text_artifact_admitted_build_budget( preferred_bytes: u64, minimum_bytes: u64, @@ -1170,10 +1146,8 @@ impl DaemonCodeTextArtifactStoreV1 { } } - /// Reserve one artifact memory ceiling. The atomic reserve also includes - /// the process-wide high-watermark headroom, then releases that - /// check-only margin before returning while the component ceiling stays - /// charged. + /// Charge the artifact's ceiling below the process admission watermark. + /// Sizing and charging share one measured headroom reading. fn reserve_resident_memory( &self, generation_id: &CodeGenerationId, @@ -1184,32 +1158,6 @@ impl DaemonCodeTextArtifactStoreV1 { .map(|(reservation, _)| reservation) } - /// Measure headroom and size the build: `(observed, watermark headroom, - /// admitted)` bytes. - fn text_artifact_admission( - &self, - preferred: NonZeroU64, - minimum: NonZeroU64, - ) -> Result<(u64, u64), RetrievalPortError> { - let admission_watermark = self.resident_memory.admission_watermark_bytes(); - let headroom = self.resident_memory.headroom_below(admission_watermark); - let watermark_headroom = self - .resident_memory - .snapshot() - .limit_bytes - .saturating_sub(admission_watermark); - let admitted_bytes = text_artifact_admitted_build_budget( - preferred.get(), - minimum.get(), - headroom.available_bytes, - ) - .inspect_err(|_| { - self.resident_memory - .wait_for_headroom(minimum.get(), admission_watermark); - })?; - Ok((watermark_headroom, admitted_bytes)) - } - fn reserve_resident_memory_up_to( &self, generation_id: &CodeGenerationId, @@ -1235,57 +1183,59 @@ impl DaemonCodeTextArtifactStoreV1 { "text-artifact minimum resident-memory reservation must be nonzero".to_owned(), ) })?; - let (watermark_headroom, admitted_bytes) = - match self.text_artifact_admission(preferred, minimum) { - Err(RetrievalPortError::ResidentMemoryRefused(detail)) => { - let released = self - .resident_owners - .shed(minimum.get(), std::time::Instant::now()); - for release in &released { - log_resident_owner_release_v1(release); - } - // Freed pages sit in the pool workers' allocator heaps - // whether or not an owner was shed. - let trim = release_process_allocator_memory_v1(); - tracing::info!( - event = "code_text_artifact_build_shed_retained_state", - released_owners = released.len(), - trimmed_bytes = trim.released_bytes(), - refused = %detail, - "text-artifact build shed retained state before re-measuring headroom" - ); - self.text_artifact_admission(preferred, minimum) + let admission_watermark = self.resident_memory.admission_watermark_bytes(); + let reserve = || { + let reservation = self + .resident_memory + .reserve_up_to( + ResidentMemoryKeyV1 { + project_id: self.project_id.clone(), + worktree_id: self.worktree_id.clone(), + generation_id: generation_id.clone(), + component, + }, + preferred, + admission_watermark, + ) + .map_err(|error| { + RetrievalPortError::ResidentMemoryRefused(format!( + "text-artifact resident-memory admission was refused: {error}" + )) + })?; + let admitted = text_artifact_admitted_build_budget( + preferred.get(), + minimum.get(), + reservation.reserved_bytes(), + )?; + Ok((reservation, admitted)) + }; + let (reservation, admitted) = match reserve() { + Err(RetrievalPortError::ResidentMemoryRefused(detail)) => { + let released = self + .resident_owners + .shed(minimum.get(), std::time::Instant::now()); + for release in &released { + log_resident_owner_release_v1(release); } - admission => admission, - }?; - let admitted = NonZeroU64::new(admitted_bytes).ok_or_else(|| { - RetrievalPortError::Contract( - "text-artifact admitted resident-memory reservation must be nonzero".to_owned(), - ) - })?; - let accounted = text_artifact_resident_memory_charge(admitted, watermark_headroom)?; - let mut reservation = self - .resident_memory - .reserve( - ResidentMemoryKeyV1 { - project_id: self.project_id.clone(), - worktree_id: self.worktree_id.clone(), - generation_id: generation_id.clone(), - component, - }, - accounted, - ) - .map_err(|error| { - RetrievalPortError::ResidentMemoryRefused(format!( - "text-artifact resident-memory admission was refused: {error}" - )) - })?; - reservation.shrink_to(admitted.get()).map_err(|error| { - RetrievalPortError::Contract(format!( - "text-artifact resident-memory headroom release failed: {error}" - )) + let trim = release_process_allocator_memory_v1(); + tracing::info!( + event = "code_text_artifact_build_shed_retained_state", + released_owners = released.len(), + trimmed_bytes = trim.released_bytes(), + refused = %detail, + "text-artifact build shed retained state before re-measuring headroom" + ); + reserve() + } + result => result, + } + .inspect_err(|error| { + if matches!(error, RetrievalPortError::ResidentMemoryRefused(_)) { + self.resident_memory + .wait_for_headroom(minimum.get(), admission_watermark); + } })?; - let admitted = usize::try_from(admitted.get()).map_err(|error| { + let admitted = usize::try_from(admitted).map_err(|error| { RetrievalPortError::Contract(format!( "text-artifact admitted reservation exceeds the platform limit: {error}" )) diff --git a/crates/tracedecay-code-index-runtime/tests/code_index_scheduler/serving.rs b/crates/tracedecay-code-index-runtime/tests/code_index_scheduler/serving.rs index 05bf0e812e..aca2a7f7bb 100644 --- a/crates/tracedecay-code-index-runtime/tests/code_index_scheduler/serving.rs +++ b/crates/tracedecay-code-index-runtime/tests/code_index_scheduler/serving.rs @@ -2085,45 +2085,6 @@ fn reader_reservation_refusal_precedes_missing_artifact_access() { assert!(latest.text_projection_needs_work()); } -#[test] -fn overlapping_text_builds_share_one_admission_watermark_headroom() { - let limit_bytes = 1_000_u64; - let watermark_headroom = 100_u64; - let requested = NonZeroU64::new(300).expect("nonzero build request"); - let mut used_bytes = 0_u64; - - for _ in 0..3 { - let accounted = - super::super::text_artifact_resident_memory_charge(requested, watermark_headroom) - .expect("bounded admission accounting"); - assert!( - used_bytes + accounted.get() <= limit_bytes, - "each overlapping build fits beneath the same 900-byte high watermark" - ); - used_bytes += requested.get(); - } - - assert_eq!( - used_bytes, 900, - "the retained ledger owns the three build ceilings, not three margins" - ); - for overflow in [ - super::super::text_artifact_resident_memory_charge( - NonZeroU64::new(u64::MAX).expect("maximum nonzero request"), - 1, - ), - super::super::text_artifact_resident_memory_charge(requested, u64::MAX), - ] { - assert!( - matches!( - overflow, - Err(tracedecay_query::retrieval::RetrievalPortError::Contract(_)) - ), - "overflow must remain a typed contract refusal: {overflow:?}" - ); - } -} - #[test] fn text_build_budget_shrinks_to_available_headroom_without_dropping_below_its_floor() { const GIB: u64 = 1024 * 1024 * 1024; diff --git a/crates/tracedecay-runtime-core/src/resident_memory.rs b/crates/tracedecay-runtime-core/src/resident_memory.rs index 34a9c445be..a16e42ca7d 100644 --- a/crates/tracedecay-runtime-core/src/resident_memory.rs +++ b/crates/tracedecay-runtime-core/src/resident_memory.rs @@ -1531,6 +1531,62 @@ impl ProcessResidentMemoryV1 { Err(self.refused(self.admission_failure(requested_bytes))) } + /// Size and charge new work from the same measured headroom reading. + /// The returned ceiling can be smaller than `preferred_bytes`; consumers + /// with a minimum working set must release a reservation below that floor. + /// Concurrent ledger charges are checked under the charge's own lock. + /// The consumer registers its working-set minimum with `wait_for_headroom` + /// when the returned budget cannot support its next operation. + #[tracing::instrument( + name = "runtime_core.resident.reserve_up_to", + level = "trace", + skip_all + )] + pub fn reserve_up_to( + self: &Arc, + key: ResidentMemoryKeyV1, + preferred_bytes: NonZeroU64, + ceiling: u64, + ) -> Result { + self.pressure.measure_admission_bytes(); + let ceiling = ceiling.min(self.limit_bytes.get()); + let mut state = self.lock_state(); + let headroom = self.headroom_from_state(&state, ceiling); + let admitted = preferred_bytes + .get() + .min(headroom.available_bytes) + .min(ceiling.saturating_sub(state.used_bytes)); + if admitted == 0 { + let failure = if self.pressure.state().is_over_budget() + || headroom.observed_bytes >= ceiling.min(self.pressure.limit_bytes()) + { + ResidentMemoryAdmissionFailureV1::ObservedOverBudget { + observed_bytes: headroom.observed_bytes, + limit_bytes: self.pressure.limit_bytes(), + high_watermark_bytes: self.pressure.high_watermark_bytes(), + requested_bytes: preferred_bytes.get(), + floor_bytes: RESIDENT_MEMORY_PRESSURE_ADMISSION_FLOOR_BYTES_V1, + } + } else { + ResidentMemoryAdmissionFailureV1::ReservationCeiling { + used_bytes: state.used_bytes, + requested_bytes: preferred_bytes.get(), + limit_bytes: ceiling, + } + }; + drop(state); + return Err(failure); + } + // `admitted` is at most ceiling - used_bytes, so both ledger sums fit. + state.used_bytes += admitted; + *state.charges.entry(key.clone()).or_default() += admitted; + Ok(ResidentMemoryReservationV1 { + authority: Arc::clone(self), + key, + reserved_bytes: admitted, + }) + } + /// Reserves one process-shared component without fabricating a project, /// worktree, or code-generation owner. These reservations use the same /// process ceiling and RAII release authority as project generations. @@ -1596,12 +1652,20 @@ impl ProcessResidentMemoryV1 { /// [`Self::reserve`] holds them. pub fn headroom_below(&self, ceiling: u64) -> ResidentMemoryHeadroomV1 { self.pressure.measure_admission_bytes(); + self.headroom_from_state(&self.lock_state(), ceiling) + } + + fn headroom_from_state( + &self, + state: &ResidentMemoryStateV1, + ceiling: u64, + ) -> ResidentMemoryHeadroomV1 { let measured_ceiling = ceiling.min(self.pressure.limit_bytes()); let observed_bytes = self .pressure .observed_bytes_for_limit(measured_ceiling) .unwrap_or(0); - let used_bytes = self.lock_state().used_bytes; + let used_bytes = state.used_bytes; let available_bytes = if self.pressure.state().is_over_budget() { 0 } else { diff --git a/crates/tracedecay-runtime-core/src/resident_memory/tests.rs b/crates/tracedecay-runtime-core/src/resident_memory/tests.rs index 557293b4ec..10fe13f033 100644 --- a/crates/tracedecay-runtime-core/src/resident_memory/tests.rs +++ b/crates/tracedecay-runtime-core/src/resident_memory/tests.rs @@ -1335,6 +1335,35 @@ fn cgroup_headroom_respects_a_larger_but_fuller_ancestor() { ); } +#[test] +fn sized_reservations_charge_only_the_measured_cgroup_headroom() { + const MIB: u64 = 1024 * 1024; + let limit = bytes(800 * MIB); + let pressure = Arc::new(ResidentMemoryPressureV1::with_sampler( + limit, + Arc::new(|| { + Some(ProcessResidentSampleV1 { + resident_bytes: 100 * MIB, + unreclaimable_bytes: 100 * MIB, + swapped_bytes: 0, + cgroup_headroom_bytes: Some(200 * MIB), + }) + }), + )); + let authority = Arc::new(ProcessResidentMemoryV1::with_pressure(limit, pressure)); + let watermark = authority.admission_watermark_bytes(); + let headroom = authority.headroom_below(watermark); + assert_eq!(headroom.available_bytes, 200 * MIB); + let owner = key("project-a", "worktree-a", "generation-a", "text-build"); + let held = authority + .reserve_up_to(owner, bytes(headroom.available_bytes + 1), watermark) + .expect("the actual allocation fits without charging the 80 MiB watermark band"); + assert_eq!(held.reserved_bytes(), 200 * MIB); + assert_eq!(authority.snapshot().used_bytes, 200 * MIB); + drop(held); + assert_eq!(authority.snapshot().used_bytes, 0); +} + #[test] fn narrowed_budgets_and_waiters_share_cgroup_capacity_without_charging_peers() { const MIB: u64 = 1024 * 1024; @@ -1619,3 +1648,101 @@ fn allocator_reclaim_preserves_cgroup_pressure_until_sample_recovers() { .is_ok() ); } + +#[test] +fn sized_admission_remeasures_and_charges_the_supported_smaller_budget() { + const MIB: u64 = 1024 * 1024; + let measured = Arc::new(AtomicU64::new(300 * MIB)); + let sampled = Arc::clone(&measured); + let reads = Arc::new(AtomicU64::new(0)); + let sampled_reads = Arc::clone(&reads); + let pressure = Arc::new(ResidentMemoryPressureV1::with_sampler( + bytes(1000 * MIB), + Arc::new(move || { + sampled_reads.fetch_add(1, Ordering::SeqCst); + let observed = sampled.load(Ordering::SeqCst); + Some(ProcessResidentSampleV1 { + resident_bytes: observed, + unreclaimable_bytes: observed, + swapped_bytes: 0, + cgroup_headroom_bytes: None, + }) + }), + )); + let authority = Arc::new(ProcessResidentMemoryV1::with_pressure( + bytes(1000 * MIB), + Arc::clone(&pressure), + )); + let watermark = authority.admission_watermark_bytes(); + assert_eq!(watermark, 900 * MIB); + assert_eq!( + authority.headroom_below(watermark).available_bytes, + 600 * MIB + ); + measured.store(700 * MIB, Ordering::SeqCst); + let before = reads.load(Ordering::SeqCst); + let reservation = authority + .reserve_up_to( + key("project-a", "worktree-a", "generation-a", "text-build"), + bytes(600 * MIB), + watermark, + ) + .expect("the smaller build fits despite growth since planning"); + assert_eq!(reads.load(Ordering::SeqCst), before + 1); + assert_eq!(reservation.reserved_bytes(), 200 * MIB); + assert_eq!(authority.snapshot().used_bytes, 200 * MIB); + drop(reservation); + assert_eq!(authority.snapshot().used_bytes, 0); + + measured.store(950 * MIB, Ordering::SeqCst); + let refused = authority + .reserve_up_to( + key("project-a", "worktree-a", "generation-b", "text-build"), + bytes(600 * MIB), + watermark, + ) + .expect_err("sizing must still refuse measured pressure"); + assert!(refused.is_observed_over_budget()); + assert_eq!(authority.snapshot().used_bytes, 0); +} + +#[test] +fn sized_admissions_share_the_watermark_without_charging_its_margin() { + let pressure = Arc::new(ResidentMemoryPressureV1::with_sampler( + bytes(1000), + Arc::new(|| None), + )); + let authority = Arc::new(ProcessResidentMemoryV1::with_pressure( + bytes(1000), + pressure, + )); + let watermark = authority.admission_watermark_bytes(); + let owner = key("project-a", "worktree-a", "generation-a", "text-build"); + let mut reservations = (0..3) + .map(|_| { + authority + .reserve_up_to(owner.clone(), bytes(300), watermark) + .unwrap() + }) + .collect::>(); + assert_eq!(authority.snapshot().used_bytes, 900); + let refused = authority + .reserve_up_to(owner.clone(), bytes(300), watermark) + .unwrap_err(); + assert_eq!( + refused, + ResidentMemoryAdmissionFailureV1::ReservationCeiling { + used_bytes: 900, + requested_bytes: 300, + limit_bytes: 900, + } + ); + drop(reservations.pop()); + let maximum = authority + .reserve_up_to(owner, bytes(u64::MAX), watermark) + .expect("bounded sizing cannot overflow while reserving available bytes"); + assert_eq!(maximum.reserved_bytes(), 300); + assert_eq!(authority.snapshot().used_bytes, 900); + drop((reservations, maximum)); + assert_eq!(authority.snapshot().used_bytes, 0); +} From 54739fcbfada65f40fa2be3fcc0172d63ec0bfb6 Mon Sep 17 00:00:00 2001 From: Zack Jackson <25274700+ScriptedAlchemy@users.noreply.github.com> Date: Wed, 21 Oct 2026 04:42:38 -0700 Subject: [PATCH 6/7] fix(tests): retire native journey code-index workers --- .../src/native_integration/journey_tests.rs | 49 ++++++++++++++----- 1 file changed, 38 insertions(+), 11 deletions(-) diff --git a/crates/tracedecay-agent-hosts/src/native_integration/journey_tests.rs b/crates/tracedecay-agent-hosts/src/native_integration/journey_tests.rs index 1e2788add4..2a7f6684de 100644 --- a/crates/tracedecay-agent-hosts/src/native_integration/journey_tests.rs +++ b/crates/tracedecay-agent-hosts/src/native_integration/journey_tests.rs @@ -469,6 +469,25 @@ fn approval_for( .expect("approval") } +struct JourneyCodeIndex { + schedulers: CodeIndexSchedulerRegistryV1, +} + +impl JourneyCodeIndex { + async fn shutdown(&self) { + self.schedulers.shutdown().await; + } +} + +impl Drop for JourneyCodeIndex { + fn drop(&mut self) { + // A failed assertion must also release the fixture's native workers. + // The analysis port uses the daemon's scheduler authority; retiring + // the transaction registry does not stop that independent authority. + self.schedulers.cancel(); + } +} + async fn mount( database: RegisteredGlobalDbLeaseV1, repository_root: std::path::PathBuf, @@ -476,6 +495,7 @@ async fn mount( DaemonNativeIntegrationServiceRegistry, super::registry::DaemonNativeIntegrationOwner, Arc, + JourneyCodeIndex, ) { let project_id = ProjectId::new("project.native.journey").expect("project id"); let identity = IndexingIdentityV1::resolve(&repository_root).expect("indexing identity"); @@ -486,7 +506,10 @@ async fn mount( identity.head_ref().cloned(), ) .expect("analysis scope"); - let schedulers = CodeIndexSchedulerRegistryV1::new(1); + let code_index = JourneyCodeIndex { + schedulers: CodeIndexSchedulerRegistryV1::new(1), + }; + let schedulers = &code_index.schedulers; let index_store = repository_root .parent() .expect("repository parent") @@ -540,13 +563,10 @@ async fn mount( if let Err(error) = initial_generation { // The bounded observation was captured inside the original deadline; // reporting starts no new reads, admission, or readiness wait. - // A failed fixture still owns the worker it mounted. Signal its - // canonical cancellation before unwinding out of the temporary root. - schedulers.cancel(); panic!("initial code-index generation seat: {error}; {last_observation}"); } let analysis = Arc::new(DaemonNativeIntegrationAnalysisV1::new( - schedulers, + schedulers.clone(), analysis_scope, )); let registry = DaemonNativeIntegrationServiceRegistry::default(); @@ -564,7 +584,7 @@ async fn mount( ) .await .expect("mount native integration owner"); - (registry, owner, analysis) + (registry, owner, analysis, code_index) } async fn preflight( @@ -793,7 +813,8 @@ async fn checked_out_declared_stack_conflict_enqueues_without_moving_refs() { let database = runtime .registered_database_lease(HostAdmissionScope::Project) .expect("registered project database"); - let (registry, owner, _analysis) = mount(database.clone(), repository_root.clone()).await; + let (registry, owner, _analysis, code_index) = + mount(database.clone(), repository_root.clone()).await; let now = UtcMicros( i64::try_from( SystemTime::now() @@ -1038,6 +1059,7 @@ async fn checked_out_declared_stack_conflict_enqueues_without_moving_refs() { drop(stack_runtime); registry.shutdown().await.expect("shutdown owner registry"); + code_index.shutdown().await; } #[tokio::test(flavor = "multi_thread")] @@ -1066,7 +1088,8 @@ async fn independent_pair_applies_supported_modes_and_survives_daemon_restart() .registered_database_lease(HostAdmissionScope::Project) .expect("registered project database"); - let (registry, owner, analysis) = mount(database.clone(), repository_root.clone()).await; + let (registry, owner, analysis, code_index) = + mount(database.clone(), repository_root.clone()).await; let request_id = format!("request.native.journey.{index}"); let request = if index == 0 { declared_preflight_request(&repository_root, &database, mode, &request_id) @@ -1410,7 +1433,8 @@ async fn independent_pair_applies_supported_modes_and_survives_daemon_restart() } registry.shutdown().await.expect("shutdown owner registry"); - let (restarted_registry, restarted_owner, _analysis) = + code_index.shutdown().await; + let (restarted_registry, restarted_owner, _analysis, restarted_code_index) = mount(database, repository_root).await; let durable = restarted_owner .service() @@ -1428,6 +1452,7 @@ async fn independent_pair_applies_supported_modes_and_survives_daemon_restart() .shutdown() .await .expect("shutdown restarted owner registry"); + restarted_code_index.shutdown().await; } } @@ -1447,7 +1472,7 @@ async fn generated_only_change_requires_semantic_review() { let database = runtime .registered_database_lease(HostAdmissionScope::Project) .expect("registered project database"); - let (registry, owner, _analysis) = mount(database, repository_root.clone()).await; + let (registry, owner, _analysis, code_index) = mount(database, repository_root.clone()).await; let preview = preflight( owner, @@ -1473,6 +1498,7 @@ async fn generated_only_change_requires_semantic_review() { vec![NativeIntegrationAnalysisGapV1::WithheldSource] ); registry.shutdown().await.expect("shutdown owner registry"); + code_index.shutdown().await; } #[tokio::test(flavor = "multi_thread")] @@ -1492,7 +1518,7 @@ async fn foreign_destination_ref_drift_terminates_without_mutating_the_foreign_t let database = runtime .registered_database_lease(HostAdmissionScope::Project) .expect("registered project database"); - let (registry, owner, _analysis) = mount(database, repository_root.clone()).await; + let (registry, owner, _analysis, code_index) = mount(database, repository_root.clone()).await; let request = preflight_request( &repository_root, MechanicalIntegrationModeV1::FastForward, @@ -1571,6 +1597,7 @@ async fn foreign_destination_ref_drift_terminates_without_mutating_the_foreign_t ) ); registry.shutdown().await.expect("shutdown owner registry"); + code_index.shutdown().await; } /// Cleanup inspection of an owned checkout, asked while another thread walks From 90e5390c5c10e2e172c6b4896a5404bc6da77f67 Mon Sep 17 00:00:00 2001 From: Zack Jackson Date: Thu, 22 Oct 2026 03:28:27 -0700 Subject: [PATCH 7/7] ci(windows): preserve the required check name --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ae92de3541..86a53cae42 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -557,7 +557,8 @@ jobs: run: test "${{ needs.linux-test.result }}" = "success" windows-test: - name: Windows Bazel check + # Branch protection requires this check name. + name: Test Windows needs: scope-gate if: >- ${{ needs.scope-gate.outputs.run-heavy == 'true' &&