-
Notifications
You must be signed in to change notification settings - Fork 350
fix(channel): retry retrigger relay sends to avoid dropped final replies #582
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -40,6 +40,7 @@ use std::collections::HashSet; | |
| use std::sync::{Arc, Weak}; | ||
| use tokio::sync::broadcast; | ||
| use tokio::sync::{RwLock, mpsc}; | ||
| use tokio::time::{Duration, sleep}; | ||
|
|
||
| /// Shared cache of in-flight worker transcript steps, keyed by worker ID. | ||
| pub type LiveWorkerTranscripts = | ||
|
|
@@ -2959,8 +2960,7 @@ impl Channel { | |
| }) | ||
| } | ||
|
|
||
| /// Send outbound text and record send metrics. | ||
| async fn send_outbound_text(&self, text: String, error_context: &str) { | ||
| async fn send_outbound_text(&self, text: String, error_context: &str) -> bool { | ||
| match self.send_routed(OutboundResponse::Text(text)).await { | ||
| Ok(()) => { | ||
| #[cfg(feature = "metrics")] | ||
|
|
@@ -2971,6 +2971,7 @@ impl Channel { | |
| .with_label_values(&[&self.deps.agent_id, channel_type]) | ||
| .inc(); | ||
| } | ||
| true | ||
| } | ||
| Err(error) => { | ||
| #[cfg(feature = "metrics")] | ||
|
|
@@ -2982,8 +2983,57 @@ impl Channel { | |
| .inc(); | ||
| } | ||
| tracing::error!(%error, channel_id = %self.id, "{error_context}"); | ||
| false | ||
| } | ||
| } | ||
| } | ||
|
|
||
| async fn send_outbound_text_with_retry( | ||
| &self, | ||
| text: String, | ||
| error_context: &str, | ||
| max_attempts: usize, | ||
| ) -> bool { | ||
| let attempts = max_attempts.max(1); | ||
| for attempt in 1..=attempts { | ||
| if self | ||
| .send_outbound_text(text.clone(), error_context) | ||
| .await | ||
| { | ||
| if attempt > 1 { | ||
| tracing::info!( | ||
| channel_id = %self.id, | ||
| attempt, | ||
| attempts, | ||
| "outbound relay succeeded after retry" | ||
| ); | ||
| } | ||
| return true; | ||
| } | ||
|
|
||
| if attempt < attempts { | ||
| let delay_ms = match attempt { | ||
| 1 => 250, | ||
| 2 => 1_000, | ||
| _ => 2_000, | ||
| }; | ||
| tracing::warn!( | ||
| channel_id = %self.id, | ||
| attempt, | ||
| attempts, | ||
| delay_ms, | ||
| "outbound relay failed; retrying" | ||
| ); | ||
| sleep(Duration::from_millis(delay_ms)).await; | ||
| } | ||
| } | ||
|
|
||
| tracing::warn!( | ||
| channel_id = %self.id, | ||
| attempts, | ||
| "outbound relay failed after retries" | ||
| ); | ||
| false | ||
| } | ||
|
|
||
| /// Dispatch the LLM result: send fallback text, log errors, clean up typing. | ||
|
|
@@ -3061,11 +3111,24 @@ impl Channel { | |
| self.state | ||
| .conversation_logger | ||
| .log_bot_message(&self.state.channel_id, &final_text); | ||
| self.send_outbound_text( | ||
| final_text, | ||
| "failed to send retrigger fallback reply", | ||
| ) | ||
| .await; | ||
| let delivered = self | ||
| .send_outbound_text_with_retry( | ||
| final_text, | ||
| "failed to send retrigger fallback reply", | ||
| 3, | ||
| ) | ||
| .await; | ||
| if delivered { | ||
| replied_flag.store(true, std::sync::atomic::Ordering::Relaxed); | ||
| } else { | ||
| let _ = self | ||
| .send_outbound_text_with_retry( | ||
| "Delivery issue: your background result is preserved. Send 'continue' to replay it.".to_string(), | ||
| "failed to send relay failure backup notice", | ||
| 1, | ||
| ) | ||
| .await; | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift Add an injected-failure test for the new retrigger fallback state machine. These paths now couple retry timing, As per coding guidelines, "For changes in async/stateful paths (worker lifecycle, cancellation, retrigger, recall cache behavior), include explicit race/terminal-state reasoning in the PR summary and run targeted tests in addition to just Also applies to: 3192-3209, 3265-3270 🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
| } else { | ||
|
|
@@ -3126,11 +3189,24 @@ impl Channel { | |
| self.state | ||
| .conversation_logger | ||
| .log_bot_message(&self.state.channel_id, &final_text); | ||
| self.send_outbound_text( | ||
| final_text, | ||
| "failed to send retrigger fallback reply", | ||
| ) | ||
| .await; | ||
| let delivered = self | ||
| .send_outbound_text_with_retry( | ||
| final_text, | ||
| "failed to send retrigger fallback reply", | ||
| 3, | ||
| ) | ||
| .await; | ||
| if delivered { | ||
| replied_flag.store(true, std::sync::atomic::Ordering::Relaxed); | ||
| } else { | ||
| let _ = self | ||
| .send_outbound_text_with_retry( | ||
| "Delivery issue: your background result is preserved. Send 'continue' to replay it.".to_string(), | ||
| "failed to send relay failure backup notice", | ||
| 1, | ||
| ) | ||
| .await; | ||
| } | ||
| } | ||
| } | ||
| } else { | ||
|
|
@@ -3186,8 +3262,12 @@ impl Channel { | |
| Some(self.agent_display_name()), | ||
| tool_calls_json, | ||
| ); | ||
| self.send_outbound_text(final_text, "failed to send fallback reply") | ||
| .await; | ||
| self.send_outbound_text_with_retry( | ||
| final_text, | ||
| "failed to send fallback reply", | ||
| 2, | ||
| ) | ||
| .await; | ||
| } | ||
| } | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.