use std::path::PathBuf;
use thiserror::Error;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ReadRequirement {
NeverRead,
Stale,
InsufficientCoverage,
}
#[derive(Error, Debug)]
pub enum WinxError {
#[error("Failed to initialize shell: {0}")]
ShellInitializationError(String),
#[error("Workspace path error: {0}")]
WorkspacePathError(String),
#[error("Failed to lock the bash state: {0}")]
BashStateLockError(String),
#[error("Bash state not initialized. Please call Initialize first with type=\"first_call\" and a valid workspace path.")]
BashStateNotInitialized,
#[error("Command execution failed: {0}")]
CommandExecutionError(String),
#[error("{0}")]
NoActiveCommand(String),
#[error("{0}")]
BackgroundSessionNotFound(String),
#[error("Failure: {action} cannot be empty")]
EmptyInteractiveInput { action: String },
#[error("{0}")]
InteractiveTargetNotRunning(String),
#[error("Failed to parse arguments: {0}")]
ArgumentParseError(String),
#[error("File access error for {path}: {message}")]
FileAccessError { path: PathBuf, message: String },
#[error("File not found: {path}")]
FileNotFound { path: PathBuf },
#[error("Fresh read required for {path}: {message}")]
FileReadRequired {
path: PathBuf,
reason: ReadRequirement,
ranges: Vec<String>,
message: String,
},
#[error("File changed after the last Winx edit: {path}: {message}")]
FileChangedAfterEdit { path: PathBuf, message: String },
#[error("No undo checkpoint exists for {path}")]
UndoCheckpointNotFound { path: PathBuf },
#[error(
"Undo checkpoint {undo_id} is unavailable for {path}; it expired or the server restarted"
)]
UndoExpired { path: PathBuf, undo_id: String },
#[error(
"Undo checkpoint {undo_id} is not the latest checkpoint for {path}; use {latest_undo_id}"
)]
UndoOutOfOrder { path: PathBuf, undo_id: String, latest_undo_id: String },
#[error("File operation denied for {path}: {message}")]
FileOperationDenied { path: PathBuf, message: String },
#[error("File changed during read after {attempts} attempts: {path}")]
ConcurrentFileModification { path: PathBuf, attempts: usize },
#[error("File revision mismatch for {path}: expected {expected}, current {actual}")]
FileRevisionMismatch { path: PathBuf, expected: String, actual: String },
#[error("Security violation: {message}")]
PathSecurityError { path: PathBuf, message: String },
#[error("Temporary artifact policy rejected {path}: {message}")]
TemporaryArtifactPolicy { path: PathBuf, temporary_artifact_dir: PathBuf, message: String },
#[error(
"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."
)]
TemporaryArtifactBudgetExceeded {
temporary_artifact_dir: PathBuf,
total_bytes: u64,
max_total_bytes: u64,
session_bytes: u64,
max_session_bytes: u64,
session_files: usize,
max_session_files: usize,
largest_file_bytes: u64,
max_file_bytes: u64,
},
#[error(
"wait_policy={wait_policy} is not valid for BashCommand action {action}; use until_complete only with a finite foreground command"
)]
InvalidWaitPolicyForAction { wait_policy: String, action: String },
#[error("Derived CodeMap budget rejected {path}: {message}")]
DerivedCodeMapBudget {
path: PathBuf,
temporary_artifact_dir: PathBuf,
calls_used: usize,
calls_limit: usize,
unique_files_used: usize,
unique_files_limit: usize,
message: String,
},
#[error("Command not allowed: {0}")]
CommandNotAllowed(String),
#[error("Thread ID mismatch: {0}")]
ThreadIdMismatch(String),
#[error(
"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."
)]
WorkspaceBindingRequired { thread_id: String },
#[error(
"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."
)]
WorkspaceThreadMismatch { thread_id: String, workspace_root: PathBuf },
#[error(
"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."
)]
WorkspaceBindingMismatch {
thread_id: String,
requested_workspace: PathBuf,
bound_workspace: PathBuf,
},
#[error(
"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."
)]
WorkspaceChangeRequiresNewSession { workspace_root: PathBuf },
#[error("Deserialization error: {0}")]
DeserializationError(String),
#[error("Serialization error: {0}")]
SerializationError(String),
#[error("Search/replace syntax error: {0}")]
SearchReplaceSyntaxError(String),
#[error("Search block not found in content: {0}")]
SearchBlockNotFound(String),
#[error("Search block matched multiple times")]
SearchBlockAmbiguous { block_content: String, match_count: usize, suggestions: Vec<String> },
#[error(
"MultiFileEdit aborted before writing anything - file {index} ({path}) failed validation: {source}"
)]
MultiFilePlanError { index: usize, path: PathBuf, source: Box<WinxError> },
#[error("{source}")]
EditContextError { path: PathBuf, source: Box<WinxError> },
#[error(
"EditFiles aborted before writing anything - file {index} ({path}, mode={mode}) failed validation: {source}"
)]
IndexedEditError { index: usize, path: PathBuf, mode: String, source: Box<WinxError> },
#[error("Search/replace syntax error: {message}")]
SearchReplaceSyntaxErrorDetailed {
message: String,
line_number: Option<usize>,
block_type: Option<String>,
suggestions: Vec<String>,
},
#[error("Invalid JSON: {0}")]
JsonParseError(String),
#[error("File {path} is too large: {size} bytes (max {max_size})")]
FileTooLarge { path: PathBuf, size: u64, max_size: u64 },
#[error("Failed to write file {path}: {message}")]
FileWriteError { path: PathBuf, message: String },
#[error("Failed to load data: {0}")]
DataLoadingError(String),
#[error("Invalid parameter: {field} - {message}")]
ParameterValidationError { field: String, message: String },
#[error("Required parameter missing: {field} - {message}")]
MissingParameterError { field: String, message: String },
#[error("Null or undefined value where object expected: {field}")]
NullValueError { field: String },
#[error("{message} - {suggestion}")]
RecoverableSuggestionError { message: String, suggestion: String },
#[error("Context save error: {0}")]
ContextSaveError(String),
#[error("Command timed out after {timeout_seconds}s: {command}")]
CommandTimeout { command: String, timeout_seconds: u64 },
#[error(
"Interactive command detected: {command}. Use appropriate flags or consider alternatives."
)]
InteractiveCommandDetected { command: String },
#[error("A command is already running: '{current_command}' (for {duration_seconds:.1}s). Use status_check, send_text, or interrupt.")]
CommandAlreadyRunning { current_command: String, duration_seconds: f64 },
#[error("Failed to cleanup process: {message}")]
ProcessCleanupError { message: String },
#[error("Command output exceeded maximum size: {size} bytes (max {max_size})")]
BufferOverflow { size: usize, max_size: usize },
#[error("Failed to recover bash session: {message}")]
SessionRecoveryError { message: String },
#[error("Resource allocation failed: {message}")]
ResourceAllocationError { message: String },
#[error("IO error: {0}")]
IoError(#[from] std::io::Error),
#[error("Configuration error: {0}")]
ConfigurationError(String),
#[error("Parse error: {0}")]
ParseError(String),
#[error("Invalid input: {0}")]
InvalidInput(String),
#[error("File error: {0}")]
FileError(String),
}
pub type Result<T> = std::result::Result<T, WinxError>;
impl From<anyhow::Error> for WinxError {
fn from(error: anyhow::Error) -> Self {
WinxError::CommandExecutionError(format!("{error:#}"))
}
}
impl WinxError {
pub(crate) fn is_shell_runtime_failure(&self) -> bool {
matches!(
self,
Self::ShellInitializationError(_)
| Self::BashStateLockError(_)
| Self::BashStateNotInitialized
| Self::CommandExecutionError(_)
| Self::CommandTimeout { .. }
| Self::ProcessCleanupError { .. }
| Self::SessionRecoveryError { .. }
)
}
pub(crate) fn is_search_match_conflict(&self) -> bool {
match self {
Self::SearchBlockNotFound(_) | Self::SearchBlockAmbiguous { .. } => true,
Self::MultiFilePlanError { source, .. }
| Self::EditContextError { source, .. }
| Self::IndexedEditError { source, .. } => source.is_search_match_conflict(),
_ => false,
}
}
pub(crate) fn edit_source(&self) -> &Self {
match self {
Self::MultiFilePlanError { source, .. }
| Self::EditContextError { source, .. }
| Self::IndexedEditError { source, .. } => source.edit_source(),
_ => self,
}
}
}
pub struct ErrorRecovery;
impl ErrorRecovery {
pub fn suggest(error: WinxError, _suggestion: &str) -> WinxError {
error
}
pub fn param_error(field: &str, message: &str) -> WinxError {
WinxError::ParameterValidationError {
field: field.to_string(),
message: message.to_string(),
}
}
pub fn missing_param(field: &str, message: &str) -> WinxError {
WinxError::MissingParameterError { field: field.to_string(), message: message.to_string() }
}
pub fn null_value(field: &str) -> WinxError {
WinxError::NullValueError { field: field.to_string() }
}
}