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