Skip to main content

atm_protocol/
message.rs

1//! Protocol message types for daemon communication.
2
3use crate::version::ProtocolVersion;
4use atm_core::{SessionId, SessionView};
5use serde::{Deserialize, Serialize};
6
7/// Message types that can be sent by clients to the daemon.
8#[derive(Debug, Clone, Serialize, Deserialize)]
9#[serde(tag = "type", rename_all = "snake_case")]
10pub enum MessageType {
11    /// Client handshake/connection request
12    Connect {
13        /// Client identifier (optional)
14        #[serde(skip_serializing_if = "Option::is_none")]
15        client_id: Option<String>,
16    },
17
18    /// Status line update from Claude Code
19    StatusUpdate {
20        /// The raw status line JSON (to be parsed)
21        data: serde_json::Value,
22    },
23
24    /// Hook event from Claude Code
25    HookEvent {
26        /// The raw hook event JSON (to be parsed)
27        data: serde_json::Value,
28    },
29
30    /// Request current session list
31    ListSessions,
32
33    /// Subscribe to session updates
34    Subscribe {
35        /// Optional filter by session ID
36        #[serde(skip_serializing_if = "Option::is_none")]
37        session_id: Option<SessionId>,
38    },
39
40    /// Unsubscribe from updates
41    Unsubscribe,
42
43    /// Ping to check connection
44    Ping {
45        /// Sequence number for matching pong response
46        seq: u64,
47    },
48
49    /// Client disconnecting gracefully
50    Disconnect,
51
52    /// Request daemon to discover existing Claude sessions
53    Discover,
54}
55
56/// Messages sent from client to daemon.
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct ClientMessage {
59    /// Protocol version
60    pub protocol_version: ProtocolVersion,
61
62    /// Message payload
63    #[serde(flatten)]
64    pub message: MessageType,
65}
66
67impl ClientMessage {
68    /// Creates a new client message with current protocol version.
69    pub fn new(message: MessageType) -> Self {
70        Self {
71            protocol_version: ProtocolVersion::CURRENT,
72            message,
73        }
74    }
75
76    /// Creates a connect message.
77    pub fn connect(client_id: Option<String>) -> Self {
78        Self::new(MessageType::Connect { client_id })
79    }
80
81    /// Creates a status update message.
82    pub fn status_update(data: serde_json::Value) -> Self {
83        Self::new(MessageType::StatusUpdate { data })
84    }
85
86    /// Creates a hook event message.
87    pub fn hook_event(data: serde_json::Value) -> Self {
88        Self::new(MessageType::HookEvent { data })
89    }
90
91    /// Creates a list sessions request.
92    pub fn list_sessions() -> Self {
93        Self::new(MessageType::ListSessions)
94    }
95
96    /// Creates a subscribe message.
97    pub fn subscribe(session_id: Option<SessionId>) -> Self {
98        Self::new(MessageType::Subscribe { session_id })
99    }
100
101    /// Creates a ping message.
102    pub fn ping(seq: u64) -> Self {
103        Self::new(MessageType::Ping { seq })
104    }
105
106    /// Creates a disconnect message.
107    pub fn disconnect() -> Self {
108        Self::new(MessageType::Disconnect)
109    }
110
111    /// Creates a discover message.
112    pub fn discover() -> Self {
113        Self::new(MessageType::Discover)
114    }
115}
116
117/// Messages sent from daemon to clients.
118#[derive(Debug, Clone, Serialize, Deserialize)]
119#[serde(tag = "type", rename_all = "snake_case")]
120pub enum DaemonMessage {
121    /// Connection accepted
122    Connected {
123        /// Daemon's protocol version
124        protocol_version: ProtocolVersion,
125        /// Assigned client ID
126        client_id: String,
127    },
128
129    /// Connection rejected (version mismatch, etc.)
130    Rejected {
131        /// Reason for rejection
132        reason: String,
133        /// Daemon's protocol version (for client to upgrade)
134        protocol_version: ProtocolVersion,
135    },
136
137    /// Full session list response
138    SessionList {
139        /// All current sessions
140        sessions: Vec<SessionView>,
141    },
142
143    /// Session was created or updated
144    SessionUpdated {
145        /// The updated session
146        session: Box<SessionView>,
147    },
148
149    /// Session was removed (stale, disconnected)
150    SessionRemoved {
151        /// ID of the removed session
152        session_id: SessionId,
153    },
154
155    /// Pong response to ping
156    Pong {
157        /// Sequence number from ping
158        seq: u64,
159    },
160
161    /// Error response
162    Error {
163        /// Error message
164        message: String,
165        /// Error code (optional)
166        #[serde(skip_serializing_if = "Option::is_none")]
167        code: Option<String>,
168    },
169
170    /// Discovery completed response
171    DiscoveryComplete {
172        /// Number of sessions discovered
173        discovered: u32,
174        /// Number of discovery failures (logged at debug)
175        failed: u32,
176    },
177}
178
179impl DaemonMessage {
180    /// Creates a connected response.
181    pub fn connected(client_id: String) -> Self {
182        Self::Connected {
183            protocol_version: ProtocolVersion::CURRENT,
184            client_id,
185        }
186    }
187
188    /// Creates a rejected response.
189    pub fn rejected(reason: &str) -> Self {
190        Self::Rejected {
191            reason: reason.to_string(),
192            protocol_version: ProtocolVersion::CURRENT,
193        }
194    }
195
196    /// Creates a session list response.
197    pub fn session_list(sessions: Vec<SessionView>) -> Self {
198        Self::SessionList { sessions }
199    }
200
201    /// Creates a session updated notification.
202    pub fn session_updated(session: SessionView) -> Self {
203        Self::SessionUpdated {
204            session: Box::new(session),
205        }
206    }
207
208    /// Creates a session removed notification.
209    pub fn session_removed(session_id: SessionId) -> Self {
210        Self::SessionRemoved { session_id }
211    }
212
213    /// Creates a pong response.
214    pub fn pong(seq: u64) -> Self {
215        Self::Pong { seq }
216    }
217
218    /// Creates an error response.
219    pub fn error(message: &str) -> Self {
220        Self::Error {
221            message: message.to_string(),
222            code: None,
223        }
224    }
225
226    /// Creates an error response with code.
227    pub fn error_with_code(message: &str, code: &str) -> Self {
228        Self::Error {
229            message: message.to_string(),
230            code: Some(code.to_string()),
231        }
232    }
233
234    /// Creates a discovery complete response.
235    pub fn discovery_complete(discovered: u32, failed: u32) -> Self {
236        Self::DiscoveryComplete { discovered, failed }
237    }
238}
239
240#[cfg(test)]
241mod tests {
242    use super::*;
243
244    #[test]
245    fn test_client_message_serialization() {
246        let msg = ClientMessage::ping(42);
247        let json = serde_json::to_string(&msg).unwrap();
248        assert!(json.contains("\"type\":\"ping\""));
249        assert!(json.contains("\"seq\":42"));
250    }
251
252    #[test]
253    fn test_daemon_message_serialization() {
254        let msg = DaemonMessage::connected("client-123".to_string());
255        let json = serde_json::to_string(&msg).unwrap();
256        assert!(json.contains("\"type\":\"connected\""));
257        assert!(json.contains("\"client_id\":\"client-123\""));
258    }
259
260    #[test]
261    fn test_message_roundtrip() {
262        let original = ClientMessage::subscribe(Some(SessionId::new("test-session")));
263        let json = serde_json::to_string(&original).unwrap();
264        let parsed: ClientMessage = serde_json::from_str(&json).unwrap();
265
266        match parsed.message {
267            MessageType::Subscribe { session_id } => {
268                assert_eq!(
269                    session_id.map(|s| s.as_str().to_string()),
270                    Some("test-session".to_string())
271                );
272            }
273            _ => panic!("Expected Subscribe message"),
274        }
275    }
276}