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 serde_json::Value;
use std::collections::{BTreeSet, HashMap};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Instant;
use crate::auto_ingest::AutoIngestState;
use crate::instance_registry::{InstanceHandle, InstanceRegistryEntry};
use crate::perspective::state::{
LockState, PeekSecurityConfig, PerspectiveLimits, PerspectiveState, WatchTrigger, WatcherEvent,
};
use crate::universal_docs::{load_document_cache, persist_document_cache, DocumentCacheState};
pub struct AgentSession {
pub agent_id: String,
pub first_seen: Instant,
pub last_seen: Instant,
pub query_count: u64,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct EditPreviewState {
pub preview_id: String,
pub agent_id: String,
pub file_path: String,
pub new_content: String,
pub source_hash: String,
pub source_exists: bool,
pub source_bytes: usize,
pub source_line_count: usize,
pub lines_added: i32,
pub lines_removed: i32,
pub bytes_written: usize,
pub unified_diff: String,
pub description: Option<String>,
pub created_at_ms: u64,
}
struct RecoveryAutoActionContext<'a> {
agent_id: &'a str,
observed_tool: &'a str,
observed_proof_state: &'a str,
observed_candidates: Option<u64>,
scope: Option<&'a str>,
reason: &'a str,
source_kind: &'a str,
arguments: &'a Value,
}
pub struct SavingsTracker {
pub queries_by_tool: HashMap<String, u64>,
pub tokens_saved: u64,
pub file_reads_avoided: u64,
pub lines_avoided: u64,
}
impl Default for SavingsTracker {
fn default() -> Self {
Self::new()
}
}
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,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct BootMemoryState {
pub entries: HashMap<String, BootMemoryEntry>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct BootMemoryEntry {
pub key: String,
pub value: Value,
pub tags: Vec<String>,
pub source_refs: Vec<String>,
pub updated_at_ms: u64,
pub updated_by_agent: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct FileInventoryEntry {
pub external_id: String,
pub file_path: String,
pub size_bytes: u64,
pub last_modified_ms: u64,
pub language: String,
pub commit_count: u32,
pub loc: Option<u32>,
pub sha256: Option<String>,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct CoverageSessionState {
pub started_at_ms: u64,
pub visited_files: BTreeSet<String>,
pub visited_nodes: BTreeSet<String>,
pub tools_used: HashMap<String, u64>,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct ProofReadyMark {
pub proved_at_ms: u64,
pub generation: u64,
pub evidence: Option<String>,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct FindingMark {
pub flagged_at_ms: u64,
pub generation: u64,
pub kind: String,
pub severity: String,
pub file_path: String,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct DaemonRuntimeState {
pub active: bool,
pub started_at_ms: Option<u64>,
pub last_tick_ms: Option<u64>,
pub last_tick_trigger: Option<String>,
pub watch_paths: Vec<String>,
pub poll_interval_ms: u64,
pub coalesce_window_ms: u64,
pub pending_rerun: bool,
pub tick_in_flight: bool,
pub last_coalesced_event_ms: Option<u64>,
pub coalesced_event_count: u64,
pub tracked_files: HashMap<String, DaemonTrackedFile>,
pub tick_count: u64,
pub last_tick_duration_ms: Option<f64>,
pub last_tick_changed_files: usize,
pub last_tick_deleted_files: usize,
pub last_tick_alerts_emitted: usize,
pub idle_streak: u32,
pub max_backoff_multiplier: u32,
pub watch_backend: String,
pub watch_backend_error: Option<String>,
pub watch_events_seen: u64,
pub watch_events_dropped: u64,
pub last_watch_event_ms: Option<u64>,
pub git_root: Option<String>,
pub git_baseline_ref: Option<String>,
pub git_baseline_kind: Option<String>,
pub git_since_ref: Option<String>,
pub git_head_ref: Option<String>,
pub last_git_scan_ms: Option<u64>,
pub last_git_changed_files: usize,
pub git_backend_error: Option<String>,
pub git_operation_in_progress: bool,
pub git_operation_kind: Option<String>,
pub deferred_ticks: u64,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DaemonTrackedFile {
pub external_id: String,
pub file_path: String,
pub last_modified_ms: u64,
pub size_bytes: u64,
pub sha256: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DaemonAlert {
pub alert_id: String,
pub severity: String,
pub kind: String,
pub message: String,
pub confidence: f32,
pub evidence: Vec<String>,
pub suggested_tool: Option<String>,
pub suggested_target: Option<String>,
pub file_path: Option<String>,
pub node_id: Option<String>,
pub created_at_ms: u64,
pub acked: bool,
pub acked_at_ms: Option<u64>,
}
pub type ApplyBatchProgressSink =
Arc<dyn Fn(&crate::protocol::surgical::ApplyBatchProgressEvent) + Send + Sync>;
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 embeddings_cache_path: PathBuf,
pub sessions: HashMap<String, AgentSession>,
pub edit_previews: HashMap<String, EditPreviewState>,
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 workspace_root: Option<String>,
pub workspace_root_source: Option<String>,
pub runtime_root: PathBuf,
pub instance: InstanceHandle,
pub apply_batch_progress_sink: Option<ApplyBatchProgressSink>,
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,
pub boot_memory_path: PathBuf,
pub boot_memory: HashMap<String, BootMemoryEntry>,
pub daemon_state_path: PathBuf,
pub daemon_state: DaemonRuntimeState,
pub daemon_alerts_path: PathBuf,
pub daemon_alerts: Vec<DaemonAlert>,
pub file_inventory: HashMap<String, FileInventoryEntry>,
pub coverage_sessions: HashMap<String, CoverageSessionState>,
pub proof_ready: HashMap<(String, String), ProofReadyMark>,
pub flagged_findings: HashMap<(String, String), FindingMark>,
pub auto_ingest: AutoIngestState,
pub document_cache: DocumentCacheState,
pub agent_memory_boot: Option<serde_json::Value>,
pub read_only: bool,
pub read_only_persist_logged: std::cell::Cell<bool>,
}
const MAX_FLAGGED_FINDINGS: usize = 4096;
const WORKSPACE_ROOT_ENV_CANDIDATES: &[&str] = &[
"M1ND_WORKSPACE_ROOT",
"M1ND_PROJECT_ROOT",
"M1ND_REPO_ROOT",
"WORKSPACE_ROOT",
"PROJECT_ROOT",
"REPO_ROOT",
"CLAUDE_PROJECT_DIR",
"CLAUDE_WORKSPACE_ROOT",
"ANTHROPIC_WORKSPACE_ROOT",
"ANTIGRAVITY_WORKSPACE_ROOT",
"ANTIGRAVITY_PROJECT_ROOT",
"GEMINI_WORKSPACE_ROOT",
"GEMINI_PROJECT_ROOT",
"CURSOR_WORKSPACE_ROOT",
"CURSOR_PROJECT_ROOT",
"WINDSURF_WORKSPACE_ROOT",
"WINDSURF_PROJECT_ROOT",
"VSCODE_WORKSPACE",
"VSCODE_CWD",
"INIT_CWD",
"PWD",
"OLDPWD",
];
const MANAGED_RUNTIME_PATH_MARKERS: &[&str] = &[
"/.codex/m1nd-runtimes/",
"\\.codex\\m1nd-runtimes\\",
"/.claude/m1nd-runtimes/",
"\\.claude\\m1nd-runtimes\\",
"/.antigravity/m1nd-runtimes/",
"\\.antigravity\\m1nd-runtimes\\",
"/.gemini/m1nd-runtimes/",
"\\.gemini\\m1nd-runtimes\\",
"/.cursor/m1nd-runtimes/",
"\\.cursor\\m1nd-runtimes\\",
"/.windsurf/m1nd-runtimes/",
"\\.windsurf\\m1nd-runtimes\\",
"/.m1nd-runtimes/",
"\\.m1nd-runtimes\\",
"/m1nd-runtimes/",
"\\m1nd-runtimes\\",
"/mcp-runtimes/",
"\\mcp-runtimes\\",
"/agent-runtimes/",
"\\agent-runtimes\\",
"/sessions/ppid-",
"\\sessions\\ppid-",
];
impl SessionState {
pub fn binding_fingerprint(&self) -> serde_json::Value {
let graph = self.graph.read();
serde_json::json!({
"schema": "m1nd-binding-fingerprint-v0",
"process_id": std::process::id(),
"current_exe": std::env::current_exe().ok().map(|path| path.to_string_lossy().to_string()),
"runtime_root": self.runtime_root.to_string_lossy(),
"graph_path": self.graph_path.to_string_lossy(),
"plasticity_path": self.plasticity_path.to_string_lossy(),
"workspace_root": self.workspace_root,
"workspace_root_source": self.workspace_root_source,
"ingest_roots": self.ingest_roots,
"graph_path_exists": self.graph_path.exists(),
"graph_generation": self.graph_generation,
"plasticity_generation": self.plasticity_generation,
"cache_generation": self.cache_generation,
"node_count": graph.num_nodes() as u64,
"edge_count": graph.num_edges() as u64,
"graph_finalized": graph.finalized,
})
}
pub fn graph_runtime_summary(&self) -> serde_json::Value {
let graph = self.graph.read();
serde_json::json!({
"node_count": graph.num_nodes(),
"edge_count": graph.num_edges(),
"finalized": graph.finalized,
"graph_generation": self.graph_generation,
"plasticity_generation": self.plasticity_generation,
"cache_generation": self.cache_generation,
"ingest_root_count": self.ingest_roots.len(),
"ingest_roots": self.ingest_roots,
"workspace_root": self.workspace_root,
"workspace_root_source": self.workspace_root_source,
"runtime_root": self.runtime_root,
"graph_path": self.graph_path,
"graph_path_exists": self.graph_path.exists(),
})
}
pub fn mini_graph_state(&self) -> serde_json::Value {
let graph = self.graph.read();
serde_json::json!({
"node_count": graph.num_nodes(),
"edge_count": graph.num_edges(),
"finalized": graph.finalized,
"graph_generation": self.graph_generation,
"ingest_root_count": self.ingest_roots.len(),
"workspace_root_known": self.workspace_root.is_some(),
"workspace_root": self.workspace_root,
"workspace_root_source": self.workspace_root_source,
"graph_path_exists": self.graph_path.exists(),
"runtime_root": self.runtime_root.to_string_lossy(),
})
}
pub fn workspace_binding_mismatch(&self, scope: Option<&str>) -> Option<serde_json::Value> {
let scope_path = Self::absolute_scope_path(scope?)?;
let mut known_roots: Vec<(&str, PathBuf)> = Vec::new();
if let Some(workspace_root) = self.workspace_root.as_deref() {
known_roots.push(("workspace_root", PathBuf::from(workspace_root)));
}
for root in &self.ingest_roots {
known_roots.push(("ingest_root", PathBuf::from(root)));
}
if known_roots
.iter()
.any(|(_, root)| Self::path_starts_with_loosely(&scope_path, root))
{
return None;
}
let requested_workspace_hint = Self::scope_workspace_hint(&scope_path);
let binding_kind = Self::scope_binding_kind_for_mismatch(
&scope_path,
&requested_workspace_hint,
&known_roots,
);
let partial_scope = binding_kind != "wrong_workspace_binding";
let (scope_reliability, recommended_usage_mode, message) = match binding_kind {
"nested_workspace_binding" => (
"partial_subtree_truth",
"partial_scope_orientation",
"The active m1nd binding is inside the requested repository, so it can guide only that subtree until the repo root is bound.",
),
"file_level_binding" => (
"document_context_only",
"partial_scope_orientation",
"The active m1nd binding points at a file-level artifact inside the requested repository, so it is document context rather than codebase coverage.",
),
_ => (
"wrong_workspace",
"isolated_probe_after_wrong_workspace_binding",
"The requested absolute scope is outside the active m1nd workspace and ingest roots.",
),
};
let requested_context_id = requested_workspace_hint
.file_name()
.and_then(|name| name.to_str())
.filter(|name| !name.trim().is_empty())
.unwrap_or("requested-workspace")
.to_string();
let known_root_values = known_roots
.iter()
.map(|(kind, root)| {
serde_json::json!({
"kind": kind,
"path": root.to_string_lossy(),
})
})
.collect::<Vec<_>>();
Some(serde_json::json!({
"schema": "m1nd-workspace-binding-mismatch-v0",
"code": "wrong_workspace_binding",
"binding_kind": binding_kind,
"partial_scope": partial_scope,
"scope_reliability": scope_reliability,
"recommended_usage_mode": recommended_usage_mode,
"requested_scope": scope.unwrap_or_default(),
"requested_scope_path": scope_path.to_string_lossy(),
"requested_workspace_hint": requested_workspace_hint.to_string_lossy(),
"requested_context_id": requested_context_id,
"active_workspace_root": self.workspace_root,
"active_workspace_root_source": self.workspace_root_source,
"active_ingest_roots": self.ingest_roots,
"known_roots_checked": known_root_values,
"runtime_root": self.runtime_root.to_string_lossy(),
"message": message,
"suggested_fix": {
"preferred": "start or rebind the MCP host with M1ND_WORKSPACE_ROOT set to requested_workspace_hint",
"env": {
"M1ND_WORKSPACE_ROOT": requested_workspace_hint.to_string_lossy(),
},
"same_binding_alternative": "call ingest on requested_workspace_hint only if this session should intentionally switch or merge context",
"cross_repo_alternative": "use federate_auto or federate when the task genuinely needs multiple repositories in one graph",
},
"non_claims": [
"Context Guard does not switch workspace automatically.",
"Context Guard does not ingest, federate, or mutate the active graph.",
"Context Guard does not prove the requested workspace is the correct task target."
],
}))
}
fn scope_binding_kind_for_mismatch(
scope_path: &std::path::Path,
requested_workspace_hint: &std::path::Path,
known_roots: &[(&str, PathBuf)],
) -> &'static str {
let partial_root = known_roots.iter().map(|(_, root)| root).find(|root| {
Self::path_starts_with_loosely(root, requested_workspace_hint)
|| Self::path_starts_with_loosely(root, scope_path)
});
match partial_root {
Some(root) if Self::is_file_level_binding_root(root) => "file_level_binding",
Some(_) => "nested_workspace_binding",
None => "wrong_workspace_binding",
}
}
fn is_file_level_binding_root(root: &std::path::Path) -> bool {
if root.is_file() {
return true;
}
matches!(
root.extension().and_then(|extension| extension.to_str()),
Some(
"bib"
| "doc"
| "docx"
| "html"
| "json"
| "l1ght"
| "light"
| "md"
| "pdf"
| "prd"
| "rst"
| "txt"
| "xml"
)
)
}
fn absolute_scope_path(scope: &str) -> Option<std::path::PathBuf> {
let scope = scope.trim();
if scope.is_empty() {
return None;
}
let scope = scope.strip_prefix("file::").unwrap_or(scope);
let candidate = std::path::PathBuf::from(scope);
if candidate.is_absolute() {
Some(candidate)
} else {
None
}
}
fn path_starts_with_loosely(path: &std::path::Path, root: &std::path::Path) -> bool {
if root.as_os_str().is_empty() {
return false;
}
if path.starts_with(root) {
return true;
}
if let (Ok(path), Ok(root)) = (path.canonicalize(), root.canonicalize()) {
if path.starts_with(root) {
return true;
}
}
let path_text = Self::normalized_path_for_compare(path);
let root_text = Self::normalized_path_for_compare(root);
if path_text == root_text {
return true;
}
path_text.starts_with(&format!("{root_text}/"))
}
fn normalized_path_for_compare(path: &std::path::Path) -> String {
path.to_string_lossy()
.replace('\\', "/")
.trim_end_matches('/')
.to_string()
}
fn scope_workspace_hint(scope_path: &std::path::Path) -> std::path::PathBuf {
let start = if scope_path.is_file() {
scope_path.parent().unwrap_or(scope_path)
} else {
scope_path
};
for ancestor in start.ancestors() {
if ancestor.join(".git").exists()
|| ancestor.join("package.json").exists()
|| ancestor.join("Cargo.toml").exists()
|| ancestor.join("pyproject.toml").exists()
{
return ancestor.to_path_buf();
}
}
start.to_path_buf()
}
fn recovery_call_arguments(
&self,
agent_id: &str,
observed_tool: &str,
observed_proof_state: &str,
observed_candidates: Option<u64>,
scope: Option<&str>,
error_text: Option<&str>,
) -> (serde_json::Value, Option<serde_json::Value>) {
let mut arguments = serde_json::json!({
"agent_id": agent_id,
"observed_tool": observed_tool,
"observed_proof_state": observed_proof_state,
});
if let Some(candidates) = observed_candidates {
arguments["observed_candidates"] = serde_json::json!(candidates);
}
if let Some(scope) = scope.filter(|value| !value.trim().is_empty()) {
arguments["scope"] = serde_json::json!(scope);
}
if let Some(error_text) = error_text.filter(|value| !value.trim().is_empty()) {
arguments["error_text"] = serde_json::json!(error_text);
}
let workspace_binding_mismatch = self.workspace_binding_mismatch(scope);
if let Some(mismatch) = workspace_binding_mismatch.clone() {
arguments["workspace_binding_mismatch"] = mismatch;
}
(arguments, workspace_binding_mismatch)
}
fn recovery_auto_action_payload(
&self,
context: RecoveryAutoActionContext<'_>,
) -> serde_json::Value {
let scope_key = if context
.scope
.filter(|value| !value.trim().is_empty())
.is_some()
{
"scoped"
} else {
"unscoped"
};
let candidate_key = context
.observed_candidates
.map(|value| value.to_string())
.unwrap_or_else(|| "none".to_string());
serde_json::json!({
"schema": "m1nd-auto-action-v0",
"status": "ready",
"action_type": "tool_call",
"tool": "recovery_playbook",
"arguments": context.arguments,
"source": {
"kind": context.source_kind,
"surface": "recovery_payload",
"agent_id": context.agent_id,
"observed_tool": context.observed_tool,
"observed_proof_state": context.observed_proof_state,
},
"reason": context.reason,
"expected_output_schema": "m1nd-recovery-playbook-v0",
"safety": {
"mutation": "read_only",
"requires_confirmation": false,
"side_effects": "none",
},
"idempotency_key": format!(
"recovery_playbook:{}:{}:{}:{}:{}",
context.agent_id, context.observed_tool, context.observed_proof_state, candidate_key, scope_key
),
})
}
pub fn doctor_recovery_payload(
&self,
agent_id: &str,
observed_tool: &str,
observed_proof_state: &str,
observed_candidates: Option<u64>,
scope: Option<&str>,
error_text: Option<&str>,
) -> serde_json::Value {
let (arguments, workspace_binding_mismatch) = self.recovery_call_arguments(
agent_id,
observed_tool,
observed_proof_state,
observed_candidates,
scope,
error_text,
);
let reason = if workspace_binding_mismatch.is_some() {
"wrong workspace binding detected; doctor can confirm the active runtime root, workspace root, ingest roots, and requested absolute scope"
} else {
"retrieval returned blocked or zero actionable candidates; doctor can distinguish empty graph, stale binding, scope filtering, and session drift"
};
let mut payload = serde_json::json!({
"suggested_tool": "doctor",
"reason": reason,
"arguments": arguments,
});
if let Some(mismatch) = workspace_binding_mismatch {
payload["binding_issue"] = serde_json::json!("wrong_workspace_binding");
payload["workspace_binding_mismatch"] = mismatch;
}
payload
}
pub fn recovery_playbook_payload(
&self,
agent_id: &str,
observed_tool: &str,
observed_proof_state: &str,
observed_candidates: Option<u64>,
scope: Option<&str>,
error_text: Option<&str>,
) -> serde_json::Value {
let (arguments, workspace_binding_mismatch) = self.recovery_call_arguments(
agent_id,
observed_tool,
observed_proof_state,
observed_candidates,
scope,
error_text,
);
let reason = if workspace_binding_mismatch.is_some() {
"wrong workspace binding detected; recovery_playbook returns the ordered context selection path before shell fallback"
} else {
"retrieval blocked or the active graph is not yet trusted for this query; recovery_playbook returns the ordered agent recovery path before deeper diagnosis"
};
let source_kind = if workspace_binding_mismatch.is_some() {
"wrong_workspace_binding"
} else {
"retrieval_needs_recovery"
};
let auto_action = self.recovery_auto_action_payload(RecoveryAutoActionContext {
agent_id,
observed_tool,
observed_proof_state,
observed_candidates,
scope,
reason,
source_kind,
arguments: &arguments,
});
let mut payload = serde_json::json!({
"suggested_tool": "recovery_playbook",
"reason": reason,
"arguments": arguments,
"fallback_tool": "doctor",
"auto_action": auto_action,
});
if let Some(mismatch) = workspace_binding_mismatch {
payload["binding_issue"] = serde_json::json!("wrong_workspace_binding");
payload["workspace_binding_mismatch"] = mismatch;
}
payload
}
pub fn retrieval_failure_context(
&self,
agent_id: &str,
observed_tool: &str,
observed_proof_state: &str,
observed_candidates: Option<u64>,
scope: Option<&str>,
error_text: Option<&str>,
) -> (Option<serde_json::Value>, Option<serde_json::Value>) {
let graph_populated = {
let graph = self.graph.read();
graph.num_nodes() > 0
};
let needs_recovery = observed_proof_state == "blocked"
|| !graph_populated
|| self.workspace_binding_mismatch(scope).is_some();
if !needs_recovery {
return (None, None);
}
(
Some(self.mini_graph_state()),
Some(self.recovery_playbook_payload(
agent_id,
observed_tool,
observed_proof_state,
observed_candidates,
scope,
error_text,
)),
)
}
pub fn agent_runtime_contract(
&self,
agent_id: &str,
observed_tool: &str,
observed_proof_state: &str,
observed_candidates: Option<u64>,
scope: Option<&str>,
error_text: Option<&str>,
) -> serde_json::Value {
let workspace_binding_mismatch = self.workspace_binding_mismatch(scope);
let graph = self.graph.read();
let node_count = graph.num_nodes() as u64;
let edge_count = graph.num_edges() as u64;
let graph_finalized = graph.finalized;
drop(graph);
let graph_populated = node_count > 0;
let observed_blocked = observed_proof_state == "blocked";
let needs_recovery =
workspace_binding_mismatch.is_some() || !graph_populated || observed_blocked;
let trust_mode = if workspace_binding_mismatch.is_some() {
"wrong_workspace_binding"
} else if !graph_populated {
"needs_ingest"
} else if observed_blocked {
"retrieval_needs_recovery"
} else {
"full_trust"
};
let status = match trust_mode {
"full_trust" => "ok",
"retrieval_needs_recovery" => "triaging",
_ => "blocked",
};
let recovery = if needs_recovery {
Some(self.recovery_playbook_payload(
agent_id,
observed_tool,
observed_proof_state,
observed_candidates,
scope,
error_text,
))
} else {
None
};
let auto_action = recovery
.as_ref()
.and_then(|payload| payload.get("auto_action"))
.cloned()
.unwrap_or(serde_json::Value::Null);
let workspace_match = workspace_binding_mismatch.is_none();
serde_json::json!({
"schema": "m1nd-agent-runtime-contract-v0",
"status": status,
"proof_state": observed_proof_state,
"trust_mode": trust_mode,
"observed": {
"tool": observed_tool,
"candidates": observed_candidates,
"error_text": error_text,
},
"session_identity": {
"agent_id": agent_id,
"tool": observed_tool,
"process_id": std::process::id(),
"binary": {
"name": "m1nd-mcp",
"version": env!("CARGO_PKG_VERSION"),
},
"current_exe": std::env::current_exe().ok().map(|path| path.to_string_lossy().to_string()),
"runtime_root": self.runtime_root.to_string_lossy(),
},
"workspace_binding": {
"requested_scope": scope,
"active_workspace_root": self.workspace_root,
"active_workspace_root_source": self.workspace_root_source,
"active_ingest_roots": self.ingest_roots,
"workspace_match": workspace_match,
"mismatch": workspace_binding_mismatch,
},
"graph_identity": {
"node_count": node_count,
"edge_count": edge_count,
"finalized": graph_finalized,
"graph_generation": self.graph_generation,
"plasticity_generation": self.plasticity_generation,
"cache_generation": self.cache_generation,
"ingest_root_count": self.ingest_roots.len(),
"graph_path": self.graph_path.to_string_lossy(),
"graph_path_exists": self.graph_path.exists(),
},
"next_suggested_tool": if needs_recovery { serde_json::Value::String("recovery_playbook".into()) } else { serde_json::Value::Null },
"next_step_hint": if needs_recovery {
serde_json::Value::String("Call recovery_playbook with the provided recovery.arguments payload before falling back to shell search.".into())
} else {
serde_json::Value::Null
},
"auto_action": auto_action,
"recovery": recovery.unwrap_or(serde_json::Value::Null),
"non_claims": [
"agent_runtime_contract does not repair the MCP host binding.",
"agent_runtime_contract does not ingest or mutate the graph.",
"agent_runtime_contract does not prove semantic retrieval correctness.",
"agent_runtime_contract does not replace compiler, test, log, or direct file truth."
],
})
}
pub fn instance_self_summary(&self) -> serde_json::Value {
let instance: InstanceRegistryEntry = self.instance.summary();
serde_json::json!({
"instance": instance,
"graph_state": self.graph_runtime_summary(),
"active_agent_sessions": self.sessions.len(),
"queries_processed": self.queries_processed,
"last_persist_secs_ago": self.last_persist_time.map(|ts| ts.elapsed().as_secs_f64()),
})
}
pub fn empty_graph_diagnostic(
&self,
tool: &str,
scope: Option<&str>,
hint: Option<&str>,
) -> serde_json::Value {
let mut next_actions = vec![
"run ingest against the intended repository or workspace".to_string(),
"confirm the tool is querying the same active graph session used by the latest ingest"
.to_string(),
];
if scope.is_some() {
next_actions.push(
"retry with both absolute and graph-relative scope forms to detect normalization drift"
.to_string(),
);
}
serde_json::json!({
"error": {
"code": "empty_graph",
"message": format!("{} cannot operate because the active graph has zero nodes", tool),
"tool": tool,
"scope": scope,
"hint": hint,
"probable_causes": [
"the latest ingest did not populate the active graph",
"the handler is reading a different graph/session state than the latest ingest",
"scope or path normalization excluded the intended graph region"
],
"next_actions": next_actions,
},
"graph_state": self.graph_runtime_summary(),
})
}
fn infer_workspace_root(
config: &crate::server::McpConfig,
runtime_root: &std::path::Path,
) -> (std::path::PathBuf, String) {
let current_dir = std::env::current_dir().ok();
Self::infer_workspace_root_with_current_dir(config, runtime_root, current_dir.as_deref())
}
fn infer_workspace_root_with_current_dir(
config: &crate::server::McpConfig,
runtime_root: &std::path::Path,
current_dir: Option<&std::path::Path>,
) -> (std::path::PathBuf, String) {
let raw_graph_parent = config
.graph_source
.parent()
.unwrap_or(runtime_root)
.to_path_buf();
let graph_parent = if raw_graph_parent.is_absolute() {
raw_graph_parent
} else if let Some(current_dir) = current_dir {
current_dir.join(&raw_graph_parent)
} else {
runtime_root.join(&raw_graph_parent)
};
if !Self::looks_like_managed_runtime_path(&graph_parent, runtime_root) {
return (graph_parent, "graph_path_parent".into());
}
for env_name in WORKSPACE_ROOT_ENV_CANDIDATES {
let Ok(value) = std::env::var(env_name) else {
continue;
};
let candidate = std::path::PathBuf::from(value);
if Self::usable_workspace_candidate(&candidate, runtime_root) {
return (candidate, format!("env:{env_name}"));
}
}
if let Some(candidate) = current_dir {
if Self::usable_workspace_candidate(candidate, runtime_root) {
return (candidate.to_path_buf(), "current_dir".into());
}
}
(graph_parent, "graph_path_parent_runtime_fallback".into())
}
fn usable_workspace_candidate(
candidate: &std::path::Path,
runtime_root: &std::path::Path,
) -> bool {
candidate.is_dir() && !Self::looks_like_managed_runtime_path(candidate, runtime_root)
}
fn looks_like_managed_runtime_path(
path: &std::path::Path,
runtime_root: &std::path::Path,
) -> bool {
if Self::path_matches_runtime_base(runtime_root) && path.starts_with(runtime_root) {
return true;
}
Self::path_matches_runtime_base(path)
}
fn path_matches_runtime_base(path: &std::path::Path) -> bool {
if let Ok(runtime_base) = std::env::var("M1ND_RUNTIME_BASE") {
let runtime_base = std::path::PathBuf::from(runtime_base);
if path.starts_with(runtime_base) {
return true;
}
}
let text = path.to_string_lossy();
MANAGED_RUNTIME_PATH_MARKERS
.iter()
.any(|marker| text.contains(marker))
}
pub fn initialize(
graph: Graph,
config: &crate::server::McpConfig,
domain: DomainConfig,
) -> M1ndResult<Self> {
let runtime_root = config.runtime_dir.clone().unwrap_or_else(|| {
config
.graph_source
.parent()
.unwrap_or(std::path::Path::new("."))
.to_path_buf()
});
std::fs::create_dir_all(&runtime_root)?;
let embeddings_cache_path = runtime_root.join("embeddings_cache.bin");
let orchestrator = QueryOrchestrator::build_with_cache(
&graph,
Some(&embeddings_cache_path),
!config.read_only,
)?;
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));
let (workspace_root, workspace_root_source) =
Self::infer_workspace_root(config, &runtime_root);
let instance_mode = if config.read_only {
crate::instance_registry::InstanceMode::ReadOnly
} else {
crate::instance_registry::InstanceMode::ReadWrite
};
let instance = InstanceHandle::acquire_with_mode(
&workspace_root,
&runtime_root,
&config.graph_source,
&config.plasticity_state,
config.registry_dir.as_deref(),
instance_mode,
)?;
if config.read_only {
eprintln!(
"[m1nd] read-only attach: holding no lease; persistence disabled; mutation tools gated."
);
}
let ingest_roots = Self::load_ingest_roots(&config.graph_source);
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(),
embeddings_cache_path,
sessions: HashMap::new(),
edit_previews: 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,
workspace_root: Some(workspace_root.to_string_lossy().to_string()),
workspace_root_source: Some(workspace_root_source),
runtime_root: runtime_root.clone(),
instance,
apply_batch_progress_sink: None,
antibodies: {
let ab_path = runtime_root.join("antibodies.json");
m1nd_core::antibody::load_antibodies(&ab_path).unwrap_or_default()
},
antibodies_path: runtime_root.join("antibodies.json"),
last_antibody_scan_generation: 0,
tremor_registry: {
let tr_path = runtime_root.join("tremor_state.json");
m1nd_core::tremor::load_tremor_state(&tr_path)
.unwrap_or_else(|_| TremorRegistry::with_defaults())
},
tremor_path: runtime_root.join("tremor_state.json"),
trust_ledger: {
let tl_path = runtime_root.join("trust_state.json");
m1nd_core::trust::load_trust_state(&tl_path).unwrap_or_else(|_| TrustLedger::new())
},
trust_path: runtime_root.join("trust_state.json"),
savings_tracker: SavingsTracker::new(),
query_log: Vec::new(),
global_savings: {
let sv_path = runtime_root.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: runtime_root.join("savings_state.json"),
session_start_node_count: 0,
session_start_edge_count: 0,
boot_memory_path: runtime_root.join("boot_memory_state.json"),
boot_memory: {
let boot_path = runtime_root.join("boot_memory_state.json");
Self::load_boot_memory(&boot_path)
},
daemon_state_path: runtime_root.join("daemon_state.json"),
daemon_state: {
let path = runtime_root.join("daemon_state.json");
Self::load_daemon_state(&path)
},
daemon_alerts_path: runtime_root.join("daemon_alerts.json"),
daemon_alerts: {
let path = runtime_root.join("daemon_alerts.json");
Self::load_daemon_alerts(&path)
},
file_inventory: HashMap::new(),
coverage_sessions: HashMap::new(),
proof_ready: HashMap::new(),
flagged_findings: HashMap::new(),
auto_ingest: AutoIngestState::load(&runtime_root),
document_cache: load_document_cache(&runtime_root),
agent_memory_boot: None,
read_only: config.read_only,
read_only_persist_logged: std::cell::Cell::new(false),
})
}
pub fn should_persist(&self) -> bool {
!self.read_only
&& self.queries_processed > 0
&& self
.queries_processed
.is_multiple_of(self.auto_persist_interval as u64)
}
fn log_read_only_persist_skip(&self) {
if !self.read_only_persist_logged.replace(true) {
eprintln!("[m1nd] read-only attach: skipping persist");
}
}
pub fn run_query(
&mut self,
config: &m1nd_core::query::QueryConfig,
) -> M1ndResult<m1nd_core::query::QueryResult> {
if self.read_only {
let graph = self.graph.read();
self.orchestrator.query_readonly(&graph, config)
} else {
let mut graph = self.graph.write();
self.orchestrator.query(&mut graph, config)
}
}
pub fn persist(&mut self) -> M1ndResult<()> {
if self.read_only {
self.log_read_only_persist_skip();
return Ok(());
}
let _ = self.instance.mark_heartbeat();
self.persist_ingest_roots();
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);
}
}
if let Err(e) = m1nd_core::trust::save_trust_state(&self.trust_ledger, &self.trust_path) {
eprintln!("[m1nd] WARNING: trust persist failed: {}", e);
}
if let Err(e) =
m1nd_core::tremor::save_tremor_state(&self.tremor_registry, &self.tremor_path)
{
eprintln!("[m1nd] WARNING: tremor persist failed: {}", e);
}
if let Err(e) = self.persist_boot_memory() {
eprintln!("[m1nd] WARNING: boot memory persist failed: {}", e);
}
if let Err(e) = self.persist_daemon_state() {
eprintln!("[m1nd] WARNING: daemon state persist failed: {}", e);
}
if let Err(e) = self.persist_daemon_alerts() {
eprintln!("[m1nd] WARNING: daemon alert persist failed: {}", e);
}
if let Err(e) = self.auto_ingest.persist(&self.runtime_root) {
eprintln!("[m1nd] WARNING: auto-ingest persist failed: {}", e);
}
if let Err(e) = persist_document_cache(&self.runtime_root, &self.document_cache) {
eprintln!("[m1nd] WARNING: document cache persist failed: {}", e);
}
self.last_persist_time = Some(Instant::now());
Ok(())
}
fn persist_ingest_roots(&mut self) {
let persist_root = self
.graph_path
.parent()
.map(std::path::Path::to_path_buf)
.unwrap_or_else(|| self.runtime_root.clone());
if let Err(e) = std::fs::create_dir_all(&persist_root) {
eprintln!("[m1nd] WARNING: ingest roots persist dir failed: {}", e);
return;
}
let ingest_roots_path = persist_root.join("ingest_roots.json");
if let Ok(json) = serde_json::to_string_pretty(&self.ingest_roots) {
if let Err(e) = std::fs::write(&ingest_roots_path, json) {
eprintln!("[m1nd] WARNING: ingest roots persist failed: {}", e);
}
}
}
fn load_ingest_roots(graph_path: &std::path::Path) -> Vec<String> {
let Some(root) = graph_path.parent() else {
return Vec::new();
};
let ingest_roots_path = root.join("ingest_roots.json");
std::fs::read_to_string(&ingest_roots_path)
.ok()
.and_then(|s| serde_json::from_str::<Vec<String>>(&s).ok())
.unwrap_or_default()
}
pub fn persist_boot_memory(&self) -> M1ndResult<()> {
if self.read_only {
self.log_read_only_persist_skip();
return Ok(());
}
let state = BootMemoryState {
entries: self.boot_memory.clone(),
};
save_json_atomic(&self.boot_memory_path, &state)
}
fn load_boot_memory(path: &Path) -> HashMap<String, BootMemoryEntry> {
std::fs::read_to_string(path)
.ok()
.and_then(|s| serde_json::from_str::<BootMemoryState>(&s).ok())
.map(|state| state.entries)
.unwrap_or_default()
}
pub fn persist_daemon_state(&self) -> M1ndResult<()> {
if self.read_only {
self.log_read_only_persist_skip();
return Ok(());
}
save_json_atomic(&self.daemon_state_path, &self.daemon_state)
}
fn load_daemon_state(path: &Path) -> DaemonRuntimeState {
std::fs::read_to_string(path)
.ok()
.and_then(|s| serde_json::from_str::<DaemonRuntimeState>(&s).ok())
.unwrap_or_default()
}
pub fn persist_daemon_alerts(&self) -> M1ndResult<()> {
if self.read_only {
self.log_read_only_persist_skip();
return Ok(());
}
save_json_atomic(&self.daemon_alerts_path, &self.daemon_alerts)
}
fn load_daemon_alerts(path: &Path) -> Vec<DaemonAlert> {
std::fs::read_to_string(path)
.ok()
.and_then(|s| serde_json::from_str::<Vec<DaemonAlert>>(&s).ok())
.unwrap_or_default()
}
pub fn record_daemon_alert(&mut self, alert: DaemonAlert) {
self.daemon_alerts.push(alert);
if self.daemon_alerts.len() > 500 {
let drain = self.daemon_alerts.len() - 500;
self.daemon_alerts.drain(0..drain);
}
}
pub fn reload_heuristic_sidecars(&mut self) {
self.antibodies =
m1nd_core::antibody::load_antibodies(&self.antibodies_path).unwrap_or_default();
self.tremor_registry = m1nd_core::tremor::load_tremor_state(&self.tremor_path)
.unwrap_or_else(|_| TremorRegistry::with_defaults());
self.trust_ledger = m1nd_core::trust::load_trust_state(&self.trust_path)
.unwrap_or_else(|_| TrustLedger::new());
}
pub fn rebuild_engines(&mut self) -> M1ndResult<()> {
{
let graph = self.graph.read();
self.orchestrator = QueryOrchestrator::build_with_cache(
&graph,
Some(&self.embeddings_cache_path),
!self.read_only,
)?;
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().is_some_and(|w| {
matches!(
(&trigger, &w.strategy),
(
WatchTrigger::Ingest,
crate::perspective::state::WatchStrategy::OnIngest,
) | (
WatchTrigger::Learn,
crate::perspective::state::WatchStrategy::OnLearn,
)
)
})
})
.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 _ = self.instance.mark_heartbeat();
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 next_edit_preview_id(&self, agent_id: &str) -> String {
let now_ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0);
let short_id = &agent_id[..agent_id.len().min(8)];
format!("preview_{}_{}", short_id, now_ms)
}
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 self.read_only {
self.log_read_only_persist_skip();
return;
}
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()
}
pub fn record_file_inventory(&mut self, entries: impl IntoIterator<Item = FileInventoryEntry>) {
for entry in entries {
self.file_inventory.insert(entry.external_id.clone(), entry);
}
}
pub fn reset_file_inventory(&mut self) {
self.file_inventory.clear();
}
pub fn note_coverage(
&mut self,
agent_id: &str,
tool: &str,
files: impl IntoIterator<Item = String>,
nodes: impl IntoIterator<Item = String>,
) {
let now_ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0);
let entry = self
.coverage_sessions
.entry(agent_id.to_string())
.or_insert_with(|| CoverageSessionState {
started_at_ms: now_ms,
..CoverageSessionState::default()
});
*entry.tools_used.entry(tool.to_string()).or_insert(0) += 1;
for file in files {
if !file.is_empty() {
entry.visited_files.insert(file);
}
}
for node in nodes {
if !node.is_empty() {
entry.visited_nodes.insert(node);
}
}
}
pub fn note_proof_ready(&mut self, agent_id: &str, raw_target: &str, evidence: &str) {
let Some(target) = crate::scope::normalize_scope_path(Some(raw_target), &self.ingest_roots)
else {
return;
};
let now_ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0);
self.proof_ready.insert(
(agent_id.to_string(), target),
ProofReadyMark {
proved_at_ms: now_ms,
generation: self.cache_generation,
evidence: Some(evidence.to_string()),
},
);
}
pub fn is_proof_ready(&self, agent_id: &str, raw_target: &str) -> bool {
let Some(target) = crate::scope::normalize_scope_path(Some(raw_target), &self.ingest_roots)
else {
return false;
};
self.proof_ready
.contains_key(&(agent_id.to_string(), target))
}
pub fn get_proof_ready(&self, agent_id: &str, raw_target: &str) -> Option<&ProofReadyMark> {
let target = crate::scope::normalize_scope_path(Some(raw_target), &self.ingest_roots)?;
self.proof_ready.get(&(agent_id.to_string(), target))
}
pub fn note_finding(
&mut self,
agent_id: &str,
node_id: &str,
kind: &str,
severity: &str,
file_path: &str,
) {
if node_id.is_empty() {
return;
}
let now_ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0);
let key = (agent_id.to_string(), node_id.to_string());
if !self.flagged_findings.contains_key(&key)
&& self.flagged_findings.len() >= MAX_FLAGGED_FINDINGS
{
if let Some(oldest) = self
.flagged_findings
.iter()
.min_by_key(|(_, mark)| mark.flagged_at_ms)
.map(|(k, _)| k.clone())
{
self.flagged_findings.remove(&oldest);
}
}
self.flagged_findings.insert(
key,
FindingMark {
flagged_at_ms: now_ms,
generation: self.cache_generation,
kind: kind.to_string(),
severity: severity.to_string(),
file_path: file_path.to_string(),
},
);
}
pub fn get_finding(&self, agent_id: &str, node_id: &str) -> Option<&FindingMark> {
self.flagged_findings
.get(&(agent_id.to_string(), node_id.to_string()))
}
pub fn take_finding(&mut self, agent_id: &str, node_id: &str) -> Option<FindingMark> {
self.flagged_findings
.remove(&(agent_id.to_string(), node_id.to_string()))
}
}
fn save_json_atomic<T: Serialize>(path: &Path, value: &T) -> M1ndResult<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let tmp = path.with_extension("tmp");
let payload = serde_json::to_vec_pretty(value)?;
std::fs::write(&tmp, payload)?;
std::fs::rename(&tmp, path)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::{SessionState, WORKSPACE_ROOT_ENV_CANDIDATES};
use crate::server::McpConfig;
use m1nd_core::domain::DomainConfig;
use m1nd_core::graph::Graph;
use m1nd_core::types::NodeType;
use std::sync::{Mutex, OnceLock};
fn env_lock() -> &'static Mutex<()> {
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
LOCK.get_or_init(|| Mutex::new(()))
}
struct EnvGuard {
saved: Vec<(&'static str, Option<String>)>,
}
impl EnvGuard {
fn clear_workspace_hints() -> Self {
let saved = WORKSPACE_ROOT_ENV_CANDIDATES
.iter()
.map(|name| (*name, std::env::var(name).ok()))
.collect::<Vec<_>>();
for name in WORKSPACE_ROOT_ENV_CANDIDATES {
std::env::remove_var(name);
}
Self { saved }
}
}
impl Drop for EnvGuard {
fn drop(&mut self) {
for (name, value) in &self.saved {
if let Some(value) = value {
std::env::set_var(name, value);
} else {
std::env::remove_var(name);
}
}
}
}
#[test]
fn workspace_root_uses_graph_parent_for_normal_graph_path() {
let temp = tempfile::tempdir().expect("tempdir");
let config = McpConfig {
graph_source: temp.path().join("graph_snapshot.json"),
plasticity_state: temp.path().join("plasticity_state.json"),
runtime_dir: Some(temp.path().to_path_buf()),
..McpConfig::default()
};
let state = SessionState::initialize(Graph::new(), &config, DomainConfig::code())
.expect("initialize session");
assert_eq!(
state.workspace_root.as_deref(),
Some(temp.path().to_string_lossy().as_ref())
);
assert_eq!(
state.workspace_root_source.as_deref(),
Some("graph_path_parent")
);
}
#[test]
fn workspace_root_uses_env_hint_for_codex_runtime_graph_path() {
let _guard = env_lock().lock().expect("env lock");
let _env = EnvGuard::clear_workspace_hints();
let temp = tempfile::tempdir().expect("tempdir");
let workspace = temp.path().join("project");
let runtime = temp
.path()
.join(".codex")
.join("m1nd-runtimes")
.join("hash")
.join("sessions")
.join("ppid-1-pid-2");
std::fs::create_dir_all(&workspace).expect("workspace dir");
std::fs::create_dir_all(&runtime).expect("runtime dir");
std::env::set_var("M1ND_WORKSPACE_ROOT", &workspace);
let config = McpConfig {
graph_source: runtime.join("graph_snapshot.json"),
plasticity_state: runtime.join("plasticity_state.json"),
runtime_dir: Some(runtime),
..McpConfig::default()
};
let state = SessionState::initialize(Graph::new(), &config, DomainConfig::code())
.expect("initialize session");
assert_eq!(
state.workspace_root.as_deref(),
Some(workspace.to_string_lossy().as_ref())
);
assert_eq!(
state.workspace_root_source.as_deref(),
Some("env:M1ND_WORKSPACE_ROOT")
);
}
#[test]
fn ingest_roots_persist_next_to_graph_not_workspace_hint() {
let _guard = env_lock().lock().expect("env lock");
let _env = EnvGuard::clear_workspace_hints();
let temp = tempfile::tempdir().expect("tempdir");
let workspace = temp.path().join("project");
let runtime = temp.path().join("runtime");
std::fs::create_dir_all(&workspace).expect("workspace dir");
std::fs::create_dir_all(&runtime).expect("runtime dir");
std::env::set_var("M1ND_WORKSPACE_ROOT", &workspace);
let config = McpConfig {
graph_source: runtime.join("graph_snapshot.json"),
plasticity_state: runtime.join("plasticity_state.json"),
runtime_dir: Some(runtime.clone()),
..McpConfig::default()
};
let mut state = SessionState::initialize(Graph::new(), &config, DomainConfig::code())
.expect("initialize session");
state.ingest_roots = vec![workspace.to_string_lossy().to_string()];
state.persist_ingest_roots();
assert!(runtime.join("ingest_roots.json").exists());
assert!(!workspace.join("ingest_roots.json").exists());
let persisted = std::fs::read_to_string(runtime.join("ingest_roots.json"))
.expect("persisted ingest roots");
let persisted_roots: Vec<String> =
serde_json::from_str(&persisted).expect("persisted ingest roots json");
assert!(persisted_roots.contains(&workspace.to_string_lossy().to_string()));
}
#[test]
fn workspace_root_uses_claude_hint_for_managed_runtime_graph_path() {
let _guard = env_lock().lock().expect("env lock");
let _env = EnvGuard::clear_workspace_hints();
let temp = tempfile::tempdir().expect("tempdir");
let workspace = temp.path().join("claude-project");
let runtime = temp
.path()
.join(".claude")
.join("m1nd-runtimes")
.join("hash")
.join("sessions")
.join("ppid-1-pid-2");
std::fs::create_dir_all(&workspace).expect("workspace dir");
std::fs::create_dir_all(&runtime).expect("runtime dir");
std::env::set_var("CLAUDE_PROJECT_DIR", &workspace);
let config = McpConfig {
graph_source: runtime.join("graph_snapshot.json"),
plasticity_state: runtime.join("plasticity_state.json"),
runtime_dir: Some(runtime),
..McpConfig::default()
};
let state = SessionState::initialize(Graph::new(), &config, DomainConfig::code())
.expect("initialize session");
assert_eq!(
state.workspace_root.as_deref(),
Some(workspace.to_string_lossy().as_ref())
);
assert_eq!(
state.workspace_root_source.as_deref(),
Some("env:CLAUDE_PROJECT_DIR")
);
}
#[test]
fn workspace_root_uses_host_hint_for_relative_graph_inside_managed_runtime() {
let _guard = env_lock().lock().expect("env lock");
let _env = EnvGuard::clear_workspace_hints();
let temp = tempfile::tempdir().expect("tempdir");
let workspace = temp.path().join("claude-project");
let runtime = temp
.path()
.join(".claude")
.join("m1nd-runtimes")
.join("hash")
.join("sessions")
.join("ppid-1-pid-2");
std::fs::create_dir_all(&workspace).expect("workspace dir");
std::fs::create_dir_all(&runtime).expect("runtime dir");
std::env::set_var("CLAUDE_PROJECT_DIR", &workspace);
let config = McpConfig {
graph_source: std::path::PathBuf::from("./graph_snapshot.json"),
plasticity_state: std::path::PathBuf::from("./plasticity_state.json"),
runtime_dir: Some(runtime.clone()),
..McpConfig::default()
};
let (workspace_root, workspace_root_source) =
SessionState::infer_workspace_root_with_current_dir(&config, &runtime, Some(&runtime));
assert_eq!(workspace_root, workspace);
assert_eq!(workspace_root_source.as_str(), "env:CLAUDE_PROJECT_DIR");
}
#[test]
fn workspace_root_prefers_pwd_over_oldpwd_for_managed_runtime_graph_path() {
let _guard = env_lock().lock().expect("env lock");
let _env = EnvGuard::clear_workspace_hints();
let temp = tempfile::tempdir().expect("tempdir");
let workspace = temp.path().join("active-project");
let stale_workspace = temp.path().join("stale-project");
let runtime = temp
.path()
.join(".codex")
.join("m1nd-runtimes")
.join("hash")
.join("sessions")
.join("ppid-1-pid-2");
std::fs::create_dir_all(&workspace).expect("workspace dir");
std::fs::create_dir_all(&stale_workspace).expect("stale workspace dir");
std::fs::create_dir_all(&runtime).expect("runtime dir");
std::env::set_var("PWD", &workspace);
std::env::set_var("OLDPWD", &stale_workspace);
let config = McpConfig {
graph_source: std::path::PathBuf::from("./graph_snapshot.json"),
plasticity_state: std::path::PathBuf::from("./plasticity_state.json"),
runtime_dir: Some(runtime.clone()),
..McpConfig::default()
};
let (workspace_root, workspace_root_source) =
SessionState::infer_workspace_root_with_current_dir(&config, &runtime, Some(&runtime));
assert_eq!(workspace_root, workspace);
assert_eq!(workspace_root_source.as_str(), "env:PWD");
}
#[test]
fn workspace_binding_mismatch_detects_absolute_scope_outside_active_roots() {
let temp = tempfile::tempdir().expect("tempdir");
let workspace = temp.path().join("workspace");
let other = temp.path().join("other");
std::fs::create_dir_all(workspace.join("src")).expect("workspace src");
std::fs::create_dir_all(other.join("src")).expect("other src");
std::fs::write(
workspace.join("Cargo.toml"),
"[package]\nname='workspace'\n",
)
.expect("workspace manifest");
std::fs::write(other.join("Cargo.toml"), "[package]\nname='other'\n")
.expect("other manifest");
let config = McpConfig {
graph_source: workspace.join("graph_snapshot.json"),
plasticity_state: workspace.join("plasticity_state.json"),
runtime_dir: Some(workspace.clone()),
..McpConfig::default()
};
let state = SessionState::initialize(Graph::new(), &config, DomainConfig::code())
.expect("initialize session");
let other_scope = other.join("src").to_string_lossy().to_string();
let mismatch = state
.workspace_binding_mismatch(Some(&other_scope))
.expect("scope outside workspace should be flagged");
assert_eq!(mismatch["code"], "wrong_workspace_binding");
assert_eq!(
mismatch["requested_workspace_hint"].as_str(),
Some(other.to_string_lossy().as_ref())
);
assert_eq!(
mismatch["active_workspace_root"].as_str(),
Some(workspace.to_string_lossy().as_ref())
);
}
#[test]
fn workspace_binding_mismatch_ignores_absolute_scope_inside_active_root() {
let temp = tempfile::tempdir().expect("tempdir");
let workspace = temp.path().join("workspace");
std::fs::create_dir_all(workspace.join("src")).expect("workspace src");
let config = McpConfig {
graph_source: workspace.join("graph_snapshot.json"),
plasticity_state: workspace.join("plasticity_state.json"),
runtime_dir: Some(workspace.clone()),
..McpConfig::default()
};
let state = SessionState::initialize(Graph::new(), &config, DomainConfig::code())
.expect("initialize session");
let workspace_scope = workspace.join("src").to_string_lossy().to_string();
assert!(state
.workspace_binding_mismatch(Some(&workspace_scope))
.is_none());
}
#[test]
fn workspace_binding_mismatch_classifies_nested_workspace_binding() {
let temp = tempfile::tempdir().expect("tempdir");
let repo = temp.path().join("repo");
let nested = repo.join("docs").join("prds");
std::fs::create_dir_all(&nested).expect("nested workspace");
std::fs::write(repo.join("package.json"), "{\"name\":\"repo\"}\n").expect("manifest");
let config = McpConfig {
graph_source: temp.path().join("runtime").join("graph_snapshot.json"),
plasticity_state: temp.path().join("runtime").join("plasticity_state.json"),
runtime_dir: Some(temp.path().join("runtime")),
..McpConfig::default()
};
let mut state = SessionState::initialize(Graph::new(), &config, DomainConfig::code())
.expect("initialize session");
state.workspace_root = Some(nested.to_string_lossy().to_string());
let repo_scope = repo.to_string_lossy().to_string();
let mismatch = state
.workspace_binding_mismatch(Some(&repo_scope))
.expect("nested workspace should be partial binding");
assert_eq!(mismatch["code"], "wrong_workspace_binding");
assert_eq!(mismatch["binding_kind"], "nested_workspace_binding");
assert_eq!(mismatch["partial_scope"], true);
assert_eq!(
mismatch["recommended_usage_mode"],
"partial_scope_orientation"
);
}
#[test]
fn workspace_binding_mismatch_classifies_file_level_binding() {
let _guard = env_lock().lock().expect("env lock");
let _env = EnvGuard::clear_workspace_hints();
let temp = tempfile::tempdir().expect("tempdir");
let repo = temp.path().join("repo");
let doc = repo.join("docs").join("PRD.md");
std::fs::create_dir_all(doc.parent().expect("doc parent")).expect("docs");
std::fs::write(repo.join("package.json"), "{\"name\":\"repo\"}\n").expect("manifest");
std::fs::write(&doc, "# PRD\n").expect("doc");
let config = McpConfig {
graph_source: temp.path().join("runtime").join("graph_snapshot.json"),
plasticity_state: temp.path().join("runtime").join("plasticity_state.json"),
runtime_dir: Some(temp.path().join("runtime")),
..McpConfig::default()
};
let mut state = SessionState::initialize(Graph::new(), &config, DomainConfig::code())
.expect("initialize session");
state.workspace_root = None;
state.ingest_roots = vec![doc.to_string_lossy().to_string()];
let repo_scope = repo.to_string_lossy().to_string();
let mismatch = state
.workspace_binding_mismatch(Some(&repo_scope))
.expect("file-level ingest root should be partial binding");
assert_eq!(mismatch["code"], "wrong_workspace_binding");
assert_eq!(mismatch["binding_kind"], "file_level_binding");
assert_eq!(mismatch["partial_scope"], true);
assert_eq!(mismatch["scope_reliability"], "document_context_only");
}
#[test]
fn agent_runtime_contract_surfaces_wrong_workspace_recovery() {
let temp = tempfile::tempdir().expect("tempdir");
let workspace = temp.path().join("workspace");
let other = temp.path().join("other");
std::fs::create_dir_all(workspace.join("src")).expect("workspace src");
std::fs::create_dir_all(other.join("src")).expect("other src");
std::fs::write(other.join("Cargo.toml"), "[package]\nname='other'\n")
.expect("other manifest");
let mut graph = Graph::new();
graph
.add_node("file::src/lib.rs", "lib.rs", NodeType::File, &[], 0.0, 0.0)
.expect("add file node");
graph.finalize().expect("finalize graph");
let config = McpConfig {
graph_source: workspace.join("graph_snapshot.json"),
plasticity_state: workspace.join("plasticity_state.json"),
runtime_dir: Some(workspace.clone()),
..McpConfig::default()
};
let state = SessionState::initialize(graph, &config, DomainConfig::code())
.expect("initialize session");
let other_scope = other.join("src").to_string_lossy().to_string();
let contract = state.agent_runtime_contract(
"jimi",
"seek",
"blocked",
Some(0),
Some(&other_scope),
None,
);
assert_eq!(contract["schema"], "m1nd-agent-runtime-contract-v0");
assert_eq!(contract["trust_mode"], "wrong_workspace_binding");
assert_eq!(contract["workspace_binding"]["workspace_match"], false);
assert_eq!(
contract["workspace_binding"]["mismatch"]["code"],
"wrong_workspace_binding"
);
assert_eq!(contract["recovery"]["suggested_tool"], "recovery_playbook");
assert_eq!(contract["auto_action"]["schema"], "m1nd-auto-action-v0");
assert_eq!(contract["auto_action"]["status"], "ready");
assert_eq!(contract["auto_action"]["tool"], "recovery_playbook");
assert_eq!(
contract["recovery"]["auto_action"]["safety"]["requires_confirmation"],
false
);
assert_eq!(
contract["session_identity"]["binary"]["version"],
env!("CARGO_PKG_VERSION")
);
}
#[test]
fn agent_runtime_contract_keeps_zero_candidates_without_blocked_proof_in_full_trust() {
let temp = tempfile::tempdir().expect("tempdir");
let workspace = temp.path().join("workspace");
std::fs::create_dir_all(workspace.join("src")).expect("workspace src");
let mut graph = Graph::new();
graph
.add_node("file::src/lib.rs", "lib.rs", NodeType::File, &[], 0.0, 0.0)
.expect("add file node");
graph.finalize().expect("finalize graph");
let config = McpConfig {
graph_source: workspace.join("graph_snapshot.json"),
plasticity_state: workspace.join("plasticity_state.json"),
runtime_dir: Some(workspace.clone()),
..McpConfig::default()
};
let state = SessionState::initialize(graph, &config, DomainConfig::code())
.expect("initialize session");
let contract =
state.agent_runtime_contract("jimi", "seek", "triaging", Some(0), None, None);
assert_eq!(contract["trust_mode"], "full_trust");
assert_eq!(contract["status"], "ok");
assert_eq!(contract["auto_action"], serde_json::Value::Null);
assert_eq!(contract["recovery"], serde_json::Value::Null);
assert_eq!(contract["next_suggested_tool"], serde_json::Value::Null);
}
#[test]
fn workspace_root_uses_antigravity_hint_for_generic_agent_runtime_graph_path() {
let _guard = env_lock().lock().expect("env lock");
let _env = EnvGuard::clear_workspace_hints();
let temp = tempfile::tempdir().expect("tempdir");
let workspace = temp.path().join("antigravity-project");
let runtime = temp
.path()
.join("agent-runtimes")
.join("hash")
.join("sessions")
.join("ppid-1-pid-2");
std::fs::create_dir_all(&workspace).expect("workspace dir");
std::fs::create_dir_all(&runtime).expect("runtime dir");
std::env::set_var("ANTIGRAVITY_WORKSPACE_ROOT", &workspace);
let config = McpConfig {
graph_source: runtime.join("graph_snapshot.json"),
plasticity_state: runtime.join("plasticity_state.json"),
runtime_dir: Some(runtime),
..McpConfig::default()
};
let state = SessionState::initialize(Graph::new(), &config, DomainConfig::code())
.expect("initialize session");
assert_eq!(
state.workspace_root.as_deref(),
Some(workspace.to_string_lossy().as_ref())
);
assert_eq!(
state.workspace_root_source.as_deref(),
Some("env:ANTIGRAVITY_WORKSPACE_ROOT")
);
}
}