Skip to main content

lean_ctx/core/
embedding_access.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Policy-gated access to the shared embedding engine. Moved down from
3//! `tools/ctx_knowledge/embeddings.rs` so engine callers (entropy, extractive,
4//! telemetry) do not depend on the tools layer.
5
6#[cfg(feature = "embeddings")]
7use crate::core::embeddings::EmbeddingEngine;
8
9/// Auto-download policy (#551): the env var, when set, wins in either
10/// direction; otherwise `[embedding].auto_download` from config; otherwise
11/// **allowed** — the soft default that lights up the semantic features
12/// (semantic recall, EFF-7 redundancy filtering) without manual setup.
13#[cfg(feature = "embeddings")]
14pub fn embeddings_auto_download_allowed() -> bool {
15    if let Ok(v) = std::env::var("LEAN_CTX_EMBEDDINGS_AUTO_DOWNLOAD") {
16        return matches!(
17            v.trim().to_lowercase().as_str(),
18            "1" | "true" | "yes" | "on"
19        );
20    }
21    crate::core::config::Config::load()
22        .embedding
23        .auto_download
24        .unwrap_or(true)
25}
26
27#[cfg(feature = "embeddings")]
28pub fn embedding_engine() -> Option<&'static EmbeddingEngine> {
29    embedding_engine_impl(false)
30}
31
32/// Non-blocking: returns engine only if already loaded. Never blocks the
33/// calling path — but kicks off a one-time background load/download (#551)
34/// so the first semantic need self-activates the engine for later calls.
35#[cfg(feature = "embeddings")]
36pub fn embedding_engine_nonblocking() -> Option<&'static EmbeddingEngine> {
37    embedding_engine_impl(true)
38}
39
40#[cfg(feature = "embeddings")]
41fn embedding_engine_impl(nonblocking: bool) -> Option<&'static EmbeddingEngine> {
42    let cfg = crate::core::config::Config::load();
43    let profile = crate::core::config::MemoryProfile::effective(&cfg);
44    if !profile.embeddings_enabled() {
45        return None;
46    }
47    if !EmbeddingEngine::is_available() && !embeddings_auto_download_allowed() {
48        return None;
49    }
50    if nonblocking {
51        let engine = crate::core::embeddings::try_shared_engine();
52        if engine.is_none() {
53            ensure_engine_background();
54        }
55        engine
56    } else {
57        crate::core::embeddings::shared_engine()
58    }
59}
60
61/// One-time background engine activation (#551): downloads the model if
62/// missing (TOFU-pinned, see `core/embeddings/download.rs`) and warms the
63/// shared engine, so non-blocking callers start succeeding without any hot
64/// path ever waiting. Policy gates (memory profile, auto-download) are
65/// checked by the caller.
66#[cfg(feature = "embeddings")]
67fn ensure_engine_background() {
68    use std::sync::atomic::{AtomicBool, Ordering};
69    // Never spawn a detached model load in a short-lived process (#519): a
70    // loader thread still running when the process returns from `main` races
71    // libonnxruntime's static-destructor teardown → SIGSEGV in onnx::OpSchema.
72    // This also keeps the process-global shared engine untouched in tests
73    // (races assertions) and avoids model-download network I/O in CI sandboxes.
74    // `background_load_allowed` covers unit tests (cfg!(test)) AND integration/
75    // bench/doctest binaries (which link the lib with cfg(test) = false).
76    if !crate::core::embeddings::background_load_allowed() {
77        return;
78    }
79    static STARTED: AtomicBool = AtomicBool::new(false);
80    if STARTED.swap(true, Ordering::SeqCst) {
81        return;
82    }
83    std::thread::spawn(|| {
84        // `shared_engine` runs ensure_model (download when absent) and the
85        // ONNX load inside the OnceLock init; concurrent blocking callers
86        // simply wait on the same init instead of duplicating work.
87        let loaded = crate::core::embeddings::shared_engine().is_some();
88        tracing::info!("embedding engine background activation finished (loaded={loaded})");
89    });
90}
91
92/// Engine status for diagnostics (#551): one honest word + reason.
93pub fn engine_status_line() -> String {
94    #[cfg(feature = "embeddings")]
95    {
96        let cfg = crate::core::config::Config::load();
97        let profile = crate::core::config::MemoryProfile::effective(&cfg);
98        if !profile.embeddings_enabled() {
99            return "off (memory profile: low)".to_string();
100        }
101        if crate::core::embeddings::try_shared_engine().is_some() {
102            return "loaded".to_string();
103        }
104        if EmbeddingEngine::is_available() {
105            return "model present, engine loads on first use".to_string();
106        }
107        if embeddings_auto_download_allowed() {
108            return "model missing — downloads in background on first semantic need".to_string();
109        }
110        "off (auto-download disabled, no model present)".to_string()
111    }
112    #[cfg(not(feature = "embeddings"))]
113    {
114        "off (binary built without embeddings feature)".to_string()
115    }
116}