From 0956a58f5d84886f7dd4fd177825d027e06258de Mon Sep 17 00:00:00 2001 From: Sanjay Ramadugu Date: Fri, 21 Aug 2026 13:42:50 -0700 Subject: [PATCH 1/2] test(buzz-acp): pin idle-pool completion-clock contract Add a note_turn_settled seam called unconditionally from the PoolEvent::Result and PoolEvent::Panic arms, and pin the contract in tests: a turn that settles after 2x the idle bound must re-anchor the clock so the pool is not immediately sleep-due, while an idle pool with no settled turn is still torn down on schedule. The reanchor test fails at this commit; the next commit supplies the bump. Signed-off-by: Sanjay Ramadugu --- crates/buzz-acp/src/lib.rs | 46 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/crates/buzz-acp/src/lib.rs b/crates/buzz-acp/src/lib.rs index 1352b31cad8..81f99e0eb2d 100644 --- a/crates/buzz-acp/src/lib.rs +++ b/crates/buzz-acp/src/lib.rs @@ -1632,6 +1632,15 @@ fn inactivity_expired( !bound.is_zero() && !turn_in_flight && now.duration_since(last_activity) >= bound } +/// Re-anchor point for the idle-reaper clock when a pool turn settles. +/// +/// Called unconditionally by the `PoolEvent::Result` and `PoolEvent::Panic` +/// arms before their trailing `dispatch_pending`, so the clock reflects the +/// most recent turn completion — not only the last dispatch. +fn note_turn_settled(last_activity: &mut tokio::time::Instant) { + let _ = last_activity; +} + /// Whether a woken lazy pool may be torn back down to the empty-slot state. /// /// True only when the pool is ready, the idle bound has elapsed with no @@ -1726,6 +1735,41 @@ mod idle_pool_sleep_tests { )); } + // Decision-level pin for block/buzz#6378: a turn that ran from T to + // T + 2B and has just settled (empty queue, nothing in flight) must + // re-anchor the clock at completion, so the pool is not sleep-due at + // T + 2B even though the dispatch-time anchor is long stale. + #[tokio::test(start_paused = true)] + async fn completed_long_turn_reanchors_clock_not_sleep_due() { + let bound = Duration::from_secs(30); + let turn_start = tokio::time::Instant::now(); + tokio::time::advance(2 * bound).await; + + let mut last_activity = turn_start; + note_turn_settled(&mut last_activity); + + assert!(!idle_pool_sleep_due( + true, + last_activity, + tokio::time::Instant::now(), + bound, + false, + false, + false, + false + )); + } + + // The bump must not defeat teardown: with no completed turn since the + // anchor T, an idle pool is still sleep-due once the bound elapses. + #[test] + fn idle_pool_without_completion_still_sleep_due_after_bound() { + let (last, now, bound) = ready_after_bound(); + assert!(idle_pool_sleep_due( + true, last, now, bound, false, false, false, false + )); + } + #[test] fn zero_bound_never_sleeps() { let (last, now, _) = ready_after_bound(); @@ -3130,6 +3174,7 @@ async fn tokio_main() -> Result<()> { match pool_event { Some(PoolEvent::Result(result)) => { + note_turn_settled(&mut last_activity); // Stop typing indicator for the completed channel. if let PromptSource::Channel(ch) = &result.source { typing_channels.remove(ch); @@ -3172,6 +3217,7 @@ async fn tokio_main() -> Result<()> { } } Some(PoolEvent::Panic(join_error)) => { + note_turn_settled(&mut last_activity); tracing::error!("agent task panicked: {join_error}"); recover_panicked_agent( &mut pool, From a09fd9fe51d92ef61ca919a47ed1c045744dba15 Mon Sep 17 00:00:00 2001 From: Sanjay Ramadugu Date: Fri, 21 Aug 2026 13:43:11 -0700 Subject: [PATCH 2/2] fix(buzz-acp): treat settled turns as activity note_turn_settled now advances last_activity, so the idle-pool reaper anchors on turn completion and panic recovery instead of dispatch time only. A long-running turn that finishes inside the idle bound is no longer torn down on the first reaper tick after it completes. Signed-off-by: Sanjay Ramadugu --- crates/buzz-acp/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/buzz-acp/src/lib.rs b/crates/buzz-acp/src/lib.rs index 81f99e0eb2d..e658012e2e2 100644 --- a/crates/buzz-acp/src/lib.rs +++ b/crates/buzz-acp/src/lib.rs @@ -1638,7 +1638,7 @@ fn inactivity_expired( /// arms before their trailing `dispatch_pending`, so the clock reflects the /// most recent turn completion — not only the last dispatch. fn note_turn_settled(last_activity: &mut tokio::time::Instant) { - let _ = last_activity; + *last_activity = tokio::time::Instant::now(); } /// Whether a woken lazy pool may be torn back down to the empty-slot state.