use m1nd_core::antibody::Antibody;
use m1nd_core::counterfactual::CounterfactualEngine;
use m1nd_core::domain::DomainConfig;
use m1nd_core::error::M1ndResult;
use m1nd_core::graph::{Graph, SharedGraph};
use m1nd_core::plasticity::PlasticityEngine;
use m1nd_core::query::QueryOrchestrator;
use m1nd_core::resonance::ResonanceEngine;
use m1nd_core::temporal::TemporalEngine;
use m1nd_core::topology::TopologyAnalyzer;
use m1nd_core::tremor::TremorRegistry;
use m1nd_core::trust::TrustLedger;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Instant;
use crate::perspective::state::{
LockState, PeekSecurityConfig, PerspectiveLimits, PerspectiveState, WatchTrigger, WatcherEvent,
};
pub struct AgentSession {
pub agent_id: String,
pub first_seen: Instant,
pub last_seen: Instant,
pub query_count: u64,
}
pub struct SavingsTracker {
pub queries_by_tool: HashMap<String, u64>,
pub tokens_saved: u64,
pub file_reads_avoided: u64,
pub lines_avoided: u64,
}
impl SavingsTracker {
pub fn new() -> Self {
Self {
queries_by_tool: HashMap::new(),
tokens_saved: 0,
file_reads_avoided: 0,
lines_avoided: 0,
}
}
pub fn record(&mut self, tool: &str, _result_nodes: usize) {
*self.queries_by_tool.entry(tool.to_string()).or_insert(0) += 1;
let (tokens, files, lines) = match tool {
"m1nd.activate" | "m1nd.seek" | "m1nd.search" => (750, 5, 500),
"m1nd.impact" | "m1nd.predict" | "m1nd.counterfactual" => (1000, 8, 800),
"m1nd.surgical.context" => (3200, 8, 300),
"m1nd.surgical.context.v2" => (4800, 12, 400),
"m1nd.hypothesize" | "m1nd.missing" => (1000, 5, 200),
"m1nd.apply" | "m1nd.apply.batch" => (900, 3, 200),
"m1nd.scan" => (1000, 4, 400),
_ => (500, 2, 200),
};
self.tokens_saved += tokens;
self.file_reads_avoided += files;
self.lines_avoided += lines;
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct QueryLogEntry {
pub tool: String,
pub agent_id: String,
pub timestamp_ms: u64,
pub elapsed_ms: f64,
pub result_count: usize,
pub query_preview: String,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct GlobalSavingsState {
pub total_sessions: u64,
pub total_queries: u64,
pub total_tokens_saved: u64,
pub total_file_reads_avoided: u64,
}
pub struct SessionState {
pub graph: SharedGraph,
pub domain: DomainConfig,
pub orchestrator: QueryOrchestrator,
pub temporal: TemporalEngine,
pub counterfactual: CounterfactualEngine,
pub topology: TopologyAnalyzer,
pub resonance: ResonanceEngine,
pub plasticity: PlasticityEngine,
pub queries_processed: u64,
pub auto_persist_interval: u32,
pub start_time: Instant,
pub last_persist_time: Option<Instant>,
pub graph_path: PathBuf,
pub plasticity_path: PathBuf,
pub sessions: HashMap<String, AgentSession>,
pub graph_generation: u64,
pub plasticity_generation: u64,
pub cache_generation: u64,
pub perspectives: HashMap<(String, String), PerspectiveState>,
pub locks: HashMap<String, LockState>,
pub perspective_counter: HashMap<String, u64>,
pub lock_counter: HashMap<String, u64>,
pub pending_watcher_events: Vec<WatcherEvent>,
pub perspective_limits: PerspectiveLimits,
pub peek_security: PeekSecurityConfig,
pub ingest_roots: Vec<String>,
pub antibodies: Vec<Antibody>,
pub antibodies_path: PathBuf,
pub last_antibody_scan_generation: u64,
pub tremor_registry: TremorRegistry,
pub tremor_path: PathBuf,
pub trust_ledger: TrustLedger,
pub trust_path: PathBuf,
pub savings_tracker: SavingsTracker,
pub query_log: Vec<QueryLogEntry>,
pub global_savings: GlobalSavingsState,
pub savings_path: PathBuf,
pub session_start_node_count: u32,
pub session_start_edge_count: u64,
}
impl SessionState {
pub fn initialize(
graph: Graph,
config: &crate::server::McpConfig,
domain: DomainConfig,
) -> M1ndResult<Self> {
let orchestrator = QueryOrchestrator::build(&graph)?;
let temporal = TemporalEngine::build(&graph)?;
let counterfactual = CounterfactualEngine::with_defaults();
let topology = TopologyAnalyzer::with_defaults();
let resonance = ResonanceEngine::with_defaults();
let plasticity =
PlasticityEngine::new(&graph, m1nd_core::plasticity::PlasticityConfig::default());
let shared = Arc::new(parking_lot::RwLock::new(graph));
Ok(Self {
graph: shared,
domain,
orchestrator,
temporal,
counterfactual,
topology,
resonance,
plasticity,
queries_processed: 0,
auto_persist_interval: config.auto_persist_interval,
start_time: Instant::now(),
last_persist_time: None,
graph_path: config.graph_source.clone(),
plasticity_path: config.plasticity_state.clone(),
sessions: HashMap::new(),
graph_generation: 0,
plasticity_generation: 0,
cache_generation: 0,
perspectives: HashMap::new(),
locks: HashMap::new(),
perspective_counter: HashMap::new(),
lock_counter: HashMap::new(),
pending_watcher_events: Vec::new(),
perspective_limits: PerspectiveLimits::default(),
peek_security: PeekSecurityConfig::default(),
ingest_roots: Vec::new(),
antibodies: {
let ab_path = config
.graph_source
.parent()
.unwrap_or(std::path::Path::new("."))
.join("antibodies.json");
m1nd_core::antibody::load_antibodies(&ab_path).unwrap_or_default()
},
antibodies_path: config
.graph_source
.parent()
.unwrap_or(std::path::Path::new("."))
.join("antibodies.json"),
last_antibody_scan_generation: 0,
tremor_registry: {
let tr_path = config
.graph_source
.parent()
.unwrap_or(std::path::Path::new("."))
.join("tremor_state.json");
m1nd_core::tremor::load_tremor_state(&tr_path)
.unwrap_or_else(|_| TremorRegistry::with_defaults())
},
tremor_path: config
.graph_source
.parent()
.unwrap_or(std::path::Path::new("."))
.join("tremor_state.json"),
trust_ledger: {
let tl_path = config
.graph_source
.parent()
.unwrap_or(std::path::Path::new("."))
.join("trust_state.json");
m1nd_core::trust::load_trust_state(&tl_path).unwrap_or_else(|_| TrustLedger::new())
},
trust_path: config
.graph_source
.parent()
.unwrap_or(std::path::Path::new("."))
.join("trust_state.json"),
savings_tracker: SavingsTracker::new(),
query_log: Vec::new(),
global_savings: {
let sv_path = config
.graph_source
.parent()
.unwrap_or(std::path::Path::new("."))
.join("savings_state.json");
std::fs::read_to_string(&sv_path)
.ok()
.and_then(|s| serde_json::from_str(&s).ok())
.unwrap_or_default()
},
savings_path: config
.graph_source
.parent()
.unwrap_or(std::path::Path::new("."))
.join("savings_state.json"),
session_start_node_count: 0,
session_start_edge_count: 0,
})
}
pub fn should_persist(&self) -> bool {
self.queries_processed > 0
&& self.queries_processed % self.auto_persist_interval as u64 == 0
}
pub fn persist(&mut self) -> M1ndResult<()> {
let graph = self.graph.read();
m1nd_core::snapshot::save_graph(&graph, &self.graph_path)?;
match self.plasticity.export_state(&graph) {
Ok(states) => {
if let Err(e) =
m1nd_core::snapshot::save_plasticity_state(&states, &self.plasticity_path)
{
eprintln!(
"[m1nd] WARNING: graph saved but plasticity persist failed: {}",
e
);
}
}
Err(e) => {
eprintln!(
"[m1nd] WARNING: graph saved but plasticity export failed: {}",
e
);
}
}
if !self.antibodies.is_empty() {
if let Err(e) =
m1nd_core::antibody::save_antibodies(&self.antibodies, &self.antibodies_path)
{
eprintln!("[m1nd] WARNING: antibody persist failed: {}", e);
}
}
self.last_persist_time = Some(Instant::now());
Ok(())
}
pub fn rebuild_engines(&mut self) -> M1ndResult<()> {
{
let graph = self.graph.read();
self.orchestrator = QueryOrchestrator::build(&graph)?;
self.temporal = TemporalEngine::build(&graph)?;
self.plasticity =
PlasticityEngine::new(&graph, m1nd_core::plasticity::PlasticityConfig::default());
}
self.invalidate_all_perspectives();
self.mark_all_lock_baselines_stale();
self.graph_generation += 1;
self.cache_generation = self.cache_generation.max(self.graph_generation);
Ok(())
}
pub fn bump_graph_generation(&mut self) {
self.graph_generation += 1;
self.cache_generation = self.cache_generation.max(self.graph_generation);
}
pub fn bump_plasticity_generation(&mut self) {
self.plasticity_generation += 1;
self.cache_generation = self.cache_generation.max(self.plasticity_generation);
}
pub fn invalidate_all_perspectives(&mut self) {
let now_ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0);
for state in self.perspectives.values_mut() {
state.stale = true;
state.route_cache = None;
state.route_set_version = now_ms;
}
}
pub fn mark_all_lock_baselines_stale(&mut self) {
for lock in self.locks.values_mut() {
lock.baseline_stale = true;
}
}
pub fn get_perspective(
&self,
agent_id: &str,
perspective_id: &str,
) -> Option<&PerspectiveState> {
self.perspectives
.get(&(agent_id.to_string(), perspective_id.to_string()))
}
pub fn get_perspective_mut(
&mut self,
agent_id: &str,
perspective_id: &str,
) -> Option<&mut PerspectiveState> {
self.perspectives
.get_mut(&(agent_id.to_string(), perspective_id.to_string()))
}
pub fn next_perspective_id(&mut self, agent_id: &str) -> String {
let counter = self
.perspective_counter
.entry(agent_id.to_string())
.or_insert(0);
*counter += 1;
let short_id = &agent_id[..agent_id.len().min(8)];
format!("persp_{}_{:03}", short_id, counter)
}
pub fn next_lock_id(&mut self, agent_id: &str) -> String {
let counter = self.lock_counter.entry(agent_id.to_string()).or_insert(0);
*counter += 1;
let short_id = &agent_id[..agent_id.len().min(8)];
format!("lock_{}_{:03}", short_id, counter)
}
pub fn agent_perspective_count(&self, agent_id: &str) -> usize {
self.perspectives
.keys()
.filter(|(a, _)| a == agent_id)
.count()
}
pub fn agent_lock_count(&self, agent_id: &str) -> usize {
self.locks
.values()
.filter(|l| l.agent_id == agent_id)
.count()
}
pub fn notify_watchers(&mut self, trigger: WatchTrigger) {
let now_ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0);
let matching_locks: Vec<String> = self
.locks
.values()
.filter(|l| {
l.watcher
.as_ref()
.map_or(false, |w| match (&trigger, &w.strategy) {
(
WatchTrigger::Ingest,
crate::perspective::state::WatchStrategy::OnIngest,
) => true,
(
WatchTrigger::Learn,
crate::perspective::state::WatchStrategy::OnLearn,
) => true,
_ => false,
})
})
.map(|l| l.lock_id.clone())
.collect();
for lock_id in matching_locks {
self.pending_watcher_events.push(WatcherEvent {
lock_id,
trigger: trigger.clone(),
timestamp_ms: now_ms,
});
}
}
pub fn cleanup_agent_state(&mut self, agent_id: &str) {
self.perspectives.retain(|(a, _), _| a != agent_id);
let agent_locks: Vec<String> = self
.locks
.values()
.filter(|l| l.agent_id == agent_id)
.map(|l| l.lock_id.clone())
.collect();
for lock_id in &agent_locks {
self.locks.remove(lock_id);
}
self.pending_watcher_events
.retain(|e| !agent_locks.contains(&e.lock_id));
self.perspective_counter.remove(agent_id);
self.lock_counter.remove(agent_id);
}
pub fn perspective_and_lock_memory_bytes(&self) -> usize {
let persp_size: usize = self
.perspectives
.values()
.map(|p| {
std::mem::size_of_val(p)
+ p.navigation_history.len() * 100
+ p.visited_nodes.len() * 40
})
.sum();
let lock_size: usize = self
.locks
.values()
.map(|l| {
std::mem::size_of_val(l)
+ l.baseline.nodes.len() * 40
+ l.baseline.edges.len() * 120
})
.sum();
persp_size + lock_size
}
pub fn uptime_seconds(&self) -> f64 {
self.start_time.elapsed().as_secs_f64()
}
pub fn track_agent(&mut self, agent_id: &str) {
let now = Instant::now();
let session = self
.sessions
.entry(agent_id.to_string())
.or_insert_with(|| AgentSession {
agent_id: agent_id.to_string(),
first_seen: now,
last_seen: now,
query_count: 0,
});
session.last_seen = now;
session.query_count += 1;
}
pub fn log_query(
&mut self,
tool: &str,
agent_id: &str,
elapsed_ms: f64,
result_count: usize,
query_preview: &str,
) {
let entry = QueryLogEntry {
tool: tool.to_string(),
agent_id: agent_id.to_string(),
timestamp_ms: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0),
elapsed_ms,
result_count,
query_preview: query_preview.chars().take(100).collect(),
};
if self.query_log.len() >= 1000 {
self.query_log.remove(0);
}
self.query_log.push(entry);
}
pub fn persist_savings(&self) {
if let Ok(json) = serde_json::to_string_pretty(&self.global_savings) {
let _ = std::fs::write(&self.savings_path, json);
}
}
pub fn session_summary(&self) -> Vec<serde_json::Value> {
self.sessions
.values()
.map(|s| {
serde_json::json!({
"agent_id": s.agent_id,
"first_seen_secs_ago": s.first_seen.elapsed().as_secs_f64(),
"last_seen_secs_ago": s.last_seen.elapsed().as_secs_f64(),
"query_count": s.query_count,
})
})
.collect()
}
}