diff --git a/crates/buzz-acp/src/lib.rs b/crates/buzz-acp/src/lib.rs index 56f82d7eed..7956e7cde9 100644 --- a/crates/buzz-acp/src/lib.rs +++ b/crates/buzz-acp/src/lib.rs @@ -2854,7 +2854,19 @@ async fn tokio_main() -> Result<()> { // agent. let native_attempted = if matches!(signal, ControlSignal::Steer) { - if queue::edit_target_id(&event_for_steer).is_some() { + let is_edit = + queue::edit_target_id(&event_for_steer).is_some(); + if queue.has_native_steer_reservations( + buzz_event.channel_id, + ) { + tracing::debug!( + channel_id = %buzz_event.channel_id, + "native steer preparation already pending; preserving channel order via cancel+merge" + ); + false + } else if is_edit + && pool.native_steer_available(buzz_event.channel_id) + { let event_id = event_for_steer.id.to_hex(); let channel_id = buzz_event.channel_id; match reserve_native_edit_preparation( @@ -2892,6 +2904,12 @@ async fn tokio_main() -> Result<()> { false } } + } else if is_edit { + tracing::debug!( + channel_id = %buzz_event.channel_id, + "native edit steer unavailable before preparation; falling back to cancel+merge" + ); + false } else { let prompt_blocks = pool::format_native_steer_prompt_sync( diff --git a/crates/buzz-acp/src/pool.rs b/crates/buzz-acp/src/pool.rs index 51c7625ff5..fda4699378 100644 --- a/crates/buzz-acp/src/pool.rs +++ b/crates/buzz-acp/src/pool.rs @@ -699,6 +699,21 @@ impl AgentPool { &mut self.task_map } + /// Return whether the current channel task can accept a native steer now. + /// + /// This is a main-loop preflight for edit enrichment: avoid withholding an + /// edit and performing REST work when the task has no steer transport or + /// its capacity-one request slot is already occupied. Availability can + /// still change while enrichment runs, so callers must retain the normal + /// send-time fallback. + pub fn native_steer_available(&self, channel_id: Uuid) -> bool { + self.task_map + .values() + .find(|meta| meta.channel_id == Some(channel_id)) + .and_then(|meta| meta.steer_tx.as_ref()) + .is_some_and(|tx| !tx.is_closed() && tx.capacity() > 0) + } + /// Try to send a goose-native steer request to the in-flight task for /// `channel_id`. /// diff --git a/crates/buzz-acp/src/queue.rs b/crates/buzz-acp/src/queue.rs index d9fb1d38b6..ba99f47e75 100644 --- a/crates/buzz-acp/src/queue.rs +++ b/crates/buzz-acp/src/queue.rs @@ -751,6 +751,13 @@ impl EventQueue { true } + /// Return whether this channel has any native-steer reservation in flight. + pub fn has_native_steer_reservations(&self, channel_id: Uuid) -> bool { + self.withheld_native_steer + .get(&channel_id) + .is_some_and(|entries| !entries.is_empty()) + } + /// Return whether this exact event still owns a native-steer reservation. /// /// Async edit preparation uses this at completion time: deadline recovery, @@ -4856,6 +4863,8 @@ mod tests { q.in_flight_deadlines.insert(ch, Instant::now()); q.in_flight_batch_sizes.insert(ch, 1); assert!(q.mark_native_steer_pending(ch, &event_id)); + assert!(q.has_native_steer_reservations(ch)); + assert!(q.has_native_steer_reservation(ch, &event_id)); // Force the in-flight deadline to be in the past, simulating the // steer ack never arriving and the read loop hanging long enough @@ -4873,6 +4882,8 @@ mod tests { // The withheld event has been moved back to `queues[ch]`. assert!(q.withheld_native_steer.is_empty()); + assert!(!q.has_native_steer_reservations(ch)); + assert!(!q.has_native_steer_reservation(ch, &event_id)); assert_eq!(pending_count(&q), 1); // Normal dispatch delivers it.