Skip to main content

velesdb_memory/mcp/
status.rs

1//! Health and configuration status for the active memory generation.
2
3use std::sync::Arc;
4
5use rmcp::handler::server::wrapper::Json;
6use rmcp::{tool, tool_router, ErrorData};
7
8use super::dto::{
9    EmbedderStatus, ExtractionStatus, MemoryCounts, MemoryStatusResult, ProvenanceStatus,
10};
11use super::{join_error, to_error, McpServer, UNREPORTED_MODEL};
12use crate::embedding_provenance::EmbeddingProvenance;
13
14type StatusSnapshot = (
15    String,
16    usize,
17    Option<EmbeddingProvenance>,
18    usize,
19    Option<usize>,
20    bool,
21    u64,
22);
23
24#[tool_router(router = status_tool_router, vis = "pub(super)")]
25impl McpServer {
26    #[tool(
27        name = "memory_status",
28        output_schema = crate::schema::wire_safe_output_schema::<MemoryStatusResult>(),
29        description = "Report this memory server's health and configuration: which embedder is running and whether recall is SEMANTIC (`embedder.semantic: false` means the offline `hash` default — recall matches surface form, not meaning, and configuring a semantic embedder is an env-var switch, no rebuild), what embedder the store was filled by per its on-disk provenance record, whether a default extraction backend is configured (`remember_extracted` may omit `extractor` iff `extraction.configured`; explicit `outline` remains available), whether the background autograph worker is active and how many enrichments a full queue dropped, and the corpus size — `memory.facts` and `memory.edges`. Read `memory.edges` when `why` seems to add nothing over `recall`: `0` means no fact was ever linked (by `relate`, `remember`'s `links`, or extraction), so `why` HAS no graph to walk and degrades to plain search — that is a wiring gap, not a defect. Call this at session start, or whenever recall quality or `why`'s evidence trails surprise you, and tell the user when the server runs degraded. Takes no parameters."
30    )]
31    async fn memory_status(&self) -> Result<Json<MemoryStatusResult>, ErrorData> {
32        let service = Arc::clone(&self.service);
33        let store_dir = self.store_dir.clone();
34        let snapshot = tokio::task::spawn_blocking(move || {
35            let recorded = store_dir
36                .as_deref()
37                .and_then(|dir| crate::embedding_provenance::read(dir).ok().flatten());
38            service.inspect_active(|model, dimension, current| {
39                (
40                    model.to_owned(),
41                    dimension,
42                    recorded,
43                    current.fact_count(),
44                    current.edge_count(),
45                    current.autograph_queue_open(),
46                    current.autograph_dropped(),
47                )
48            })
49        })
50        .await
51        .map_err(join_error)?
52        .map_err(to_error)?;
53        Ok(Json(self.status_result(snapshot)))
54    }
55
56    fn status_result(&self, snapshot: StatusSnapshot) -> MemoryStatusResult {
57        let (model, dimension, provenance, facts, edges, autograph_active, autograph_dropped) =
58            snapshot;
59        MemoryStatusResult {
60            embedder: embedder_status(model, dimension),
61            provenance: provenance_status(provenance),
62            extraction: ExtractionStatus {
63                configured: self.extractors.read().default_is_configured(),
64                autograph_active,
65                autograph_dropped,
66            },
67            memory: MemoryCounts { facts, edges },
68        }
69    }
70}
71
72fn embedder_status(model: String, dimension: usize) -> EmbedderStatus {
73    if model == UNREPORTED_MODEL {
74        return EmbedderStatus {
75            model: None,
76            dimension: None,
77            semantic: None,
78        };
79    }
80    EmbedderStatus {
81        semantic: Some(model != "hash"),
82        model: Some(model),
83        dimension: Some(dimension),
84    }
85}
86
87fn provenance_status(record: Option<EmbeddingProvenance>) -> ProvenanceStatus {
88    record.map_or(
89        ProvenanceStatus {
90            recorded: false,
91            model: None,
92            dimension: None,
93        },
94        |record| ProvenanceStatus {
95            recorded: true,
96            model: Some(record.model),
97            dimension: Some(record.dimension),
98        },
99    )
100}