agent_tui/daemon/
error.rs

1//! Domain errors for daemon operations.
2//!
3//! These errors are mapped to specific JSON-RPC error codes and include
4//! structured context for AI agents to handle programmatically.
5
6use crate::ipc::error_codes::{self, ErrorCategory};
7use crate::terminal::PtyError;
8use serde_json::{Value, json};
9use thiserror::Error;
10
11/// Session-level errors with structured context for AI agents.
12#[derive(Error, Debug)]
13pub enum SessionError {
14    #[error("Session not found: {0}")]
15    NotFound(String),
16    #[error("No active session")]
17    NoActiveSession,
18    #[error("PTY error: {0}")]
19    Pty(#[from] PtyError),
20    #[error("Element not found: {0}")]
21    ElementNotFound(String),
22    #[error("Element {element_ref} is a {actual} not a {expected}")]
23    WrongElementType {
24        element_ref: String,
25        actual: String,
26        expected: String,
27    },
28    #[error("Invalid key: {0}")]
29    InvalidKey(String),
30    #[error("Session limit reached: maximum {0} sessions allowed")]
31    LimitReached(usize),
32    #[error("Persistence error during {operation}: {reason}")]
33    Persistence { operation: String, reason: String },
34}
35
36impl SessionError {
37    /// Returns the JSON-RPC error code for this error.
38    pub fn code(&self) -> i32 {
39        match self {
40            SessionError::NotFound(_) => error_codes::SESSION_NOT_FOUND,
41            SessionError::NoActiveSession => error_codes::NO_ACTIVE_SESSION,
42            SessionError::ElementNotFound(_) => error_codes::ELEMENT_NOT_FOUND,
43            SessionError::WrongElementType { .. } => error_codes::WRONG_ELEMENT_TYPE,
44            SessionError::InvalidKey(_) => error_codes::INVALID_KEY,
45            SessionError::LimitReached(_) => error_codes::SESSION_LIMIT,
46            SessionError::Pty(_) => error_codes::PTY_ERROR,
47            SessionError::Persistence { .. } => error_codes::PERSISTENCE_ERROR,
48        }
49    }
50
51    /// Returns the error category for programmatic handling.
52    pub fn category(&self) -> ErrorCategory {
53        error_codes::category_for_code(self.code())
54    }
55
56    /// Returns structured context about the error for debugging.
57    pub fn context(&self) -> Value {
58        match self {
59            SessionError::NotFound(id) => json!({ "session_id": id }),
60            SessionError::NoActiveSession => json!({}),
61            SessionError::ElementNotFound(element_ref) => json!({ "element_ref": element_ref }),
62            SessionError::WrongElementType {
63                element_ref,
64                actual,
65                expected,
66            } => {
67                json!({
68                    "element_ref": element_ref,
69                    "actual_type": actual,
70                    "expected_type": expected
71                })
72            }
73            SessionError::InvalidKey(key) => json!({ "key": key }),
74            SessionError::LimitReached(max) => json!({ "max_sessions": max }),
75            SessionError::Pty(pty_err) => pty_err.context(),
76            SessionError::Persistence { operation, reason } => {
77                json!({ "operation": operation, "reason": reason })
78            }
79        }
80    }
81
82    /// Returns a helpful suggestion for resolving the error.
83    pub fn suggestion(&self) -> String {
84        match self {
85            SessionError::NotFound(_) | SessionError::NoActiveSession => {
86                "Run 'sessions' to list active sessions or 'spawn <cmd>' to start a new one."
87                    .to_string()
88            }
89            SessionError::ElementNotFound(element_ref) => {
90                format!(
91                    "Element '{}' not found. Run 'snapshot -i' to see current elements and their refs.",
92                    element_ref
93                )
94            }
95            SessionError::WrongElementType {
96                element_ref,
97                actual,
98                ..
99            } => suggest_command_for_type(actual, element_ref),
100            SessionError::InvalidKey(_) => {
101                "Supported keys: Enter, Tab, Escape, Backspace, Delete, ArrowUp/Down/Left/Right, Home, End, PageUp/Down, F1-F12. Modifiers: Ctrl+, Alt+, Shift+".to_string()
102            }
103            SessionError::LimitReached(_) => {
104                "Kill unused sessions with 'kill <session_id>' or increase limit with AGENT_TUI_MAX_SESSIONS env var.".to_string()
105            }
106            SessionError::Pty(pty_err) => pty_err.suggestion(),
107            SessionError::Persistence { .. } => {
108                "Persistence error is non-fatal. Session continues to operate normally.".to_string()
109            }
110        }
111    }
112
113    /// Returns whether this error is potentially transient and may succeed on retry.
114    pub fn is_retryable(&self) -> bool {
115        match self {
116            SessionError::Pty(pty_err) => pty_err.is_retryable(),
117            SessionError::Persistence { .. } => true,
118            _ => error_codes::is_retryable(self.code()),
119        }
120    }
121}
122
123/// Daemon startup and lifecycle errors.
124#[derive(Error, Debug)]
125pub enum DaemonError {
126    #[error("Failed to bind socket: {0}")]
127    SocketBind(String),
128    #[error("Another daemon instance is already running")]
129    AlreadyRunning,
130    #[error("Failed to acquire lock: {0}")]
131    LockFailed(String),
132    #[error("Failed to setup signal handler: {0}")]
133    SignalSetup(String),
134    #[error("Failed to create thread pool: {0}")]
135    ThreadPool(String),
136}
137
138impl DaemonError {
139    /// Returns the JSON-RPC error code for this error.
140    pub fn code(&self) -> i32 {
141        error_codes::DAEMON_ERROR
142    }
143
144    /// Returns the error category for programmatic handling.
145    pub fn category(&self) -> ErrorCategory {
146        ErrorCategory::External
147    }
148
149    /// Returns structured context about the error for debugging.
150    pub fn context(&self) -> Value {
151        match self {
152            DaemonError::SocketBind(reason) => {
153                json!({ "operation": "socket_bind", "reason": reason })
154            }
155            DaemonError::AlreadyRunning => {
156                json!({ "operation": "startup", "reason": "another instance running" })
157            }
158            DaemonError::LockFailed(reason) => json!({ "operation": "lock", "reason": reason }),
159            DaemonError::SignalSetup(reason) => {
160                json!({ "operation": "signal_setup", "reason": reason })
161            }
162            DaemonError::ThreadPool(reason) => {
163                json!({ "operation": "thread_pool", "reason": reason })
164            }
165        }
166    }
167
168    /// Returns a helpful suggestion for resolving the error.
169    pub fn suggestion(&self) -> String {
170        match self {
171            DaemonError::SocketBind(_) => {
172                "Check if the socket directory is writable. Try: rm /tmp/agent-tui.sock".to_string()
173            }
174            DaemonError::AlreadyRunning => {
175                "Another daemon is running. Use 'agent-tui sessions' to connect or kill existing daemon.".to_string()
176            }
177            DaemonError::LockFailed(_) => {
178                "Lock file issue. Try removing the lock file: rm /tmp/agent-tui.sock.lock".to_string()
179            }
180            DaemonError::SignalSetup(_) => {
181                "Signal handler setup failed. Check system signal configuration.".to_string()
182            }
183            DaemonError::ThreadPool(_) => {
184                "Thread pool creation failed. Check system thread limits (ulimit -u).".to_string()
185            }
186        }
187    }
188
189    /// Returns whether this error is potentially transient and may succeed on retry.
190    pub fn is_retryable(&self) -> bool {
191        matches!(self, DaemonError::LockFailed(_))
192    }
193}
194
195/// Domain-specific errors with semantic codes and structured context.
196#[derive(Error, Debug)]
197pub enum DomainError {
198    #[error("Session not found: {session_id}")]
199    SessionNotFound { session_id: String },
200
201    #[error("No active session")]
202    NoActiveSession,
203
204    #[error("Element not found: {element_ref}")]
205    ElementNotFound {
206        element_ref: String,
207        session_id: Option<String>,
208    },
209
210    #[error("Element {element_ref} is a {actual} not a {expected}")]
211    WrongElementType {
212        element_ref: String,
213        actual: String,
214        expected: String,
215    },
216
217    #[error("Invalid key: {key}")]
218    InvalidKey { key: String },
219
220    #[error("Session limit reached: maximum {max} sessions allowed")]
221    SessionLimitReached { max: usize },
222
223    #[error("Lock timeout{}", session_id.as_ref().map(|id| format!(" for session: {}", id)).unwrap_or_default())]
224    LockTimeout { session_id: Option<String> },
225
226    #[error("PTY error during {operation}: {reason}")]
227    PtyError { operation: String, reason: String },
228
229    #[error("Timeout waiting for: {condition}")]
230    WaitTimeout {
231        condition: String,
232        elapsed_ms: u64,
233        timeout_ms: u64,
234    },
235
236    #[error("Command not found: {command}")]
237    CommandNotFound { command: String },
238
239    #[error("Permission denied: {command}")]
240    PermissionDenied { command: String },
241
242    #[error("{message}")]
243    Generic { message: String },
244}
245
246impl DomainError {
247    /// Returns the JSON-RPC error code for this error.
248    pub fn code(&self) -> i32 {
249        match self {
250            DomainError::SessionNotFound { .. } => error_codes::SESSION_NOT_FOUND,
251            DomainError::NoActiveSession => error_codes::NO_ACTIVE_SESSION,
252            DomainError::ElementNotFound { .. } => error_codes::ELEMENT_NOT_FOUND,
253            DomainError::WrongElementType { .. } => error_codes::WRONG_ELEMENT_TYPE,
254            DomainError::InvalidKey { .. } => error_codes::INVALID_KEY,
255            DomainError::SessionLimitReached { .. } => error_codes::SESSION_LIMIT,
256            DomainError::LockTimeout { .. } => error_codes::LOCK_TIMEOUT,
257            DomainError::PtyError { .. } => error_codes::PTY_ERROR,
258            DomainError::WaitTimeout { .. } => error_codes::WAIT_TIMEOUT,
259            DomainError::CommandNotFound { .. } => error_codes::COMMAND_NOT_FOUND,
260            DomainError::PermissionDenied { .. } => error_codes::PERMISSION_DENIED,
261            DomainError::Generic { .. } => error_codes::GENERIC_ERROR,
262        }
263    }
264
265    /// Returns the error category for programmatic handling.
266    pub fn category(&self) -> ErrorCategory {
267        error_codes::category_for_code(self.code())
268    }
269
270    /// Returns structured context about the error for debugging.
271    pub fn context(&self) -> Value {
272        match self {
273            DomainError::SessionNotFound { session_id } => {
274                json!({ "session_id": session_id })
275            }
276            DomainError::NoActiveSession => json!({}),
277            DomainError::ElementNotFound {
278                element_ref,
279                session_id,
280            } => {
281                let mut ctx = json!({ "element_ref": element_ref });
282                if let Some(sid) = session_id {
283                    ctx["session_id"] = json!(sid);
284                }
285                ctx
286            }
287            DomainError::WrongElementType {
288                element_ref,
289                actual,
290                expected,
291            } => {
292                json!({
293                    "element_ref": element_ref,
294                    "actual_type": actual,
295                    "expected_type": expected
296                })
297            }
298            DomainError::InvalidKey { key } => {
299                json!({ "key": key })
300            }
301            DomainError::SessionLimitReached { max } => {
302                json!({ "max_sessions": max })
303            }
304            DomainError::LockTimeout { session_id } => match session_id {
305                Some(id) => json!({ "session_id": id }),
306                None => json!({}),
307            },
308            DomainError::PtyError { operation, reason } => {
309                json!({
310                    "operation": operation,
311                    "reason": reason
312                })
313            }
314            DomainError::WaitTimeout {
315                condition,
316                elapsed_ms,
317                timeout_ms,
318            } => {
319                json!({
320                    "condition": condition,
321                    "elapsed_ms": elapsed_ms,
322                    "timeout_ms": timeout_ms
323                })
324            }
325            DomainError::CommandNotFound { command } => {
326                json!({ "command": command })
327            }
328            DomainError::PermissionDenied { command } => {
329                json!({ "command": command })
330            }
331            DomainError::Generic { message } => {
332                json!({ "message": message })
333            }
334        }
335    }
336
337    /// Returns a helpful suggestion for resolving the error.
338    pub fn suggestion(&self) -> String {
339        match self {
340            DomainError::SessionNotFound { .. } | DomainError::NoActiveSession => {
341                "Run 'sessions' to list active sessions or 'spawn <cmd>' to start a new one."
342                    .to_string()
343            }
344            DomainError::ElementNotFound { element_ref, .. } => {
345                format!(
346                    "Element '{}' not found. Run 'snapshot -i' to see current elements and their refs.",
347                    element_ref
348                )
349            }
350            DomainError::WrongElementType {
351                element_ref,
352                actual,
353                ..
354            } => suggest_command_for_type(actual, element_ref),
355            DomainError::InvalidKey { .. } => {
356                "Supported keys: Enter, Tab, Escape, Backspace, Delete, ArrowUp/Down/Left/Right, Home, End, PageUp/Down, F1-F12. Modifiers: Ctrl+, Alt+, Shift+".to_string()
357            }
358            DomainError::SessionLimitReached { .. } => {
359                "Kill unused sessions with 'kill <session_id>' or increase limit with AGENT_TUI_MAX_SESSIONS env var.".to_string()
360            }
361            DomainError::LockTimeout { .. } => {
362                "Session is busy. Try again in a moment, or run 'sessions' to check session status."
363                    .to_string()
364            }
365            DomainError::PtyError { .. } => {
366                "Terminal communication error. The session may have ended. Run 'sessions' to check status.".to_string()
367            }
368            DomainError::WaitTimeout { condition, .. } => {
369                format!(
370                    "Condition '{}' not met. The app may still be loading. Try 'wait --stable' or increase timeout with '-t'.",
371                    condition
372                )
373            }
374            DomainError::CommandNotFound { command } => {
375                format!(
376                    "Command '{}' not found. Check if the command exists and is in PATH.",
377                    command
378                )
379            }
380            DomainError::PermissionDenied { command } => {
381                format!(
382                    "Cannot execute '{}'. Check file permissions.",
383                    command
384                )
385            }
386            DomainError::Generic { .. } => {
387                "Run 'snapshot -i' to see current screen state.".to_string()
388            }
389        }
390    }
391}
392
393fn suggest_command_for_type(element_type: &str, element_ref: &str) -> String {
394    let hint = match element_type {
395        "button" | "menuitem" | "listitem" => format!("Try: click {}", element_ref),
396        "checkbox" | "radio" => format!("Try: toggle {} or click {}", element_ref, element_ref),
397        "input" => format!("Try: fill {} <value>", element_ref),
398        "select" => format!("Try: select {} <option>", element_ref),
399        _ => "Run 'snapshot -i' to see element types.".to_string(),
400    };
401    hint
402}
403
404impl From<SessionError> for DomainError {
405    fn from(err: SessionError) -> Self {
406        match err {
407            SessionError::NotFound(id) => DomainError::SessionNotFound { session_id: id },
408            SessionError::NoActiveSession => DomainError::NoActiveSession,
409            SessionError::ElementNotFound(element_ref) => DomainError::ElementNotFound {
410                element_ref,
411                session_id: None,
412            },
413            SessionError::WrongElementType {
414                element_ref,
415                actual,
416                expected,
417            } => DomainError::WrongElementType {
418                element_ref,
419                actual,
420                expected,
421            },
422            SessionError::InvalidKey(key) => DomainError::InvalidKey { key },
423            SessionError::LimitReached(max) => DomainError::SessionLimitReached { max },
424            SessionError::Pty(pty_err) => DomainError::PtyError {
425                operation: pty_err.operation().to_string(),
426                reason: pty_err.reason().to_string(),
427            },
428            SessionError::Persistence { operation, reason } => DomainError::Generic {
429                message: format!("Persistence error during {}: {}", operation, reason),
430            },
431        }
432    }
433}
434
435#[cfg(test)]
436mod tests {
437    use super::*;
438
439    #[test]
440    fn test_session_not_found_code() {
441        let err = DomainError::SessionNotFound {
442            session_id: "abc123".into(),
443        };
444        assert_eq!(err.code(), error_codes::SESSION_NOT_FOUND);
445    }
446
447    #[test]
448    fn test_element_not_found_category() {
449        let err = DomainError::ElementNotFound {
450            element_ref: "@btn1".into(),
451            session_id: None,
452        };
453        assert_eq!(err.category(), ErrorCategory::NotFound);
454    }
455
456    #[test]
457    fn test_lock_timeout_is_retryable() {
458        let err = DomainError::LockTimeout {
459            session_id: Some("abc".into()),
460        };
461        assert!(error_codes::is_retryable(err.code()));
462    }
463
464    #[test]
465    fn test_element_not_found_not_retryable() {
466        let err = DomainError::ElementNotFound {
467            element_ref: "@btn1".into(),
468            session_id: None,
469        };
470        assert!(!error_codes::is_retryable(err.code()));
471    }
472
473    #[test]
474    fn test_context_includes_element_ref() {
475        let err = DomainError::ElementNotFound {
476            element_ref: "@btn5".into(),
477            session_id: Some("sess1".into()),
478        };
479        let ctx = err.context();
480        assert_eq!(ctx["element_ref"], "@btn5");
481        assert_eq!(ctx["session_id"], "sess1");
482    }
483
484    #[test]
485    fn test_wrong_element_type_context() {
486        let err = DomainError::WrongElementType {
487            element_ref: "@el1".into(),
488            actual: "button".into(),
489            expected: "input".into(),
490        };
491        let ctx = err.context();
492        assert_eq!(ctx["element_ref"], "@el1");
493        assert_eq!(ctx["actual_type"], "button");
494        assert_eq!(ctx["expected_type"], "input");
495    }
496
497    #[test]
498    fn test_suggestion_for_button() {
499        let err = DomainError::WrongElementType {
500            element_ref: "@btn1".into(),
501            actual: "button".into(),
502            expected: "input".into(),
503        };
504        assert!(err.suggestion().contains("click @btn1"));
505    }
506
507    #[test]
508    fn test_from_session_error() {
509        let session_err = SessionError::NotFound("test123".into());
510        let domain_err: DomainError = session_err.into();
511        assert_eq!(domain_err.code(), error_codes::SESSION_NOT_FOUND);
512    }
513
514    #[test]
515    fn test_display_session_not_found() {
516        let err = DomainError::SessionNotFound {
517            session_id: "abc".into(),
518        };
519        assert_eq!(err.to_string(), "Session not found: abc");
520    }
521
522    #[test]
523    fn test_display_wrong_element_type() {
524        let err = DomainError::WrongElementType {
525            element_ref: "@el1".into(),
526            actual: "button".into(),
527            expected: "input".into(),
528        };
529        assert_eq!(err.to_string(), "Element @el1 is a button not a input");
530    }
531
532    // SessionError tests
533    #[test]
534    fn test_session_error_not_found_code() {
535        let err = SessionError::NotFound("abc123".into());
536        assert_eq!(err.code(), error_codes::SESSION_NOT_FOUND);
537    }
538
539    #[test]
540    fn test_session_error_no_active_session_code() {
541        let err = SessionError::NoActiveSession;
542        assert_eq!(err.code(), error_codes::NO_ACTIVE_SESSION);
543    }
544
545    #[test]
546    fn test_session_error_element_not_found_code() {
547        let err = SessionError::ElementNotFound("@btn1".into());
548        assert_eq!(err.code(), error_codes::ELEMENT_NOT_FOUND);
549    }
550
551    #[test]
552    fn test_session_error_invalid_key_code() {
553        let err = SessionError::InvalidKey("BadKey".into());
554        assert_eq!(err.code(), error_codes::INVALID_KEY);
555    }
556
557    #[test]
558    fn test_session_error_limit_reached_code() {
559        let err = SessionError::LimitReached(16);
560        assert_eq!(err.code(), error_codes::SESSION_LIMIT);
561    }
562
563    #[test]
564    fn test_session_error_category() {
565        let err = SessionError::NotFound("abc".into());
566        assert_eq!(err.category(), ErrorCategory::NotFound);
567
568        let err = SessionError::InvalidKey("x".into());
569        assert_eq!(err.category(), ErrorCategory::InvalidInput);
570
571        let err = SessionError::LimitReached(10);
572        assert_eq!(err.category(), ErrorCategory::Busy);
573    }
574
575    #[test]
576    fn test_session_error_context() {
577        let err = SessionError::NotFound("sess123".into());
578        let ctx = err.context();
579        assert_eq!(ctx["session_id"], "sess123");
580
581        let err = SessionError::ElementNotFound("@btn5".into());
582        let ctx = err.context();
583        assert_eq!(ctx["element_ref"], "@btn5");
584
585        let err = SessionError::LimitReached(16);
586        let ctx = err.context();
587        assert_eq!(ctx["max_sessions"], 16);
588    }
589
590    #[test]
591    fn test_session_error_suggestion() {
592        let err = SessionError::NotFound("x".into());
593        assert!(err.suggestion().contains("sessions"));
594
595        let err = SessionError::ElementNotFound("@btn1".into());
596        assert!(err.suggestion().contains("snapshot"));
597
598        let err = SessionError::InvalidKey("x".into());
599        assert!(err.suggestion().contains("Enter"));
600    }
601
602    #[test]
603    fn test_session_error_is_retryable() {
604        assert!(!SessionError::NotFound("x".into()).is_retryable());
605        assert!(!SessionError::NoActiveSession.is_retryable());
606        assert!(!SessionError::ElementNotFound("x".into()).is_retryable());
607        assert!(!SessionError::InvalidKey("x".into()).is_retryable());
608    }
609
610    // SessionError::Persistence tests
611    #[test]
612    fn test_session_error_persistence_code() {
613        let err = SessionError::Persistence {
614            operation: "save".into(),
615            reason: "disk full".into(),
616        };
617        assert_eq!(err.code(), error_codes::PERSISTENCE_ERROR);
618    }
619
620    #[test]
621    fn test_session_error_persistence_context() {
622        let err = SessionError::Persistence {
623            operation: "write_json".into(),
624            reason: "permission denied".into(),
625        };
626        let ctx = err.context();
627        assert_eq!(ctx["operation"], "write_json");
628        assert_eq!(ctx["reason"], "permission denied");
629    }
630
631    #[test]
632    fn test_session_error_persistence_is_retryable() {
633        let err = SessionError::Persistence {
634            operation: "save".into(),
635            reason: "disk full".into(),
636        };
637        assert!(err.is_retryable());
638    }
639
640    #[test]
641    fn test_session_error_persistence_display() {
642        let err = SessionError::Persistence {
643            operation: "write".into(),
644            reason: "disk full".into(),
645        };
646        assert_eq!(err.to_string(), "Persistence error during write: disk full");
647    }
648
649    // DaemonError tests
650    #[test]
651    fn test_daemon_error_socket_bind() {
652        let err = DaemonError::SocketBind("address in use".into());
653        assert_eq!(err.code(), error_codes::DAEMON_ERROR);
654        assert_eq!(err.category(), ErrorCategory::External);
655        assert!(err.suggestion().contains("socket"));
656    }
657
658    #[test]
659    fn test_daemon_error_already_running() {
660        let err = DaemonError::AlreadyRunning;
661        assert_eq!(err.code(), error_codes::DAEMON_ERROR);
662        assert!(err.suggestion().contains("Another daemon"));
663    }
664
665    #[test]
666    fn test_daemon_error_lock_failed() {
667        let err = DaemonError::LockFailed("permission denied".into());
668        assert_eq!(err.code(), error_codes::DAEMON_ERROR);
669        assert!(err.is_retryable());
670    }
671
672    #[test]
673    fn test_daemon_error_not_retryable() {
674        assert!(!DaemonError::SocketBind("x".into()).is_retryable());
675        assert!(!DaemonError::AlreadyRunning.is_retryable());
676        assert!(!DaemonError::SignalSetup("x".into()).is_retryable());
677        assert!(!DaemonError::ThreadPool("x".into()).is_retryable());
678    }
679
680    #[test]
681    fn test_daemon_error_context() {
682        let err = DaemonError::SocketBind("address in use".into());
683        let ctx = err.context();
684        assert_eq!(ctx["operation"], "socket_bind");
685        assert_eq!(ctx["reason"], "address in use");
686    }
687
688    #[test]
689    fn test_daemon_error_display() {
690        let err = DaemonError::AlreadyRunning;
691        assert_eq!(
692            err.to_string(),
693            "Another daemon instance is already running"
694        );
695    }
696
697    // PtyError conversion test
698    #[test]
699    fn test_pty_error_conversion_preserves_context() {
700        let pty_err = PtyError::Write("broken pipe".into());
701        let session_err = SessionError::Pty(pty_err);
702        let domain_err: DomainError = session_err.into();
703
704        match domain_err {
705            DomainError::PtyError { operation, reason } => {
706                assert_eq!(operation, "write");
707                assert_eq!(reason, "broken pipe");
708            }
709            _ => panic!("Expected PtyError variant"),
710        }
711    }
712}