1use std::path::PathBuf;
2use thiserror::Error;
3
4#[derive(Error, Debug)]
6pub enum WinxError {
7 #[error("Failed to initialize shell: {0}")]
9 ShellInitializationError(String),
10
11 #[error("Workspace path error: {0}")]
13 WorkspacePathError(String),
14
15 #[error("Failed to lock the bash state: {0}")]
17 BashStateLockError(String),
18
19 #[error("Bash state not initialized. Please call Initialize first with type=\"first_call\" and a valid workspace path.")]
21 BashStateNotInitialized,
22
23 #[error("Command execution failed: {0}")]
25 CommandExecutionError(String),
26
27 #[error("{0}")]
29 NoActiveCommand(String),
30
31 #[error("{0}")]
33 BackgroundSessionNotFound(String),
34
35 #[error("Failure: {action} cannot be empty")]
37 EmptyInteractiveInput { action: String },
38
39 #[error("{0}")]
41 InteractiveTargetNotRunning(String),
42
43 #[error("Failed to parse arguments: {0}")]
45 ArgumentParseError(String),
46
47 #[error("File access error for {path}: {message}")]
49 FileAccessError { path: PathBuf, message: String },
50
51 #[error("Security violation: {message}")]
53 PathSecurityError { path: PathBuf, message: String },
54
55 #[error("Temporary artifact policy rejected {path}: {message}")]
57 TemporaryArtifactPolicy { path: PathBuf, temporary_artifact_dir: PathBuf, message: String },
58
59 #[error(
62 "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."
63 )]
64 TemporaryArtifactBudgetExceeded {
65 temporary_artifact_dir: PathBuf,
66 total_bytes: u64,
67 max_total_bytes: u64,
68 session_bytes: u64,
69 max_session_bytes: u64,
70 session_files: usize,
71 max_session_files: usize,
72 largest_file_bytes: u64,
73 max_file_bytes: u64,
74 },
75
76 #[error(
78 "wait_policy={wait_policy} is not valid for BashCommand action {action}; use until_complete only with a finite foreground command"
79 )]
80 InvalidWaitPolicyForAction { wait_policy: String, action: String },
81
82 #[error("Derived CodeMap budget rejected {path}: {message}")]
84 DerivedCodeMapBudget {
85 path: PathBuf,
86 temporary_artifact_dir: PathBuf,
87 calls_used: usize,
88 calls_limit: usize,
89 unique_files_used: usize,
90 unique_files_limit: usize,
91 message: String,
92 },
93
94 #[error("Command not allowed: {0}")]
96 CommandNotAllowed(String),
97
98 #[error("Thread ID mismatch: {0}")]
100 ThreadIdMismatch(String),
101
102 #[error(
105 "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."
106 )]
107 WorkspaceBindingRequired { thread_id: String },
108
109 #[error(
112 "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."
113 )]
114 WorkspaceThreadMismatch { thread_id: String, workspace_root: PathBuf },
115
116 #[error(
119 "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."
120 )]
121 WorkspaceBindingMismatch {
122 thread_id: String,
123 requested_workspace: PathBuf,
124 bound_workspace: PathBuf,
125 },
126
127 #[error(
130 "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."
131 )]
132 WorkspaceChangeRequiresNewSession { workspace_root: PathBuf },
133
134 #[error("Deserialization error: {0}")]
136 DeserializationError(String),
137
138 #[error("Serialization error: {0}")]
140 SerializationError(String),
141
142 #[error("Search/replace syntax error: {0}")]
144 SearchReplaceSyntaxError(String),
145
146 #[error("Search block not found in content: {0}")]
148 SearchBlockNotFound(String),
149
150 #[error("Search block matched multiple times")]
152 SearchBlockAmbiguous { block_content: String, match_count: usize, suggestions: Vec<String> },
153
154 #[error(
158 "MultiFileEdit aborted before writing anything - file {index} ({path}) failed validation: {source}"
159 )]
160 MultiFilePlanError { index: usize, path: PathBuf, source: Box<WinxError> },
161
162 #[error("Search/replace syntax error: {message}")]
164 SearchReplaceSyntaxErrorDetailed {
165 message: String,
166 line_number: Option<usize>,
167 block_type: Option<String>,
168 suggestions: Vec<String>,
169 },
170
171 #[error("Invalid JSON: {0}")]
173 JsonParseError(String),
174
175 #[error("File {path} is too large: {size} bytes (max {max_size})")]
177 FileTooLarge { path: PathBuf, size: u64, max_size: u64 },
178
179 #[error("Failed to write file {path}: {message}")]
181 FileWriteError { path: PathBuf, message: String },
182
183 #[error("Failed to load data: {0}")]
185 DataLoadingError(String),
186
187 #[error("Invalid parameter: {field} - {message}")]
189 ParameterValidationError { field: String, message: String },
190
191 #[error("Required parameter missing: {field} - {message}")]
193 MissingParameterError { field: String, message: String },
194
195 #[error("Null or undefined value where object expected: {field}")]
197 NullValueError { field: String },
198
199 #[error("{message} - {suggestion}")]
201 RecoverableSuggestionError { message: String, suggestion: String },
202
203 #[error("Context save error: {0}")]
205 ContextSaveError(String),
206
207 #[error("Command timed out after {timeout_seconds}s: {command}")]
209 CommandTimeout { command: String, timeout_seconds: u64 },
210
211 #[error(
213 "Interactive command detected: {command}. Use appropriate flags or consider alternatives."
214 )]
215 InteractiveCommandDetected { command: String },
216
217 #[error("A command is already running: '{current_command}' (for {duration_seconds:.1}s). Use status_check, send_text, or interrupt.")]
219 CommandAlreadyRunning { current_command: String, duration_seconds: f64 },
220
221 #[error("Failed to cleanup process: {message}")]
223 ProcessCleanupError { message: String },
224
225 #[error("Command output exceeded maximum size: {size} bytes (max {max_size})")]
227 BufferOverflow { size: usize, max_size: usize },
228
229 #[error("Failed to recover bash session: {message}")]
231 SessionRecoveryError { message: String },
232
233 #[error("Resource allocation failed: {message}")]
235 ResourceAllocationError { message: String },
236
237 #[error("IO error: {0}")]
239 IoError(#[from] std::io::Error),
240
241 #[error("Configuration error: {0}")]
243 ConfigurationError(String),
244
245 #[error("Parse error: {0}")]
247 ParseError(String),
248
249 #[error("Invalid input: {0}")]
251 InvalidInput(String),
252
253 #[error("File error: {0}")]
255 FileError(String),
256}
257
258pub type Result<T> = std::result::Result<T, WinxError>;
260
261impl From<anyhow::Error> for WinxError {
263 fn from(error: anyhow::Error) -> Self {
264 WinxError::CommandExecutionError(format!("{error:#}"))
268 }
269}
270
271pub struct ErrorRecovery;
273
274impl ErrorRecovery {
275 pub fn suggest(error: WinxError, _suggestion: &str) -> WinxError {
276 error
277 }
278
279 pub fn param_error(field: &str, message: &str) -> WinxError {
280 WinxError::ParameterValidationError {
281 field: field.to_string(),
282 message: message.to_string(),
283 }
284 }
285
286 pub fn missing_param(field: &str, message: &str) -> WinxError {
287 WinxError::MissingParameterError { field: field.to_string(), message: message.to_string() }
288 }
289
290 pub fn null_value(field: &str) -> WinxError {
291 WinxError::NullValueError { field: field.to_string() }
292 }
293}
294
295impl Clone for WinxError {
297 #[allow(clippy::too_many_lines)] fn clone(&self) -> Self {
299 match self {
300 Self::ShellInitializationError(msg) => Self::ShellInitializationError(msg.clone()),
301 Self::WorkspacePathError(msg) => Self::WorkspacePathError(msg.clone()),
302 Self::BashStateLockError(msg) => Self::BashStateLockError(msg.clone()),
303 Self::BashStateNotInitialized => Self::BashStateNotInitialized,
304 Self::CommandExecutionError(msg) => Self::CommandExecutionError(msg.clone()),
305 Self::NoActiveCommand(msg) => Self::NoActiveCommand(msg.clone()),
306 Self::BackgroundSessionNotFound(msg) => Self::BackgroundSessionNotFound(msg.clone()),
307 Self::EmptyInteractiveInput { action } => {
308 Self::EmptyInteractiveInput { action: action.clone() }
309 }
310 Self::InteractiveTargetNotRunning(msg) => {
311 Self::InteractiveTargetNotRunning(msg.clone())
312 }
313 Self::CommandNotAllowed(msg) => Self::CommandNotAllowed(msg.clone()),
314 Self::ThreadIdMismatch(msg) => Self::ThreadIdMismatch(msg.clone()),
315 Self::WorkspaceBindingRequired { thread_id } => {
316 Self::WorkspaceBindingRequired { thread_id: thread_id.clone() }
317 }
318 Self::WorkspaceThreadMismatch { thread_id, workspace_root } => {
319 Self::WorkspaceThreadMismatch {
320 thread_id: thread_id.clone(),
321 workspace_root: workspace_root.clone(),
322 }
323 }
324 Self::WorkspaceBindingMismatch { thread_id, requested_workspace, bound_workspace } => {
325 Self::WorkspaceBindingMismatch {
326 thread_id: thread_id.clone(),
327 requested_workspace: requested_workspace.clone(),
328 bound_workspace: bound_workspace.clone(),
329 }
330 }
331 Self::WorkspaceChangeRequiresNewSession { workspace_root } => {
332 Self::WorkspaceChangeRequiresNewSession { workspace_root: workspace_root.clone() }
333 }
334 Self::ArgumentParseError(msg) => Self::ArgumentParseError(msg.clone()),
335 Self::FileAccessError { path, message } => {
336 Self::FileAccessError { path: path.clone(), message: message.clone() }
337 }
338 Self::TemporaryArtifactPolicy { path, temporary_artifact_dir, message } => {
339 Self::TemporaryArtifactPolicy {
340 path: path.clone(),
341 temporary_artifact_dir: temporary_artifact_dir.clone(),
342 message: message.clone(),
343 }
344 }
345 Self::TemporaryArtifactBudgetExceeded {
346 temporary_artifact_dir,
347 total_bytes,
348 max_total_bytes,
349 session_bytes,
350 max_session_bytes,
351 session_files,
352 max_session_files,
353 largest_file_bytes,
354 max_file_bytes,
355 } => Self::TemporaryArtifactBudgetExceeded {
356 temporary_artifact_dir: temporary_artifact_dir.clone(),
357 total_bytes: *total_bytes,
358 max_total_bytes: *max_total_bytes,
359 session_bytes: *session_bytes,
360 max_session_bytes: *max_session_bytes,
361 session_files: *session_files,
362 max_session_files: *max_session_files,
363 largest_file_bytes: *largest_file_bytes,
364 max_file_bytes: *max_file_bytes,
365 },
366 Self::InvalidWaitPolicyForAction { wait_policy, action } => {
367 Self::InvalidWaitPolicyForAction {
368 wait_policy: wait_policy.clone(),
369 action: action.clone(),
370 }
371 }
372 Self::DerivedCodeMapBudget {
373 path,
374 temporary_artifact_dir,
375 calls_used,
376 calls_limit,
377 unique_files_used,
378 unique_files_limit,
379 message,
380 } => Self::DerivedCodeMapBudget {
381 path: path.clone(),
382 temporary_artifact_dir: temporary_artifact_dir.clone(),
383 calls_used: *calls_used,
384 calls_limit: *calls_limit,
385 unique_files_used: *unique_files_used,
386 unique_files_limit: *unique_files_limit,
387 message: message.clone(),
388 },
389 Self::DeserializationError(msg) => Self::DeserializationError(msg.clone()),
390 Self::SerializationError(msg) => Self::SerializationError(msg.clone()),
391 Self::SearchReplaceSyntaxError(msg) => Self::SearchReplaceSyntaxError(msg.clone()),
392 Self::SearchBlockNotFound(msg) => Self::SearchBlockNotFound(msg.clone()),
393 Self::SearchBlockAmbiguous { block_content, match_count, suggestions } => {
394 Self::SearchBlockAmbiguous {
395 block_content: block_content.clone(),
396 match_count: *match_count,
397 suggestions: suggestions.clone(),
398 }
399 }
400 Self::MultiFilePlanError { index, path, source } => Self::MultiFilePlanError {
401 index: *index,
402 path: path.clone(),
403 source: Box::new((**source).clone()),
404 },
405 Self::SearchReplaceSyntaxErrorDetailed {
406 message,
407 line_number,
408 block_type,
409 suggestions,
410 } => Self::SearchReplaceSyntaxErrorDetailed {
411 message: message.clone(),
412 line_number: *line_number,
413 block_type: block_type.clone(),
414 suggestions: suggestions.clone(),
415 },
416 Self::JsonParseError(msg) => Self::JsonParseError(msg.clone()),
417 Self::FileTooLarge { path, size, max_size } => {
418 Self::FileTooLarge { path: path.clone(), size: *size, max_size: *max_size }
419 }
420 Self::FileWriteError { path, message } => {
421 Self::FileWriteError { path: path.clone(), message: message.clone() }
422 }
423 Self::DataLoadingError(msg) => Self::DataLoadingError(msg.clone()),
424 Self::ParameterValidationError { field, message } => {
425 Self::ParameterValidationError { field: field.clone(), message: message.clone() }
426 }
427 Self::MissingParameterError { field, message } => {
428 Self::MissingParameterError { field: field.clone(), message: message.clone() }
429 }
430 Self::NullValueError { field } => Self::NullValueError { field: field.clone() },
431 Self::RecoverableSuggestionError { message, suggestion } => {
432 Self::RecoverableSuggestionError {
433 message: message.clone(),
434 suggestion: suggestion.clone(),
435 }
436 }
437 Self::ContextSaveError(msg) => Self::ContextSaveError(msg.clone()),
438 Self::CommandTimeout { command, timeout_seconds } => {
439 Self::CommandTimeout { command: command.clone(), timeout_seconds: *timeout_seconds }
440 }
441 Self::InteractiveCommandDetected { command } => {
442 Self::InteractiveCommandDetected { command: command.clone() }
443 }
444 Self::CommandAlreadyRunning { current_command, duration_seconds } => {
445 Self::CommandAlreadyRunning {
446 current_command: current_command.clone(),
447 duration_seconds: *duration_seconds,
448 }
449 }
450 Self::ProcessCleanupError { message } => {
451 Self::ProcessCleanupError { message: message.clone() }
452 }
453 Self::BufferOverflow { size, max_size } => {
454 Self::BufferOverflow { size: *size, max_size: *max_size }
455 }
456 Self::SessionRecoveryError { message } => {
457 Self::SessionRecoveryError { message: message.clone() }
458 }
459 Self::ResourceAllocationError { message } => {
460 Self::ResourceAllocationError { message: message.clone() }
461 }
462 Self::IoError(err) => Self::IoError(std::io::Error::new(err.kind(), err.to_string())),
463 Self::ConfigurationError(msg) => Self::ConfigurationError(msg.clone()),
464 Self::ParseError(msg) => Self::ParseError(msg.clone()),
465 Self::InvalidInput(msg) => Self::InvalidInput(msg.clone()),
466 Self::FileError(msg) => Self::FileError(msg.clone()),
467 Self::PathSecurityError { path, message } => {
468 Self::PathSecurityError { path: path.clone(), message: message.clone() }
469 }
470 }
471 }
472}