Skip to main content

lean_ctx/core/shadow/
runtime.rs

1//! Opt-in runtime buffer for Shadow Mode reports.
2use super::{ShadowEngine, ShadowReport, ShadowTask};
3use crate::core::config::{Config, ShadowConfig};
4use std::{
5    panic::{AssertUnwindSafe, catch_unwind},
6    sync::{Mutex, OnceLock},
7};
8
9pub use super::persistence::{list_reports, load_report, persist_report};
10
11#[derive(Default)]
12pub(crate) struct State {
13    tasks: Vec<ShadowTask>,
14    latest: Option<ShadowReport>,
15}
16pub(crate) static STATE: OnceLock<Mutex<State>> = OnceLock::new();
17pub(crate) fn state() -> &'static Mutex<State> {
18    STATE.get_or_init(|| Mutex::new(State::default()))
19}
20
21/// Process-wide Shadow Mode coordinator.
22pub struct ShadowRuntime;
23impl ShadowRuntime {
24    pub fn is_enabled() -> bool {
25        catch_unwind(AssertUnwindSafe(|| load_shadow_config().enabled)).unwrap_or(false)
26    }
27    pub fn on_task_complete(task: &ShadowTask) {
28        let _ = catch_unwind(AssertUnwindSafe(|| Self::on_task_complete_inner(task)));
29    }
30    pub fn get_latest_report() -> Option<ShadowReport> {
31        catch_unwind(AssertUnwindSafe(|| {
32            state()
33                .lock()
34                .ok()
35                .and_then(|state| state.latest.clone())
36                .or_else(|| list_reports().last().and_then(|path| load_report(path)))
37        }))
38        .unwrap_or(None)
39    }
40    pub fn force_report() -> Option<ShadowReport> {
41        catch_unwind(AssertUnwindSafe(Self::report_pending)).unwrap_or(None)
42    }
43
44    fn on_task_complete_inner(task: &ShadowTask) {
45        if !Self::is_enabled() {
46            return;
47        }
48        let interval = load_shadow_config().report_interval.max(1);
49        let ready = state().lock().ok().and_then(|mut state| {
50            state.tasks.push(task.clone());
51            (state.tasks.len() >= interval).then(|| std::mem::take(&mut state.tasks))
52        });
53        if let Some(tasks) = ready {
54            let _ = Self::generate_report(&tasks);
55        }
56    }
57
58    fn report_pending() -> Option<ShadowReport> {
59        let tasks = state()
60            .lock()
61            .ok()
62            .map(|mut state| std::mem::take(&mut state.tasks))?;
63        Self::generate_report(&tasks)
64    }
65
66    fn generate_report(tasks: &[ShadowTask]) -> Option<ShadowReport> {
67        if tasks.is_empty() {
68            return None;
69        }
70        // `[shadow] baseline_model` is deliberately not used: each task keeps its host model.
71        let report = ShadowEngine::run_comparison(tasks);
72        let _ = persist_report(&report);
73        if let Ok(mut state) = state().lock() {
74            state.latest = Some(report.clone());
75        }
76        Some(report)
77    }
78}
79pub fn load_shadow_config() -> ShadowConfig {
80    catch_unwind(AssertUnwindSafe(|| Config::load().shadow)).unwrap_or_default()
81}