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(
82 "Undo checkpoint {undo_id} is unavailable for {path}; it expired or the server restarted"
83 )]
84 UndoExpired { path: PathBuf, undo_id: String },
85
86 #[error(
88 "Undo checkpoint {undo_id} is not the latest checkpoint for {path}; use {latest_undo_id}"
89 )]
90 UndoOutOfOrder { path: PathBuf, undo_id: String, latest_undo_id: String },
91
92 #[error("File operation denied for {path}: {message}")]
94 FileOperationDenied { path: PathBuf, message: String },
95
96 #[error("File changed during read after {attempts} attempts: {path}")]
98 ConcurrentFileModification { path: PathBuf, attempts: usize },
99
100 #[error("File revision mismatch for {path}: expected {expected}, current {actual}")]
102 FileRevisionMismatch { path: PathBuf, expected: String, actual: String },
103
104 #[error("Security violation: {message}")]
106 PathSecurityError { path: PathBuf, message: String },
107
108 #[error("Temporary artifact policy rejected {path}: {message}")]
110 TemporaryArtifactPolicy { path: PathBuf, temporary_artifact_dir: PathBuf, message: String },
111
112 #[error(
115 "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."
116 )]
117 TemporaryArtifactBudgetExceeded {
118 temporary_artifact_dir: PathBuf,
119 total_bytes: u64,
120 max_total_bytes: u64,
121 session_bytes: u64,
122 max_session_bytes: u64,
123 session_files: usize,
124 max_session_files: usize,
125 largest_file_bytes: u64,
126 max_file_bytes: u64,
127 },
128
129 #[error(
131 "wait_policy={wait_policy} is not valid for BashCommand action {action}; use until_complete only with a finite foreground command"
132 )]
133 InvalidWaitPolicyForAction { wait_policy: String, action: String },
134
135 #[error("Derived CodeMap budget rejected {path}: {message}")]
137 DerivedCodeMapBudget {
138 path: PathBuf,
139 temporary_artifact_dir: PathBuf,
140 calls_used: usize,
141 calls_limit: usize,
142 unique_files_used: usize,
143 unique_files_limit: usize,
144 message: String,
145 },
146
147 #[error("Command not allowed: {0}")]
149 CommandNotAllowed(String),
150
151 #[error("Thread ID mismatch: {0}")]
153 ThreadIdMismatch(String),
154
155 #[error(
158 "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."
159 )]
160 WorkspaceBindingRequired { thread_id: String },
161
162 #[error(
165 "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."
166 )]
167 WorkspaceThreadMismatch { thread_id: String, workspace_root: PathBuf },
168
169 #[error(
172 "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."
173 )]
174 WorkspaceBindingMismatch {
175 thread_id: String,
176 requested_workspace: PathBuf,
177 bound_workspace: PathBuf,
178 },
179
180 #[error(
183 "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."
184 )]
185 WorkspaceChangeRequiresNewSession { workspace_root: PathBuf },
186
187 #[error("Deserialization error: {0}")]
189 DeserializationError(String),
190
191 #[error("Serialization error: {0}")]
193 SerializationError(String),
194
195 #[error("Search/replace syntax error: {0}")]
197 SearchReplaceSyntaxError(String),
198
199 #[error("Search block not found in content: {0}")]
201 SearchBlockNotFound(String),
202
203 #[error("Search block matched multiple times")]
205 SearchBlockAmbiguous { block_content: String, match_count: usize, suggestions: Vec<String> },
206
207 #[error(
211 "MultiFileEdit aborted before writing anything - file {index} ({path}) failed validation: {source}"
212 )]
213 MultiFilePlanError { index: usize, path: PathBuf, source: Box<WinxError> },
214
215 #[error("{source}")]
219 EditContextError { path: PathBuf, source: Box<WinxError> },
220
221 #[error(
224 "EditFiles aborted before writing anything - file {index} ({path}, mode={mode}) failed validation: {source}"
225 )]
226 IndexedEditError { index: usize, path: PathBuf, mode: String, source: Box<WinxError> },
227
228 #[error("Search/replace syntax error: {message}")]
230 SearchReplaceSyntaxErrorDetailed {
231 message: String,
232 line_number: Option<usize>,
233 block_type: Option<String>,
234 suggestions: Vec<String>,
235 },
236
237 #[error("Invalid JSON: {0}")]
239 JsonParseError(String),
240
241 #[error("File {path} is too large: {size} bytes (max {max_size})")]
243 FileTooLarge { path: PathBuf, size: u64, max_size: u64 },
244
245 #[error("Failed to write file {path}: {message}")]
247 FileWriteError { path: PathBuf, message: String },
248
249 #[error("Failed to load data: {0}")]
251 DataLoadingError(String),
252
253 #[error("Invalid parameter: {field} - {message}")]
255 ParameterValidationError { field: String, message: String },
256
257 #[error("Required parameter missing: {field} - {message}")]
259 MissingParameterError { field: String, message: String },
260
261 #[error("Null or undefined value where object expected: {field}")]
263 NullValueError { field: String },
264
265 #[error("{message} - {suggestion}")]
267 RecoverableSuggestionError { message: String, suggestion: String },
268
269 #[error("Context save error: {0}")]
271 ContextSaveError(String),
272
273 #[error("Command timed out after {timeout_seconds}s: {command}")]
275 CommandTimeout { command: String, timeout_seconds: u64 },
276
277 #[error(
279 "Interactive command detected: {command}. Use appropriate flags or consider alternatives."
280 )]
281 InteractiveCommandDetected { command: String },
282
283 #[error("A command is already running: '{current_command}' (for {duration_seconds:.1}s). Use status_check, send_text, or interrupt.")]
285 CommandAlreadyRunning { current_command: String, duration_seconds: f64 },
286
287 #[error("Failed to cleanup process: {message}")]
289 ProcessCleanupError { message: String },
290
291 #[error("Command output exceeded maximum size: {size} bytes (max {max_size})")]
293 BufferOverflow { size: usize, max_size: usize },
294
295 #[error("Failed to recover bash session: {message}")]
297 SessionRecoveryError { message: String },
298
299 #[error("Resource allocation failed: {message}")]
301 ResourceAllocationError { message: String },
302
303 #[error("IO error: {0}")]
305 IoError(#[from] std::io::Error),
306
307 #[error("Configuration error: {0}")]
309 ConfigurationError(String),
310
311 #[error("Parse error: {0}")]
313 ParseError(String),
314
315 #[error("Invalid input: {0}")]
317 InvalidInput(String),
318
319 #[error("File error: {0}")]
321 FileError(String),
322}
323
324pub type Result<T> = std::result::Result<T, WinxError>;
326
327impl From<anyhow::Error> for WinxError {
329 fn from(error: anyhow::Error) -> Self {
330 WinxError::CommandExecutionError(format!("{error:#}"))
334 }
335}
336
337impl WinxError {
338 pub(crate) fn is_shell_runtime_failure(&self) -> bool {
343 matches!(
344 self,
345 Self::ShellInitializationError(_)
346 | Self::BashStateLockError(_)
347 | Self::BashStateNotInitialized
348 | Self::CommandExecutionError(_)
349 | Self::CommandTimeout { .. }
350 | Self::ProcessCleanupError { .. }
351 | Self::SessionRecoveryError { .. }
352 )
353 }
354
355 pub(crate) fn is_search_match_conflict(&self) -> bool {
359 match self {
360 Self::SearchBlockNotFound(_) | Self::SearchBlockAmbiguous { .. } => true,
361 Self::MultiFilePlanError { source, .. }
362 | Self::EditContextError { source, .. }
363 | Self::IndexedEditError { source, .. } => source.is_search_match_conflict(),
364 _ => false,
365 }
366 }
367
368 pub(crate) fn edit_source(&self) -> &Self {
371 match self {
372 Self::MultiFilePlanError { source, .. }
373 | Self::EditContextError { source, .. }
374 | Self::IndexedEditError { source, .. } => source.edit_source(),
375 _ => self,
376 }
377 }
378}
379
380pub struct ErrorRecovery;
382
383impl ErrorRecovery {
384 pub fn suggest(error: WinxError, _suggestion: &str) -> WinxError {
385 error
386 }
387
388 pub fn param_error(field: &str, message: &str) -> WinxError {
389 WinxError::ParameterValidationError {
390 field: field.to_string(),
391 message: message.to_string(),
392 }
393 }
394
395 pub fn missing_param(field: &str, message: &str) -> WinxError {
396 WinxError::MissingParameterError { field: field.to_string(), message: message.to_string() }
397 }
398
399 pub fn null_value(field: &str) -> WinxError {
400 WinxError::NullValueError { field: field.to_string() }
401 }
402}