fix: add thinking indicator and 30s timeout to prevent deadlock
All checks were successful
BotServer CI/CD / build (push) Successful in 3m16s

This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2026-04-13 21:40:50 -03:00
parent 3ddcc5a1d1
commit 200b026efe

View file

@ -6,6 +6,7 @@ pub mod tool_context;
use tool_context::get_session_tools; use tool_context::get_session_tools;
pub mod tool_executor; pub mod tool_executor;
use tool_executor::ToolExecutor; use tool_executor::ToolExecutor;
use std::sync::atomic::Ordering;
#[cfg(feature = "llm")] #[cfg(feature = "llm")]
use crate::core::config::ConfigManager; use crate::core::config::ConfigManager;
@ -1047,44 +1048,68 @@ impl BotOrchestrator {
analysis_buffer.push_str(&chunk); analysis_buffer.push_str(&chunk);
// Safety: if we've been in analysis > 30 seconds without completion, force exit
// This prevents getting stuck if model doesn't send closing tags
const ANALYSIS_TIMEOUT_SECS: u64 = 30;
static ANALYSIS_START_TIME: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
if !in_analysis && handler.has_analysis_markers(&analysis_buffer) { if !in_analysis && handler.has_analysis_markers(&analysis_buffer) {
in_analysis = true; in_analysis = true;
ANALYSIS_START_TIME.store(
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs(),
Ordering::SeqCst,
);
log::debug!( log::debug!(
"Detected start of thinking/analysis content for model {}", "Detected start of thinking/analysis content for model {}",
model model
); );
let processed = handler.process_content(&analysis_buffer); // Send thinking indicator (not the filtered content!)
if !processed.is_empty() && processed != analysis_buffer { let thinking_msg = BotResponse {
full_response.push_str(&processed); bot_id: message.bot_id.clone(),
user_id: message.user_id.clone(),
session_id: message.session_id.clone(),
channel: message.channel.clone(),
content: "🤔 Pensando...".to_string(),
message_type: MessageType::BOT_RESPONSE,
stream_token: None,
is_complete: false,
suggestions: Vec::new(),
context_name: None,
context_length: 0,
context_max_length: 0,
};
let response = BotResponse { if response_tx.send(thinking_msg).await.is_err() {
bot_id: message.bot_id.clone(), warn!("Response channel closed");
user_id: message.user_id.clone(), break;
session_id: message.session_id.clone(), }
channel: message.channel.clone(), continue; // Skip sending raw content during thinking
content: processed, }
message_type: MessageType::BOT_RESPONSE,
stream_token: None, // Check timeout
is_complete: false, if in_analysis {
suggestions: Vec::new(), let elapsed = std::time::SystemTime::now()
context_name: None, .duration_since(std::time::UNIX_EPOCH)
context_length: 0, .unwrap()
context_max_length: 0, .as_secs()
}; - ANALYSIS_START_TIME.load(Ordering::SeqCst);
if elapsed > ANALYSIS_TIMEOUT_SECS {
if response_tx.send(response).await.is_err() { warn!("Analysis timeout after {}s, forcing exit", elapsed);
warn!("Response channel closed"); in_analysis = false;
break;
}
} }
continue;
} }
if in_analysis && handler.is_analysis_complete(&analysis_buffer) { if in_analysis && handler.is_analysis_complete(&analysis_buffer) {
in_analysis = false; in_analysis = false;
trace!("Detected end of thinking for model {}", model); trace!("Detected end of thinking for model {}", model);
// Clear thinking indicator - we'll send empty content that frontend understands
// Actually skip this - the next content will replace the thinking indicator
let processed = handler.process_content(&analysis_buffer); let processed = handler.process_content(&analysis_buffer);
if !processed.is_empty() { if !processed.is_empty() {
full_response.push_str(&processed); full_response.push_str(&processed);