1use rmcp::ErrorData as McpError;
11use schemars::JsonSchema;
12use serde::Serialize;
13use serde_json::json;
14
15use crate::envelope::{Freshness, RuntimeFacts, RuntimeFailure};
16use lean_rs_worker_parent::LeanWorkerError;
17
18#[derive(Debug, Clone, Serialize, JsonSchema, thiserror::Error)]
19#[error("worker unavailable: {reason}")]
20pub struct WorkerUnavailable {
21 pub(crate) retryable: bool,
22 pub(crate) worker_restarted: bool,
23 pub(crate) project_root: String,
24 pub(crate) project_hash: String,
25 pub(crate) imports: Vec<String>,
26 pub(crate) session_id: String,
27 pub(crate) lean_toolchain: String,
28 pub(crate) worker_generation: u64,
29 pub(crate) reason: String,
30 pub(crate) restart_cause: Option<String>,
31 pub(crate) rss_kib: Option<u64>,
32 pub(crate) limit_kib: Option<u64>,
33 pub(crate) retry_after_millis: Option<u64>,
34 pub(crate) restarts_in_window: Option<u64>,
35 pub(crate) window_millis: Option<u64>,
36 pub(crate) runtime: RuntimeFacts,
37 #[serde(skip)]
45 #[schemars(skip)]
46 pub(crate) toolchain_advisories: Vec<String>,
47}
48
49impl WorkerUnavailable {
50 pub(crate) fn freshness(&self) -> Freshness {
51 Freshness {
52 project_root: self.project_root.clone(),
53 project_hash: self.project_hash.clone(),
54 imports: self.imports.clone(),
55 session_id: self.session_id.clone(),
56 lean_toolchain: self.lean_toolchain.clone(),
57 toolchain_advisories: self.toolchain_advisories.clone(),
58 }
59 }
60
61 pub(crate) fn failure(&self) -> RuntimeFailure {
62 RuntimeFailure {
63 reason: self.reason.clone(),
64 retryable: self.retryable,
65 project_root: self.project_root.clone(),
66 session_id: self.session_id.clone(),
67 worker_generation: self.worker_generation,
68 worker_restarted: self.worker_restarted,
69 restart_cause: self.restart_cause.clone(),
70 rss_kib: self.rss_kib,
71 limit_kib: self.limit_kib,
72 retry_after_millis: self.retry_after_millis,
73 restarts_in_window: self.restarts_in_window,
74 window_millis: self.window_millis,
75 }
76 }
77}
78
79#[derive(Debug, thiserror::Error)]
80pub enum ServerError {
81 #[error("lean runtime: {0}")]
82 Lean(String),
83
84 #[error("session thread is gone")]
85 SessionGone,
86
87 #[error("{0}")]
88 WorkerUnavailable(Box<WorkerUnavailable>),
89
90 #[error("lake project not usable: {0}")]
91 BadProject(String),
92
93 #[error("io: {0}")]
94 Io(#[from] std::io::Error),
95
96 #[error("internal: {0}")]
97 Internal(String),
98}
99
100pub type Result<T> = std::result::Result<T, ServerError>;
101
102impl ServerError {
103 pub(crate) fn worker_unavailable(info: WorkerUnavailable) -> Self {
104 Self::WorkerUnavailable(Box::new(info))
105 }
106}
107
108impl From<ServerError> for McpError {
109 fn from(err: ServerError) -> Self {
110 match err {
113 ServerError::WorkerUnavailable(info) => {
114 let message = info.to_string();
115 let data = json!({
116 "retryable": info.retryable,
117 "worker_restarted": info.worker_restarted,
118 "project_root": &info.project_root,
119 "project_hash": &info.project_hash,
120 "imports": &info.imports,
121 "session_id": &info.session_id,
122 "lean_toolchain": &info.lean_toolchain,
123 "worker_generation": info.worker_generation,
124 "reason": &info.reason,
125 "restart_cause": &info.restart_cause,
126 "rss_kib": info.rss_kib,
127 "limit_kib": info.limit_kib,
128 "retry_after_millis": info.retry_after_millis,
129 "restarts_in_window": info.restarts_in_window,
130 "window_millis": info.window_millis,
131 });
132 Self::internal_error(message, Some(data))
133 }
134 other @ (ServerError::Lean(_)
135 | ServerError::SessionGone
136 | ServerError::BadProject(_)
137 | ServerError::Io(_)
138 | ServerError::Internal(_)) => Self::internal_error(other.to_string(), None),
139 }
140 }
141}
142
143impl From<anyhow::Error> for ServerError {
144 fn from(err: anyhow::Error) -> Self {
145 Self::Internal(err.to_string())
146 }
147}
148
149#[allow(
155 clippy::needless_pass_by_value,
156 clippy::wildcard_enum_match_arm,
157 reason = "LeanWorkerError is upstream-evolving; everything outside the bootstrap-classification set maps to Lean for the MCP wire"
158)]
159pub(crate) fn map_worker_err(err: LeanWorkerError) -> ServerError {
160 match err {
161 LeanWorkerError::WorkerChildUnresolved { .. }
162 | LeanWorkerError::WorkerChildNotExecutable { .. }
163 | LeanWorkerError::Bootstrap { .. }
164 | LeanWorkerError::CapabilityBuild { .. }
165 | LeanWorkerError::Setup { .. }
166 | LeanWorkerError::Handshake { .. }
167 | LeanWorkerError::CapabilityMetadataMismatch { .. } => ServerError::BadProject(err.to_string()),
168 LeanWorkerError::ChildPanicOrAbort { .. } | LeanWorkerError::ChildExited { .. } => ServerError::Lean(format!(
169 "worker process exited; project worker will restart before the next request: {err}"
170 )),
171 _ => ServerError::Lean(err.to_string()),
172 }
173}