Skip to main content

meerkat_mobkit/unified_runtime/
module_ops.rs

1//! Module lifecycle operations — registration, health checks, and capability probing.
2
3use std::time::Duration;
4
5use crate::runtime::{
6    DeliveryHistoryRequest, DeliveryHistoryResponse, DeliveryRecord, DeliverySendError,
7    DeliverySendRequest, GatingAuditEntry, GatingDecideError, GatingDecideRequest,
8    GatingDecisionResult, GatingEvaluateRequest, GatingEvaluateResult, GatingPendingEntry,
9    LifecycleEvent, MemoryIndexError, MemoryIndexRequest, MemoryIndexResult, MemoryQueryRequest,
10    MemoryQueryResult, MemoryStoreInfo, ModuleHealthTransition, RoutingResolution,
11    RoutingResolveError, RoutingResolveRequest, RuntimeMutationError, RuntimeRoute,
12    RuntimeRouteMutationError, ScheduleDefinition, ScheduleEvaluation, ScheduleValidationError,
13    SubscribeRequest, SubscribeResponse,
14};
15use crate::types::{EventEnvelope, UnifiedEvent};
16use crate::{ModuleRouteError, ModuleRouteRequest, ModuleRouteResponse, route_module_call};
17
18use super::UnifiedRuntime;
19use super::types::UnifiedRuntimeError;
20
21/// Run a blocking closure on a dedicated thread to isolate it from the
22/// tokio runtime. MCP boundary calls check `Handle::try_current()` and
23/// refuse to block inside an active runtime, so `block_in_place` is not
24/// sufficient — we need a thread that has no runtime handle at all.
25///
26/// Uses `std::thread::scope` so the closure can borrow from the caller.
27fn run_blocking<F, R>(f: F) -> R
28where
29    F: FnOnce() -> R + Send,
30    R: Send,
31{
32    std::thread::scope(|scope| {
33        scope
34            .spawn(f)
35            .join()
36            .unwrap_or_else(|e| std::panic::resume_unwind(e))
37    })
38}
39
40impl UnifiedRuntime {
41    pub async fn module_is_running(&self) -> bool {
42        self.module_runtime.lock().await.is_running()
43    }
44
45    pub async fn loaded_modules(&self) -> Vec<String> {
46        self.module_runtime.lock().await.loaded_modules()
47    }
48
49    /// Reconcile modules — runs blocking subprocess I/O via `block_in_place`.
50    pub async fn reconcile_modules(
51        &self,
52        modules: Vec<String>,
53        timeout: Duration,
54    ) -> Result<usize, RuntimeMutationError> {
55        let mut rt = self.module_runtime.lock().await;
56        run_blocking(|| rt.reconcile_modules(modules, timeout))
57    }
58
59    /// Resolve routing — runs blocking MCP boundary call via `block_in_place`.
60    pub async fn resolve_routing(
61        &self,
62        request: RoutingResolveRequest,
63    ) -> Result<RoutingResolution, RoutingResolveError> {
64        let mut rt = self.module_runtime.lock().await;
65        run_blocking(|| rt.resolve_routing(request))
66    }
67
68    /// Send delivery — runs blocking MCP boundary call via `block_in_place`.
69    pub async fn send_delivery(
70        &self,
71        request: DeliverySendRequest,
72    ) -> Result<DeliveryRecord, DeliverySendError> {
73        let mut rt = self.module_runtime.lock().await;
74        run_blocking(|| rt.send_delivery(request))
75    }
76
77    pub async fn evaluate_schedule_tick(
78        &self,
79        schedules: &[ScheduleDefinition],
80        tick_ms: u64,
81    ) -> Result<ScheduleEvaluation, ScheduleValidationError> {
82        self.module_runtime
83            .lock()
84            .await
85            .evaluate_schedule_tick(schedules, tick_ms)
86    }
87
88    pub async fn list_runtime_routes(&self) -> Vec<RuntimeRoute> {
89        self.module_runtime.lock().await.list_runtime_routes()
90    }
91
92    pub async fn add_runtime_route(
93        &self,
94        route: RuntimeRoute,
95    ) -> Result<RuntimeRoute, RuntimeRouteMutationError> {
96        self.module_runtime.lock().await.add_runtime_route(route)
97    }
98
99    pub async fn delete_runtime_route(
100        &self,
101        route_key: &str,
102    ) -> Result<RuntimeRoute, RuntimeRouteMutationError> {
103        self.module_runtime
104            .lock()
105            .await
106            .delete_runtime_route(route_key)
107    }
108
109    pub async fn delivery_history(
110        &self,
111        request: DeliveryHistoryRequest,
112    ) -> DeliveryHistoryResponse {
113        self.module_runtime.lock().await.delivery_history(request)
114    }
115
116    pub async fn memory_stores(&self) -> Vec<MemoryStoreInfo> {
117        self.module_runtime.lock().await.memory_stores()
118    }
119
120    /// Index into the memory backend. When a backend with a health-check
121    /// endpoint is configured, `persist_memory_state` performs a synchronous
122    /// blocking TCP healthcheck (`std::net::TcpStream::connect_timeout` +
123    /// blocking socket read/write, bounded by
124    /// `MEMORY_LEDGER_HEALTHCHECK_TIMEOUT`). Run it on a dedicated thread
125    /// via `run_blocking` like the other blocking module ops, so the tokio
126    /// worker (and every operation waiting on the `module_runtime` mutex) is
127    /// not stalled for up to ~2s per call when the backend is slow/unreachable.
128    pub async fn memory_index(
129        &self,
130        request: MemoryIndexRequest,
131    ) -> Result<MemoryIndexResult, MemoryIndexError> {
132        let mut rt = self.module_runtime.lock().await;
133        run_blocking(|| rt.memory_index(request))
134    }
135
136    pub async fn memory_query(&self, request: MemoryQueryRequest) -> MemoryQueryResult {
137        // No I/O on this path — see `memory.query` note; safe to run inline.
138        self.module_runtime.lock().await.memory_query(request)
139    }
140
141    /// Evaluate a gating action — the R3 approval-notification path (and the
142    /// R2/R3 memory-conflict probe) performs blocking MCP boundary calls, so
143    /// the evaluation runs on a dedicated thread via `run_blocking`. Without
144    /// it the approval notification silently fails with
145    /// `RuntimeUnavailable("cannot execute blocking MCP boundary call inside
146    /// an active tokio runtime")` and the pending request times out to
147    /// safe_draft.
148    pub async fn evaluate_gating_action(
149        &self,
150        request: GatingEvaluateRequest,
151    ) -> GatingEvaluateResult {
152        let mut rt = self.module_runtime.lock().await;
153        run_blocking(|| rt.evaluate_gating_action(request))
154    }
155
156    pub async fn list_gating_pending(&self) -> Vec<GatingPendingEntry> {
157        self.module_runtime.lock().await.list_gating_pending()
158    }
159
160    pub async fn decide_gating_action(
161        &self,
162        request: GatingDecideRequest,
163    ) -> Result<GatingDecisionResult, GatingDecideError> {
164        self.module_runtime
165            .lock()
166            .await
167            .decide_gating_action(request)
168    }
169
170    pub async fn gating_audit_entries(&self, limit: usize) -> Vec<GatingAuditEntry> {
171        self.module_runtime.lock().await.gating_audit_entries(limit)
172    }
173
174    /// Spawn a module member — runs blocking subprocess I/O via `block_in_place`.
175    pub async fn spawn_member(
176        &self,
177        module_id: &str,
178        timeout: Duration,
179    ) -> Result<(), RuntimeMutationError> {
180        let mut rt = self.module_runtime.lock().await;
181        run_blocking(|| rt.spawn_member(module_id, timeout))
182    }
183
184    /// Route a module call — runs blocking MCP boundary call via `block_in_place`.
185    pub async fn route_module_call(
186        &self,
187        request: &ModuleRouteRequest,
188        timeout: Duration,
189    ) -> Result<ModuleRouteResponse, ModuleRouteError> {
190        let rt = self.module_runtime.lock().await;
191        run_blocking(|| route_module_call(&rt, request, timeout))
192    }
193
194    pub async fn module_lifecycle_events(&self) -> Vec<LifecycleEvent> {
195        self.module_runtime.lock().await.lifecycle_events.clone()
196    }
197
198    pub async fn module_health_transitions(&self) -> Vec<ModuleHealthTransition> {
199        self.module_runtime
200            .lock()
201            .await
202            .supervisor_report
203            .transitions
204            .clone()
205    }
206
207    pub async fn module_events(&self) -> Vec<EventEnvelope<UnifiedEvent>> {
208        self.module_runtime.lock().await.merged_events().to_vec()
209    }
210
211    pub async fn subscribe_events(
212        &self,
213        request: SubscribeRequest,
214    ) -> Result<SubscribeResponse, UnifiedRuntimeError> {
215        self.drain_mob_agent_events().await?;
216        self.module_runtime
217            .lock()
218            .await
219            .subscribe_events(request)
220            .map_err(UnifiedRuntimeError::Subscribe)
221    }
222}