pub mod message; pub use crate::domain::messages::ContentBlock; pub use message::{ ChatMessage, InboundMessage, MediaItem, OutboundMessage, SYSTEM_CONTEXT_AGENT_PROMPT, SYSTEM_CONTEXT_HISTORY_COMPACTION, SYSTEM_CONTEXT_SCHEDULED_PROMPT, }; use std::sync::Arc; use tokio::sync::{Mutex, mpsc}; // ============================================================================ // MessageBus - async inbound/outbound queues // ============================================================================ pub struct MessageBus { inbound_tx: mpsc::Sender, outbound_tx: mpsc::Sender, inbound_rx: Mutex>, outbound_rx: Mutex>, } impl MessageBus { /// Create a new MessageBus with the given channel capacity pub fn new(capacity: usize) -> Arc { let (inbound_tx, inbound_rx) = mpsc::channel(capacity); let (outbound_tx, outbound_rx) = mpsc::channel(capacity); Arc::new(Self { inbound_tx, outbound_tx, inbound_rx: Mutex::new(inbound_rx), outbound_rx: Mutex::new(outbound_rx), }) } /// Publish a message to the inbound queue pub async fn publish_inbound(&self, msg: InboundMessage) -> Result<(), BusError> { tracing::debug!( channel = %msg.channel, sender = %msg.sender_id, chat_id = %msg.chat_id, trace_id = %msg.trace_id, content_len = %msg.content.len(), media_count = %msg.media.len(), "Bus: publishing inbound message" ); self.inbound_tx .send(msg) .await .map_err(|_| BusError::Closed) } /// Consume a message from the inbound queue. /// Returns `None` when the channel is closed (all senders dropped). pub async fn consume_inbound(&self) -> Option { let msg = self.inbound_rx.lock().await.recv().await?; tracing::debug!( channel = %msg.channel, sender = %msg.sender_id, chat_id = %msg.chat_id, trace_id = %msg.trace_id, "Bus: consuming inbound message" ); Some(msg) } /// Publish a message to the outbound queue. /// /// Uses `try_send` (non-blocking): if the queue is full, the message is /// dropped immediately with a warning. This ensures the agent loop is never /// blocked by slow or disconnected display consumers. Persistent state is /// unaffected — messages are stored in SQLite independently. pub async fn publish_outbound(&self, msg: OutboundMessage) -> Result<(), BusError> { tracing::debug!( channel = %msg.channel, chat_id = %msg.chat_id, trace_id = %msg.trace_id, content_len = %msg.content.len(), "Bus: publishing outbound message" ); match self.outbound_tx.try_send(msg) { Ok(()) => Ok(()), Err(tokio::sync::mpsc::error::TrySendError::Full(msg)) => { tracing::warn!( channel = %msg.channel, chat_id = %msg.chat_id, trace_id = %msg.trace_id, "Outbound bus full, dropping message" ); Err(BusError::Dropped) } Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => Err(BusError::Closed), } } /// Consume an outbound message from the outbound queue. /// Returns `None` when the channel is closed (all senders dropped). pub async fn consume_outbound(&self) -> Option { self.outbound_rx.lock().await.recv().await } } // ============================================================================ // BusError // ============================================================================ #[derive(Debug)] pub enum BusError { Closed, Dropped, } impl std::fmt::Display for BusError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { BusError::Closed => write!(f, "Bus channel closed"), BusError::Dropped => write!(f, "Bus full, message dropped"), } } } impl std::error::Error for BusError {}