Skip to main content

winx_code_agent/
errors.rs

1use std::path::PathBuf;
2use thiserror::Error;
3
4/// Errors that can occur in the Winx application
5#[derive(Error, Debug)]
6pub enum WinxError {
7    /// Error when initializing the shell
8    #[error("Failed to initialize shell: {0}")]
9    ShellInitializationError(String),
10
11    /// Error when operating on a workspace path
12    #[error("Workspace path error: {0}")]
13    WorkspacePathError(String),
14
15    /// Error when locking the bash state
16    #[error("Failed to lock the bash state: {0}")]
17    BashStateLockError(String),
18
19    /// Error when the bash state is not initialized
20    #[error("Bash state not initialized. Please call Initialize first with type=\"first_call\" and a valid workspace path.")]
21    BashStateNotInitialized,
22
23    /// Error when a command fails for an internal execution reason.
24    #[error("Command execution failed: {0}")]
25    CommandExecutionError(String),
26
27    /// A status/input action targeted a shell with no running command.
28    #[error("{0}")]
29    NoActiveCommand(String),
30
31    /// A background command identifier is unknown or has already exited.
32    #[error("{0}")]
33    BackgroundSessionNotFound(String),
34
35    /// An interactive input action was supplied without any payload.
36    #[error("Failure: {action} cannot be empty")]
37    EmptyInteractiveInput { action: String },
38
39    /// Interactive bytes were sent to an idle shell instead of a running program.
40    #[error("{0}")]
41    InteractiveTargetNotRunning(String),
42
43    /// Error when parsing arguments
44    #[error("Failed to parse arguments: {0}")]
45    ArgumentParseError(String),
46
47    /// Error when trying to access a file or directory
48    #[error("File access error for {path}: {message}")]
49    FileAccessError { path: PathBuf, message: String },
50
51    /// Security error - path traversal or symlink escape attempt
52    #[error("Security violation: {message}")]
53    PathSecurityError { path: PathBuf, message: String },
54
55    /// A model-owned helper violated the managed `.winx/tmp/<session>` contract.
56    #[error("Temporary artifact policy rejected {path}: {message}")]
57    TemporaryArtifactPolicy { path: PathBuf, temporary_artifact_dir: PathBuf, message: String },
58
59    /// Syntax navigation over derived helpers exceeded its bounded session budget.
60    #[error("Derived CodeMap budget rejected {path}: {message}")]
61    DerivedCodeMapBudget {
62        path: PathBuf,
63        temporary_artifact_dir: PathBuf,
64        calls_used: usize,
65        calls_limit: usize,
66        unique_files_used: usize,
67        unique_files_limit: usize,
68        message: String,
69    },
70
71    /// Error when a command is not allowed in the current mode
72    #[error("Command not allowed: {0}")]
73    CommandNotAllowed(String),
74
75    /// Error when chat IDs don't match
76    #[error("Thread ID mismatch: {0}")]
77    ThreadIdMismatch(String),
78
79    /// A remote stateful call omitted the explicit workspace half of its
80    /// session binding.
81    #[error(
82        "Remote session binding is incomplete for thread_id `{thread_id}`: pass the exact workspace_root returned by Initialize. No operation was executed. workspace_root identifies the project session; it does not restrict target paths."
83    )]
84    WorkspaceBindingRequired { thread_id: String },
85
86    /// The supplied workspace and thread cannot belong to the same affinity
87    /// key, so executing the request could select another project's shell.
88    #[error(
89        "Session coherence check failed: thread_id `{thread_id}` does not belong to workspace_root `{workspace_root}`. No operation was executed. Initialize that workspace and preserve the returned thread_id/workspace_root pair."
90    )]
91    WorkspaceThreadMismatch { thread_id: String, workspace_root: PathBuf },
92
93    /// The requested binding disagrees with the workspace stored in the
94    /// selected durable session.
95    #[error(
96        "Session coherence check failed: thread_id `{thread_id}` is bound to `{bound_workspace}`, not `{requested_workspace}`. No operation was executed. Initialize the intended workspace and use its returned pair."
97    )]
98    WorkspaceBindingMismatch {
99        thread_id: String,
100        requested_workspace: PathBuf,
101        bound_workspace: PathBuf,
102    },
103
104    /// Remote sessions never change project identity in place. A new first
105    /// call gives the target workspace its own coherent session binding.
106    #[error(
107        "Remote workspace changes require a new session binding. No operation was executed. Call Initialize with type=\"first_call\" for `{workspace_root}` and use the new thread_id/workspace_root pair."
108    )]
109    WorkspaceChangeRequiresNewSession { workspace_root: PathBuf },
110
111    /// Error when deserializing data
112    #[error("Deserialization error: {0}")]
113    DeserializationError(String),
114
115    /// Error when serializing data
116    #[error("Serialization error: {0}")]
117    SerializationError(String),
118
119    /// Error in the search/replace format
120    #[error("Search/replace syntax error: {0}")]
121    SearchReplaceSyntaxError(String),
122
123    /// Error when search block is not found in content
124    #[error("Search block not found in content: {0}")]
125    SearchBlockNotFound(String),
126
127    /// Error when search block matches multiple locations (WCGW-style)
128    #[error("Search block matched multiple times")]
129    SearchBlockAmbiguous { block_content: String, match_count: usize, suggestions: Vec<String> },
130
131    /// Path-aware context for a planning failure in an atomic multi-file edit.
132    /// The boxed source preserves the original recovery class instead of
133    /// flattening it into an argument/string error.
134    #[error(
135        "MultiFileEdit aborted before writing anything - file {index} ({path}) failed validation: {source}"
136    )]
137    MultiFilePlanError { index: usize, path: PathBuf, source: Box<WinxError> },
138
139    /// Enhanced search/replace syntax error with detailed context
140    #[error("Search/replace syntax error: {message}")]
141    SearchReplaceSyntaxErrorDetailed {
142        message: String,
143        line_number: Option<usize>,
144        block_type: Option<String>,
145        suggestions: Vec<String>,
146    },
147
148    /// Error when JSON parsing fails
149    #[error("Invalid JSON: {0}")]
150    JsonParseError(String),
151
152    /// Error when a file is too large for operation
153    #[error("File {path} is too large: {size} bytes (max {max_size})")]
154    FileTooLarge { path: PathBuf, size: u64, max_size: u64 },
155
156    /// Error when writing to a file
157    #[error("Failed to write file {path}: {message}")]
158    FileWriteError { path: PathBuf, message: String },
159
160    /// Error loading data
161    #[error("Failed to load data: {0}")]
162    DataLoadingError(String),
163
164    /// Parameter validation error
165    #[error("Invalid parameter: {field} - {message}")]
166    ParameterValidationError { field: String, message: String },
167
168    /// Required parameter missing error
169    #[error("Required parameter missing: {field} - {message}")]
170    MissingParameterError { field: String, message: String },
171
172    /// Null or undefined value error
173    #[error("Null or undefined value where object expected: {field}")]
174    NullValueError { field: String },
175
176    /// Recovery suggestion error with potential solutions
177    #[error("{message} - {suggestion}")]
178    RecoverableSuggestionError { message: String, suggestion: String },
179
180    /// Context save error
181    #[error("Context save error: {0}")]
182    ContextSaveError(String),
183
184    /// Command timeout error
185    #[error("Command timed out after {timeout_seconds}s: {command}")]
186    CommandTimeout { command: String, timeout_seconds: u64 },
187
188    /// Interactive command detected error
189    #[error(
190        "Interactive command detected: {command}. Use appropriate flags or consider alternatives."
191    )]
192    InteractiveCommandDetected { command: String },
193
194    /// Command already running error
195    #[error("A command is already running: '{current_command}' (for {duration_seconds:.1}s). Use status_check, send_text, or interrupt.")]
196    CommandAlreadyRunning { current_command: String, duration_seconds: f64 },
197
198    /// Process cleanup error
199    #[error("Failed to cleanup process: {message}")]
200    ProcessCleanupError { message: String },
201
202    /// Buffer overflow error
203    #[error("Command output exceeded maximum size: {size} bytes (max {max_size})")]
204    BufferOverflow { size: usize, max_size: usize },
205
206    /// Session recovery error
207    #[error("Failed to recover bash session: {message}")]
208    SessionRecoveryError { message: String },
209
210    /// Resource allocation error
211    #[error("Resource allocation failed: {message}")]
212    ResourceAllocationError { message: String },
213
214    /// IO error
215    #[error("IO error: {0}")]
216    IoError(#[from] std::io::Error),
217
218    /// Configuration error
219    #[error("Configuration error: {0}")]
220    ConfigurationError(String),
221
222    /// Parse error for responses
223    #[error("Parse error: {0}")]
224    ParseError(String),
225
226    /// Invalid input error
227    #[error("Invalid input: {0}")]
228    InvalidInput(String),
229
230    /// File error for file operations
231    #[error("File error: {0}")]
232    FileError(String),
233}
234
235/// Type alias for Result with `WinxError`
236pub type Result<T> = std::result::Result<T, WinxError>;
237
238/// Conversion from `anyhow::Error` to `WinxError`
239impl From<anyhow::Error> for WinxError {
240    fn from(error: anyhow::Error) -> Self {
241        // `{:#}` renders the full anyhow context chain inline ("failed X: caused
242        // by Y: root Z") instead of just the outermost message, so a PTY-spawn or
243        // persistence failure keeps its real cause instead of a bare one-liner.
244        WinxError::CommandExecutionError(format!("{error:#}"))
245    }
246}
247
248/// Advanced error recovery and suggestion options
249pub struct ErrorRecovery;
250
251impl ErrorRecovery {
252    pub fn suggest(error: WinxError, _suggestion: &str) -> WinxError {
253        error
254    }
255
256    pub fn param_error(field: &str, message: &str) -> WinxError {
257        WinxError::ParameterValidationError {
258            field: field.to_string(),
259            message: message.to_string(),
260        }
261    }
262
263    pub fn missing_param(field: &str, message: &str) -> WinxError {
264        WinxError::MissingParameterError { field: field.to_string(), message: message.to_string() }
265    }
266
267    pub fn null_value(field: &str) -> WinxError {
268        WinxError::NullValueError { field: field.to_string() }
269    }
270}
271
272/// Enable cloning for `WinxError`
273impl Clone for WinxError {
274    #[allow(clippy::too_many_lines)] // exhaustive variant-preserving clone
275    fn clone(&self) -> Self {
276        match self {
277            Self::ShellInitializationError(msg) => Self::ShellInitializationError(msg.clone()),
278            Self::WorkspacePathError(msg) => Self::WorkspacePathError(msg.clone()),
279            Self::BashStateLockError(msg) => Self::BashStateLockError(msg.clone()),
280            Self::BashStateNotInitialized => Self::BashStateNotInitialized,
281            Self::CommandExecutionError(msg) => Self::CommandExecutionError(msg.clone()),
282            Self::NoActiveCommand(msg) => Self::NoActiveCommand(msg.clone()),
283            Self::BackgroundSessionNotFound(msg) => Self::BackgroundSessionNotFound(msg.clone()),
284            Self::EmptyInteractiveInput { action } => {
285                Self::EmptyInteractiveInput { action: action.clone() }
286            }
287            Self::InteractiveTargetNotRunning(msg) => {
288                Self::InteractiveTargetNotRunning(msg.clone())
289            }
290            Self::CommandNotAllowed(msg) => Self::CommandNotAllowed(msg.clone()),
291            Self::ThreadIdMismatch(msg) => Self::ThreadIdMismatch(msg.clone()),
292            Self::WorkspaceBindingRequired { thread_id } => {
293                Self::WorkspaceBindingRequired { thread_id: thread_id.clone() }
294            }
295            Self::WorkspaceThreadMismatch { thread_id, workspace_root } => {
296                Self::WorkspaceThreadMismatch {
297                    thread_id: thread_id.clone(),
298                    workspace_root: workspace_root.clone(),
299                }
300            }
301            Self::WorkspaceBindingMismatch { thread_id, requested_workspace, bound_workspace } => {
302                Self::WorkspaceBindingMismatch {
303                    thread_id: thread_id.clone(),
304                    requested_workspace: requested_workspace.clone(),
305                    bound_workspace: bound_workspace.clone(),
306                }
307            }
308            Self::WorkspaceChangeRequiresNewSession { workspace_root } => {
309                Self::WorkspaceChangeRequiresNewSession { workspace_root: workspace_root.clone() }
310            }
311            Self::ArgumentParseError(msg) => Self::ArgumentParseError(msg.clone()),
312            Self::FileAccessError { path, message } => {
313                Self::FileAccessError { path: path.clone(), message: message.clone() }
314            }
315            Self::TemporaryArtifactPolicy { path, temporary_artifact_dir, message } => {
316                Self::TemporaryArtifactPolicy {
317                    path: path.clone(),
318                    temporary_artifact_dir: temporary_artifact_dir.clone(),
319                    message: message.clone(),
320                }
321            }
322            Self::DerivedCodeMapBudget {
323                path,
324                temporary_artifact_dir,
325                calls_used,
326                calls_limit,
327                unique_files_used,
328                unique_files_limit,
329                message,
330            } => Self::DerivedCodeMapBudget {
331                path: path.clone(),
332                temporary_artifact_dir: temporary_artifact_dir.clone(),
333                calls_used: *calls_used,
334                calls_limit: *calls_limit,
335                unique_files_used: *unique_files_used,
336                unique_files_limit: *unique_files_limit,
337                message: message.clone(),
338            },
339            Self::DeserializationError(msg) => Self::DeserializationError(msg.clone()),
340            Self::SerializationError(msg) => Self::SerializationError(msg.clone()),
341            Self::SearchReplaceSyntaxError(msg) => Self::SearchReplaceSyntaxError(msg.clone()),
342            Self::SearchBlockNotFound(msg) => Self::SearchBlockNotFound(msg.clone()),
343            Self::SearchBlockAmbiguous { block_content, match_count, suggestions } => {
344                Self::SearchBlockAmbiguous {
345                    block_content: block_content.clone(),
346                    match_count: *match_count,
347                    suggestions: suggestions.clone(),
348                }
349            }
350            Self::MultiFilePlanError { index, path, source } => Self::MultiFilePlanError {
351                index: *index,
352                path: path.clone(),
353                source: Box::new((**source).clone()),
354            },
355            Self::SearchReplaceSyntaxErrorDetailed {
356                message,
357                line_number,
358                block_type,
359                suggestions,
360            } => Self::SearchReplaceSyntaxErrorDetailed {
361                message: message.clone(),
362                line_number: *line_number,
363                block_type: block_type.clone(),
364                suggestions: suggestions.clone(),
365            },
366            Self::JsonParseError(msg) => Self::JsonParseError(msg.clone()),
367            Self::FileTooLarge { path, size, max_size } => {
368                Self::FileTooLarge { path: path.clone(), size: *size, max_size: *max_size }
369            }
370            Self::FileWriteError { path, message } => {
371                Self::FileWriteError { path: path.clone(), message: message.clone() }
372            }
373            Self::DataLoadingError(msg) => Self::DataLoadingError(msg.clone()),
374            Self::ParameterValidationError { field, message } => {
375                Self::ParameterValidationError { field: field.clone(), message: message.clone() }
376            }
377            Self::MissingParameterError { field, message } => {
378                Self::MissingParameterError { field: field.clone(), message: message.clone() }
379            }
380            Self::NullValueError { field } => Self::NullValueError { field: field.clone() },
381            Self::RecoverableSuggestionError { message, suggestion } => {
382                Self::RecoverableSuggestionError {
383                    message: message.clone(),
384                    suggestion: suggestion.clone(),
385                }
386            }
387            Self::ContextSaveError(msg) => Self::ContextSaveError(msg.clone()),
388            Self::CommandTimeout { command, timeout_seconds } => {
389                Self::CommandTimeout { command: command.clone(), timeout_seconds: *timeout_seconds }
390            }
391            Self::InteractiveCommandDetected { command } => {
392                Self::InteractiveCommandDetected { command: command.clone() }
393            }
394            Self::CommandAlreadyRunning { current_command, duration_seconds } => {
395                Self::CommandAlreadyRunning {
396                    current_command: current_command.clone(),
397                    duration_seconds: *duration_seconds,
398                }
399            }
400            Self::ProcessCleanupError { message } => {
401                Self::ProcessCleanupError { message: message.clone() }
402            }
403            Self::BufferOverflow { size, max_size } => {
404                Self::BufferOverflow { size: *size, max_size: *max_size }
405            }
406            Self::SessionRecoveryError { message } => {
407                Self::SessionRecoveryError { message: message.clone() }
408            }
409            Self::ResourceAllocationError { message } => {
410                Self::ResourceAllocationError { message: message.clone() }
411            }
412            Self::IoError(err) => Self::IoError(std::io::Error::new(err.kind(), err.to_string())),
413            Self::ConfigurationError(msg) => Self::ConfigurationError(msg.clone()),
414            Self::ParseError(msg) => Self::ParseError(msg.clone()),
415            Self::InvalidInput(msg) => Self::InvalidInput(msg.clone()),
416            Self::FileError(msg) => Self::FileError(msg.clone()),
417            Self::PathSecurityError { path, message } => {
418                Self::PathSecurityError { path: path.clone(), message: message.clone() }
419            }
420        }
421    }
422}