use std::sync::Arc;
use uuid::Uuid;
use super::WhatsAppState;
use crate::brain::agent::AgentService;
use crate::brain::agent::{MessageQueueCallback, QueuedUserMessage};
pub(crate) struct ActiveTurnGuard {
state: Arc<WhatsAppState>,
session_id: Uuid,
}
impl Drop for ActiveTurnGuard {
fn drop(&mut self) {
if let Ok(mut set) = self.state.active_turns.lock() {
set.remove(&self.session_id);
}
}
}
impl WhatsAppState {
pub(crate) fn try_begin_turn(self: &Arc<Self>, session_id: Uuid) -> Option<ActiveTurnGuard> {
let mut set = self.active_turns.lock().ok()?;
if !set.insert(session_id) {
return None;
}
Some(ActiveTurnGuard {
state: self.clone(),
session_id,
})
}
pub(crate) fn enqueue_turn_message(&self, session_id: Uuid, msg: QueuedUserMessage) {
crate::brain::agent::service::notify_queue::persist(session_id, &msg);
match self.turn_queue.lock() {
Ok(mut map) => map.entry(session_id).or_default().push_back(msg),
Err(e) => {
tracing::error!(
"WhatsApp: could not queue a follow-up for session {session_id}, \
it is dropped: {e}"
);
}
}
}
pub(crate) fn drain_turn_messages(&self, session_id: Uuid) -> Option<QueuedUserMessage> {
let mut map = self.turn_queue.lock().ok()?;
let queue = map.remove(&session_id)?;
for msg in &queue {
crate::brain::agent::service::notify_queue::clear_on_delivery(session_id, msg);
}
let msgs: Vec<QueuedUserMessage> = queue.into_iter().collect();
QueuedUserMessage::join(&msgs)
}
pub(crate) fn has_queued_turn_message(&self, session_id: Uuid) -> bool {
self.turn_queue
.lock()
.map(|map| map.get(&session_id).is_some_and(|q| !q.is_empty()))
.unwrap_or(false)
}
pub(crate) fn followup_queue_callback(self: &Arc<Self>) -> MessageQueueCallback {
let state = self.clone();
Arc::new(move |session_id: Uuid| {
let state = state.clone();
Box::pin(async move { state.drain_turn_messages(session_id) })
})
}
}
pub(crate) fn flush_after_turn(
state: Arc<WhatsAppState>,
agent: Arc<AgentService>,
session_id: Uuid,
) {
if !state.has_queued_turn_message(session_id) {
return;
}
tokio::spawn(async move {
let Some(joined) = state.drain_turn_messages(session_id) else {
return;
};
let Some(guard) = state.try_begin_turn(session_id) else {
state.enqueue_turn_message(session_id, joined);
return;
};
let Some(jid_str) = state.session_jid(session_id).await else {
tracing::warn!(
"WhatsApp: queued follow-up for session {session_id} has no chat jid; dropping"
);
return;
};
let Ok(jid) = jid_str.parse::<wacore_binary::jid::Jid>() else {
tracing::warn!("WhatsApp: bad jid '{jid_str}' for queued follow-up; dropping");
return;
};
let context_text = joined.context_text;
let result = agent
.send_push_turn(
session_id,
context_text,
None,
None,
None,
None,
"whatsapp",
Some(&jid_str),
None,
)
.await;
drop(guard);
match result {
Ok(response) => {
let Some(client) = state.client().await else {
tracing::warn!(
"WhatsApp: flushed follow-up answered but the transport is down; \
the answer for session {session_id} was not delivered"
);
return;
};
let (text, _imgs) = crate::utils::extract_img_markers(&response.content);
let text = crate::utils::sanitize::sanitize_outbound(&text);
if text.trim().is_empty() {
return;
}
for chunk in super::handler::split_message(&text, 4000) {
let out = waproto::whatsapp::Message {
conversation: Some(chunk.to_string()),
..Default::default()
};
if let Err(e) = client.send_message(jid.clone(), out).await {
tracing::warn!(
"WhatsApp: failed to deliver the flushed follow-up answer: {e}"
);
break;
}
}
}
Err(e) => {
tracing::warn!(
"WhatsApp: flushed follow-up turn failed for session {session_id}: {e}"
);
}
}
});
}