1use std::path::PathBuf;
2use thiserror::Error;
3
4#[derive(Clone, Copy, Debug, Eq, PartialEq)]
6pub enum ReadRequirement {
7 NeverRead,
8 Stale,
9 InsufficientCoverage,
10}
11
12#[derive(Error, Debug)]
14pub enum WinxError {
15 #[error("Failed to initialize shell: {0}")]
17 ShellInitializationError(String),
18
19 #[error("Workspace path error: {0}")]
21 WorkspacePathError(String),
22
23 #[error("Failed to lock the bash state: {0}")]
25 BashStateLockError(String),
26
27 #[error("Bash state not initialized. Please call Initialize first with type=\"first_call\" and a valid workspace path.")]
29 BashStateNotInitialized,
30
31 #[error("Command execution failed: {0}")]
33 CommandExecutionError(String),
34
35 #[error("{0}")]
37 NoActiveCommand(String),
38
39 #[error("{0}")]
41 BackgroundSessionNotFound(String),
42
43 #[error("Failure: {action} cannot be empty")]
45 EmptyInteractiveInput { action: String },
46
47 #[error("{0}")]
49 InteractiveTargetNotRunning(String),
50
51 #[error("Failed to parse arguments: {0}")]
53 ArgumentParseError(String),
54
55 #[error("File access error for {path}: {message}")]
57 FileAccessError { path: PathBuf, message: String },
58
59 #[error("File not found: {path}")]
61 FileNotFound { path: PathBuf },
62
63 #[error("Fresh read required for {path}: {message}")]
65 FileReadRequired {
66 path: PathBuf,
67 reason: ReadRequirement,
68 ranges: Vec<String>,
69 message: String,
70 },
71
72 #[error("File changed after the last Winx edit: {path}: {message}")]
74 FileChangedAfterEdit { path: PathBuf, message: String },
75
76 #[error("No undo checkpoint exists for {path}")]
78 UndoCheckpointNotFound { path: PathBuf },
79
80 #[error("File operation denied for {path}: {message}")]
82 FileOperationDenied { path: PathBuf, message: String },
83
84 #[error("File changed during read after {attempts} attempts: {path}")]
86 ConcurrentFileModification { path: PathBuf, attempts: usize },
87
88 #[error("File revision mismatch for {path}: expected {expected}, current {actual}")]
90 FileRevisionMismatch { path: PathBuf, expected: String, actual: String },
91
92 #[error("Security violation: {message}")]
94 PathSecurityError { path: PathBuf, message: String },
95
96 #[error("Temporary artifact policy rejected {path}: {message}")]
98 TemporaryArtifactPolicy { path: PathBuf, temporary_artifact_dir: PathBuf, message: String },
99
100 #[error(
103 "Temporary artifact budget exceeded in {temporary_artifact_dir}: workspace {total_bytes}/{max_total_bytes} bytes; session {session_files} files / {session_bytes} bytes (limits: {max_session_files} files / {max_session_bytes} bytes; largest file {largest_file_bytes} bytes, limit {max_file_bytes}). Run an explicit cleanup-only BashCommand against $WINX_TEMP_DIR before continuing."
104 )]
105 TemporaryArtifactBudgetExceeded {
106 temporary_artifact_dir: PathBuf,
107 total_bytes: u64,
108 max_total_bytes: u64,
109 session_bytes: u64,
110 max_session_bytes: u64,
111 session_files: usize,
112 max_session_files: usize,
113 largest_file_bytes: u64,
114 max_file_bytes: u64,
115 },
116
117 #[error(
119 "wait_policy={wait_policy} is not valid for BashCommand action {action}; use until_complete only with a finite foreground command"
120 )]
121 InvalidWaitPolicyForAction { wait_policy: String, action: String },
122
123 #[error("Derived CodeMap budget rejected {path}: {message}")]
125 DerivedCodeMapBudget {
126 path: PathBuf,
127 temporary_artifact_dir: PathBuf,
128 calls_used: usize,
129 calls_limit: usize,
130 unique_files_used: usize,
131 unique_files_limit: usize,
132 message: String,
133 },
134
135 #[error("Command not allowed: {0}")]
137 CommandNotAllowed(String),
138
139 #[error("Thread ID mismatch: {0}")]
141 ThreadIdMismatch(String),
142
143 #[error(
146 "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."
147 )]
148 WorkspaceBindingRequired { thread_id: String },
149
150 #[error(
153 "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."
154 )]
155 WorkspaceThreadMismatch { thread_id: String, workspace_root: PathBuf },
156
157 #[error(
160 "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."
161 )]
162 WorkspaceBindingMismatch {
163 thread_id: String,
164 requested_workspace: PathBuf,
165 bound_workspace: PathBuf,
166 },
167
168 #[error(
171 "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."
172 )]
173 WorkspaceChangeRequiresNewSession { workspace_root: PathBuf },
174
175 #[error("Deserialization error: {0}")]
177 DeserializationError(String),
178
179 #[error("Serialization error: {0}")]
181 SerializationError(String),
182
183 #[error("Search/replace syntax error: {0}")]
185 SearchReplaceSyntaxError(String),
186
187 #[error("Search block not found in content: {0}")]
189 SearchBlockNotFound(String),
190
191 #[error("Search block matched multiple times")]
193 SearchBlockAmbiguous { block_content: String, match_count: usize, suggestions: Vec<String> },
194
195 #[error(
199 "MultiFileEdit aborted before writing anything - file {index} ({path}) failed validation: {source}"
200 )]
201 MultiFilePlanError { index: usize, path: PathBuf, source: Box<WinxError> },
202
203 #[error("Search/replace syntax error: {message}")]
205 SearchReplaceSyntaxErrorDetailed {
206 message: String,
207 line_number: Option<usize>,
208 block_type: Option<String>,
209 suggestions: Vec<String>,
210 },
211
212 #[error("Invalid JSON: {0}")]
214 JsonParseError(String),
215
216 #[error("File {path} is too large: {size} bytes (max {max_size})")]
218 FileTooLarge { path: PathBuf, size: u64, max_size: u64 },
219
220 #[error("Failed to write file {path}: {message}")]
222 FileWriteError { path: PathBuf, message: String },
223
224 #[error("Failed to load data: {0}")]
226 DataLoadingError(String),
227
228 #[error("Invalid parameter: {field} - {message}")]
230 ParameterValidationError { field: String, message: String },
231
232 #[error("Required parameter missing: {field} - {message}")]
234 MissingParameterError { field: String, message: String },
235
236 #[error("Null or undefined value where object expected: {field}")]
238 NullValueError { field: String },
239
240 #[error("{message} - {suggestion}")]
242 RecoverableSuggestionError { message: String, suggestion: String },
243
244 #[error("Context save error: {0}")]
246 ContextSaveError(String),
247
248 #[error("Command timed out after {timeout_seconds}s: {command}")]
250 CommandTimeout { command: String, timeout_seconds: u64 },
251
252 #[error(
254 "Interactive command detected: {command}. Use appropriate flags or consider alternatives."
255 )]
256 InteractiveCommandDetected { command: String },
257
258 #[error("A command is already running: '{current_command}' (for {duration_seconds:.1}s). Use status_check, send_text, or interrupt.")]
260 CommandAlreadyRunning { current_command: String, duration_seconds: f64 },
261
262 #[error("Failed to cleanup process: {message}")]
264 ProcessCleanupError { message: String },
265
266 #[error("Command output exceeded maximum size: {size} bytes (max {max_size})")]
268 BufferOverflow { size: usize, max_size: usize },
269
270 #[error("Failed to recover bash session: {message}")]
272 SessionRecoveryError { message: String },
273
274 #[error("Resource allocation failed: {message}")]
276 ResourceAllocationError { message: String },
277
278 #[error("IO error: {0}")]
280 IoError(#[from] std::io::Error),
281
282 #[error("Configuration error: {0}")]
284 ConfigurationError(String),
285
286 #[error("Parse error: {0}")]
288 ParseError(String),
289
290 #[error("Invalid input: {0}")]
292 InvalidInput(String),
293
294 #[error("File error: {0}")]
296 FileError(String),
297}
298
299pub type Result<T> = std::result::Result<T, WinxError>;
301
302impl From<anyhow::Error> for WinxError {
304 fn from(error: anyhow::Error) -> Self {
305 WinxError::CommandExecutionError(format!("{error:#}"))
309 }
310}
311
312impl WinxError {
313 pub(crate) fn is_search_match_conflict(&self) -> bool {
317 match self {
318 Self::SearchBlockNotFound(_) | Self::SearchBlockAmbiguous { .. } => true,
319 Self::MultiFilePlanError { source, .. } => source.is_search_match_conflict(),
320 _ => false,
321 }
322 }
323}
324
325pub struct ErrorRecovery;
327
328impl ErrorRecovery {
329 pub fn suggest(error: WinxError, _suggestion: &str) -> WinxError {
330 error
331 }
332
333 pub fn param_error(field: &str, message: &str) -> WinxError {
334 WinxError::ParameterValidationError {
335 field: field.to_string(),
336 message: message.to_string(),
337 }
338 }
339
340 pub fn missing_param(field: &str, message: &str) -> WinxError {
341 WinxError::MissingParameterError { field: field.to_string(), message: message.to_string() }
342 }
343
344 pub fn null_value(field: &str) -> WinxError {
345 WinxError::NullValueError { field: field.to_string() }
346 }
347}