//! Per-connection state. Server→client messages fan out to a connection-scoped //! stream, a per-session stream for each active `sessionId`, and an //! all-outbound stream consumed by WebSocket. use std::{ collections::{HashMap, VecDeque}, sync::Arc, }; use anyhow::Result; use serde_json::Value; use tokio::sync::{broadcast, mpsc, Mutex, RwLock}; use tokio_util::compat::{TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt}; use tracing::{error, info, trace, warn}; use crate::acp::adapters::{ReceiverToAsyncRead, SenderToAsyncWrite}; use crate::acp::server_factory::AcpServer; const OUTBOUND_BROADCAST_CAPACITY: usize = 1024; /// Buffers messages emitted before a subscriber attaches (e.g. session /// notifications that land before the client opens the session GET stream). const PRE_SUBSCRIBE_BUFFER_CAPACITY: usize = 1024; #[derive(Clone, Debug)] pub(crate) enum ResponseRoute { Connection, Session(String), } struct OutboundStream { tx: broadcast::Sender, pre_subscribe_buffer: Mutex>>, } impl OutboundStream { fn new() -> Self { let (tx, _) = broadcast::channel(OUTBOUND_BROADCAST_CAPACITY); Self { tx, pre_subscribe_buffer: Mutex::new(Some(VecDeque::new())), } } async fn push(&self, msg: String) { let mut guard = self.pre_subscribe_buffer.lock().await; match guard.as_mut() { Some(buf) => { if buf.len() >= PRE_SUBSCRIBE_BUFFER_CAPACITY { warn!( "Pre-subscribe buffer full ({} messages); dropping oldest", PRE_SUBSCRIBE_BUFFER_CAPACITY ); buf.pop_front(); } buf.push_back(msg); } None => { drop(guard); let _ = self.tx.send(msg); } } } async fn subscribe_with_replay(&self) -> (Vec, broadcast::Receiver) { let mut guard = self.pre_subscribe_buffer.lock().await; let receiver = self.tx.subscribe(); let replay = guard.take().map(Vec::from).unwrap_or_default(); (replay, receiver) } } pub(crate) struct Connection { pub to_agent_tx: mpsc::Sender, /// Consumed once by `handle_initialize` to read the synchronous initialize /// response before the router task takes over. pub init_receiver: Mutex>>, pub init_complete: Mutex, pub agent_handle: tokio::task::JoinHandle<()>, pub router_handle: Mutex>>, connection_stream: Arc, session_streams: Arc>>>, all_outbound: Arc, pending_routes: Arc>>, } pub(crate) struct ConnectionRegistry { pub server: Arc, connections: RwLock>>, } impl ConnectionRegistry { pub fn new(server: Arc) -> Self { Self { server, connections: RwLock::new(HashMap::new()), } } pub async fn create_connection(&self) -> Result<(String, Arc)> { let (to_agent_tx, to_agent_rx) = mpsc::channel::(256); let (from_agent_tx, from_agent_rx) = mpsc::unbounded_channel::(); let agent = self.server.create_agent().await?; let connection_id = uuid::Uuid::new_v4().to_string(); let read_stream = ReceiverToAsyncRead::new(to_agent_rx); let write_stream = SenderToAsyncWrite::new(from_agent_tx); let fut = crate::acp::server::serve(agent, read_stream.compat(), write_stream.compat_write()); let conn_id_for_task = connection_id.clone(); let agent_handle = tokio::spawn(async move { if let Err(e) = fut.await { error!(connection_id = %conn_id_for_task, "ACP agent task error: {}", e); } }); let connection = Arc::new(Connection { to_agent_tx, init_receiver: Mutex::new(Some(from_agent_rx)), init_complete: Mutex::new(false), agent_handle, router_handle: Mutex::new(None), connection_stream: Arc::new(OutboundStream::new()), session_streams: Arc::new(RwLock::new(HashMap::new())), all_outbound: Arc::new(OutboundStream::new()), pending_routes: Arc::new(Mutex::new(HashMap::new())), }); self.connections .write() .await .insert(connection_id.clone(), connection.clone()); info!(connection_id = %connection_id, "Connection created"); Ok((connection_id, connection)) } pub async fn get(&self, connection_id: &str) -> Option> { self.connections.read().await.get(connection_id).cloned() } pub async fn remove(&self, connection_id: &str) -> Option> { self.connections.write().await.remove(connection_id) } } impl Connection { pub async fn start_router(self: &Arc) { let mut complete = self.init_complete.lock().await; if *complete { return; } let Some(mut rx) = self.init_receiver.lock().await.take() else { return; }; let me = self.clone(); let handle = tokio::spawn(async move { while let Some(msg) = rx.recv().await { me.route_outbound(msg).await; } }); *self.router_handle.lock().await = Some(handle); *complete = true; } async fn route_outbound(self: &Arc, msg: String) { self.all_outbound.push(msg.clone()).await; let parsed: Option = serde_json::from_str(&msg).ok(); let target = match parsed.as_ref() { Some(v) => self.classify(v).await, None => Target::Connection, }; match target { Target::Connection => { trace!(target = "connection", "→ connection-scoped stream"); self.connection_stream.push(msg).await; } Target::Session(sid) => { trace!(target = %sid, "→ session-scoped stream"); let stream = self.get_or_create_session_stream(&sid).await; stream.push(msg).await; } } } async fn classify(self: &Arc, v: &Value) -> Target { let has_method = v.get("method").is_some(); let has_id = v.get("id").is_some(); let has_result_or_error = v.get("result").is_some() || v.get("error").is_some(); if has_method { if let Some(sid) = extract_session_id_from_params(v) { return Target::Session(sid); } return Target::Connection; } if has_id && has_result_or_error { let id = v.get("id").cloned().unwrap_or(Value::Null); let route = self.pending_routes.lock().await.remove(&id); return match route { Some(ResponseRoute::Session(sid)) => Target::Session(sid), Some(ResponseRoute::Connection) | None => Target::Connection, }; } Target::Connection } pub async fn record_pending_route(&self, id: Value, route: ResponseRoute) { if id.is_null() { return; } self.pending_routes.lock().await.insert(id, route); } pub async fn subscribe_connection_stream(&self) -> (Vec, broadcast::Receiver) { self.connection_stream.subscribe_with_replay().await } pub async fn subscribe_session_stream( &self, session_id: &str, ) -> Option<(Vec, broadcast::Receiver)> { let stream = self.session_streams.read().await.get(session_id).cloned()?; Some(stream.subscribe_with_replay().await) } pub async fn ensure_session(&self, session_id: &str) { self.get_or_create_session_stream(session_id).await; } async fn get_or_create_session_stream(&self, session_id: &str) -> Arc { if let Some(s) = self.session_streams.read().await.get(session_id) { return s.clone(); } let mut w = self.session_streams.write().await; w.entry(session_id.to_string()) .or_insert_with(|| Arc::new(OutboundStream::new())) .clone() } pub async fn subscribe_all_outbound(&self) -> (Vec, broadcast::Receiver) { self.all_outbound.subscribe_with_replay().await } pub async fn shutdown(&self) { self.agent_handle.abort(); if let Some(h) = self.router_handle.lock().await.take() { h.abort(); } } } #[derive(Debug)] enum Target { Connection, Session(String), } fn extract_session_id_from_params(v: &Value) -> Option { v.get("params") .and_then(|p| p.get("sessionId")) .and_then(|s| s.as_str()) .map(|s| s.to_string()) } #[cfg(test)] mod tests { use super::*; use std::time::Duration; use tokio::time::timeout; fn fake_connection() -> (Arc, mpsc::UnboundedSender) { let (to_agent_tx, _to_agent_rx) = mpsc::channel::(256); let (from_agent_tx, from_agent_rx) = mpsc::unbounded_channel::(); let agent_handle = tokio::spawn(async { std::future::pending::<()>().await; }); let connection = Arc::new(Connection { to_agent_tx, init_receiver: Mutex::new(Some(from_agent_rx)), init_complete: Mutex::new(false), agent_handle, router_handle: Mutex::new(None), connection_stream: Arc::new(OutboundStream::new()), session_streams: Arc::new(RwLock::new(HashMap::new())), all_outbound: Arc::new(OutboundStream::new()), pending_routes: Arc::new(Mutex::new(HashMap::new())), }); (connection, from_agent_tx) } #[tokio::test] async fn buffers_connection_scoped_messages_before_first_subscribe() { let (conn, agent_tx) = fake_connection(); conn.start_router().await; agent_tx .send(r#"{"id":1,"result":{"capabilities":{}}}"#.to_string()) .unwrap(); tokio::time::sleep(Duration::from_millis(20)).await; let (replay, _rx) = conn.subscribe_connection_stream().await; assert_eq!(replay.len(), 1); assert!(replay[0].contains("\"capabilities\"")); conn.shutdown().await; } #[tokio::test] async fn routes_session_scoped_notification_to_session_stream() { let (conn, agent_tx) = fake_connection(); conn.start_router().await; conn.ensure_session("sess_abc").await; let (_, mut rx) = conn.subscribe_session_stream("sess_abc").await.unwrap(); agent_tx .send( r#"{"method":"session/update","params":{"sessionId":"sess_abc","update":{}}}"# .to_string(), ) .unwrap(); let got = timeout(Duration::from_secs(1), rx.recv()) .await .unwrap() .unwrap(); assert!(got.contains("session/update")); let (replay, _) = conn.subscribe_connection_stream().await; assert!( replay.is_empty(), "connection stream should not have session-scoped messages" ); conn.shutdown().await; } #[tokio::test] async fn routes_response_using_pending_route_table() { let (conn, agent_tx) = fake_connection(); conn.start_router().await; conn.ensure_session("sess_xyz").await; conn.record_pending_route( Value::from(42), ResponseRoute::Session("sess_xyz".to_string()), ) .await; let (_, mut rx) = conn.subscribe_session_stream("sess_xyz").await.unwrap(); agent_tx .send(r#"{"id":42,"result":{"stopReason":"end_turn"}}"#.to_string()) .unwrap(); let got = timeout(Duration::from_secs(1), rx.recv()) .await .unwrap() .unwrap(); assert!(got.contains("\"stopReason\"")); conn.shutdown().await; } #[tokio::test] async fn websocket_all_outbound_sees_everything() { let (conn, agent_tx) = fake_connection(); conn.start_router().await; agent_tx .send(r#"{"id":1,"result":{}}"#.to_string()) .unwrap(); agent_tx .send(r#"{"method":"session/update","params":{"sessionId":"s1"}}"#.to_string()) .unwrap(); tokio::time::sleep(Duration::from_millis(20)).await; let (replay, _all_rx) = conn.subscribe_all_outbound().await; assert_eq!(replay.len(), 2); assert!(replay[0].contains("\"id\":1")); assert!(replay[1].contains("session/update")); conn.shutdown().await; } #[tokio::test] async fn unknown_session_subscribe_returns_none() { let (conn, _agent_tx) = fake_connection(); conn.start_router().await; assert!(conn.subscribe_session_stream("nope").await.is_none()); conn.shutdown().await; } #[tokio::test] async fn pre_subscribe_buffer_is_bounded() { let (conn, agent_tx) = fake_connection(); conn.start_router().await; for i in 0..(PRE_SUBSCRIBE_BUFFER_CAPACITY + 50) { agent_tx .send(format!(r#"{{"id":{},"result":{{}}}}"#, i)) .unwrap(); } tokio::time::sleep(Duration::from_millis(50)).await; let (replay, _rx) = conn.subscribe_connection_stream().await; assert_eq!(replay.len(), PRE_SUBSCRIBE_BUFFER_CAPACITY); } }