Skip to main content

claude_session_types/events/
system.rs

1//! System event types (Level 6)
2//!
3//! System events are rare but important for understanding session structure.
4//! They mark boundaries (compaction), errors, and system-level state changes.
5//!
6//! # System Event Subtypes
7//!
8//! ```text
9//! System Events (.subtype when .type == 'system')
10//! ├── compact_boundary       - Conversation compaction point (~150k tokens)
11//! ├── microcompact_boundary  - Smaller compaction point
12//! ├── error                  - API errors, overload errors
13//! └── ... (more types)
14//! ```
15//!
16//! # Compact Boundaries
17//!
18//! Compact boundaries are CRITICAL for session segmentation. They mark points
19//! where Claude Code compacted conversation history to stay within token limits.
20//!
21//! These are ideal break points for:
22//! - Creating session summaries
23//! - Segmenting long sessions
24//! - Understanding conversation flow
25
26use chrono::{DateTime, Utc};
27use serde::{Deserialize, Serialize};
28use serde_json::Value as JsonValue;
29
30use super::metadata::EventMetadata;
31
32/// System event
33///
34/// Represents system-level events:
35/// - Compact boundaries (conversation compaction)
36/// - API errors (overload, rate limits)
37/// - System reminders
38///
39/// # Special Fields
40///
41/// System events don't use flattened EventMetadata because:
42/// - `uuid` can be None for some system events
43/// - Need special handling for compact boundaries
44///
45/// # Frequency
46///
47/// ~6 occurrences per large session (rare but important!)
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct SystemEvent {
50    /// Event UUID (can be None for some system events)
51    pub uuid: Option<String>,
52
53    /// Parent UUID
54    #[serde(rename = "parentUuid")]
55    pub parent_uuid: Option<String>,
56
57    /// Logical parent UUID (for compact boundaries)
58    ///
59    /// Compact boundaries have both:
60    /// - `parent_uuid` - points to compact boundary system message
61    /// - `logical_parent_uuid` - points to last real message before compaction
62    ///
63    /// This preserves conversation flow across compaction.
64    #[serde(rename = "logicalParentUuid")]
65    pub logical_parent_uuid: Option<String>,
66
67    /// Session ID
68    #[serde(rename = "sessionId")]
69    pub session_id: String,
70
71    /// Timestamp
72    pub timestamp: DateTime<Utc>,
73
74    /// Is this part of a sidechain
75    #[serde(rename = "isSidechain")]
76    pub is_sidechain: bool,
77
78    /// User type (typically None for system events)
79    #[serde(rename = "userType")]
80    pub user_type: Option<String>,
81
82    /// Current working directory
83    pub cwd: Option<String>,
84
85    /// Claude Code version
86    pub version: Option<String>,
87
88    /// Git branch
89    #[serde(rename = "gitBranch")]
90    pub git_branch: Option<String>,
91
92    /// Agent slug
93    pub slug: Option<String>,
94
95    /// System event subtype
96    ///
97    /// Common values:
98    /// - `"compact_boundary"` - Conversation compaction point
99    /// - `"microcompact_boundary"` - Smaller compaction
100    /// - `"error"` - System error
101    pub subtype: Option<String>,
102
103    /// Content/message
104    pub content: Option<String>,
105
106    /// Is this a meta event
107    #[serde(rename = "isMeta")]
108    pub is_meta: Option<bool>,
109
110    /// Severity level: "info", "warning", "error"
111    pub level: Option<String>,
112
113    /// Compact boundary metadata (only for compact_boundary subtype)
114    #[serde(rename = "compactMetadata")]
115    pub compact_metadata: Option<CompactMetadata>,
116
117    /// Error details (only for error subtype)
118    pub error: Option<ErrorDetails>,
119}
120
121impl SystemEvent {
122    /// Check if this is a compact boundary
123    pub fn is_compact_boundary(&self) -> bool {
124        self.subtype.as_deref() == Some("compact_boundary")
125    }
126
127    /// Check if this is a microcompact boundary
128    pub fn is_microcompact_boundary(&self) -> bool {
129        self.subtype.as_deref() == Some("microcompact_boundary")
130    }
131
132    /// Check if this is an error event
133    pub fn is_error(&self) -> bool {
134        self.subtype.as_deref() == Some("error")
135    }
136
137    /// Get compact metadata if this is a compact boundary
138    pub fn compact_metadata(&self) -> Option<&CompactMetadata> {
139        self.compact_metadata.as_ref()
140    }
141
142    /// Convert to EventMetadata (for consistent interface)
143    pub fn metadata(&self) -> EventMetadata {
144        EventMetadata {
145            uuid: self.uuid.clone().unwrap_or_default(),
146            parent_uuid: self.parent_uuid.clone(),
147            session_id: self.session_id.clone(),
148            timestamp: self.timestamp,
149            is_sidechain: self.is_sidechain,
150            user_type: self.user_type.clone(),
151            cwd: self.cwd.clone(),
152            version: self.version.clone(),
153            git_branch: self.git_branch.clone(),
154            slug: self.slug.clone(),
155        }
156    }
157}
158
159/// Compact boundary metadata
160///
161/// Marks natural conversation break points where context was compacted.
162///
163/// # When Compaction Occurs
164///
165/// Claude Code automatically compacts conversation when:
166/// - Token count reaches ~150k-160k tokens
167/// - User manually triggers compaction
168///
169/// # Usage for Segmentation
170///
171/// Compact boundaries are IDEAL for:
172/// - Breaking long sessions into segments
173/// - Creating segment summaries
174/// - Understanding conversation phases
175///
176/// # Example
177///
178/// ```json
179/// {
180///   "trigger": "auto",
181///   "preTokens": 156594
182/// }
183/// ```
184#[derive(Debug, Clone, Serialize, Deserialize)]
185pub struct CompactMetadata {
186    /// Trigger type: "auto" or "manual"
187    ///
188    /// - `"auto"` - Automatic compaction at ~150k tokens
189    /// - `"manual"` - User-triggered compaction
190    pub trigger: String,
191
192    /// Token count before compaction
193    ///
194    /// Typically ~150k-160k for auto-compaction
195    #[serde(rename = "preTokens")]
196    pub pre_tokens: u64,
197
198    /// Token count after compaction (optional)
199    #[serde(rename = "postTokens", skip_serializing_if = "Option::is_none")]
200    #[serde(default)]
201    pub post_tokens: Option<u64>,
202}
203
204/// Error details for error system events
205#[derive(Debug, Clone, Serialize, Deserialize)]
206pub struct ErrorDetails {
207    /// Error type
208    ///
209    /// Common values:
210    /// - `"error"` - Generic error
211    /// - `"overloaded_error"` - API overload
212    /// - `"rate_limit_error"` - Rate limit exceeded
213    #[serde(rename = "type")]
214    pub error_type: String,
215
216    /// Error message
217    pub message: String,
218
219    /// Additional error context
220    #[serde(flatten)]
221    pub extra: JsonValue,
222}
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227
228    #[test]
229    fn test_parse_compact_boundary() {
230        let json = r#"{
231            "type": "system",
232            "subtype": "compact_boundary",
233            "uuid": "boundary-uuid",
234            "parentUuid": null,
235            "logicalParentUuid": "last-message-uuid",
236            "sessionId": "session-123",
237            "timestamp": "2024-01-01T00:00:00Z",
238            "isSidechain": false,
239            "cwd": "/test",
240            "version": "2.1.19",
241            "gitBranch": "main",
242            "content": "Conversation compacted",
243            "level": "info",
244            "compactMetadata": {
245                "trigger": "auto",
246                "preTokens": 156594,
247                "postTokens": 50000
248            }
249        }"#;
250
251        let event: SystemEvent = serde_json::from_str(json).unwrap();
252        assert!(event.is_compact_boundary());
253        assert!(!event.is_microcompact_boundary());
254        assert!(!event.is_error());
255
256        let metadata = event.compact_metadata().unwrap();
257        assert_eq!(metadata.trigger, "auto");
258        assert_eq!(metadata.pre_tokens, 156_594);
259        assert_eq!(metadata.post_tokens, Some(50_000));
260
261        assert_eq!(
262            event.logical_parent_uuid,
263            Some("last-message-uuid".to_string())
264        );
265    }
266
267    #[test]
268    fn test_parse_microcompact_boundary() {
269        let json = r#"{
270            "type": "system",
271            "subtype": "microcompact_boundary",
272            "uuid": "micro-uuid",
273            "parentUuid": null,
274            "sessionId": "session-123",
275            "timestamp": "2024-01-01T00:00:00Z",
276            "isSidechain": false,
277            "cwd": "/test"
278        }"#;
279
280        let event: SystemEvent = serde_json::from_str(json).unwrap();
281        assert!(event.is_microcompact_boundary());
282        assert!(!event.is_compact_boundary());
283    }
284
285    #[test]
286    fn test_parse_error_event() {
287        let json = r#"{
288            "type": "system",
289            "subtype": "error",
290            "uuid": "error-uuid",
291            "parentUuid": null,
292            "sessionId": "session-123",
293            "timestamp": "2024-01-01T00:00:00Z",
294            "isSidechain": false,
295            "cwd": "/test",
296            "level": "error",
297            "content": "API overloaded",
298            "error": {
299                "type": "overloaded_error",
300                "message": "API is currently overloaded, please try again"
301            }
302        }"#;
303
304        let event: SystemEvent = serde_json::from_str(json).unwrap();
305        assert!(event.is_error());
306        assert_eq!(event.level, Some("error".to_string()));
307        assert_eq!(event.content, Some("API overloaded".to_string()));
308
309        if let Some(error) = &event.error {
310            assert_eq!(error.error_type, "overloaded_error");
311            assert_eq!(
312                error.message,
313                "API is currently overloaded, please try again"
314            );
315        }
316    }
317
318    #[test]
319    fn test_system_event_metadata() {
320        let event = SystemEvent {
321            uuid: Some("test-uuid".to_string()),
322            parent_uuid: Some("parent-uuid".to_string()),
323            logical_parent_uuid: None,
324            session_id: "session-123".to_string(),
325            timestamp: Utc::now(),
326            is_sidechain: false,
327            user_type: None,
328            cwd: Some("/test".to_string()),
329            version: Some("2.1.19".to_string()),
330            git_branch: Some("main".to_string()),
331            slug: None,
332            subtype: Some("compact_boundary".to_string()),
333            content: None,
334            is_meta: None,
335            level: None,
336            compact_metadata: None,
337            error: None,
338        };
339
340        let metadata = event.metadata();
341        assert_eq!(metadata.uuid, "test-uuid");
342        assert_eq!(metadata.parent_uuid, Some("parent-uuid".to_string()));
343        assert_eq!(metadata.session_id, "session-123");
344    }
345
346    #[test]
347    fn test_system_event_without_uuid() {
348        let json = r#"{
349            "type": "system",
350            "subtype": "info",
351            "parentUuid": null,
352            "sessionId": "session-123",
353            "timestamp": "2024-01-01T00:00:00Z",
354            "isSidechain": false,
355            "cwd": "/test",
356            "content": "System notification"
357        }"#;
358
359        let event: SystemEvent = serde_json::from_str(json).unwrap();
360        assert!(event.uuid.is_none());
361
362        // metadata() should handle None uuid gracefully
363        let metadata = event.metadata();
364        assert_eq!(metadata.uuid, "");
365    }
366}