Introduction
m1nd is a neuro-symbolic connectome engine built in Rust. It ingests a codebase into a weighted, directed graph — then lets you fire queries into that graph and watch where the energy goes. Signal propagates across structural, semantic, temporal, and causal dimensions. The graph learns from every interaction via Hebbian plasticity: paths you use get stronger, paths you ignore decay. The result is a code intelligence layer that adapts to how you think about your codebase. It ships as an MCP server with 43 tools, runs on stdio, and works with any MCP-compatible client.
The Problem
AI coding agents are powerful reasoners but terrible navigators. An LLM can analyze a function you paste into its context window, but it cannot find the right function in a codebase of 10,000 files without burning tokens on speculative grep, glob, and tree walks. The existing tools fail in different ways:
- Full-text search finds what you said, not what you meant. Searching for “authentication” won’t surface the session middleware that enforces it.
- RAG retrieves chunks by embedding similarity, but each retrieval is stateless. It has no memory of what it retrieved last time, and no way to express relationships between results.
- Static analysis produces call graphs and ASTs, but they are frozen snapshots. They cannot answer “what if I remove this module?” or “what changed since my last session?”
- Knowledge graphs require manual curation and only return what was explicitly encoded.
m1nd solves this with a fundamentally different approach: spreading activation on a learned graph. Instead of matching tokens or embedding vectors, it propagates energy from seed nodes through weighted edges and observes the activation pattern that emerges. The graph topology determines where signal flows. Hebbian learning determines how strongly.
335 files -> 9,767 nodes -> 26,557 edges in 0.91s
activate in 31ms | impact in 5ms | trace in 3.5ms | learn in <1ms
No LLM calls. No API keys. No network. The binary is ~8MB of pure Rust.
Key Capabilities
Spreading Activation
The core query primitive. Fire a signal into the graph from one or more seed nodes. The signal propagates through the CSR adjacency structure, decaying at each hop, inhibited by negative edges, and scored across four dimensions:
| Dimension | Source | What It Captures |
|---|---|---|
| Structural | CSR adjacency + PageRank | Graph distance, edge types, centrality |
| Semantic | Trigram matching on identifiers | Naming patterns, token overlap |
| Temporal | Git history + learn feedback | Co-change frequency, recency decay |
| Causal | Stacktrace mapping + call chains | Error proximity, suspiciousness |
The engine selects between a wavefront (BFS-parallel) and a heap (priority-queue) propagation strategy at runtime based on seed density and average degree. Results are merged with adaptive dimension weighting and a resonance bonus for nodes that score across 3 or 4 dimensions.
// Ask: "What's related to authentication?"
{"method": "tools/call", "params": {
"name": "m1nd.activate",
"arguments": {"query": "authentication", "agent_id": "dev"}
}}
// -> auth.py fires -> propagates to session, middleware, JWT, user model
// 4-dimensional relevance ranking in 31ms
Hebbian Plasticity
The graph learns. When you tell m1nd that results were useful, edge weights strengthen along the activated paths (delta_w = learning_rate * activation_src * activation_tgt). When results go unused, inactive edges decay. After sustained strengthening, edges receive a permanent Long-Term Potentiation (LTP) bonus. After sustained weakening, Long-Term Depression (LTD) applies a permanent penalty. Homeostatic normalization prevents runaway weights by scaling incoming edges when their sum exceeds a ceiling.
// Tell the graph what was useful
{"method": "tools/call", "params": {
"name": "m1nd.learn",
"arguments": {
"feedback": "correct",
"node_ids": ["file::auth.py", "file::middleware.py"],
"agent_id": "dev"
}
}}
// -> 740 edges strengthened. Next query for "authentication" is smarter.
Plasticity state persists to disk. Across sessions, the graph evolves to match how your team navigates the codebase.
Structural Hole Detection
m1nd.missing finds gaps in the graph — nodes or edges that should exist based on the surrounding topology but don’t. If every other module in a cluster has error handling and one doesn’t, that’s a structural hole. If two subsystems communicate through a single bridge node, that’s a fragility point. This turns the graph into a specification-free audit tool.
Counterfactual Simulation
“What breaks if I delete worker.py?” The counterfactual engine uses a zero-allocation bitset mask (no graph clone) to virtually remove nodes and their incident edges, then re-runs spreading activation to measure the impact. The output includes orphaned nodes, weakened nodes, reachability loss, and cascade depth. Synergy analysis reveals whether removing two modules together is worse than the sum of removing them individually.
counterfactual("worker.py") -> 4,189 affected nodes, cascade at depth 3
counterfactual("config.py") -> 2,531 affected nodes (despite being universally imported)
Perspectives
Stateful graph navigation. Open a perspective anchored to a node, list available routes, follow edges, peek at source code, and branch explorations. Perspectives carry confidence calibration and epistemic safety checks. Two agents can open independent perspectives on the same graph and later compare them to find shared nodes and divergent conclusions.
XLR Noise Cancellation
Borrowed from professional audio engineering. Like a balanced XLR cable, m1nd transmits signal on two inverted channels — hot (from your query seeds) and cold (from automatically selected anti-seeds that are structurally similar but semantically distant). The cold signal cancels common-mode noise: the generic infrastructure that every query touches. What survives is the differential signal specific to your actual question. Sigmoid gating, density-adaptive strength, and seed immunity prevent over-cancellation.
Who This Is For
- AI agent developers who need their agents to navigate code without wasting context tokens on trial-and-error search.
- IDE and tool builders looking for an MCP-compatible code intelligence backend that goes beyond static analysis.
- Anyone using MCP clients (Claude Code, Cursor, Windsurf, Zed, Cline, Roo Code, Continue, OpenCode, Amazon Q, GitHub Copilot) who wants a graph that learns from their workflow.
- Multi-agent orchestrators who need a shared, persistent code graph that multiple agents can query, learn from, and lock regions of concurrently.
m1nd is not a replacement for full-text search, and it does not use neural embeddings (v1 uses trigram matching for the semantic dimension). If you need “find code that means X but never uses the word,” m1nd will not do it yet. See the FAQ for more on current limitations.
How to Read This Wiki
This wiki is organized into four sections. Read them in order for a full understanding, or jump to the section you need.
Architecture — How m1nd is built. The three crates (m1nd-core, m1nd-ingest, m1nd-mcp), the CSR graph representation, the activation engine hierarchy, and the MCP server protocol layer. Start here if you want to contribute or understand the internals.
Concepts — The ideas behind the implementation. Spreading activation, Hebbian plasticity, XLR noise cancellation, and structural hole detection explained with enough depth to reason about behavior and tune parameters.
API Reference — All 43 MCP tools documented with parameters, return types, and usage examples. Organized by function: activation and queries, analysis and prediction, memory and learning, exploration and discovery, perspective navigation, and lifecycle administration.
Tutorials — Practical walkthroughs. Quick start, your first query, and multi-agent workflows with trail save/resume/merge.
The Benchmarks page has real performance numbers from a production codebase (335 files, ~52K lines, 9,767 nodes, 26,557 edges). The Changelog tracks what changed between versions.
cargo build --release
./target/release/m1nd-mcp
The server starts, listens on stdio, and waits for JSON-RPC. The graph is empty until you call m1nd.ingest. From there, every query teaches it something.
System Architecture Overview
m1nd is a neuro-symbolic connectome engine that transforms codebases (and other structured domains) into a weighted property graph, then runs biologically-inspired algorithms over it: spreading activation, Hebbian plasticity, spectral noise cancellation, standing wave resonance, and temporal co-change analysis. It exposes these capabilities as 43 MCP tools over JSON-RPC stdio, serving multiple agents concurrently from a single process.
Three-Crate Workspace
The system is organized as a Cargo workspace with three crates:
[workspace]
members = ["m1nd-core", "m1nd-ingest", "m1nd-mcp"]
resolver = "2"
[workspace.dependencies]
thiserror = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
smallvec = { version = "1.13", features = ["serde"] }
parking_lot = "0.12"
rayon = "1.10"
static_assertions = "1"
| Crate | Role | Key Dependency |
|---|---|---|
| m1nd-core | Graph engine, activation, plasticity, XLR, resonance, temporal, semantic | parking_lot, smallvec, static_assertions |
| m1nd-ingest | File walking, language-specific extraction, reference resolution, diff | rayon, walkdir, regex |
| m1nd-mcp | JSON-RPC transport, tool dispatch, session management, persistence | tokio, serde_json |
Dependencies flow strictly downward: m1nd-mcp depends on both m1nd-core and m1nd-ingest; m1nd-ingest depends on m1nd-core; m1nd-core has no internal crate dependencies.
Data Flow
graph TD
subgraph "m1nd-mcp (Transport)"
STDIO["JSON-RPC stdio<br/>dual: framed + line"]
DISPATCH["Tool Dispatch<br/>43 tools"]
SESSION["SessionState<br/>SharedGraph + Engines"]
PERSIST["Auto-Persist<br/>every 50 queries"]
end
subgraph "m1nd-ingest (Extraction)"
WALK["DirectoryWalker<br/>walkdir + binary detect"]
EXTRACT["Parallel Extraction<br/>rayon threadpool"]
RESOLVE["ReferenceResolver<br/>proximity disambiguation"]
DIFF["GraphDiff<br/>incremental updates"]
MEMORY["MemoryAdapter<br/>markdown → graph"]
end
subgraph "m1nd-core (Engine)"
GRAPH["CSR Graph<br/>forward + reverse"]
NODES["NodeStorage (SoA)<br/>hot|warm|cold paths"]
ACT["Activation Engine<br/>hybrid heap/wavefront"]
SEM["Semantic Engine<br/>trigram TF-IDF + PPMI"]
TEMP["Temporal Engine<br/>co-change + decay"]
PLAST["Plasticity Engine<br/>Hebbian LTP/LTD"]
XLR["XLR Engine<br/>spectral noise cancel"]
RES["Resonance Engine<br/>standing waves"]
SNAP["Snapshot<br/>atomic JSON persist"]
end
STDIO --> DISPATCH
DISPATCH --> SESSION
SESSION --> PERSIST
PERSIST --> SNAP
SESSION -->|ingest| WALK
WALK --> EXTRACT
EXTRACT --> RESOLVE
RESOLVE --> GRAPH
DIFF -->|incremental| GRAPH
SESSION -->|activate| ACT
SESSION -->|impact/predict| TEMP
SESSION -->|learn| PLAST
SESSION -->|activate+xlr| XLR
SESSION -->|resonate| RES
ACT --> GRAPH
ACT --> SEM
ACT --> NODES
SEM --> GRAPH
TEMP --> GRAPH
PLAST --> GRAPH
XLR --> GRAPH
RES --> GRAPH
GRAPH --> NODES
Request Lifecycle
A typical m1nd.activate query flows through these stages:
- Transport: JSON-RPC message arrives on stdin (either Content-Length framed or raw line JSON).
- Dispatch:
McpServer.serve()parses the JSON-RPC request, matches the tool name, extracts parameters. - Session: The tool handler acquires a read lock on
SharedGraph(Arc<parking_lot::RwLock<Graph>>). - Seed Finding:
SeedFinderlocates matching nodes via a 5-level cascade: exact label, prefix, substring, tag, fuzzy trigram. - Activation:
HybridEngineauto-selects heap or wavefront strategy based on seed ratio and average degree. - Dimensions: Four dimensions run: Structural (BFS/heap propagation), Semantic (trigram TF-IDF + co-occurrence PPMI), Temporal (decay + velocity), Causal (forward/backward with discount).
- Merge:
merge_dimensions()combines results with adaptive weights[0.35, 0.25, 0.15, 0.25]and resonance bonus (4-dim: 1.5x, 3-dim: 1.3x). - XLR: If enabled,
AdaptiveXlrEngineruns spectral noise cancellation with dual hot/cold pulses and sigmoid gating. - Plasticity:
PlasticityEngine.update()runs the 5-step Hebbian cycle on co-activated edges. - Response: Results serialized to JSON-RPC response, written to stdout in the same transport mode as the request.
Key Design Decisions
Compressed Sparse Row (CSR) Graph
The graph uses CSR format rather than adjacency lists or adjacency matrices. CSR provides O(1) neighbor iteration start, cache-friendly sequential access, and compact memory layout. Edge weights are stored as AtomicU32 (bit-reinterpreted f32) for lock-free plasticity updates via CAS with a 64-retry limit.
Forward and reverse CSR arrays are maintained in parallel, enabling both outgoing and incoming edge traversal without full graph scans.
Struct-of-Arrays Node Storage
NodeStorage uses SoA layout with explicit hot/warm/cold path separation:
- Hot path (every query):
activation: Vec<[FiniteF32; 4]>,pagerank: Vec<FiniteF32> - Warm path (plasticity):
plasticity: Vec<PlasticityNode> - Cold path (seed finding, export):
label,node_type,tags,last_modified,change_frequency,provenance
This layout ensures that activation queries touch only hot-path arrays, maximizing L1/L2 cache utilization.
FiniteF32 Type Safety
All floating-point values in the graph are wrapped in FiniteF32, a #[repr(transparent)] newtype that makes NaN and Infinity impossible by construction:
#![allow(unused)]
fn main() {
#[derive(Clone, Copy, Default, PartialEq)]
#[repr(transparent)]
pub struct FiniteF32(f32);
impl FiniteF32 {
#[inline]
pub fn new(v: f32) -> Self {
debug_assert!(v.is_finite(), "FiniteF32::new received non-finite: {v}");
Self(if v.is_finite() { v } else { 0.0 })
}
}
}
Because NaN is excluded, FiniteF32 can safely implement Ord, Eq, and Hash – operations that are unsound on raw f32. Related newtypes PosF32 (strictly positive), LearningRate (0, 1]), and DecayFactor (0, 1]) provide compile-time invariant enforcement for their respective domains.
String Interning
All node labels, tags, and relation names are interned via StringInterner. Once interned, string comparisons become u32 == u32 (zero-allocation, single CPU cycle). The interner maps strings to InternedStr(u32) handles and provides O(1) bidirectional lookup.
Parallel Extraction, Sequential Building
Ingestion uses rayon for parallel file reading and language-specific extraction across all CPU cores, but graph construction is single-threaded. This avoids the complexity of concurrent graph mutation while still saturating I/O bandwidth during the most expensive phase (parsing hundreds of files).
Atomic Persistence
Graph and plasticity state are saved via atomic write: serialize to a temporary file, then rename() over the target. This prevents corruption on crash (FM-PL-008). The NaN firewall at the export boundary rejects any non-finite values that might have leaked through.
Performance Characteristics
Benchmarks on the a production codebase (335 files, ~52K lines, Python + Rust + TypeScript):
| Operation | Time | Notes |
|---|---|---|
| Full ingest | ~910ms | Walk + parallel extract + resolve + finalize (CSR + PageRank) |
| Activate query | ~31ms | 4-dimension with XLR, top-20 results |
| Impact analysis | ~5ms | BFS blast radius, 3-hop default |
| Predict (co-change) | ~8ms | Co-change matrix lookup + velocity scoring |
| Graph persist | ~45ms | Atomic JSON write, ~2MB snapshot |
| Plasticity update | ~2ms | 5-step Hebbian cycle on co-activated edges |
Memory footprint scales linearly with graph size. A 10K-node graph with 25K edges uses approximately 15MB of heap (graph + all engine indexes). The CSR representation is 3-5x more compact than equivalent adjacency list representations.
Scaling Bounds
- Node limit:
NodeId(u32)supports up to ~4 billion nodes. Practical limit is themax_nodesconfig (default: 500K). - Edge weights:
AtomicU32CAS with 64-retry limit. Under high contention (>32 concurrent plasticity updates on the same edge), CAS may exhaust retries and returnCasRetryExhausted. - Co-occurrence index: Disabled above 50K nodes (
COOCCURRENCE_MAX_NODES) to avoid O(N * walks * length) random walk cost. - Co-change matrix: Hard budget of 500K entries with per-row cap of 100 entries.
- Resonance pulse budget: 50K pulses per standing wave analysis to prevent combinatorial explosion in dense subgraphs.
Multi-Agent Model
m1nd runs as a single process serving multiple agents through the same JSON-RPC stdio channel. All agents share one graph and one set of engines. Writes (plasticity updates, ingestion) are immediately visible to all readers through SharedGraph = Arc<parking_lot::RwLock<Graph>>.
Each agent gets its own AgentSession tracking first/last seen timestamps and query count. The perspective system (per-agent branching views) and lock system (per-agent change tracking) provide isolation where needed without duplicating the underlying graph.
parking_lot::RwLock is used instead of std::sync::RwLock to prevent writer starvation – a critical property when plasticity updates (writes) must interleave with activation queries (reads).
Graph Engine (m1nd-core)
m1nd-core is the computational core of the connectome. It owns the graph data structure, all activation and analysis engines, the plasticity system, and the type-safe numeric primitives that prevent NaN/Inf corruption system-wide.
Source: mcp/m1nd/m1nd-core/src/
Type System
Numeric Primitives
Every floating-point value in m1nd flows through one of four newtype wrappers defined in types.rs:
| Type | Invariant | Use |
|---|---|---|
FiniteF32 | Never NaN or Inf | All activation scores, edge weights, scores |
PosF32 | Strictly positive, finite | Wavelength, frequency, half-life, decay rate, threshold |
LearningRate | (0.0, 1.0] | Plasticity learning rate |
DecayFactor | (0.0, 1.0] | Signal decay per hop |
FiniteF32 is the foundation. In debug builds, constructing one from a non-finite value panics. In release builds, it clamps to 0.0. Because NaN is excluded by construction, FiniteF32 implements Ord, Eq, and Hash – all unsound on raw f32:
#![allow(unused)]
fn main() {
impl Ord for FiniteF32 {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.0.total_cmp(&other.0)
}
}
}
Index Types
Thin #[repr(transparent)] wrappers over u32 provide type-safe indexing:
| Type | Wraps | Purpose |
|---|---|---|
NodeId(u32) | Node index | Index into NodeStorage parallel arrays |
EdgeIdx(u32) | Edge index | Index into CSR parallel arrays |
InternedStr(u32) | String handle | Opaque index into StringInterner |
CommunityId(u32) | Community | Louvain community membership |
Generation(u64) | Mutation counter | Plasticity engine desync detection |
Node and Edge Classification
#![allow(unused)]
fn main() {
#[repr(u8)]
pub enum NodeType {
File = 0, Directory = 1, Function = 2, Class = 3,
Struct = 4, Enum = 5, Type = 6, Module = 7,
Reference = 8, Concept = 9, Material = 10, Process = 11,
Product = 12, Supplier = 13, Regulatory = 14, System = 15,
Cost = 16, Custom(u8),
}
#[repr(u8)]
pub enum EdgeDirection {
Forward = 0,
Bidirectional = 1,
}
}
Variants 0-8 are code-domain types. Variants 9-16 support non-code domains (supply chain, manufacturing). Custom(u8) is the extension point for future domains.
Dimension System
Activation operates across four dimensions with fixed weights:
#![allow(unused)]
fn main() {
pub enum Dimension {
Structural = 0, // Graph topology (BFS/heap propagation)
Semantic = 1, // Text similarity (trigram TF-IDF + co-occurrence)
Temporal = 2, // Time-based (decay + velocity)
Causal = 3, // Causal chains (forward/backward with discount)
}
pub const DIMENSION_WEIGHTS: [f32; 4] = [0.35, 0.25, 0.15, 0.25];
}
When fewer than 4 dimensions contribute to a result, weights are adaptively redistributed. Results that fire across multiple dimensions receive a resonance bonus: 1.5x for all 4, 1.3x for 3.
Property Graph Model
String Interner
All strings pass through StringInterner before entering the graph. The interner maps strings to InternedStr(u32) handles via a HashMap<String, InternedStr> and resolves handles back via a Vec<String> indexed by the u32 value.
Once interned, all string comparisons become integer comparisons – zero-allocation, single CPU cycle.
#![allow(unused)]
fn main() {
pub struct StringInterner {
strings: Vec<String>,
index: HashMap<String, InternedStr>,
}
}
CSR Graph
The graph uses Compressed Sparse Row (CSR) format with both forward and reverse adjacency. For node i, outgoing edges span offsets[i]..offsets[i+1] into parallel arrays for targets, weights, inhibitory flags, relations, directions, and causal strengths.
#![allow(unused)]
fn main() {
pub struct CsrGraph {
// Forward CSR
pub offsets: Vec<u64>, // num_nodes + 1
pub targets: Vec<NodeId>, // total_edges
pub weights: Vec<AtomicU32>, // bit-reinterpreted f32 for lock-free CAS
pub inhibitory: Vec<bool>, // edge polarity
pub relations: Vec<InternedStr>, // edge type (interned)
pub directions: Vec<EdgeDirection>, // forward or bidirectional
pub causal_strengths: Vec<FiniteF32>,
// Reverse CSR (built at finalize)
pub rev_offsets: Vec<u64>,
pub rev_sources: Vec<NodeId>,
pub rev_edge_idx: Vec<EdgeIdx>, // maps back to forward arrays
// Pre-finalize staging
pub pending_edges: Vec<PendingEdge>,
}
}
The CSR is immutable after finalization. Edges are added to pending_edges during graph building, then sorted by source and compacted into the CSR arrays by Graph.finalize(). Bidirectional edges are expanded into two forward entries during finalization.
Atomic Weight Updates
Edge weights are stored as AtomicU32 rather than f32. The plasticity engine updates weights concurrently with read queries using Compare-And-Swap:
#![allow(unused)]
fn main() {
pub fn atomic_max_weight(
&self, edge: EdgeIdx, new_val: FiniteF32, max_retries: u32,
) -> M1ndResult<()> {
let slot = &self.weights[edge.as_usize()];
let new_bits = new_val.get().to_bits();
for _ in 0..max_retries {
let old_bits = slot.load(Ordering::Relaxed);
let old_val = f32::from_bits(old_bits);
if old_val >= new_val.get() { return Ok(()); }
if slot.compare_exchange_weak(
old_bits, new_bits, Ordering::Release, Ordering::Relaxed
).is_ok() { return Ok(()); }
}
Err(M1ndError::CasRetryExhausted { edge, limit: max_retries })
}
}
Two CAS operations are provided: atomic_max_weight (only increases, for activation scatter-max) and atomic_write_weight (unconditional, for plasticity). Both retry up to 64 times (constant CAS_RETRY_LIMIT).
Node Storage (SoA)
All per-node data lives in NodeStorage, organized as Struct-of-Arrays with explicit cache-path separation:
#![allow(unused)]
fn main() {
pub struct NodeStorage {
pub count: u32,
// Hot path: activation engine reads every query
pub activation: Vec<[FiniteF32; 4]>, // [structural, semantic, temporal, causal]
pub pagerank: Vec<FiniteF32>,
// Warm path: plasticity reads per query
pub plasticity: Vec<PlasticityNode>,
// Cold path: seed finding, display, export
pub label: Vec<InternedStr>,
pub node_type: Vec<NodeType>,
pub tags: Vec<SmallVec<[InternedStr; 6]>>,
pub last_modified: Vec<f64>,
pub change_frequency: Vec<FiniteF32>,
pub provenance: Vec<NodeProvenance>,
}
}
The SmallVec<[InternedStr; 6]> for tags avoids heap allocation for nodes with 6 or fewer tags (the common case), while still supporting arbitrary tag counts.
Node Provenance
Each node carries source metadata for tracing back to the original code:
#![allow(unused)]
fn main() {
pub struct NodeProvenance {
pub source_path: Option<InternedStr>,
pub line_start: u32,
pub line_end: u32,
pub excerpt: Option<InternedStr>,
pub namespace: Option<InternedStr>,
pub canonical: bool,
}
}
Generation Counter
Generation(u64) tracks graph mutations. The plasticity engine stores the generation at initialization. Every plasticity operation asserts that the current graph generation matches; a mismatch (from concurrent ingestion) causes a controlled rebuild rather than operating on stale state.
Activation Propagation
Engine Selection (HybridEngine)
The HybridEngine auto-selects between two propagation strategies based on graph characteristics:
flowchart TD
START["HybridEngine.propagate()"]
CHECK{"seed_ratio < 0.001<br/>AND avg_degree < 8?"}
HEAP["HeapEngine<br/>(priority queue)"]
WAVE["WavefrontEngine<br/>(BFS depth-parallel)"]
MERGE["Collect sparse results"]
START --> CHECK
CHECK -->|yes| HEAP
CHECK -->|no| WAVE
HEAP --> MERGE
WAVE --> MERGE
- HeapEngine: Max-heap priority queue. Processes strongest signal first. Early-terminates when the heap top drops below threshold. Uses a double-hashing
BloomFilterfor O(1) amortized visited checks. Best for sparse queries with few seeds in low-degree graphs. - WavefrontEngine: BFS depth-parallel. All active nodes at current depth fire simultaneously. Signal accumulated via scatter-max into next depth’s buffer. Best for dense queries or high-degree graphs.
Structural Propagation (D1)
The wavefront engine is the reference implementation. Signal propagates depth by depth:
- Seed nodes initialized with their scores (capped at
saturation_cap). - For each depth (up to
max_depth=5, hard cap 20):- Each frontier node
srcwith activation abovethreshold=0.04fires. - For each outgoing edge:
signal = src_activation * weight * decay(0.55). - Inhibitory edges:
signal = -signal * inhibitory_factor(0.5), subtracted from target (floored at 0). - Excitatory edges: scatter-max into target (keep strongest arrival).
- Each frontier node
- Collect all nodes with non-zero activation, sorted descending.
Semantic Dimension (D2)
Two indexes power semantic matching:
CharNgramIndex: FNV-1a 24-bit trigram hashing over node labels with TF-IDF weighting. An inverted index maps trigram hashes to node lists, enabling O(K) query time (K = number of matching trigrams) instead of O(N) full scan.
CoOccurrenceIndex: DeepWalk-lite random walks (20 walks/node, length 10, window 4) generate co-occurrence counts, normalized to Positive Pointwise Mutual Information (PPMI). Sorted vectors enable O(D) merge-intersection for similarity queries. Disabled above 50K nodes to bound walk cost.
The semantic engine also includes a SynonymExpander with 15 default groups covering code terminology and Portuguese domain terms.
Query pipeline: Phase 1 ngram candidates (3x top_k) -> Phase 2 multi-seed co-occurrence re-rank.
Temporal Dimension (D3)
Three scorers combine:
- TemporalDecayScorer: Per-NodeType half-lives (File=7 days, Function=14 days, Module/Directory=30 days). Formula:
exp(-ln(2) * age_hours / half_life). Dormant nodes (>35 days) get a resurrection bonus with an additive floor. - VelocityScorer: Z-score based change velocity. Nodes changing faster than average receive higher scores.
- CoChangeMatrix: Sparse matrix (budget: 500K entries, 100 per row) bootstrapped from BFS depth 3, refined with git co-change observations.
Causal Dimension (D4)
CausalChainDetector: Budget-limited priority-queue DFS along causal_strength edges. Forward propagation follows contains/imports/calls edges; backward propagation reverses direction. Discount factor 0.7 per hop. Chain depth capped at 6 by default.
Dimension Merging
merge_dimensions() combines all four dimension results:
- For each activated node, compute weighted sum:
score = sum(dim_score * DIMENSION_WEIGHTS[dim]). - If a dimension produced no results, redistribute its weight proportionally to active dimensions.
- Apply resonance bonus: 1.5x if all 4 dimensions contributed, 1.3x if 3 contributed.
- Sort by final score, truncate to top_k.
Seed Finding
SeedFinder resolves query strings to graph nodes via a 5-level matching cascade:
- Exact label match (highest priority)
- Prefix match (e.g., “auth_” matches “auth_handler”)
- Substring match (e.g., “handler” matches “auth_handler”)
- Tag match (e.g., “#api” tag)
- Fuzzy trigram (cosine similarity of trigram vectors, lowest priority)
A semantic re-ranking phase (0.6 basic score / 0.4 semantic blend) refines results when multiple candidates match.
Weight Systems
Hebbian Plasticity
The PlasticityEngine implements biological Hebbian learning with 5 phases executed after every activation query:
flowchart LR
A["1. Hebbian Strengthen<br/>delta_w = lr * act_src * act_tgt"]
B["2. Synaptic Decay<br/>w *= (1 - decay_rate) for inactive"]
C["3. LTP/LTD<br/>permanent bonus/penalty<br/>after N consecutive"]
D["4. Homeostatic Normalize<br/>scale if total > ceiling"]
E["5. Record Query<br/>ring buffer + bigrams"]
A --> B --> C --> D --> E
Constants (from plasticity.rs):
| Parameter | Value | Purpose |
|---|---|---|
DEFAULT_LEARNING_RATE | 0.08 | Hebbian weight change rate |
DEFAULT_DECAY_RATE | 0.005 | Inactive synapse decay per query |
LTP_THRESHOLD | 5 | Consecutive strengthens before permanent bonus |
LTD_THRESHOLD | 5 | Consecutive weakens before permanent penalty |
LTP_BONUS | 0.15 | Permanent weight increase |
LTD_PENALTY | 0.15 | Permanent weight decrease |
HOMEOSTATIC_CEILING | 5.0 | Max sum of incoming weights per node |
WEIGHT_FLOOR | 0.05 | Minimum edge weight (prevents extinction) |
WEIGHT_CAP | 3.0 | Maximum edge weight |
Hebbian update: For each edge where both source and target were activated: delta_w = learning_rate * activation_source * activation_target. Applied via atomic CAS (atomic_write_weight).
Homeostatic normalization: If the sum of incoming weights for any node exceeds HOMEOSTATIC_CEILING, all incoming weights are scaled proportionally to bring the total back under the ceiling. This prevents runaway positive feedback.
Query Memory: A ring buffer of 1000 entries tracks recent queries. Each record stores the query text, seed nodes, activated nodes, and timestamp. The memory tracks:
- Node frequency: how often each node appears across recent queries.
- Seed bigrams: co-occurring seed pairs, used for priming signals.
When the buffer wraps, evicted records have their frequency and bigram counts decremented – maintaining accurate sliding-window statistics.
Persistence: Plasticity state (per-edge SynapticState) is exported as JSON using triple-based identity matching (source_label, target_label, relation). This allows plasticity to survive graph rebuilds as long as the semantic structure remains similar.
XLR Differential Processing
XLR (eXcitatory-Lateral-inhibitory Response) is a spectral noise cancellation system that separates signal from noise in activation results.
Constants:
| Parameter | Value | Purpose |
|---|---|---|
F_HOT | 1.0 | Hot signal frequency |
F_COLD | 3.7 | Cold signal frequency |
SPECTRAL_BANDWIDTH | 0.8 | Gaussian kernel bandwidth |
IMMUNITY_HOPS | 2 | BFS immunity radius |
SIGMOID_STEEPNESS | 6.0 | Gating function steepness |
6-step pipeline:
- Anti-seed selection: Pick nodes dissimilar to seeds (Jaccard similarity < 0.2, degree ratio filter).
- Immunity computation: BFS 2 hops from seeds. Immune nodes cannot be suppressed.
- Hot propagation: Spread
SpectralPulsewith frequencyF_HOT=1.0from seed nodes. Pulses carry amplitude, phase, frequency, and a bounded recent path ([NodeId; 3], not unbounded Vec – FM-RES-007). - Cold propagation: Spread from anti-seeds with frequency
F_COLD=3.7. - Spectral overlap + density modulation: Compute overlap between hot and cold spectra using Gaussian kernel (
bw=0.8). Dense neighborhoods get modulated (clamped to[0.3, 2.0]). - Sigmoid gating: Apply
1 / (1 + exp(-steepness * (hot - cold)))to produce final scores.
Over-cancellation fallback (FM-XLR-010): If all results score zero after gating (cold dominates everywhere), fall back to hot-only results. This prevents total signal erasure in adversarial topologies.
Standing Wave Resonance
The resonance engine discovers structural harmonics by propagating wave pulses through the graph:
#![allow(unused)]
fn main() {
pub struct WavePulse {
pub node: NodeId,
pub amplitude: FiniteF32, // can be negative for destructive interference
pub phase: FiniteF32, // [0, 2*pi)
pub frequency: PosF32, // MUST be positive (FM-RES-002)
pub wavelength: PosF32, // MUST be positive (FM-RES-001)
pub hops: u8,
pub prev_node: NodeId,
}
}
Wave interference: Pulses accumulate at each node as complex numbers (real + imaginary). Constructive interference occurs when pulses arrive in phase; destructive when out of phase. The WaveAccumulator tracks sum(amplitude * cos(phase)) and sum(amplitude * sin(phase)), with resultant amplitude sqrt(real^2 + imag^2).
Reflection rules:
- Dead-end reflection: At leaf nodes (degree 1), pulse reflects with a pi phase shift (
REFLECTION_PHASE_SHIFT). - Hub partial reflection: At hub nodes (degree > 4x average), 30% of amplitude reflects (
HUB_REFLECTION_COEFF).
Budget: DEFAULT_PULSE_BUDGET = 50,000 prevents combinatorial explosion in dense subgraphs. Pulses are processed in BFS order via a VecDeque.
Harmonic analysis: HarmonicAnalyzer sweeps across frequencies, groups nodes by harmonic response, and identifies resonant frequencies (local maxima in the sweep). SympatheticResonanceDetector finds nodes that resonate across regions (outside 2-hop seed neighborhood) – indicating structural coupling without direct edges.
PageRank
Computed once at Graph.finalize() via power iteration:
- Damping factor: 0.85
- Max iterations: 50
- Convergence threshold: 1e-6
PageRank scores are stored in NodeStorage.pagerank and used as a static importance signal in seed finding and result ranking.
Persistence
Graph Snapshot
snapshot.rs serializes the graph to JSON format (version 3):
#![allow(unused)]
fn main() {
struct GraphSnapshot {
version: u32, // SNAPSHOT_VERSION = 3
nodes: Vec<NodeSnapshot>,
edges: Vec<EdgeSnapshot>,
}
}
Each NodeSnapshot includes external_id, label, node_type, tags, timestamps, and optional provenance. Each EdgeSnapshot includes source/target IDs, relation, weight, direction, inhibitory flag, and causal strength.
Atomic write (FM-PL-008): Write to a temporary file, then rename() over the target. The rename is atomic on POSIX filesystems, so a crash mid-write leaves the previous snapshot intact.
NaN firewall: All values are checked for finiteness at the export boundary. Non-finite values are rejected before writing.
Plasticity State
Per-edge SynapticState (original weight, current weight, strengthen/weaken counts, LTP/LTD flags) is serialized to a separate JSON file. Import uses triple-based matching (source_label, target_label, relation) to reconnect state to edges after graph rebuilds.
Auto-Persist
The MCP server triggers persistence every auto_persist_interval queries (default: 50). Ordering: graph first (source of truth), then plasticity. If graph save fails, plasticity save is skipped to prevent inconsistent state.
Ingestion Pipeline (m1nd-ingest)
m1nd-ingest transforms codebases and structured documents into the property graph consumed by m1nd-core. It handles file discovery, language-specific code extraction, cross-file reference resolution, incremental diff computation, and memory/markdown ingestion.
Source: mcp/m1nd/m1nd-ingest/src/
Module Map
| Module | Purpose |
|---|---|
lib.rs | Ingestor pipeline, IngestAdapter trait, config, stats |
walker.rs | DirectoryWalker, binary detection, git enrichment |
extract/mod.rs | Extractor trait, comment stripping, CommentSyntax |
extract/python.rs | Python: classes, functions, decorators, imports |
extract/typescript.rs | TypeScript/JS: classes, functions, interfaces, imports |
extract/rust_lang.rs | Rust: structs, enums, impls, traits, functions, mods |
extract/go.rs | Go: structs, interfaces, functions, packages |
extract/java.rs | Java: classes, interfaces, methods, packages |
extract/generic.rs | Fallback: file-level node with tag extraction |
resolve.rs | ReferenceResolver, proximity disambiguation |
cross_file.rs | Cross-file edge generation (directory contains, sibling) |
diff.rs | GraphDiff for incremental updates |
json_adapter.rs | Generic JSON-to-graph adapter |
memory_adapter.rs | Markdown/memory document adapter |
merge.rs | Graph merge utilities |
Pipeline Overview
flowchart TD
subgraph "Phase 1: Walk"
DIR["Directory Root"]
WALK["DirectoryWalker<br/>walkdir + skip rules"]
BIN["Binary Detection<br/>NUL in first 8KB"]
GIT["Git Enrichment<br/>commit counts + timestamps"]
FILES["DiscoveredFile[]"]
end
subgraph "Phase 2: Extract (parallel)"
RAYON["rayon::par_iter"]
PY["PythonExtractor"]
TS["TypeScriptExtractor"]
RS["RustExtractor"]
GO["GoExtractor"]
JAVA["JavaExtractor"]
GEN["GenericExtractor"]
RESULTS["ExtractionResult[]"]
end
subgraph "Phase 3: Build Graph"
NODES["Add all nodes<br/>with provenance + timestamps"]
EDGES["Add resolved edges<br/>with causal strengths"]
CAUSAL["Causal strength assignment<br/>contains=0.8, imports=0.6, etc."]
end
subgraph "Phase 4: Resolve"
REFS["ReferenceResolver<br/>ref:: → actual nodes"]
PROX["Proximity Disambiguation<br/>same file > same dir > same project"]
HINTS["Import Hints<br/>module path → target preference"]
XFILE["Cross-File Edges<br/>directory contains, siblings"]
end
subgraph "Phase 5: Finalize"
CSR["Build CSR<br/>sort edges, compact arrays"]
BIDIR["Expand Bidirectional<br/>contains, implements"]
REV["Build Reverse CSR"]
PR["PageRank<br/>power iteration"]
end
DIR --> WALK --> BIN --> GIT --> FILES
FILES --> RAYON
RAYON --> PY & TS & RS & GO & JAVA & GEN
PY & TS & RS & GO & JAVA & GEN --> RESULTS
RESULTS --> NODES --> EDGES --> CAUSAL
CAUSAL --> REFS --> PROX
PROX --> HINTS --> XFILE
XFILE --> CSR --> BIDIR --> REV --> PR
Phase 1: Directory Walking
DirectoryWalker uses the walkdir crate to traverse the filesystem. It applies skip rules, detects binary files, and enriches results with git metadata.
Skip Rules
Default skip directories (configured via IngestConfig):
#![allow(unused)]
fn main() {
skip_dirs: vec![
".git", "node_modules", "__pycache__", ".venv",
"target", "dist", "build", ".next", "vendor",
],
skip_files: vec![
"package-lock.json", "yarn.lock", "Cargo.lock", "poetry.lock",
],
}
Hidden directories (starting with .) are skipped unless they are the root. Symlinks are not followed.
Binary Detection (FM-ING-004)
After discovering a file, the walker reads the first 8KB and checks for NUL bytes (0x00). Any file containing NUL is classified as binary and skipped. This prevents feeding compiled binaries, images, or other non-text files into the language extractors.
Git Enrichment
If the root is inside a git repository, the walker runs git log --format=%at --name-only to extract:
- Commit count per file: Used to compute
change_frequency(1 commit = 0.1, 50+ = 1.0, capped). - Most recent commit timestamp: Used for temporal decay scoring.
- Commit groups: Sets of files that changed together in the same commit. Fed into the
CoChangeMatrixafter graph finalization.
The result is a WalkResult containing Vec<DiscoveredFile> and Vec<Vec<String>> commit groups.
#![allow(unused)]
fn main() {
pub struct DiscoveredFile {
pub path: PathBuf,
pub relative_path: String,
pub extension: Option<String>,
pub size_bytes: u64,
pub last_modified: f64,
pub commit_count: u32,
pub last_commit_time: f64,
}
}
Phase 2: Parallel Extraction
Files are distributed across rayon’s thread pool for concurrent extraction. Each file is assigned a language-specific extractor based on its extension:
| Extension | Extractor | Extracted Entities |
|---|---|---|
.py, .pyi | PythonExtractor | Classes, functions, decorators, imports, global assignments |
.ts, .tsx, .js, .jsx, .mjs, .cjs | TypeScriptExtractor | Classes, functions, interfaces, type aliases, imports, exports |
.rs | RustExtractor | Structs, enums, traits, impls, functions, modules, macros |
.go | GoExtractor | Structs, interfaces, functions, methods, packages |
.java | JavaExtractor | Classes, interfaces, methods, fields, packages |
| everything else | GenericExtractor | File-level node with tag extraction from content |
Extractor Interface
All extractors implement the Extractor trait:
#![allow(unused)]
fn main() {
pub trait Extractor: Send + Sync {
fn extract(&self, content: &[u8], file_id: &str) -> M1ndResult<ExtractionResult>;
fn extensions(&self) -> &[&str];
}
}
An ExtractionResult contains:
#![allow(unused)]
fn main() {
pub struct ExtractionResult {
pub nodes: Vec<ExtractedNode>,
pub edges: Vec<ExtractedEdge>,
pub unresolved_refs: Vec<String>,
}
}
Each extracted node carries:
#![allow(unused)]
fn main() {
pub struct ExtractedNode {
pub id: String, // e.g. "file::backend/handler.py::ChatHandler"
pub label: String, // e.g. "ChatHandler"
pub node_type: NodeType,
pub tags: Vec<String>,
pub line: u32,
pub end_line: u32,
}
}
Comment and String Stripping
Before extraction, each file’s content passes through strip_comments_and_strings() which removes comments and string literals to prevent false-positive matches from regex extractors. The function preserves import line string content (so from "react" still resolves) but strips string bodies elsewhere.
Comment syntax is per-language:
#![allow(unused)]
fn main() {
pub struct CommentSyntax {
pub line: &'static str, // e.g. "//" or "#"
pub block_open: &'static str, // e.g. "/*"
pub block_close: &'static str, // e.g. "*/"
}
}
Supported: Rust (//, /* */), Python (#, """ """), C-style (//, /* */), Go (//, /* */), Generic (#, none).
Node ID Format
Extracted nodes use a hierarchical ID scheme: file::{relative_path}::{entity_name}. For example:
file::backend/handler.py(file node)file::backend/handler.py::ChatHandler(class node)file::backend/handler.py::ChatHandler::handle_message(method node)
Unresolved references use the prefix ref::: ref::Config, ref::react. These are resolved to actual nodes in Phase 4.
Phase 3: Graph Building
After parallel extraction completes, results are collected and processed sequentially (graph mutation is single-threaded).
Node Creation
For each ExtractedNode:
- Look up the file timestamp from git enrichment (or filesystem mtime).
- Compute
change_frequencyfrom git commit count:(commits / 50).clamp(0.1, 1.0). Default 0.3 for non-git repos. - Call
graph.add_node()with the external ID, label, node type, tags, timestamp, and change frequency. - Set provenance:
source_path,line_start,line_end,namespace="code". - On
DuplicateNodeerror, increment collision counter and continue.
Edge Creation
For each ExtractedEdge (skipping ref:: targets, which are deferred):
- Resolve source and target IDs to
NodeId. - Assign causal strength by relation type:
| Relation | Causal Strength | Direction |
|---|---|---|
contains | 0.8 | Bidirectional |
implements | 0.7 | Bidirectional |
imports | 0.6 | Forward |
calls | 0.5 | Forward |
references | 0.3 | Forward |
| other | 0.4 | Forward |
contains and implements edges are bidirectional so that both parent-to-child and child-to-parent navigation work.
Safety Guards (FM-ING-002)
Two budget checks run between sequential file processing:
- Timeout: If
start.elapsed() > config.timeout(default 300s), stop processing. - Node budget: If
nodes >= config.max_nodes(default 500K), stop processing.
Both log a warning and break from the build loop, producing a partial but consistent graph from whatever was processed.
Phase 4: Reference Resolution
ReferenceResolver
Unresolved references (ref::Config, ref::FastAPI, etc.) are resolved to actual graph nodes using the ReferenceResolver.
Multi-value label index (FM-ING-008): The resolver builds a HashMap from labels to lists of matching NodeIds. When multiple nodes share a label (e.g., multiple files define a Config class), proximity disambiguation selects the best match:
| Proximity | Score | Condition |
|---|---|---|
| Same file | 100 | Source and target share the same file:: prefix |
| Same directory | 50 | Source and target share the same directory |
| Same project | 10 | Default (both exist in the graph) |
Import hint disambiguation: When the extractor sees from foo.bar import Baz, it records an import hint mapping (source_file, "ref::Baz") to the module path foo.bar. The resolver uses this hint to prefer the Baz node under foo/bar/ over a same-named node elsewhere.
Resolution outcome per reference:
- Resolved: Exactly one match (or best proximity match). Edge created with resolved
NodeId. - Ambiguous: Multiple matches with equal proximity. Best guess selected, counted in stats.
- Unresolved: No match found. Counted in stats, no edge created.
Cross-File Edges
After reference resolution, cross_file.rs generates structural edges that span files:
- Directory contains:
dir::backend/ --contains--> file::backend/main.py - Sibling edges: Files in the same directory get weak bidirectional edges.
Phase 5: Finalization
Graph.finalize() transforms the mutable graph into its read-optimized CSR form:
- Sort edges by source: All pending edges are sorted by
source.0(node index). - Build forward CSR: Compute offsets array, pack targets/weights/relations/etc into parallel arrays.
- Expand bidirectional edges: For each bidirectional edge
(A, B), ensure bothA->BandB->Aexist in the CSR. - Build reverse CSR: Sort edges by target, build
rev_offsets,rev_sources,rev_edge_idx(mapping back to forward array indices). - Rebuild plasticity arrays: Allocate
PlasticityNodefor each node with default ceiling. - Compute PageRank: Power iteration with damping 0.85, max 50 iterations, convergence 1e-6.
After finalization, the graph is immutable (except for atomic weight updates by plasticity).
Incremental Ingestion (GraphDiff)
diff.rs enables incremental updates without full re-ingestion.
#![allow(unused)]
fn main() {
pub enum DiffAction {
AddNode(ExtractedNode),
RemoveNode(String),
ModifyNode { external_id, new_label, new_tags, new_last_modified },
AddEdge(ExtractedEdge),
RemoveEdge { source_id, target_id, relation },
ModifyEdgeWeight { source_id, target_id, relation, new_weight },
}
}
GraphDiff::compute() compares old and new extraction results by indexing both into HashMaps by external ID, then classifying each node/edge as added, removed, or modified.
GraphDiff::apply() executes the diff against a live graph. Note: CSR does not support true node/edge removal. “Removed” nodes are tombstoned (zero weight, empty label) rather than physically deleted. A full re-ingest is needed to reclaim space.
When to use incremental vs full:
| Scenario | Strategy |
|---|---|
| Single file changed | Incremental diff (fast, ~10ms) |
| Many files changed (>20%) | Full re-ingest (cleaner CSR, correct PageRank) |
| New codebase | Full ingest |
| Plasticity state important | Full ingest + plasticity reimport (triple matching) |
Memory Adapter
MemoryIngestAdapter converts markdown documents into graph nodes. It implements the IngestAdapter trait with domain "memory".
Supported Formats
Files with extensions .md, .markdown, or .txt are accepted. The adapter walks a directory of memory files and parses each one into:
- Section nodes (
NodeType::Concept): Created from markdown headings (#,##, etc.). - Entry nodes (
NodeType::Process): Created from list items under sections. - File reference nodes (
NodeType::Reference): Created from file paths mentioned in content.
Entry Classification
List items are classified by content patterns:
| Classification | Pattern | Example |
|---|---|---|
| Task | Contains “TODO”, “FIXME”, “pending”, “implement” | “- TODO: add tests” |
| Decision | Contains “decision:”, “decided:”, “chose” | “- Decision: use CSR format” |
| State | Contains “status:”, “state:”, “current:” | “- Status: in progress” |
| Event | Contains date pattern (YYYY-MM-DD) | “- 2026-03-12: deployed” |
| Note | Default | “- Config lives in settings.py” |
Edges
The adapter creates:
containsedges from section to child entries.referencesedges from entries to file reference nodes.followsedges between sequential entries in the same section.
This allows the activation engine to traverse from a concept (“plasticity”) through memory entries to referenced code files, bridging the semantic gap between human notes and source code.
IngestAdapter Trait
The IngestAdapter trait enables domain-specific ingestion beyond code:
#![allow(unused)]
fn main() {
pub trait IngestAdapter: Send + Sync {
fn domain(&self) -> &str;
fn ingest(&self, root: &Path) -> M1ndResult<(Graph, IngestStats)>;
}
}
Implemented adapters:
| Adapter | Domain | Input |
|---|---|---|
Ingestor | "code" | Source code directories |
MemoryIngestAdapter | "memory" | Markdown/text documents |
JsonIngestAdapter | "generic" | Arbitrary JSON with nodes[] and edges[] |
The JSON adapter is the escape hatch for importing graphs from external tools. It expects a JSON document with nodes (array of {id, label, type, tags}) and edges (array of {source, target, relation, weight}).
Configuration Reference
#![allow(unused)]
fn main() {
pub struct IngestConfig {
pub root: PathBuf,
pub timeout: Duration, // default: 300s
pub max_nodes: u64, // default: 500_000
pub skip_dirs: Vec<String>, // default: [".git", "node_modules", ...]
pub skip_files: Vec<String>, // default: ["package-lock.json", ...]
pub parallelism: usize, // default: 8 (rayon threads)
}
}
Statistics
Every ingest run produces IngestStats:
#![allow(unused)]
fn main() {
pub struct IngestStats {
pub files_scanned: u64,
pub files_parsed: u64,
pub files_skipped_binary: u64,
pub files_skipped_encoding: u64,
pub nodes_created: u64,
pub edges_created: u64,
pub references_resolved: u64,
pub references_unresolved: u64,
pub label_collisions: u64,
pub elapsed_ms: f64,
pub commit_groups: Vec<Vec<String>>,
}
}
commit_groups is passed to the CoChangeMatrix in m1nd-core after graph finalization, seeding the temporal co-change model with real git history.
MCP Server (m1nd-mcp)
m1nd-mcp is the transport and session layer. It exposes m1nd-core and m1nd-ingest as 43 MCP tools over JSON-RPC stdio, manages the shared graph lifecycle, handles multi-agent sessions, and provides perspective branching and lock-based change tracking.
Source: mcp/m1nd/m1nd-mcp/src/
Module Map
| Module | Purpose |
|---|---|
main.rs | Binary entry point, config loading, tokio runtime, SIGINT handling |
server.rs | McpServer, JSON-RPC transport (framed + line), tool schema registry |
session.rs | SessionState, engine lifecycle, auto-persist, perspective/lock management |
tools.rs | Tool handler implementations for all 43 MCP tools |
engine_ops.rs | Read-only engine wrappers for perspective synthesis |
protocol.rs | JSON-RPC request/response types |
perspective/ | Perspective branching, lock state, watcher events |
layer_handlers.rs | Layer-based tool dispatch |
Transport Layer
Dual Transport Mode
m1nd-mcp accepts two JSON-RPC transport formats on stdin, auto-detected per message:
Framed mode (HTTP-style headers):
Content-Length: 142\r\n
\r\n
{"jsonrpc":"2.0","method":"tools/call","params":{"name":"m1nd.activate","arguments":{"query":"chat","agent_id":"orchestrator"}},"id":1}
Line mode (raw JSON):
{"jsonrpc":"2.0","method":"tools/call","params":{"name":"m1nd.activate","arguments":{"query":"chat","agent_id":"orchestrator"}},"id":1}
Detection is based on the first non-whitespace byte: if it is { or [, the message is treated as line-mode JSON. Otherwise, it is parsed as framed with Content-Length headers. Responses are written in the same mode as the incoming request.
#![allow(unused)]
fn main() {
fn read_request_payload<R: BufRead>(
reader: &mut R,
) -> std::io::Result<Option<(String, TransportMode)>> {
loop {
let buffer = reader.fill_buf()?;
if buffer.is_empty() { return Ok(None); }
let first_non_ws = buffer.iter().copied()
.find(|byte| !byte.is_ascii_whitespace());
let starts_framed = matches!(first_non_ws, Some(byte) if byte != b'{' && byte != b'[');
// ...
}
}
}
This dual-mode support allows m1nd to work with both Claude Code (which uses framed headers) and other MCP clients that send raw line JSON.
Request/Response Format
Requests follow the JSON-RPC 2.0 specification with MCP conventions:
{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "m1nd.activate",
"arguments": {
"query": "chat handler",
"agent_id": "orchestrator",
"top_k": 20,
"dimensions": ["structural", "semantic", "temporal", "causal"],
"xlr": true
}
},
"id": 1
}
Responses:
{
"jsonrpc": "2.0",
"result": {
"content": [{
"type": "text",
"text": "..."
}]
},
"id": 1
}
The tools/list method returns all 43 tool schemas with full inputSchema per MCP spec, enabling auto-discovery by any MCP client.
Server Lifecycle
sequenceDiagram
participant Main as main.rs
participant Server as McpServer
participant Session as SessionState
participant Tokio as tokio runtime
Main->>Main: load_config()
Main->>Server: McpServer::new(config)
Server->>Session: SessionState::initialize(graph, config, domain)
Session->>Session: Build all engines (orchestrator, temporal, counterfactual, topology, resonance, plasticity)
Main->>Server: server.start()
Main->>Tokio: spawn_blocking(server.serve())
loop Until EOF or SIGINT
Tokio->>Server: read JSON-RPC request
Server->>Server: dispatch to tool handler
Server->>Session: execute tool logic
Session->>Session: auto-persist check
Server->>Tokio: write JSON-RPC response
end
Tokio->>Server: server.shutdown()
Server->>Session: final persist
Configuration
Configuration is resolved in priority order: CLI argument (JSON file path) > environment variables > defaults.
#![allow(unused)]
fn main() {
pub struct McpConfig {
pub graph_source: PathBuf, // default: "./graph_snapshot.json"
pub plasticity_state: PathBuf, // default: "./plasticity_state.json"
pub auto_persist_interval: u32, // default: 50 (queries between persists)
pub learning_rate: f32, // default: 0.08
pub decay_rate: f32, // default: 0.005
pub xlr_enabled: bool, // default: true
pub max_concurrent_reads: usize, // default: 32
pub write_queue_size: usize, // default: 64
pub domain: Option<String>, // default: None ("code")
}
}
Environment variables: M1ND_GRAPH_SOURCE, M1ND_PLASTICITY_STATE, M1ND_XLR_ENABLED.
Startup Sequence
load_config(): Resolve config from CLI args, env vars, or defaults.McpServer::new(config): Load graph snapshot from disk (or create empty graph). Load plasticity state. InitializeSessionStatewith all engines.server.start(): Prepare the server for serving (no-op currently, reserved for future setup).tokio::task::spawn_blocking(server.serve()): The serve loop does synchronous stdio I/O in a blocking task.tokio::select!waits for either SIGINT (ctrl_c()) or serve loop completion.
Shutdown
On SIGINT or stdin EOF:
server.shutdown(): Final persist of graph and plasticity state.- Process exits.
The atomic write pattern (temp file + rename) ensures that even if shutdown is interrupted, the previous snapshot remains intact.
Tool Registration and Dispatch
Schema Registry
tool_schemas() returns a JSON array of all 43 tool definitions with full inputSchema objects. Each tool specifies:
name: Dot-namespaced (e.g.,m1nd.activate)description: Human-readable purposeinputSchema: JSON Schema withproperties,required,type, defaults
Example schema entry:
{
"name": "m1nd.activate",
"description": "Spreading activation query across the connectome",
"inputSchema": {
"type": "object",
"properties": {
"query": { "type": "string" },
"agent_id": { "type": "string" },
"top_k": { "type": "integer", "default": 20 },
"dimensions": {
"type": "array",
"items": { "type": "string", "enum": ["structural", "semantic", "temporal", "causal"] },
"default": ["structural", "semantic", "temporal", "causal"]
},
"xlr": { "type": "boolean", "default": true }
},
"required": ["query", "agent_id"]
}
}
Tool Categories
The 43 tools are organized into functional groups:
Core Query Tools (13):
| Tool | Purpose | Key Parameters |
|---|---|---|
m1nd.activate | Spreading activation query | query, top_k, dimensions, xlr |
m1nd.impact | Blast radius analysis | node_id, direction (forward/reverse/both) |
m1nd.missing | Structural hole detection | query, min_sibling_activation |
m1nd.why | Path explanation between nodes | source, target, max_hops |
m1nd.warmup | Task-based priming | task_description, boost_strength |
m1nd.counterfactual | Node removal simulation | node_ids, include_cascade |
m1nd.predict | Co-change prediction | changed_node, top_k, include_velocity |
m1nd.fingerprint | Equivalence detection | target_node |
m1nd.drift | Weight changes since baseline | since |
m1nd.learn | Hebbian feedback | feedback (correct/wrong) |
m1nd.resonate | Standing wave analysis | query, frequencies, num_harmonics |
m1nd.seek | Seed-level node lookup | query |
m1nd.scan | Full graph summary | (none) |
Graph Mutation Tools:
| Tool | Purpose |
|---|---|
m1nd.ingest | Ingest codebase into graph |
m1nd.health | Server diagnostics |
m1nd.timeline | Temporal event timeline |
Perspective Tools (12):
| Tool | Purpose |
|---|---|
m1nd.perspective_start | Open a named perspective branch |
m1nd.perspective_close | Close a perspective |
m1nd.perspective_list | List open perspectives for an agent |
m1nd.perspective_inspect | View perspective state and cached results |
m1nd.perspective_compare | Diff two perspectives |
m1nd.perspective_branch | Fork a perspective |
m1nd.perspective_suggest | Generate suggestions from perspective context |
m1nd.perspective_back | Undo last perspective operation |
m1nd.perspective_peek | Read source file content from within perspective |
m1nd.perspective_follow | Follow links from perspective results |
m1nd.perspective_routes | View cached activation routes |
m1nd.perspective_affinity | Cross-perspective affinity analysis |
Lock Tools (5):
| Tool | Purpose |
|---|---|
m1nd.lock_create | Create a baseline snapshot for change tracking |
m1nd.lock_diff | Diff current state against lock baseline |
m1nd.lock_rebase | Update lock baseline to current state |
m1nd.lock_release | Release a lock |
m1nd.lock_watch | Watch for changes against lock baseline |
Trail Tools (4):
| Tool | Purpose |
|---|---|
m1nd.trail_save | Save current exploration trail |
m1nd.trail_list | List saved trails |
m1nd.trail_resume | Resume a saved trail |
m1nd.trail_merge | Merge trails |
Topology Tools:
| Tool | Purpose |
|---|---|
m1nd.federate | Cross-graph federation |
m1nd.diverge | Divergence analysis between graph regions |
m1nd.differential | Differential activation (compare two queries) |
m1nd.hypothesize | Generate hypotheses from graph structure |
m1nd.validate_plan | Validate an implementation plan against graph |
Dispatch
All tools require an agent_id parameter. The serve loop matches the tool name from the JSON-RPC request and dispatches to the corresponding handler in tools.rs. The handler extracts parameters from the arguments JSON object, acquires the appropriate lock on SessionState, executes the operation, and returns the result as a JSON-RPC response.
Session Management
SessionState
SessionState is the central state object. It owns the graph, all engines, and all session metadata:
#![allow(unused)]
fn main() {
pub struct SessionState {
pub graph: SharedGraph, // Arc<RwLock<Graph>>
pub domain: DomainConfig,
pub orchestrator: QueryOrchestrator, // activation + XLR + semantic
pub temporal: TemporalEngine,
pub counterfactual: CounterfactualEngine,
pub topology: TopologyAnalyzer,
pub resonance: ResonanceEngine,
pub plasticity: PlasticityEngine,
pub queries_processed: u64,
pub auto_persist_interval: u32, // default: 50
pub start_time: Instant,
pub last_persist_time: Option<Instant>,
pub graph_path: PathBuf,
pub plasticity_path: PathBuf,
pub sessions: HashMap<String, AgentSession>,
// Perspective/lock state
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>,
}
}
SharedGraph
SharedGraph = Arc<parking_lot::RwLock<Graph>> provides concurrent access:
- Reads (activation, impact, predict, etc.): Acquire read lock. Multiple concurrent reads allowed.
- Writes (ingest, learn): Acquire write lock. Exclusive access. Blocks all readers.
parking_lot::RwLock is used instead of std::sync::RwLock for two reasons:
- Writer starvation prevention: parking_lot uses a fair queue, so plasticity writes do not starve behind continuous read queries.
- Performance: parking_lot’s implementation is faster for the read-heavy, write-rare access pattern of m1nd.
Engine Rebuild
After ingestion replaces the graph, all engines must be rebuilt because they hold indexes derived from the old graph:
#![allow(unused)]
fn main() {
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, 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(())
}
}
The rebuild also invalidates all perspective and lock state, bumping generation counters so that stale caches are detected.
Auto-Persist
Every auto_persist_interval queries (default 50), the session persists state to disk:
- Graph first:
save_graph()writes the CSR graph to JSON via atomic temp-file-then-rename. - Plasticity second:
export_state()extracts per-edgeSynapticState, thensave_plasticity_state()writes to JSON. - If graph save fails, plasticity save is skipped (prevents inconsistent state).
- If plasticity save fails after graph succeeds, a warning is logged but the server continues.
#![allow(unused)]
fn main() {
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) = save_plasticity_state(&states, &self.plasticity_path) {
eprintln!("[m1nd] WARNING: plasticity persist failed: {}", e);
}
}
Err(e) => eprintln!("[m1nd] WARNING: plasticity export failed: {}", e),
}
self.last_persist_time = Some(Instant::now());
Ok(())
}
}
Multi-Agent Support
Agent Sessions
Each unique agent_id gets an AgentSession:
#![allow(unused)]
fn main() {
pub struct AgentSession {
pub agent_id: String,
pub first_seen: Instant,
pub last_seen: Instant,
pub query_count: u64,
}
}
Sessions are created on first query and updated on each subsequent query. The last_seen timestamp enables timeout-based cleanup.
Agent Isolation
All agents share one graph (writes are immediately visible), but isolation is provided through:
- Perspectives: Per-agent branching views with independent route caches. A perspective opened by Agent A is invisible to Agent B.
- Locks: Per-agent change tracking baselines. Each agent can create independent locks to track changes from their perspective.
- Query Memory: The plasticity engine’s ring buffer is global (shared learning), but perspective-level route caches are per-agent.
Generation Counters
Three generation counters detect stale state:
| Counter | Bumped By | Purpose |
|---|---|---|
graph_generation | Ingest, rebuild_engines | Detects stale engine indexes |
plasticity_generation | Learn | Detects stale plasticity state |
cache_generation | max(graph_gen, plasticity_gen) | Unified staleness for perspective caches |
When a perspective’s cached results were computed at a different generation than the current cache_generation, the perspective is marked stale and results are recomputed on next access.
Perspective System
Perspectives are per-agent named branches that cache activation results and enable comparative analysis.
Lifecycle
stateDiagram-v2
[*] --> Open: perspective_start
Open --> Active: first query (activate/impact/etc)
Active --> Active: additional queries
Active --> Branched: perspective_branch
Active --> Stale: graph mutation (ingest/learn)
Stale --> Active: re-query (auto-refresh)
Active --> [*]: perspective_close
Open --> [*]: perspective_close
Perspective IDs
Generated as persp_{agent_prefix}_{counter:03}. Each agent has an independent monotonic counter. Example: Agent “jimi” creates perspectives persp_jimi_001, persp_jimi_002, etc.
Resource Limits
PerspectiveLimits caps resource usage:
- Maximum open perspectives per agent
- Maximum total perspectives across all agents
- Maximum locks per agent
Peek Security
perspective_peek allows reading source file content from within a perspective context. Security restrictions:
- Files must be within an
ingest_rootsallow-list (populated during ingest). - Path traversal (
..) is blocked. - Symlinks outside the allow-list are rejected.
Lock System
Locks capture a baseline snapshot of graph state for change tracking.
Lock Lifecycle
stateDiagram-v2
[*] --> Created: lock_create
Created --> Active: baseline captured
Active --> Diffed: lock_diff (non-destructive)
Diffed --> Active: continue tracking
Active --> Rebased: lock_rebase (new baseline)
Rebased --> Active: continue tracking
Active --> Stale: graph mutation
Stale --> Rebased: lock_rebase
Active --> [*]: lock_release
Operations
- lock_create: Captures current graph state (generation counter + weight snapshot for tracked edges).
- lock_diff: Compares current state against baseline. Reports weight changes, new/removed edges. Non-destructive.
- lock_rebase: Updates baseline to current state. Clears staleness flag.
- lock_release: Frees the lock.
- lock_watch: Returns pending watcher events (changes that occurred since last check).
When a graph mutation occurs (ingest, learn), all locks are marked baseline_stale = true. lock_diff reports this staleness and suggests lock_rebase.
Engine Operations (Perspective Synthesis)
engine_ops.rs provides read-only wrappers around engine operations for use within perspective synthesis. These wrappers operate under a SynthesisBudget:
- Max calls: 8 engine calls per synthesis operation.
- Wall-clock timeout: 500ms total.
This prevents perspective synthesis from monopolizing the server. Each wrapper (activate_readonly, impact_readonly, etc.) checks budget before executing and returns a budget-exhausted error if limits are exceeded.
Concurrency Model
flowchart TD
STDIN["stdin (JSON-RPC)"]
TOKIO["tokio::task::spawn_blocking"]
SERVE["McpServer.serve()<br/>(synchronous loop)"]
RW["SharedGraph<br/>Arc<parking_lot::RwLock<Graph>>"]
READ["Read Lock<br/>(activate, impact, predict, ...)"]
WRITE["Write Lock<br/>(ingest, graph swap)"]
PERSIST["Auto-Persist<br/>(write lock on graph)"]
STDIN --> TOKIO
TOKIO --> SERVE
SERVE --> READ
SERVE --> WRITE
SERVE --> PERSIST
READ --> RW
WRITE --> RW
PERSIST --> RW
The serve loop is single-threaded (synchronous stdio I/O), but graph access is concurrent-safe through SharedGraph. Within a single request:
- Read operations acquire a read lock (shared, non-blocking with other reads).
- Write operations acquire a write lock (exclusive, blocks all other access).
- Plasticity weight updates use atomic CAS (no lock needed for individual weight writes).
The tokio runtime is used solely for the select! between SIGINT and serve loop completion. All actual tool processing happens synchronously within spawn_blocking.
Spreading Activation
Spreading activation is the core query mechanism in m1nd. Instead of searching for text matches, m1nd fires a signal into a weighted graph and observes where the energy propagates. The resulting activation pattern tells you what is structurally, semantically, temporally, and causally related to your query.
The neuroscience origin
The concept comes from cognitive psychology. In 1975, Allan Collins and Elizabeth Loftus proposed that human semantic memory is organized as a network of interconnected concepts. When you think of “doctor,” activation spreads along weighted connections to “nurse,” “hospital,” “stethoscope” – but not to “bicycle.” The strength of the connection determines how much activation passes through. Distant or weak connections receive less energy. The pattern of activated nodes is the meaning.
m1nd applies this model to code. Files, functions, classes, and modules become nodes. Import relationships, call chains, type references, and co-change history become weighted edges. Querying “authentication” does not find the string “authentication” – it fires signal into nodes that match and watches energy flow outward through the graph’s structure.
Four dimensions of activation
Most spreading activation systems operate on a single graph. m1nd runs four independent activation passes and fuses the results. Each dimension captures a different kind of relationship.
D1: Structural
Graph distance, edge types, and edge weights. This is classical spreading activation – signal decays as it hops along edges. A file that imports another file is one hop away. A function that calls through two intermediate modules is three hops away. The signal strength at each node tells you how structurally close it is to the query.
The structural dimension uses m1nd’s CSR (Compressed Sparse Row) adjacency matrix with forward and reverse indices. PageRank scores computed at ingest time provide a static importance baseline.
D2: Semantic
Token overlap and naming patterns. This dimension scores nodes based on how their identifiers (file names, function names, class names) relate to the query text. m1nd uses a TF-IDF weighted trigram index built at ingest time. The trigrams for “authentication” overlap with “auth_handler,” “authenticate_user,” and “auth_middleware” – without requiring exact string matching.
Co-occurrence embeddings from short random walks on the graph add a second signal: nodes that tend to appear near each other in the graph structure score higher, even if their names share no characters.
D3: Temporal
Recency and change frequency. Files modified recently score higher than files untouched for months. Files that change often (high velocity) score higher than stable ones. The temporal dimension uses exponential decay with a 7-day half-life:
recency = exp(-0.693 * age_seconds / (168 * 3600))
score = recency * 0.6 + frequency * 0.4
This ensures that recently active parts of the codebase surface in activation results, even if they are structurally distant.
D4: Causal
Forward and backward causal chain traversal. Edges with causal_strength > 0 (derived from call chains, error propagation paths, and stacktrace mappings) form a causal subgraph. Signal propagates forward along causal edges with full strength, and backward with a 0.7 discount factor. This surfaces nodes that cause or are caused by the query target.
The activation wavefront algorithm
m1nd implements two propagation strategies and a hybrid selector that chooses between them at runtime.
WavefrontEngine (BFS)
Breadth-first, depth-parallel propagation. All active nodes at the current depth fire simultaneously. Signal accumulates into the next depth’s buffer via a scatter-max operation: when multiple sources send signal to the same target, only the strongest arrival survives.
for each depth 0..max_depth:
for each node in current frontier:
if activation[node] < threshold: skip
for each outgoing edge (node -> target):
signal = activation[node] * edge_weight * decay
if inhibitory: signal = -signal * inhibitory_factor
if signal > activation[target]:
activation[target] = signal // scatter-max
add target to next frontier
frontier = next_frontier
This processes all nodes at the same distance in one pass. Optimal when many seeds fire simultaneously or the graph has high average degree.
HeapEngine (priority queue)
Max-heap propagation. The strongest signal fires first. Early-terminates when the heap top drops below threshold. Uses a Bloom filter for fast visited checks (probabilistic, with FPR ~ 1% at typical graph sizes).
Optimal for sparse queries: few seeds, low graph density. The seed-to-node ratio and average degree determine the crossover point.
HybridEngine (auto-select)
The HybridEngine chooses at runtime:
#![allow(unused)]
fn main() {
fn prefer_heap(graph: &Graph, seed_count: usize) -> bool {
let seed_ratio = seed_count as f64 / graph.num_nodes().max(1) as f64;
seed_ratio < 0.001 && graph.avg_degree() < 8.0
}
}
If fewer than 0.1% of nodes are seeds and the average degree is below 8, the HeapEngine wins. Otherwise, the WavefrontEngine is faster because it avoids heap overhead for dense propagation.
Seed node selection
Before activation can begin, the query text must be converted into seed nodes. The SeedFinder performs a multi-strategy match against all node labels:
| Strategy | Relevance score | Example |
|---|---|---|
| Exact label match | 1.0 | “auth” matches node “auth” |
| Prefix match | 0.9 | “auth” matches “auth_handler” |
| Substring match | 0.8 | “auth” matches “pre_auth_check” |
| Tag match | 0.85 | “auth” matches node tagged “authentication” |
| Fuzzy trigram | 0.7 * sim | “authenticaton” (typo) matches “authentication” |
Trigram similarity uses cosine distance between character n-gram frequency vectors. A match threshold of 0.3 catches most typos without generating noise.
For enhanced matching, find_seeds_semantic runs a two-phase process: basic seed finding produces candidates, then the semantic engine re-ranks them with a 0.6/0.4 blend of basic and semantic scores. Maximum 200 seeds are returned (capped by MAX_SEEDS).
Decay and damping
Signal attenuates at every hop:
signal_at_target = signal_at_source * edge_weight * decay_factor
The default decay factor is 0.55 per hop (from DecayFactor::DEFAULT). After 3 hops, the signal has decayed to 0.55^3 = 16.6% of its original strength. After 5 hops (the default max_depth), it is at 5%. The max depth is hard-capped at 20 to prevent runaway propagation.
Additional controls:
- Threshold (default 0.04): signal below this value is discarded. This prunes the wavefront and prevents ghost activations.
- Saturation cap (default 1.0): seeds cannot inject more than this amount of activation. Prevents a single high-PageRank node from dominating.
- Inhibitory factor (default 0.5): inhibitory edges (negative relationships like “replaces,” “deprecates”) apply capped proportional suppression rather than simple negation.
How dimensions are combined
After all four dimensions produce their scored node lists, the merge_dimensions function fuses them into a single ranking.
Weighted sum
Each dimension has a base weight:
#![allow(unused)]
fn main() {
pub const DIMENSION_WEIGHTS: [f32; 4] = [0.35, 0.25, 0.15, 0.25];
// D1 D2 D3 D4
// Struct Seman Temp Causal
}
If a dimension produces no results (e.g., no temporal data available), its weight is redistributed proportionally to the active dimensions. This adaptive redistribution prevents zero-result dimensions from diluting the signal.
Resonance bonus
Nodes that appear in multiple dimensions receive a multiplicative bonus:
#![allow(unused)]
fn main() {
pub const RESONANCE_BONUS_3DIM: f32 = 1.3; // 3 dimensions agree
pub const RESONANCE_BONUS_4DIM: f32 = 1.5; // all 4 dimensions agree
}
A node that scores above 0.01 in all four dimensions receives a 1.5x boost. This is a powerful signal: structural proximity, naming similarity, recent change activity, and causal relationship all point to the same node. These are the nodes the agent should examine first.
The check order matters. m1nd checks dim_count >= 4 before dim_count >= 3. The original Python implementation had a dead elif branch that never triggered the 4-dimension bonus – this was fixed during the Rust port (FM-ACT-001).
PageRank boost
After dimension fusion, each node receives a small PageRank boost:
#![allow(unused)]
fn main() {
const PAGERANK_BOOST: f32 = 0.1;
node.activation += node.pagerank * PAGERANK_BOOST;
}
This ensures that structurally important nodes (high PageRank) surface slightly higher in results, all else being equal. The factor is intentionally small (10% of PageRank) to avoid overwhelming the activation signal.
Visual example
Consider a simplified graph of a backend application:
graph LR
main["main.py<br/>PR: 0.15"]
auth["auth.py<br/>PR: 0.12"]
session["session.py<br/>PR: 0.08"]
middleware["middleware.py<br/>PR: 0.10"]
user["user_model.py<br/>PR: 0.06"]
db["database.py<br/>PR: 0.09"]
routes["routes.py<br/>PR: 0.11"]
main -->|imports| auth
main -->|imports| routes
routes -->|imports| auth
routes -->|imports| middleware
auth -->|imports| session
auth -->|imports| user
auth -->|imports| db
session -->|imports| db
middleware -->|imports| session
user -->|imports| db
style auth fill:#ff6b6b,stroke:#333,color:#fff
style session fill:#ffa07a,stroke:#333
style middleware fill:#ffa07a,stroke:#333
style user fill:#ffd700,stroke:#333
style db fill:#ffd700,stroke:#333
style routes fill:#ffa07a,stroke:#333
style main fill:#ffd700,stroke:#333
Query: "authentication"
Step 1: Seed selection. auth.py matches with relevance 0.9 (prefix match). session.py matches with relevance 0.85 (tag “auth” in its tags). These become seeds with activation 0.9 and 0.85 respectively.
Step 2: Structural propagation (D1). From auth.py (0.9), signal flows to:
session.py: 0.9 * 1.0 * 0.55 = 0.495user_model.py: 0.9 * 1.0 * 0.55 = 0.495database.py: 0.9 * 1.0 * 0.55 = 0.495
From session.py (already at 0.85 from seed, plus 0.495 from structural – scatter-max keeps the higher):
database.py: 0.85 * 1.0 * 0.55 = 0.467 (less than existing 0.495, discarded)
Reverse propagation (via reverse CSR):
routes.pyimportsauth.py, soroutes.pyreceives: 0.9 * 1.0 * 0.55 = 0.495main.pyimportsauth.py, somain.pyreceives: 0.9 * 1.0 * 0.55 = 0.495middleware.pyimportssession.py, somiddleware.pyreceives: 0.85 * 1.0 * 0.55 = 0.467
Step 3: Merge. After running all four dimensions, the final ranking (with resonance bonuses for multi-dimension hits) produces:
| Node | D1 | D2 | D3 | D4 | Dims | Bonus | Final |
|---|---|---|---|---|---|---|---|
| auth.py | 0.90 | 0.88 | 0.70 | 0.65 | 4 | 1.5x | 0.88 |
| session.py | 0.85 | 0.40 | 0.50 | 0.45 | 4 | 1.5x | 0.64 |
| user_model.py | 0.50 | 0.30 | 0.10 | 0.20 | 4 | 1.5x | 0.40 |
| middleware.py | 0.47 | 0.15 | 0.60 | 0.30 | 4 | 1.5x | 0.39 |
| database.py | 0.50 | 0.05 | 0.20 | 0.10 | 3 | 1.3x | 0.28 |
The result tells the agent: start with auth.py, then examine session.py and user_model.py. The four-dimension resonance bonus pushes middleware.py ahead of database.py even though their structural scores are similar – because middleware was recently modified (temporal) and sits on a causal chain (causal).
What this means in practice
An agent calling m1nd.activate("authentication") gets back a ranked list of the 20 most relevant nodes in 31ms. It does not need to know file names, directory structure, or grep patterns. The activation pattern encodes structural proximity, naming affinity, temporal relevance, and causal relationships – fused into a single score.
Over time, as the agent provides feedback via m1nd.learn, the edge weights shift through Hebbian plasticity (see Hebbian Plasticity). The paths that led to useful results strengthen. The paths that led to noise weaken. The same query tomorrow will produce better results.
Hebbian Plasticity
m1nd’s graph learns. Every query changes the edge weights. Paths that lead to useful results get stronger. Paths that lead to noise get weaker. Over time, the graph evolves to match how your team thinks about your codebase. No other code intelligence tool does this.
The neuroscience principle
In 1949, Donald Hebb proposed a theory of synaptic learning: “When an axon of cell A is near enough to excite cell B and repeatedly or persistently takes part in firing it, some growth process or metabolic change takes place in one or both cells such that A’s efficiency, as one of the cells firing B, is increased.”
The popular summary: neurons that fire together wire together.
The converse is equally important: neurons that fire independently weaken their connections. This bidirectional learning – strengthening co-active pathways, weakening inactive ones – is the foundation of how biological neural networks adapt to experience.
m1nd applies this principle to code graphs. Nodes are modules. Edges are relationships. “Firing” means being activated by a spreading activation query. When an agent confirms that a result was useful, the edges connecting those activated nodes strengthen. When the agent marks results as wrong, those paths weaken. The graph remembers what worked.
The five-step learning cycle
Every query that passes through m1nd’s PlasticityEngine triggers a five-step update cycle. This runs automatically – no explicit training phase required.
Step 1: Hebbian strengthening
For every edge where both the source and target were activated in the query results, the weight increases:
delta_w = learning_rate * activation_source * activation_target
new_weight = min(current_weight + delta_w, weight_cap)
The default learning rate is 0.08 (from LearningRate::DEFAULT). The weight cap is 3.0 – no edge can grow stronger than 3x its original weight. This prevents runaway positive feedback.
From the source code:
#![allow(unused)]
fn main() {
// Hebbian: delta_w = lr * act_src * act_tgt
let delta = lr * src_act.get() * tgt_act;
let new_weight = (current + delta).min(cap);
}
The product activation_source * activation_target means that strongly co-activated pairs get the largest boost. A pair where both nodes scored 0.8 gets 0.08 * 0.8 * 0.8 = 0.051 added to their edge weight. A pair where one scored 0.2 gets only 0.08 * 0.8 * 0.2 = 0.013. This is faithful to Hebb’s rule: the strength of the update is proportional to the correlation of the firing.
Step 2: Synaptic decay
Edges whose source nodes were not activated in this query decay slightly:
new_weight = max(current_weight * (1 - decay_rate), weight_floor)
The default decay rate is 0.005 per query (0.5%). The weight floor is 0.05 – edges never decay below 5% of their original weight. This ensures that even unused paths retain some connectivity, preventing the graph from fragmenting.
#![allow(unused)]
fn main() {
let decay_factor = 1.0 - self.config.decay_rate.get(); // 0.995
let new_weight = (current * decay_factor).max(floor);
}
The asymmetry is intentional. Strengthening applies a fixed delta (additive). Decay applies a multiplicative factor. This means strong edges resist decay (a weight-3.0 edge loses 0.015 per query) while weak edges decay faster in relative terms (a weight-0.1 edge loses 0.0005 per query). Frequently activated paths grow monotonically. Rarely activated paths slowly fade but never disappear.
Step 3: Long-Term Potentiation / Long-Term Depression
After an edge has been strengthened 5 consecutive times (the LTP threshold), it receives a one-time +0.15 bonus:
#![allow(unused)]
fn main() {
if !graph.edge_plasticity.ltp_applied[j]
&& graph.edge_plasticity.strengthen_count[j] >= self.config.ltp_threshold
{
let new_weight = (current + self.config.ltp_bonus.get()).min(cap);
graph.edge_plasticity.ltp_applied[j] = true;
}
}
Conversely, after an edge has been weakened 5 consecutive times (the LTD threshold), it receives a one-time -0.15 penalty:
#![allow(unused)]
fn main() {
if !graph.edge_plasticity.ltd_applied[j]
&& graph.edge_plasticity.weaken_count[j] >= self.config.ltd_threshold
{
let new_weight = (current - self.config.ltd_penalty.get()).max(floor);
graph.edge_plasticity.ltd_applied[j] = true;
}
}
These thresholds model biological LTP/LTD – the transition from short-term to long-term memory. Five consecutive activations is a signal of sustained relevance, not a fluke. The one-time bonus/penalty is permanent: it does not reset, and it does not apply again for the same edge. This prevents unbounded weight inflation from repeated queries.
Step 4: Homeostatic normalization
After strengthening and LTP/LTD, the total incoming weight for each node is checked against a ceiling of 5.0:
#![allow(unused)]
fn main() {
if total_incoming > ceiling {
let scale = ceiling / total_incoming;
for each incoming edge:
new_weight = current * scale;
}
}
This is homeostatic plasticity – a biological mechanism that prevents individual neurons from becoming over-stimulated. In m1nd, it prevents hub nodes (like config.py or main.py) from accumulating so much incoming weight that they dominate every activation query regardless of the actual query content.
The normalization is proportional: all incoming edges are scaled by the same factor. This preserves relative strengths while enforcing an absolute ceiling. A node with 10 incoming edges at weight 1.0 each (total 10.0) would have all edges scaled to 0.5, bringing the total to 5.0.
Step 5: Query memory recording
The query, its seeds, and its activated nodes are recorded in a bounded ring buffer (capacity: 1000 queries). This memory serves two purposes:
-
Priming signal: future queries that share seeds with past queries get a boost from nodes that frequently appeared in those past results. This implements a form of associative memory – “things I looked at near authentication tend to be relevant when I look at authentication again.”
-
Seed bigrams: pairs of seeds that co-occur across multiple queries are tracked. This supports the
m1nd.warmuptool, which uses query memory to pre-activate frequently queried paths.
How m1nd.learn works
The automatic plasticity cycle runs on every m1nd.activate call. But agents can also provide explicit feedback via m1nd.learn:
Positive feedback: m1nd.learn(feedback="correct", node_ids=[...])
When an agent confirms that specific nodes were useful:
- The edges connecting those nodes are strengthened with an amplified Hebbian update (the activation values are set to 1.0 for the confirmed nodes, producing maximum
delta_w). - The strengthen counters increment, moving edges closer to the LTP threshold.
- Query memory records the confirmed nodes with high weight, boosting them in future priming signals.
Negative feedback: m1nd.learn(feedback="wrong", node_ids=[...])
When an agent marks results as irrelevant:
- The edges connecting those nodes receive decay as if they were inactive, even though they were activated.
- The weaken counters increment, moving edges closer to the LTD threshold.
- Query memory records the rejection, reducing the priming signal for those nodes.
This feedback loop is what makes m1nd adaptive. A team that mostly works on the payment system will gradually strengthen all paths around payment-related modules. An agent investigating authentication will produce different results than an agent investigating billing – even on the same codebase – because their feedback histories have shaped different edge weight landscapes.
Plasticity state persistence
The learned weights are valuable. Losing them means losing the graph’s adaptation to your workflow. m1nd persists plasticity state in two ways:
Per-edge state (SynapticState)
Each edge’s plasticity state is captured as a serializable record:
#![allow(unused)]
fn main() {
pub struct SynapticState {
pub source_label: String,
pub target_label: String,
pub relation: String,
pub original_weight: f32,
pub current_weight: f32,
pub strengthen_count: u16,
pub weaken_count: u16,
pub ltp_applied: bool,
pub ltd_applied: bool,
}
}
This is exported via PlasticityEngine::export_state() and persisted to M1ND_PLASTICITY_STATE (a JSON file). The export includes a NaN firewall (FM-PL-001): any non-finite weight falls back to the original weight. The write is atomic (temp file + rename, FM-PL-008) to prevent corruption on crash.
Importing state
When m1nd restarts, import_state restores learned weights. Edge identity matching uses (source_label, target_label, relation) triples – not numeric indices – because re-ingesting the codebase may produce different node numbering. This means plasticity survives codebase re-ingestion: if auth.py -> session.py was strengthened, that strengthening persists even if auth.py gets a different NodeId after re-ingest.
Weights are clamped to [weight_floor, weight_cap] on import. Invalid JSON triggers a schema validation error (FM-PL-007) rather than corrupting the graph.
Persistence frequency
The graph auto-persists every 50 queries and on server shutdown. This is a balance between durability (don’t lose too much learning) and disk I/O (don’t write on every query).
The drift tool
After persistence, the natural question is: how much has the graph changed? The m1nd.drift tool answers this.
m1nd.drift compares the current edge weights against their original (ingest-time) baselines and reports:
- Total edges changed: how many edges have weights different from their original values.
- Average weight change: the mean absolute delta across all modified edges.
- Top strengthened edges: the edges that have grown the most relative to their baseline.
- Top weakened edges: the edges that have decayed the most.
- LTP/LTD counts: how many edges have crossed the long-term potentiation or depression thresholds.
This is designed for session recovery. When an agent starts a new session, m1nd.drift tells it what has changed since the graph was last loaded. The agent can see that “paths around the payment module strengthened significantly since yesterday” and adjust its investigation accordingly.
Session 1:
ingest -> activate("auth") -> agent uses results -> learn(correct)
→ 740 edges strengthened, 12,340 edges decayed slightly
Session 2:
drift(since=session_1) -> auth paths now 15% stronger on average
activate("auth") -> better results, faster convergence to useful nodes
Session N:
the graph has adapted to how your team thinks about auth
How this makes the graph adapt
The combination of automatic Hebbian updates, explicit feedback, LTP/LTD thresholds, and homeostatic normalization creates a self-tuning system:
-
Short-term adaptation (within a session): edges on frequently queried paths strengthen immediately. The next query about the same topic converges faster.
-
Long-term memory (across sessions): edges that cross the LTP threshold receive a permanent bonus. Persistent investigation patterns are encoded in the graph structure.
-
Forgetting (controlled decay): paths that are never queried slowly fade toward the weight floor. This prevents the graph from becoming saturated with historical patterns that no longer reflect the codebase’s structure.
-
Stability (homeostatic normalization): no node can accumulate unbounded incoming weight. Hub nodes stay important but do not become black holes that absorb all activation energy.
The result is a graph that starts generic (all edges at their ingest-time weights, reflecting code structure) and gradually becomes specific (edges weighted by how you use the codebase). Two teams working on the same repository will develop different plasticity landscapes. This is a feature: the graph models the team’s mental model, not just the code’s static structure.
Constants reference
| Parameter | Default | Purpose |
|---|---|---|
DEFAULT_LEARNING_RATE | 0.08 | Hebbian delta_w scaling |
DEFAULT_DECAY_RATE | 0.005 | Per-query inactive edge decay |
LTP_THRESHOLD | 5 | Consecutive strengthens for long-term bonus |
LTD_THRESHOLD | 5 | Consecutive weakens for long-term penalty |
LTP_BONUS | 0.15 | One-time weight bonus at LTP threshold |
LTD_PENALTY | 0.15 | One-time weight penalty at LTD threshold |
HOMEOSTATIC_CEILING | 5.0 | Max total incoming weight per node |
WEIGHT_FLOOR | 0.05 | Minimum edge weight (never decays below) |
WEIGHT_CAP | 3.0 | Maximum edge weight (never strengthens above) |
DEFAULT_MEMORY_CAPACITY | 1000 | Ring buffer size for query memory |
CAS_RETRY_LIMIT | 64 | Atomic weight update retries |
XLR Noise Cancellation
Every code graph is noisy. Utility modules, shared constants, logging wrappers – they connect to everything, and they show up in every activation query whether they are relevant or not. m1nd solves this with a technique borrowed from professional audio engineering: differential signal processing with inverted channels. The same principle that makes balanced XLR cables immune to electromagnetic interference makes m1nd’s activation queries immune to structural noise.
The origin: balanced audio cables
In a recording studio, audio signals travel over long cables. The cable acts as an antenna, picking up electromagnetic interference (EMI) from power supplies, lighting, and radio transmissions. An unbalanced cable delivers a noisy signal: the audio you want plus the interference you do not.
A balanced XLR cable solves this by carrying two copies of the audio signal: one normal (hot, pin 2) and one inverted (cold, pin 3). Both signals travel the same physical path, so they pick up the same interference. At the receiving end, the cold signal is flipped back and added to the hot signal. The audio doubles in strength. The interference – identical on both channels – cancels out.
Hot channel: signal + noise
Cold channel: -signal + noise
At receiver: hot - cold = (signal + noise) - (-signal + noise) = 2 * signal
The noise vanishes. The signal survives.
The problem in code graphs
When you ask m1nd “what is related to the payment system?”, structural spreading activation fires from payment-related seed nodes and propagates outward. The result includes genuinely relevant modules (billing, invoicing, refunds) but also noisy modules that connect to everything:
config.py– imported by 40+ fileslogger.py– called from every moduledatabase.py– the persistence layer for everythingutils.py– helper functions used everywhere__init__.py– package marker files
These modules score high in structural activation not because they are relevant to payments, but because they are structurally central. They are the electromagnetic interference of code graphs: noise that correlates with structure, not with the query.
Text search (grep, ripgrep) cannot solve this. It finds files that contain the word “payment,” which misses files that handle payments but never use that string. Static analysis cannot solve it either – the call graph from payment modules genuinely includes config.py and logger.py.
The problem is not that these edges are wrong. They are structurally correct. The problem is that they carry noise mixed in with signal, and the noise is stronger.
How m1nd implements differential processing
m1nd’s AdaptiveXlrEngine implements a six-stage pipeline that mirrors the physics of balanced XLR cables.
Stage 1: Select anti-seeds
The cold channel needs its own starting points – nodes that represent “not the query.” m1nd calls these anti-seeds. Good anti-seeds are structurally similar to the seeds (similar degree, similar graph position) but semantically distant (low neighborhood overlap).
The selection criteria:
#![allow(unused)]
fn main() {
// Degree ratio filter: anti-seed must have similar connectivity
if avg_seed_degree > 0.0 {
let ratio = degree / avg_seed_degree;
if ratio < 0.3 { continue; } // Too peripheral
}
// Jaccard similarity: anti-seed must have different neighbors
let jaccard = intersection as f32 / union_size as f32;
if jaccard > 0.2 { continue; } // Too similar to seeds
}
A node with similar degree but different neighbors is the ideal anti-seed. It will activate the same noisy hub nodes (because it has similar connectivity) but different domain-specific nodes (because it lives in a different part of the graph).
By default, 3 anti-seeds are selected.
Stage 2: Compute seed immunity
Nodes close to the seeds – within 2 BFS hops – are marked as immune to cold-channel cancellation. This prevents the anti-seed signal from cancelling legitimate results near the query target.
#![allow(unused)]
fn main() {
// BFS from seeds, mark everything within immunity_hops as immune
while let Some((node, d)) = queue.pop_front() {
if d >= self.params.immunity_hops { continue; }
for each neighbor:
immune[neighbor] = true;
queue.push_back((neighbor, d + 1));
}
}
Immunity is the key insight that prevents over-cancellation. Without it, the cold channel could erase results that are genuinely close to the query. With it, only distant noise gets cancelled.
Stage 3: Propagate hot signal
Spectral pulses propagate from the seed nodes at frequency F_HOT = 1.0. Each pulse carries an amplitude, a phase, and a frequency. The signal decays along edges and attenuates at inhibitory edges (by INHIBITORY_COLD_ATTENUATION = 0.5).
#![allow(unused)]
fn main() {
let pulse = SpectralPulse {
node: seed,
amplitude: seed_activation,
phase: 0.0,
frequency: F_HOT, // 1.0
hops: 0,
prev_node: seed,
};
}
Propagation is budget-limited (default 50,000 total pulses across hot and cold channels) to prevent runaway computation on dense graphs.
Stage 4: Propagate cold signal
The same propagation runs from the anti-seed nodes at frequency F_COLD = 3.7. The cold frequency is intentionally different from the hot frequency. This spectral separation allows m1nd to distinguish which contributions came from which channel.
#![allow(unused)]
fn main() {
let cold_freq = PosF32::new(F_COLD).unwrap(); // 3.7
let anti_seed_pairs: Vec<(NodeId, FiniteF32)> = anti_seeds
.iter()
.map(|&n| (n, FiniteF32::ONE))
.collect();
let cold_pulses = self.propagate_spectral(
graph, &anti_seed_pairs, cold_freq, config, half_budget
)?;
}
Stage 5: Adaptive differential
At each node, the hot and cold amplitudes are combined. Immune nodes receive the full hot signal with no cold cancellation. Non-immune nodes get the differential:
#![allow(unused)]
fn main() {
let effective_cold = if immune { 0.0 } else { cold_amp[i] };
let raw = hot - effective_cold;
}
The raw differential is then modulated by local graph density. Nodes with degree near the graph average get density 1.0. High-degree nodes (hubs) get density > 1.0, amplifying the differential effect. Low-degree nodes (leaves) get density < 1.0, attenuating it. This is density-adaptive strength – a hub with both hot and cold signal has its differential amplified because hubs are the primary source of noise.
#![allow(unused)]
fn main() {
let density = if avg_deg > 0.0 {
(out_deg / avg_deg).max(DENSITY_FLOOR).min(DENSITY_CAP)
} else {
1.0
};
}
Density is clamped to [0.3, 2.0] to prevent extreme values from distorting results.
Stage 6: Sigmoid gating
The density-modulated differential passes through a sigmoid gate:
#![allow(unused)]
fn main() {
pub fn sigmoid_gate(net_signal: FiniteF32) -> FiniteF32 {
let x = net_signal.get() * SIGMOID_STEEPNESS; // * 6.0
let clamped = x.max(-20.0).min(20.0);
let result = 1.0 / (1.0 + (-clamped).exp());
FiniteF32::new(result)
}
}
The sigmoid converts the raw differential (which can be any value) into a smooth activation in [0, 1]. The steepness factor of 6.0 creates a sharp transition: small positive differentials map close to 0.5, strong positive differentials map close to 1.0, and negative differentials (more cold than hot) map close to 0.0 and are effectively suppressed.
graph LR
subgraph "XLR Pipeline"
S["Seeds<br/>F=1.0 (hot)"] --> HP["Hot<br/>propagation"]
AS["Anti-seeds<br/>F=3.7 (cold)"] --> CP["Cold<br/>propagation"]
HP --> DIFF["Differential<br/>hot - cold"]
CP --> DIFF
DIFF --> DEN["Density<br/>modulation"]
DEN --> SIG["Sigmoid<br/>gating"]
SIG --> OUT["Clean<br/>activation"]
end
subgraph "Immunity"
IMM["2-hop BFS<br/>from seeds"] -.->|"immune = true"| DIFF
end
style S fill:#ff6b6b,stroke:#333,color:#fff
style AS fill:#4dabf7,stroke:#333,color:#fff
style OUT fill:#51cf66,stroke:#333,color:#fff
style IMM fill:#ffd43b,stroke:#333
Over-cancellation fallback
If the cold channel is too aggressive – cancelling all hot signal and leaving zero results – m1nd triggers a fallback (FM-XLR-010):
#![allow(unused)]
fn main() {
let fallback = all_zero && !hot_pulses.is_empty();
if fallback {
// Return hot-only (no cancellation)
activations.clear();
for i in 0..n {
if hot_amp[i] > 0.01 {
activations.push((NodeId::new(i as u32), FiniteF32::new(hot_amp[i])));
}
}
}
}
This ensures that XLR noise cancellation never makes results worse than plain activation. It can either improve them (by removing noise) or fall back to the unfiltered result. The xlr_fallback_used flag in the activation result tells the caller that this happened.
Before and after
Without XLR
Query: “payment processing”
| Rank | Node | Activation | Relevant? |
|---|---|---|---|
| 1 | config.py | 0.82 | No – imports everywhere |
| 2 | payment_handler.py | 0.78 | Yes |
| 3 | database.py | 0.75 | No – generic persistence |
| 4 | logger.py | 0.72 | No – called from everything |
| 5 | billing.py | 0.70 | Yes |
| 6 | utils.py | 0.68 | No – shared utilities |
| 7 | invoice.py | 0.65 | Yes |
| 8 | middleware.py | 0.63 | No – request pipeline |
| 9 | refund.py | 0.60 | Yes |
| 10 | routes.py | 0.58 | No – URL dispatch |
Signal-to-noise ratio: 4 relevant out of 10.
With XLR
The anti-seeds (e.g., nodes from the user management subsystem) produce cold signal that reaches the same hub nodes – config.py, database.py, logger.py, utils.py, middleware.py, routes.py. These hubs receive both hot and cold signal. The differential cancels them.
Payment-specific nodes (payment_handler.py, billing.py, invoice.py, refund.py) receive hot signal but little cold signal, because the anti-seeds live in a different part of the graph. Their differential stays high.
| Rank | Node | Hot | Cold | Differential | Relevant? |
|---|---|---|---|---|---|
| 1 | payment_handler.py | 0.78 | 0.05 | 0.73 | Yes |
| 2 | billing.py | 0.70 | 0.08 | 0.62 | Yes |
| 3 | invoice.py | 0.65 | 0.06 | 0.59 | Yes |
| 4 | refund.py | 0.60 | 0.04 | 0.56 | Yes |
| 5 | payment_models.py | 0.55 | 0.03 | 0.52 | Yes |
| 6 | stripe_adapter.py | 0.50 | 0.02 | 0.48 | Yes |
| 7 | config.py | 0.82 | 0.79 | 0.03 | No (cancelled) |
| 8 | database.py | 0.75 | 0.71 | 0.04 | No (cancelled) |
Signal-to-noise ratio: 6 relevant out of 8.
The hub nodes did not disappear entirely – they still have small positive differentials. But they dropped from ranks 1, 3, 4, 6 to ranks 7, 8. The payment-specific nodes rose to the top.
Why the spectral model matters
The use of different frequencies for hot (1.0) and cold (3.7) channels enables a richer cancellation model than simple subtraction. The spectral_overlap function measures how much the hot and cold frequency distributions overlap at each node:
#![allow(unused)]
fn main() {
// Overlap = sum(min(hot_buckets, cold_buckets)) / sum(hot_buckets)
}
Nodes where hot and cold signals arrive at similar frequencies have high spectral overlap – these are the noise nodes that respond to any query. Nodes where the hot signal dominates a different frequency band than the cold signal have low spectral overlap – these are the signal nodes that respond specifically to this query.
This is directly analogous to common-mode rejection in electrical engineering: the interference is the component that appears identically on both channels, and the signal is the component that differs.
Constants reference
| Parameter | Value | Purpose |
|---|---|---|
F_HOT | 1.0 | Hot channel frequency |
F_COLD | 3.7 | Cold channel frequency |
SPECTRAL_BANDWIDTH | 0.8 | Gaussian kernel width for overlap |
IMMUNITY_HOPS | 2 | BFS depth for seed immunity |
SIGMOID_STEEPNESS | 6.0 | Sharpness of activation gate |
SPECTRAL_BUCKETS | 20 | Resolution of frequency overlap |
DENSITY_FLOOR | 0.3 | Minimum density modulation |
DENSITY_CAP | 2.0 | Maximum density modulation |
INHIBITORY_COLD_ATTENUATION | 0.5 | Cold signal reduction at inhibitory edges |
| Default anti-seeds | 3 | Number of cold-channel origins |
| Default pulse budget | 50,000 | Total pulse limit (hot + cold) |
Structural Holes
Most developer tools find things that exist. m1nd finds things that should exist but do not. A missing error handler. A test file that was never written. A validation function that every sibling module has except one. These are structural holes – gaps in the code graph that reveal omissions, risks, and design inconsistencies.
The network theory origin
The concept of structural holes comes from sociologist Ronald Burt’s 1992 book Structural Holes: The Social Structure of Competition. Burt observed that in social networks, the most valuable positions are not the most connected ones – they are the ones that bridge otherwise disconnected groups. The gap between two groups is a structural hole. The person who spans it controls information flow.
Burt was studying people and organizations, but the insight transfers directly to code. In a codebase graph, a structural hole is a place where a connection should exist based on the surrounding structure but does not. The module that imports five out of six modules in a cluster but skips the sixth. The service that handles create, read, update, but not delete. The directory where every file has a test except one.
These gaps are invisible to text search. You cannot grep for something that is absent. You cannot find a missing test by scanning the test directory – you have to know what should be there and check whether it is there. This requires structural reasoning over the graph, not pattern matching over text.
How m1nd detects structural holes
m1nd detects structural holes through a combination of activation context and neighborhood analysis. The detection is embedded in the query pipeline and can also be invoked directly via the m1nd.missing tool.
The algorithm
The core insight is: a node surrounded by activated neighbors but not activated itself is probably missing something.
After a spreading activation query produces its ranked results, the structural hole detector scans every node in the graph:
#![allow(unused)]
fn main() {
pub fn detect_structural_holes(
&self,
graph: &Graph,
activation: &ActivationResult,
min_sibling_activation: FiniteF32,
) -> M1ndResult<Vec<StructuralHole>> {
// Build activation lookup
let mut act_map = vec![0.0f32; n];
for a in &activation.activated {
act_map[a.node.as_usize()] = a.activation.get();
}
for each node i:
// Skip if already activated (it is in the results)
if act_map[i] > 0.01: continue
// Count activated neighbors
for each neighbor of i:
if act_map[neighbor] > min_sibling_activation:
activated_neighbors += 1
neighbor_act_sum += act_map[neighbor]
// If 2+ neighbors are activated but this node is not: structural hole
if activated_neighbors >= 2:
holes.push(StructuralHole {
node: i,
sibling_avg_activation: neighbor_act_sum / activated_neighbors,
reason: format!("{} activated neighbors (avg={:.2}) but node inactive",
activated_neighbors, avg),
})
}
The threshold of 2 activated neighbors prevents false positives from nodes that happen to touch one activated node. The min_sibling_activation parameter (default 0.3) filters out weakly activated neighbors. Results are sorted by the average activation of the surrounding neighbors – the higher the average, the more conspicuous the absence.
Why this works
Consider a query about “authentication.” The activation pattern covers auth.py, session.py, middleware.py, user_model.py, and their neighbors. Now suppose auth_test.py does not exist. The test files for every other module in the auth cluster exist and are activated (because they have edges to the modules they test). auth_test.py is absent from the graph entirely. But if it did exist, it would be surrounded by activated nodes. Its absence is detectable by the gap it leaves.
In the more subtle case, auth_test.py exists but lacks tests for the password reset flow. The test file node exists and is activated, but password_reset_handler.py is activated while auth_test.py’s test coverage edges do not include it. The neighborhood analysis detects this: password_reset_handler.py’s siblings all have test edges, but this one does not.
The m1nd.missing tool
The m1nd.missing tool wraps structural hole detection in a purpose-built interface. Instead of requiring the caller to run an activation query first, m1nd.missing accepts a natural-language query, runs internal activation, and returns only the holes.
The tool exposes the detection through the MCP protocol. Behind the scenes, it:
- Finds seed nodes matching the query text.
- Runs full four-dimension spreading activation (structural, semantic, temporal, causal).
- Analyzes the activation pattern for structural holes using the neighborhood algorithm.
- Returns the top 10 holes sorted by surrounding activation strength.
Each hole includes:
- node_id: the node at the center of the gap.
- label: human-readable identifier.
- node_type: file, function, class, etc.
- reason: a description of why this is a hole (e.g., “4 activated neighbors (avg=0.72) but node inactive”).
- sibling_avg_activation: quantifies how strongly the surrounding nodes were activated.
Real examples
Missing error handler
Query: “error handling in the payment flow”
Activation lights up: payment_handler.py, billing.py, refund.py, payment_errors.py, retry_logic.py.
Structural hole detected: webhook_handler.py has 3 activated neighbors (payment_handler.py, billing.py, payment_errors.py) but is itself not activated. Investigation reveals: the webhook handler processes Stripe callbacks but has no error handling – raw exceptions propagate to the caller. Every other module in the payment cluster catches and wraps errors via payment_errors.py. The webhook handler does not.
StructuralHole {
node: "file::webhook_handler.py",
sibling_avg_activation: 0.71,
reason: "3 activated neighbors (avg=0.71) but node inactive"
}
Missing test file
Query: “authentication testing”
Activation lights up: test_auth.py, test_session.py, test_middleware.py, auth.py, session.py.
Structural hole detected: password_reset.py has 4 activated neighbors but zero activation itself. The module handles password reset logic. It imports from auth.py and session.py (both activated). It is imported by routes.py (activated). But there is no test_password_reset.py in the graph, and password_reset.py has no test-relationship edges. Every sibling module in the auth cluster has a corresponding test file. This one does not.
StructuralHole {
node: "file::password_reset.py",
sibling_avg_activation: 0.68,
reason: "4 activated neighbors (avg=0.68) but node inactive"
}
Missing validation
Query: “input validation for API endpoints”
Activation lights up: validate_user_input.py, validate_payment_input.py, validate_search_params.py, schema_validator.py.
Structural hole detected: admin_routes.py has 3 activated neighbors (the validation modules it shares with other route handlers) but is not activated itself. Investigation reveals: user routes, payment routes, and search routes all import and call validation functions. Admin routes do not. The admin API accepts raw input without validation – a security risk hidden in a structural gap.
StructuralHole {
node: "file::admin_routes.py",
sibling_avg_activation: 0.65,
reason: "3 activated neighbors (avg=0.65) but node inactive"
}
Why no other tool can do this
Text search (grep, ripgrep)
Text search finds strings. It cannot find the absence of a string. You cannot grep for “the test file that should exist but does not.” You would need to know in advance what you are looking for, which defeats the purpose.
Static analysis (AST, linters)
Linters can enforce specific rules (“every function must have a docstring”) but cannot detect structural patterns across the graph (“every module in this cluster has a test file except this one”). They operate on individual files, not on the relationships between files.
RAG / embedding search
Embedding search finds documents similar to a query. It cannot detect the gap between documents. If test_password_reset.py does not exist, there is no document to embed. The absence is invisible.
Code coverage tools
Coverage tools tell you which lines of code are executed during tests. They can identify untested code within a file. They cannot identify missing files – a test file that was never created has zero coverage, indistinguishable from a file that exists but is not run.
m1nd’s advantage
m1nd operates on the graph structure, not on file contents. It reasons about the topology of relationships: which nodes connect to which other nodes, and where those connections are unexpectedly absent. The spreading activation query establishes a context (“what is related to authentication?”), and the structural hole detector finds gaps within that context.
This is a fundamentally different capability. It is not better text search, or better static analysis, or better coverage. It is structural reasoning about the shape of the code graph – something that requires a graph in the first place, and an activation pattern to establish context.
Connection to other concepts
Structural hole detection works best when combined with other m1nd capabilities:
-
Spreading activation (see Spreading Activation) provides the activation context that defines “which neighbors count.” Without activation, every node would be checked against every other node – too noisy to be useful.
-
Hebbian plasticity (see Hebbian Plasticity) sharpens the detection over time. As edges on well-tested paths strengthen and edges on untested paths weaken, the contrast between activated and inactive nodes increases. Holes become more conspicuous.
-
XLR noise cancellation (see XLR Noise Cancellation) removes hub-node noise from the activation pattern before hole detection runs. Without XLR,
config.pybeing activated might cause false-positive holes in unrelated modules that happen to import config. With XLR, the activation pattern is cleaner, and the holes that surface are more meaningful.
Interpreting results
A structural hole is not necessarily a bug. It is a signal that something deviates from the surrounding pattern. Possible interpretations:
| Hole type | What it means | Action |
|---|---|---|
| Missing test file | A module has no tests while its siblings do | Write tests or mark as intentionally untested |
| Missing error handling | A module does not use the error patterns its siblings use | Add error handling or document why it is unnecessary |
| Missing validation | An endpoint lacks input validation that peer endpoints have | Add validation – likely a security gap |
| Missing import | A module does not import a shared utility that all siblings import | Check if the module implements the functionality differently |
| Missing documentation | A module has no doc-edges while siblings do | Write documentation or accept the gap |
| Intentional isolation | A module is deliberately decoupled from a cluster | No action – but the hole confirms the isolation is real |
The sibling_avg_activation score helps prioritize. A hole where surrounding nodes have an average activation of 0.85 is more suspicious than one at 0.35. The former means the node is deeply embedded in a strongly activated cluster and conspicuously absent. The latter means the surrounding nodes were only weakly related to the query – the “hole” may just be a node in a different domain.
API Reference Overview
m1nd exposes 43 MCP tools over JSON-RPC 2.0 via stdio. Every tool requires agent_id as a parameter. Tools are organized into seven groups.
Tool Index
Core Activation (3 tools)
| Tool | Description |
|---|---|
m1nd.activate | Spreading activation query across the connectome |
m1nd.warmup | Task-based warmup and priming |
m1nd.resonate | Resonance analysis: harmonics, sympathetic pairs, and resonant frequencies |
Analysis (7 tools)
| Tool | Description |
|---|---|
m1nd.impact | Impact radius / blast analysis for a node |
m1nd.predict | Co-change prediction for a modified node |
m1nd.counterfactual | What-if node removal simulation |
m1nd.fingerprint | Activation fingerprint and equivalence detection |
m1nd.hypothesize | Graph-based hypothesis testing against structural claims |
m1nd.differential | Focused structural diff between two graph snapshots |
m1nd.diverge | Structural drift between a baseline and current graph state |
Memory & Learning (7 tools)
| Tool | Description |
|---|---|
m1nd.learn | Explicit feedback-based edge adjustment |
m1nd.drift | Weight and structural drift analysis |
m1nd.why | Path explanation between two nodes |
m1nd.trail.save | Persist current investigation state |
m1nd.trail.resume | Restore a saved investigation |
m1nd.trail.list | List saved investigation trails |
m1nd.trail.merge | Combine two or more investigation trails |
Exploration (6 tools)
| Tool | Description |
|---|---|
m1nd.seek | Intent-aware semantic code search |
m1nd.scan | Pattern-aware structural code analysis |
m1nd.missing | Detect structural holes and missing connections |
m1nd.trace | Map runtime errors to structural root causes |
m1nd.timeline | Git-based temporal history for a node |
m1nd.federate | Multi-repository federated graph ingestion |
Perspectives (12 tools)
| Tool | Description |
|---|---|
m1nd.perspective.start | Enter a perspective: navigable route surface from a query |
m1nd.perspective.routes | Browse the current route set with pagination |
m1nd.perspective.inspect | Expand a route with metrics, provenance, and affinity |
m1nd.perspective.peek | Extract a code/doc slice from a route target |
m1nd.perspective.follow | Follow a route: move focus to target, synthesize new routes |
m1nd.perspective.suggest | Get the next best move suggestion |
m1nd.perspective.affinity | Discover probable connections a route target might have |
m1nd.perspective.branch | Fork navigation state into a new branch |
m1nd.perspective.back | Navigate back to previous focus |
m1nd.perspective.compare | Compare two perspectives on shared/unique nodes |
m1nd.perspective.list | List all perspectives for an agent |
m1nd.perspective.close | Close a perspective and release associated locks |
Lifecycle & Locks (8 tools)
| Tool | Description |
|---|---|
m1nd.ingest | Ingest or re-ingest a codebase, descriptor, or memory corpus |
m1nd.health | Server health and statistics |
m1nd.validate_plan | Validate a modification plan against the code graph |
m1nd.lock.create | Pin a subgraph region and capture a baseline |
m1nd.lock.watch | Set a watcher strategy on a lock |
m1nd.lock.diff | Compute what changed in a locked region since baseline |
m1nd.lock.rebase | Re-capture lock baseline from current graph |
m1nd.lock.release | Release a lock and free its resources |
Common Parameters
Every tool requires the agent_id parameter:
| Parameter | Type | Required | Description |
|---|---|---|---|
agent_id | string | Yes | Identifier for the calling agent. Used for session tracking, perspective ownership, lock ownership, and multi-agent coordination. |
Agent IDs are free-form strings. Convention: use the agent’s name or role identifier (e.g. "orchestrator", "auditor-1", "builder-api").
JSON-RPC Request Format
m1nd uses the MCP protocol over JSON-RPC 2.0 via stdio. All tool calls use the tools/call method.
Request structure
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "m1nd.activate",
"arguments": {
"agent_id": "orchestrator",
"query": "session management"
}
}
}
Successful response
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [
{
"type": "text",
"text": "{ ... pretty-printed JSON result ... }"
}
]
}
}
The text field contains the tool-specific output as a pretty-printed JSON string. Parse it to get the structured result.
Error response (tool execution error)
Tool execution errors are returned as MCP isError content, not as JSON-RPC errors:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [
{
"type": "text",
"text": "Error: Node not found: file::nonexistent.py"
}
],
"isError": true
}
}
Error response (protocol error)
JSON-RPC protocol errors use standard error codes:
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32601,
"message": "Method not found: bad_method"
}
}
Error Codes
| Code | Meaning |
|---|---|
-32700 | Parse error – invalid JSON |
-32601 | Method not found – unknown JSON-RPC method |
-32603 | Internal error – server-level failure |
Tool-specific errors (node not found, perspective not found, lock ownership violation, etc.) are returned via isError: true in the content, not as JSON-RPC errors.
Transport
m1nd supports two stdio transport modes, auto-detected per message:
- Framed:
Content-Length: N\r\n\r\n{json}– standard MCP/LSP framing. Used by Claude Code, Cursor, and most MCP clients. - Line:
{json}\n– one JSON object per line. Used by simple scripts and testing.
The server auto-detects which mode each incoming message uses and responds in the same mode.
Tool Name Normalization
Tool names accept both dots and underscores as separators. The server normalizes _ to . before dispatch:
m1nd.activateandm1nd_activateboth workm1nd.perspective.startandm1nd_perspective_startboth workm1nd.lock.createandm1nd_lock_createboth work
Protocol Handshake
Before calling tools, MCP clients perform a handshake:
{"jsonrpc":"2.0","id":0,"method":"initialize","params":{}}
Response:
{
"jsonrpc": "2.0",
"id": 0,
"result": {
"protocolVersion": "2024-11-05",
"serverInfo": { "name": "m1nd-mcp", "version": "0.1.0" },
"capabilities": { "tools": {} }
}
}
Then list available tools:
{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}
This returns the full schema for all 43 tools with inputSchema for each.
Activation Tools
Three tools for querying the connectome through spreading activation, task-based priming, and resonance analysis.
m1nd.activate
Spreading activation query across the connectome. The primary search tool – propagates signal from seed nodes through the graph across four dimensions (structural, semantic, temporal, causal), with XLR noise cancellation and optional ghost edge / structural hole detection.
Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
query | string | Yes | – | Search query for spreading activation. Matched against node labels, tags, and provenance to find seed nodes. |
agent_id | string | Yes | – | Calling agent identifier. |
top_k | integer | No | 20 | Number of top activated nodes to return. |
dimensions | string[] | No | ["structural", "semantic", "temporal", "causal"] | Activation dimensions to include. Each dimension contributes independently to the final activation score. Values: "structural", "semantic", "temporal", "causal". |
xlr | boolean | No | true | Enable XLR noise cancellation. Filters low-confidence activations to reduce false positives. |
include_ghost_edges | boolean | No | true | Include ghost edge detection. Ghost edges are probable but unconfirmed connections inferred from activation patterns. |
include_structural_holes | boolean | No | false | Include structural hole detection. Identifies nodes that should be connected but are not. |
Example Request
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "m1nd.activate",
"arguments": {
"agent_id": "orchestrator",
"query": "session pool management",
"top_k": 5,
"include_ghost_edges": true
}
}
}
Example Response
{
"query": "session pool management",
"seeds": [
{ "node_id": "file::pool.py", "label": "pool.py", "relevance": 0.95 }
],
"activated": [
{
"node_id": "file::pool.py",
"label": "pool.py",
"type": "file",
"activation": 0.89,
"dimensions": { "structural": 0.92, "semantic": 0.95, "temporal": 0.78, "causal": 0.71 },
"pagerank": 0.635,
"tags": ["session", "pool"],
"provenance": {
"source_path": "backend/pool.py",
"line_start": 1,
"line_end": 245,
"canonical": true
}
},
{
"node_id": "file::pool.py::class::ConnectionPool",
"label": "ConnectionPool",
"type": "class",
"activation": 0.84,
"dimensions": { "structural": 0.88, "semantic": 0.91, "temporal": 0.72, "causal": 0.65 },
"pagerank": 0.412,
"tags": ["pool", "session"],
"provenance": {
"source_path": "backend/pool.py",
"line_start": 15,
"line_end": 180,
"canonical": true
}
}
],
"ghost_edges": [
{
"source": "pool.py",
"target": "recovery.py",
"shared_dimensions": ["semantic", "causal"],
"strength": 0.34
}
],
"structural_holes": [],
"plasticity": {
"edges_strengthened": 12,
"edges_decayed": 3,
"ltp_events": 1,
"priming_nodes": 5
},
"elapsed_ms": 31.2
}
When to Use
- Primary search – the default way to ask “what in the codebase relates to X?”
- Exploration – when you know a topic but not the specific files
- Context building – before working on a feature, activate its topic to find all related code
- Gap detection – enable
include_structural_holesto find missing connections
Side Effects
Activate has plasticity side effects: it strengthens edges between activated nodes and decays inactive edges. This makes the graph learn from usage patterns over time.
Related Tools
m1nd.warmup– activate + prime for a specific taskm1nd.seek– intent-aware search (finds code by purpose, not just keywords)m1nd.perspective.start– wraps activate into a navigable perspectivem1nd.learn– explicitly provide feedback on activation results
m1nd.warmup
Task-based warmup and priming. Activates the graph around a task description and applies a temporary boost to relevant nodes, preparing the graph for focused work. The boost decays naturally over time.
Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
task_description | string | Yes | – | Description of the task to warm up for. Natural language. |
agent_id | string | Yes | – | Calling agent identifier. |
boost_strength | number | No | 0.15 | Priming boost strength applied to relevant nodes. Range: 0.0 to 1.0. Higher values make the primed nodes more dominant in subsequent queries. |
Example Request
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "m1nd.warmup",
"arguments": {
"agent_id": "orchestrator",
"task_description": "Refactor the messaging routing module to support group chats",
"boost_strength": 0.2
}
}
}
Example Response
{
"task": "Refactor the messaging routing module to support group chats",
"primed_nodes": 23,
"top_primed": [
{ "node_id": "file::messaging_routes.py", "label": "messaging_routes.py", "boost": 0.2 },
{ "node_id": "file::messaging.py", "label": "messaging.py", "boost": 0.18 },
{ "node_id": "file::messaging_models.py", "label": "messaging_models.py", "boost": 0.15 },
{ "node_id": "file::handler.py", "label": "handler.py", "boost": 0.12 }
],
"elapsed_ms": 18.5
}
When to Use
- Session start – warm up before a focused work session to bias the graph toward relevant code
- Context switch – when changing tasks, warm up the new topic
- Before complex queries – warmup biases subsequent
activate,impact, andwhyqueries toward the warmed-up region
Side Effects
Applies temporary priming boosts to node activations. These boosts decay naturally and are NOT persisted across server restarts.
Related Tools
m1nd.activate– raw activation query without the priming boostm1nd.trail.resume– restores a full investigation context including activation boosts
m1nd.resonate
Resonance analysis: standing waves, harmonics, sympathetic pairs, and resonant frequencies in the graph. Identifies nodes that form natural clusters of mutual reinforcement – the “harmonics” of the connectome.
Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
query | string | No | – | Search query to find seed nodes for resonance analysis. Provide either query or node_id (or neither for global resonance). |
node_id | string | No | – | Specific node identifier to use as seed. Alternative to query. |
agent_id | string | Yes | – | Calling agent identifier. |
top_k | integer | No | 20 | Number of top resonance results to return. |
Example Request
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "m1nd.resonate",
"arguments": {
"agent_id": "orchestrator",
"query": "authentication flow",
"top_k": 10
}
}
}
Example Response
{
"seed": "authentication flow",
"harmonics": [
{
"node_id": "file::auth_discovery.py",
"label": "auth_discovery.py",
"amplitude": 0.92,
"harmonic_order": 1
},
{
"node_id": "file::middleware.py",
"label": "middleware.py",
"amplitude": 0.71,
"harmonic_order": 2
},
{
"node_id": "file::principal_registry.py",
"label": "principal_registry.py",
"amplitude": 0.68,
"harmonic_order": 2
}
],
"sympathetic_pairs": [
{ "a": "auth_discovery.py", "b": "principal_registry.py", "coupling": 0.84 }
],
"elapsed_ms": 45.0
}
When to Use
- Deep structural analysis – find natural clusters of mutually reinforcing code
- Pattern discovery – identify which modules form coherent subsystems
- Architecture review – see which modules resonate together (and which do not)
- Refactoring – resonance groups suggest natural module boundaries
Side Effects
Read-only. No plasticity side effects.
Related Tools
m1nd.activate– simpler spreading activation without harmonic analysism1nd.fingerprint– finds structurally equivalent nodesm1nd.missing– finds gaps in the resonance structure
Analysis Tools
Seven tools for impact analysis, prediction, counterfactual simulation, fingerprinting, hypothesis testing, structural diffing, and drift detection.
m1nd.impact
Impact radius / blast analysis for a node. Propagates signal outward from a source node to determine which other nodes would be affected by a change. Supports forward (downstream), reverse (upstream), and bidirectional analysis.
Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
node_id | string | Yes | – | Target node identifier. Can be an external_id (file::backend/config.py) or a node label. |
agent_id | string | Yes | – | Calling agent identifier. |
direction | string | No | "forward" | Propagation direction. Values: "forward" (what does this affect?), "reverse" (what affects this?), "both". |
include_causal_chains | boolean | No | true | Include causal chain detection. Shows the specific paths through which impact propagates. |
Example Request
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "m1nd.impact",
"arguments": {
"agent_id": "orchestrator",
"node_id": "file::handler.py",
"direction": "forward",
"include_causal_chains": true
}
}
}
Example Response
{
"source": "file::handler.py",
"source_label": "handler.py",
"direction": "forward",
"blast_radius": [
{ "node_id": "file::chat_routes.py", "label": "chat_routes.py", "type": "file", "signal_strength": 0.91, "hop_distance": 1 },
{ "node_id": "file::ws_relay.py", "label": "ws_relay.py", "type": "file", "signal_strength": 0.78, "hop_distance": 1 },
{ "node_id": "file::parser.py", "label": "parser.py", "type": "file", "signal_strength": 0.65, "hop_distance": 2 }
],
"total_energy": 4271.0,
"max_hops_reached": 3,
"causal_chains": [
{
"path": ["handler.py", "chat_routes.py", "main.py"],
"relations": ["imported_by", "registered_in"],
"cumulative_strength": 0.82
}
]
}
When to Use
- Before modifying code – understand the blast radius before touching a file
- Risk assessment – high total_energy = high-risk change
- Scope validation – verify that a planned change does not leak beyond expected boundaries
- Reverse analysis – find all upstream dependencies that could cause a bug in a given module
Related Tools
m1nd.predict– predicts which files will co-change (more actionable than blast radius)m1nd.counterfactual– simulates deletion rather than changem1nd.validate_plan– validates an entire modification plan
m1nd.predict
Co-change prediction for a modified node. Given a node that was just changed, predicts which other nodes are most likely to need changes too, ranked by confidence. Uses historical co-change patterns, structural proximity, and velocity scoring.
Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
changed_node | string | Yes | – | Node identifier that was changed. |
agent_id | string | Yes | – | Calling agent identifier. |
top_k | integer | No | 10 | Number of top predictions to return. |
include_velocity | boolean | No | true | Include velocity scoring. Velocity considers how recently and frequently nodes co-changed. |
Example Request
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "m1nd.predict",
"arguments": {
"agent_id": "orchestrator",
"changed_node": "file::pool.py",
"top_k": 5
}
}
}
Example Response
{
"changed_node": "file::pool.py",
"predictions": [
{ "node_id": "file::worker.py", "label": "worker.py", "confidence": 0.89, "velocity": 0.72, "reason": "high co-change frequency + structural coupling" },
{ "node_id": "file::process_manager.py", "label": "process_manager.py", "confidence": 0.76, "velocity": 0.65, "reason": "imports pool" },
{ "node_id": "file::tests/test_pool.py", "label": "test_pool.py", "confidence": 0.71, "velocity": 0.80, "reason": "test file" },
{ "node_id": "file::worker.py", "label": "worker.py", "confidence": 0.54, "velocity": 0.41, "reason": "2-hop dependency" },
{ "node_id": "file::config.py", "label": "config.py", "confidence": 0.32, "velocity": 0.28, "reason": "shared configuration" }
],
"elapsed_ms": 8.3
}
When to Use
- After modifying a module – find what else needs updating
- Before committing – verify you have not missed a co-change partner
- Code review – check if a PR is missing changes to coupled modules
Related Tools
m1nd.impact– blast radius (broader, less actionable)m1nd.timeline– detailed co-change historym1nd.validate_plan– validates an entire plan against co-change predictions
m1nd.counterfactual
What-if node removal simulation. Simulates removing one or more nodes from the graph and reports the cascade: orphaned nodes, lost activation energy, and the resulting blast radius. Non-destructive – the graph is not modified.
Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
node_ids | string[] | Yes | – | Node identifiers to simulate removal of. |
agent_id | string | Yes | – | Calling agent identifier. |
include_cascade | boolean | No | true | Include cascade analysis. Shows multi-hop propagation of the removal. |
Example Request
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "m1nd.counterfactual",
"arguments": {
"agent_id": "orchestrator",
"node_ids": ["file::worker.py"],
"include_cascade": true
}
}
}
Example Response
{
"removed_nodes": ["file::worker.py"],
"cascade": [
{ "depth": 1, "affected": 23 },
{ "depth": 2, "affected": 456 },
{ "depth": 3, "affected": 3710 }
],
"total_affected": 4189,
"orphaned_count": 0,
"pct_activation_lost": 0.41,
"elapsed_ms": 3.1
}
When to Use
- Before deleting/rewriting modules – understand the full cascade before removing code
- Dependency audit – find modules whose removal would be catastrophic
- Architecture planning – evaluate the cost of removing a subsystem
Related Tools
m1nd.impact– change impact (less extreme than removal)m1nd.hypothesize– test a structural claim about dependencies
m1nd.fingerprint
Activation fingerprint and equivalence detection. Computes a structural fingerprint for a target node (or the entire graph) and finds functionally equivalent or duplicate nodes. Uses probe queries to generate activation patterns and compares them via cosine similarity.
Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
target_node | string | No | – | Target node to find equivalents for. If omitted, performs global fingerprinting. |
agent_id | string | Yes | – | Calling agent identifier. |
similarity_threshold | number | No | 0.85 | Cosine similarity threshold for equivalence. Range: 0.0 to 1.0. Lower values find more (but weaker) matches. |
probe_queries | string[] | No | – | Optional probe queries for fingerprinting. Auto-generated from the node’s neighborhood if omitted. |
Example Request
{
"jsonrpc": "2.0",
"id": 4,
"method": "tools/call",
"params": {
"name": "m1nd.fingerprint",
"arguments": {
"agent_id": "orchestrator",
"target_node": "file::pool.py",
"similarity_threshold": 0.7
}
}
}
Example Response
{
"target": "file::pool.py",
"fingerprint": [0.92, 0.45, 0.78, 0.33, 0.67],
"equivalents": [
{ "node_id": "file::worker.py", "label": "worker.py", "similarity": 0.88, "reason": "similar pool lifecycle pattern" },
{ "node_id": "file::fast_pool.py", "label": "fast_pool.py", "similarity": 0.74, "reason": "shared structural role" }
],
"elapsed_ms": 55.2
}
When to Use
- Duplicate detection – find code doing the same thing in different places
- Consolidation audit – identify candidates for unification
- Post-build review – verify new code does not duplicate existing functionality
Related Tools
m1nd.resonate– finds harmonically coupled nodes (complementary, not duplicate)m1nd.activate– simpler search without equivalence scoring
m1nd.hypothesize
Graph-based hypothesis testing. Takes a natural language claim about the codebase, parses it into a structural query pattern, and returns evidence for and against the claim with a Bayesian confidence score.
Supported claim patterns (auto-detected from natural language): NEVER_CALLS, ALWAYS_BEFORE, DEPENDS_ON, NO_DEPENDENCY, COUPLING, ISOLATED, GATEWAY, CIRCULAR.
Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
claim | string | Yes | – | Natural language claim about the codebase. Examples: "handler never validates session tokens", "all external calls go through the router", "auditor is independent of messaging". |
agent_id | string | Yes | – | Calling agent identifier. |
max_hops | integer | No | 5 | Maximum BFS hops for evidence search. |
include_ghost_edges | boolean | No | true | Include ghost edges as weak evidence. Ghost edges count as lower-weight supporting evidence. |
include_partial_flow | boolean | No | true | Include partial flow when full path not found. Shows how far the search reached. |
path_budget | integer | No | 1000 | Budget cap for all-paths enumeration. Limits computation on dense graphs. |
Example Request
{
"jsonrpc": "2.0",
"id": 5,
"method": "tools/call",
"params": {
"name": "m1nd.hypothesize",
"arguments": {
"agent_id": "orchestrator",
"claim": "worker depends on messaging at runtime"
}
}
}
Example Response
{
"claim": "worker depends on messaging at runtime",
"claim_type": "depends_on",
"subject_nodes": ["file::worker.py"],
"object_nodes": ["file::messaging.py"],
"verdict": "likely_true",
"confidence": 0.72,
"supporting_evidence": [
{
"type": "path_found",
"description": "2-hop path via process_manager.cancel",
"likelihood_factor": 3.5,
"nodes": ["file::worker.py", "file::process_manager.py", "file::messaging.py"],
"relations": ["calls", "imports"],
"path_weight": 0.68
}
],
"contradicting_evidence": [],
"paths_explored": 25015,
"elapsed_ms": 58.0
}
Verdict Values
| Verdict | Confidence Range | Meaning |
|---|---|---|
"likely_true" | > 0.8 | Strong structural evidence supports the claim |
"likely_false" | < 0.2 | Strong structural evidence contradicts the claim |
"inconclusive" | 0.2 – 0.8 | Evidence exists both for and against |
When to Use
- Architecture validation – test claims about module boundaries and dependencies
- Bug investigation – test whether a suspected dependency exists
- Code review – verify architectural invariants are maintained
- Security audit – test isolation claims (e.g. “auth module is isolated from user input”)
Related Tools
m1nd.why– finds the path between two specific nodesm1nd.impact– measures downstream impact rather than testing a claimm1nd.scan– structural analysis with predefined patterns
m1nd.differential
Focused structural diff between two graph snapshots. Compares edges, weights, nodes, and coupling between snapshot A and snapshot B, optionally narrowed by a focus question.
Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
agent_id | string | Yes | – | Calling agent identifier. |
snapshot_a | string | Yes | – | Path to snapshot A, or "current" for the in-memory graph. |
snapshot_b | string | Yes | – | Path to snapshot B, or "current" for the in-memory graph. |
question | string | No | – | Focus filter question. Narrows the diff output. Examples: "what new coupling was introduced?", "what modules became isolated?". |
focus_nodes | string[] | No | [] | Limit diff to neighborhood of specific nodes. |
Example Request
{
"jsonrpc": "2.0",
"id": 6,
"method": "tools/call",
"params": {
"name": "m1nd.differential",
"arguments": {
"agent_id": "orchestrator",
"snapshot_a": "/path/to/before.json",
"snapshot_b": "current",
"question": "what new coupling was introduced?"
}
}
}
Example Response
{
"snapshot_a": "/path/to/before.json",
"snapshot_b": "current",
"new_edges": [
{ "source": "messaging_routes.py", "target": "handler.py", "relation": "calls", "weight": 0.45 }
],
"removed_edges": [],
"weight_changes": [
{ "source": "pool.py", "target": "worker.py", "relation": "imports", "old_weight": 0.5, "new_weight": 0.78, "delta": 0.28 }
],
"new_nodes": ["file::messaging_bridge.py"],
"removed_nodes": [],
"coupling_deltas": [
{ "community_a": "messaging", "community_b": "handler", "old_coupling": 0.2, "new_coupling": 0.65, "delta": 0.45 }
],
"summary": "1 new edge, 1 new node, 1 weight change, 1 coupling increase",
"elapsed_ms": 120.5
}
When to Use
- Pre/post refactor comparison – snapshot before, refactor, then diff against current
- PR review – compare graph before and after a branch’s changes
- Architecture drift monitoring – periodic snapshot comparison
Related Tools
m1nd.diverge– higher-level drift analysis with anomaly detectionm1nd.lock.diff– diff within a locked region (no snapshot file needed)
m1nd.diverge
Structural drift detection between a baseline and the current graph state. Higher-level than differential – includes anomaly detection (test deficits, velocity spikes, new coupling), coupling matrix changes, and a Jaccard-based structural drift score.
Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
agent_id | string | Yes | – | Calling agent identifier. |
baseline | string | Yes | – | Baseline reference. Values: ISO date ("2026-03-01"), git ref (SHA or tag), or "last_session" to use the saved GraphFingerprint. |
scope | string | No | – | File path glob to limit scope. Example: "backend/orchestrator*". None = all nodes. |
include_coupling_changes | boolean | No | true | Include coupling matrix delta between communities. |
include_anomalies | boolean | No | true | Detect anomalies: test deficits, velocity spikes, new coupling, isolation. |
Example Request
{
"jsonrpc": "2.0",
"id": 7,
"method": "tools/call",
"params": {
"name": "m1nd.diverge",
"arguments": {
"agent_id": "orchestrator",
"baseline": "2026-03-01",
"scope": "backend/orchestrator*"
}
}
}
Example Response
{
"baseline": "2026-03-01",
"baseline_commit": "a1b2c3d",
"scope": "backend/orchestrator*",
"structural_drift": 0.23,
"new_nodes": ["file::orchestrator_runtime_guard.py"],
"removed_nodes": [],
"modified_nodes": [
{ "file": "orchestrator_v2.py", "delta": "+450/-30", "growth_ratio": 1.85 }
],
"coupling_changes": [
{ "pair": ["orchestrator_v2.py", "orchestrator_routes.py"], "was": 0.4, "now": 0.72, "direction": "strengthened" }
],
"anomalies": [
{ "type": "test_deficit", "file": "orchestrator_runtime_guard.py", "detail": "New file with 0 test files", "severity": "warning" },
{ "type": "velocity_spike", "file": "orchestrator_v2.py", "detail": "450 lines added in 12 days", "severity": "info" }
],
"summary": "23% structural drift. 1 new file (untested). orchestrator coupling strengthened.",
"elapsed_ms": 85.0
}
Anomaly Types
| Type | Description |
|---|---|
test_deficit | New or modified file with no corresponding test file |
velocity_spike | Unusually high churn rate |
new_coupling | Previously independent modules are now coupled |
isolation | Module that was connected became isolated |
When to Use
- Session start –
m1nd.driftshows weight-level changes;m1nd.divergeshows structural-level changes - Sprint retrospective – how much did the architecture change this sprint?
- Quality gate – flag files with test deficits before merging
Related Tools
m1nd.drift– weight-level drift (lighter, faster)m1nd.differential– lower-level snapshot diffm1nd.timeline– single-node temporal history
Memory & Learning Tools
Seven tools for feedback-based learning, drift analysis, path explanation, and investigation trail management.
m1nd.learn
Explicit feedback-based edge adjustment. After using activate or other query tools, call learn to tell m1nd whether the results were correct, wrong, or partial. This applies Hebbian learning to strengthen or weaken edges between the query seeds and the reported nodes.
Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
query | string | Yes | – | The original query this feedback relates to. Must match the query used in the activation. |
agent_id | string | Yes | – | Calling agent identifier. |
feedback | string | Yes | – | Feedback type. Values: "correct" (strengthen edges), "wrong" (weaken edges), "partial" (strengthen confirmed nodes only). |
node_ids | string[] | Yes | – | Node identifiers to apply feedback to. For "correct", these are the relevant results. For "wrong", these are the irrelevant ones. |
strength | number | No | 0.2 | Feedback strength for edge adjustment. Range: 0.0 to 1.0. Higher = stronger plasticity effect. |
Example Request
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "m1nd.learn",
"arguments": {
"agent_id": "orchestrator",
"query": "session pool management",
"feedback": "correct",
"node_ids": ["file::pool.py", "file::worker.py"],
"strength": 0.3
}
}
}
Example Response
{
"query": "session pool management",
"feedback": "correct",
"edges_adjusted": 8,
"nodes_affected": 2,
"plasticity_delta": 0.3,
"elapsed_ms": 2.1
}
When to Use
- After every activate where results were used – always provide feedback to improve future results
- After investigation – report which activated nodes were actually relevant
- Continuous improvement – the graph learns from your feedback over time
Side Effects
Modifies edge weights in the graph. Changes are persisted on the next auto-persist cycle (every 50 queries) and on shutdown.
Related Tools
m1nd.activate– the query tool whose results you are providing feedback onm1nd.drift– see cumulative weight changes from learning
m1nd.drift
Weight and structural drift analysis. Compares the current graph state against a baseline (typically "last_session") to show what changed – new edges, removed edges, and weight drift. Useful for context recovery at session start.
Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
agent_id | string | Yes | – | Calling agent identifier. |
since | string | No | "last_session" | Baseline reference point. Values: "last_session" (saved state from previous session), or a timestamp. |
include_weight_drift | boolean | No | true | Include edge weight drift analysis. Shows which edges strengthened or weakened. |
Example Request
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "m1nd.drift",
"arguments": {
"agent_id": "orchestrator",
"since": "last_session",
"include_weight_drift": true
}
}
}
Example Response
{
"since": "last_session",
"node_count_delta": 15,
"edge_count_delta": 42,
"new_nodes": ["file::forem_publisher.py", "file::forem_routes.py"],
"removed_nodes": [],
"top_weight_drifts": [
{ "edge": "pool.py -> worker.py", "old": 0.45, "new": 0.72, "delta": 0.27 },
{ "edge": "handler.py -> parser.py", "old": 0.60, "new": 0.48, "delta": -0.12 }
],
"elapsed_ms": 12.0
}
When to Use
- Session start – first tool to call after
m1nd.healthto recover context - After ingest – see what the new ingest changed
- After extended learning – track cumulative drift from feedback
Related Tools
m1nd.diverge– higher-level structural drift with anomaly detectionm1nd.health– basic server health (call before drift)
m1nd.why
Path explanation between two nodes. Finds and explains the relationship paths connecting a source node to a target node. Returns all paths up to max_hops, ranked by cumulative edge strength.
Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
source | string | Yes | – | Source node identifier. |
target | string | Yes | – | Target node identifier. |
agent_id | string | Yes | – | Calling agent identifier. |
max_hops | integer | No | 6 | Maximum hops in path search. Higher values find more indirect paths but take longer. |
Example Request
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "m1nd.why",
"arguments": {
"agent_id": "orchestrator",
"source": "file::worker.py",
"target": "file::messaging.py",
"max_hops": 4
}
}
}
Example Response
{
"source": "file::worker.py",
"target": "file::messaging.py",
"paths": [
{
"nodes": ["worker.py", "process_manager.py", "messaging.py"],
"relations": ["calls::cancel", "imports"],
"cumulative_strength": 0.68,
"hops": 2
},
{
"nodes": ["worker.py", "worker.py", "handler.py", "messaging.py"],
"relations": ["imported_by", "calls", "imports"],
"cumulative_strength": 0.31,
"hops": 3
}
],
"total_paths_found": 2,
"elapsed_ms": 15.0
}
When to Use
- Understanding dependencies – “why are these two modules connected?”
- Tracing influence – find the relationship chain between distant modules
- Bug investigation – understand how a change in A could affect B
Related Tools
m1nd.hypothesize– tests a claim about the relationship (more powerful)m1nd.impact– finds all affected nodes (broader scope)
m1nd.trail.save
Persist the current investigation state – nodes visited, hypotheses formed, conclusions reached, and open questions. Captures activation boosts for later restoration.
Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
agent_id | string | Yes | – | Calling agent identifier. |
label | string | Yes | – | Human-readable label for this investigation. |
hypotheses | object[] | No | [] | Hypotheses formed during investigation. Each object has: statement (string, required), confidence (number, default 0.5), supporting_nodes (string[]), contradicting_nodes (string[]). |
conclusions | object[] | No | [] | Conclusions reached. Each object has: statement (string, required), confidence (number, default 0.5), from_hypotheses (string[]), supporting_nodes (string[]). |
open_questions | string[] | No | [] | Open questions remaining for future investigation. |
tags | string[] | No | [] | Tags for organization and search. |
summary | string | No | – | Optional summary. Auto-generated if omitted. |
visited_nodes | object[] | No | [] | Explicitly list visited nodes with annotations. Each object has: node_external_id (string, required), annotation (string, optional), relevance (number, default 0.5). If omitted, captured from active perspective state. |
activation_boosts | object | No | {} | Map of node_external_id to boost weight [0.0, 1.0]. Re-injected on resume. |
Example Request
{
"jsonrpc": "2.0",
"id": 4,
"method": "tools/call",
"params": {
"name": "m1nd.trail.save",
"arguments": {
"agent_id": "orchestrator",
"label": "auth-leak-investigation",
"hypotheses": [
{
"statement": "Auth tokens leak through session pool",
"confidence": 0.7,
"supporting_nodes": ["file::pool.py", "file::auth_discovery.py"]
},
{
"statement": "Rate limiter missing from auth chain",
"confidence": 0.9,
"supporting_nodes": ["file::middleware.py"]
}
],
"open_questions": ["Does the rate limiter apply to WebSocket connections?"],
"tags": ["security", "auth", "session"],
"activation_boosts": {
"file::pool.py": 0.8,
"file::auth_discovery.py": 0.6
}
}
}
}
Example Response
{
"trail_id": "trail_jimi_001_a1b2c3",
"label": "auth-leak-investigation",
"agent_id": "orchestrator",
"nodes_saved": 47,
"hypotheses_saved": 2,
"conclusions_saved": 0,
"open_questions_saved": 1,
"graph_generation_at_creation": 42,
"created_at_ms": 1710300000000
}
When to Use
- End of investigation session – save your work before ending a session
- Before context compaction – checkpoint your investigation state
- Cross-session continuity – resume exactly where you left off
Related Tools
m1nd.trail.resume– restore a saved trailm1nd.trail.list– find saved trailsm1nd.trail.merge– combine trails from parallel investigations
m1nd.trail.resume
Restore a saved investigation. Re-injects activation boosts into the graph, validates that saved nodes still exist, detects staleness, and optionally downgrades hypotheses whose supporting nodes are missing.
Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
agent_id | string | Yes | – | Calling agent identifier. |
trail_id | string | Yes | – | Trail ID to resume (from trail.save or trail.list). |
force | boolean | No | false | Resume even if trail is stale (>50% missing nodes). Default behavior: refuse to resume stale trails. |
Example Request
{
"jsonrpc": "2.0",
"id": 5,
"method": "tools/call",
"params": {
"name": "m1nd.trail.resume",
"arguments": {
"agent_id": "orchestrator",
"trail_id": "trail_jimi_001_a1b2c3"
}
}
}
Example Response
{
"trail_id": "trail_jimi_001_a1b2c3",
"label": "auth-leak-investigation",
"stale": false,
"generations_behind": 3,
"missing_nodes": [],
"nodes_reactivated": 47,
"hypotheses_downgraded": [],
"trail": {
"trail_id": "trail_jimi_001_a1b2c3",
"agent_id": "orchestrator",
"label": "auth-leak-investigation",
"status": "active",
"created_at_ms": 1710300000000,
"last_modified_ms": 1710300000000,
"node_count": 47,
"hypothesis_count": 2,
"conclusion_count": 0,
"open_question_count": 1,
"tags": ["security", "auth", "session"],
"summary": "Investigating auth token leaks through session pool"
},
"elapsed_ms": 22.5
}
When to Use
- Session start – restore a previous investigation
- Cross-agent handoff – agent B resumes agent A’s trail
- After re-ingest – check if investigation nodes survived the graph update
Related Tools
m1nd.trail.save– save a trail to resume laterm1nd.warmup– simpler priming without full trail restoration
m1nd.trail.list
List saved investigation trails with optional filters. Returns compact summaries suitable for selecting a trail to resume.
Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
agent_id | string | Yes | – | Calling agent identifier. |
filter_agent_id | string | No | – | Filter to a specific agent’s trails. None = all agents. |
filter_status | string | No | – | Filter by status: "active", "saved", "archived", "stale", "merged". |
filter_tags | string[] | No | [] | Filter by tags (any match). |
Example Request
{
"jsonrpc": "2.0",
"id": 6,
"method": "tools/call",
"params": {
"name": "m1nd.trail.list",
"arguments": {
"agent_id": "orchestrator",
"filter_status": "saved",
"filter_tags": ["security"]
}
}
}
Example Response
{
"trails": [
{
"trail_id": "trail_jimi_001_a1b2c3",
"agent_id": "orchestrator",
"label": "auth-leak-investigation",
"status": "saved",
"created_at_ms": 1710300000000,
"last_modified_ms": 1710300000000,
"node_count": 47,
"hypothesis_count": 2,
"conclusion_count": 0,
"open_question_count": 1,
"tags": ["security", "auth", "session"]
}
],
"total_count": 1
}
When to Use
- Session start – see what investigations are available to resume
- Multi-agent coordination – see trails from other agents
- Cleanup – find stale or merged trails
Related Tools
m1nd.trail.resume– resume a trail from this listm1nd.trail.merge– combine related trails
m1nd.trail.merge
Combine two or more investigation trails. Merges visited nodes, hypotheses, and conclusions. Uses confidence+recency scoring for conflict resolution. Discovers cross-connections between independently explored areas.
Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
agent_id | string | Yes | – | Calling agent identifier. |
trail_ids | string[] | Yes | – | Two or more trail IDs to merge. |
label | string | No | – | Label for the merged trail. Auto-generated if omitted. |
Example Request
{
"jsonrpc": "2.0",
"id": 7,
"method": "tools/call",
"params": {
"name": "m1nd.trail.merge",
"arguments": {
"agent_id": "orchestrator",
"trail_ids": ["trail_jimi_001_a1b2c3", "trail_analyst_002_d4e5f6"],
"label": "combined-auth-investigation"
}
}
}
Example Response
{
"merged_trail_id": "trail_jimi_003_g7h8i9",
"label": "combined-auth-investigation",
"source_trails": ["trail_jimi_001_a1b2c3", "trail_analyst_002_d4e5f6"],
"nodes_merged": 83,
"hypotheses_merged": 5,
"conflicts": [
{
"hypothesis_a": "Session pool leaks tokens",
"hypothesis_b": "Session pool tokens are properly scoped",
"resolution": "resolved",
"winner": "Session pool leaks tokens",
"score_delta": 0.35
}
],
"connections_discovered": [
{
"type": "bridge_edge",
"detail": "auth_discovery.py connects the auth trail to the session trail",
"from_node": "file::auth_discovery.py",
"to_node": "file::pool.py",
"weight": 0.72
}
],
"elapsed_ms": 45.0
}
Conflict Resolution
When merging hypotheses that contradict each other:
- confidence+recency scoring determines the winner
- If the score delta is too small, the conflict is marked
"unresolved"for human review - Source trails are set to
"merged"status after a successful merge
When to Use
- Multi-agent investigation – combine findings from parallel agents
- Investigation continuation – merge an old investigation with new findings
- Consolidation – clean up related but separate investigation threads
Related Tools
m1nd.trail.save– save individual trailsm1nd.trail.list– find trails to merge
Exploration Tools
Six tools for intent-aware search, pattern-based scanning, structural hole detection, stacktrace analysis, temporal history, and multi-repository federation.
m1nd.seek
Intent-aware semantic code search. Finds code by purpose, not text pattern. Combines keyword matching, graph activation (PageRank), and trigram similarity for ranking.
Unlike activate (which propagates signal through edges), seek scores every node independently against the query, making it better for finding specific code when you know what it does but not where it lives.
Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
query | string | Yes | – | Natural language description of what the agent is looking for. Example: "code that validates user credentials". |
agent_id | string | Yes | – | Calling agent identifier. |
top_k | integer | No | 20 | Maximum results to return. |
scope | string | No | – | File path prefix to limit search scope. Example: "backend/". None = entire graph. |
node_types | string[] | No | [] | Filter by node type: "function", "class", "struct", "module", "file". Empty = all types. |
min_score | number | No | 0.1 | Minimum combined score threshold. Range: 0.0 to 1.0. |
graph_rerank | boolean | No | true | Whether to run graph re-ranking (PageRank weighting) on candidates. Disable for pure text matching. |
Scoring Formula (V1)
combined = keyword_match * 0.6 + graph_activation * 0.3 + trigram * 0.1
V2 upgrade path will replace keyword matching with real embeddings (fastembed-rs + jina-embeddings-v2-base-code).
Example Request
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "m1nd.seek",
"arguments": {
"agent_id": "orchestrator",
"query": "code that validates user credentials",
"top_k": 5,
"node_types": ["function", "class"]
}
}
}
Example Response
{
"query": "code that validates user credentials",
"results": [
{
"node_id": "file::auth_discovery.py::fn::validate_credentials",
"label": "validate_credentials",
"type": "function",
"score": 0.87,
"score_breakdown": {
"embedding_similarity": 0.92,
"graph_activation": 0.78,
"temporal_recency": 0.65
},
"intent_summary": "Validates user credentials against provider store",
"file_path": "backend/auth_discovery.py",
"line_start": 45,
"line_end": 82,
"connections": [
{ "node_id": "file::principal_registry.py", "label": "principal_registry.py", "relation": "calls" }
]
}
],
"total_candidates_scanned": 9767,
"embeddings_used": false,
"elapsed_ms": 25.0
}
When to Use
- “Find the code that does X” – when you know the purpose, not the filename
- Codebase onboarding – exploring unfamiliar code by intent
- Pre-modification search – find all code related to a feature before changing it
Related Tools
m1nd.activate– graph-propagation search (better for exploring neighborhoods)m1nd.scan– pattern-based structural analysis (finds anti-patterns, not features)
m1nd.scan
Pattern-aware structural code analysis with graph-validated findings. Detects structural issues using predefined patterns, then validates each finding against the graph to filter false positives. Works across file boundaries.
Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
pattern | string | Yes | – | Pattern ID or custom pattern string. Built-in patterns: "error_handling", "resource_cleanup", "api_surface", "state_mutation", "concurrency", "auth_boundary", "test_coverage", "dependency_injection". |
agent_id | string | Yes | – | Calling agent identifier. |
scope | string | No | – | File path prefix to limit scan scope. |
severity_min | number | No | 0.3 | Minimum severity threshold. Range: 0.0 to 1.0. |
graph_validate | boolean | No | true | Validate findings against graph edges (cross-file analysis). Disable for raw pattern matching only. |
limit | integer | No | 50 | Maximum findings to return. |
Example Request
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "m1nd.scan",
"arguments": {
"agent_id": "orchestrator",
"pattern": "error_handling",
"scope": "backend/",
"severity_min": 0.5
}
}
}
Example Response
{
"pattern": "error_handling",
"findings": [
{
"pattern": "error_handling",
"status": "confirmed",
"severity": 0.78,
"node_id": "file::worker.py::fn::spawn_agent",
"label": "spawn_agent",
"file_path": "backend/worker.py",
"line": 89,
"message": "Bare except clause catches all exceptions including KeyboardInterrupt",
"graph_context": [
{ "node_id": "file::process_manager.py", "label": "process_manager.py", "relation": "calls" }
]
}
],
"files_scanned": 77,
"total_matches_raw": 23,
"total_matches_validated": 8,
"elapsed_ms": 150.0
}
Finding Status Values
| Status | Meaning |
|---|---|
"confirmed" | Graph validation confirms the issue is real |
"mitigated" | The issue exists but is handled by a related module |
"false_positive" | Graph context shows the pattern match is not actually an issue |
When to Use
- Code quality audit – scan for anti-patterns across the codebase
- Security review – use
"auth_boundary"to find auth bypass paths - Pre-deploy check – scan for
"error_handling"and"resource_cleanup"issues - Test gaps – use
"test_coverage"to find untested code
Related Tools
m1nd.hypothesize– test a specific structural claimm1nd.trace– analyze a specific error (not patterns)
m1nd.missing
Detect structural holes and missing connections. Given a topic query, finds areas where the graph suggests something should exist but does not. Identifies absent abstractions, missing connections, and incomplete patterns.
Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
query | string | Yes | – | Search query to find structural holes around. |
agent_id | string | Yes | – | Calling agent identifier. |
min_sibling_activation | number | No | 0.3 | Minimum sibling activation threshold. Siblings with activation below this are not considered for hole detection. |
Example Request
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "m1nd.missing",
"arguments": {
"agent_id": "orchestrator",
"query": "database connection pooling",
"min_sibling_activation": 0.3
}
}
}
Example Response
{
"query": "database connection pooling",
"holes": [
{
"node_id": "structural_hole_1",
"label": "connection lifecycle",
"type": "structural_hole",
"reason": "No dedicated connection pool abstraction -- 4 adjacent modules manage connections independently"
},
{
"node_id": "structural_hole_2",
"label": "pool metrics",
"type": "structural_hole",
"reason": "No pool health monitoring -- 3 adjacent modules expose metrics but pool does not"
}
],
"total_holes": 9,
"elapsed_ms": 67.0
}
When to Use
- Gap analysis – “what am I missing?” before implementing a feature
- Pre-spec – identify areas that need design before building
- Architecture review – find missing abstractions or connections
- Feature completeness – after building, check for structural holes around the feature
Related Tools
m1nd.activate– activate withinclude_structural_holes: truefor inline hole detectionm1nd.hypothesize– test a specific claim about missing structure
m1nd.trace
Map runtime errors to structural root causes via stacktrace analysis. Parses the stacktrace, maps frames to graph nodes, and scores each node’s suspiciousness using trace depth, modification recency, and centrality. Also finds co-change suspects (files modified around the same time as the top suspect).
Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
error_text | string | Yes | – | Full error output (stacktrace + error message). |
agent_id | string | Yes | – | Calling agent identifier. |
language | string | No | – | Language hint: "python", "rust", "typescript", "javascript", "go". Auto-detected if omitted. |
window_hours | number | No | 24.0 | Temporal window (hours) for co-change suspect scan. |
top_k | integer | No | 10 | Max suspects to return. |
Example Request
{
"jsonrpc": "2.0",
"id": 4,
"method": "tools/call",
"params": {
"name": "m1nd.trace",
"arguments": {
"agent_id": "orchestrator",
"error_text": "Traceback (most recent call last):\n File \"backend/handler.py\", line 234, in handle_message\n File \"backend/pool.py\", line 89, in acquire\n File \"backend/worker.py\", line 156, in submit\nTimeoutError: pool exhausted",
"language": "python",
"top_k": 5
}
}
}
Example Response
{
"language_detected": "python",
"error_type": "TimeoutError",
"error_message": "pool exhausted",
"frames_parsed": 3,
"frames_mapped": 3,
"suspects": [
{
"node_id": "file::worker.py::fn::submit",
"label": "submit",
"type": "function",
"suspiciousness": 0.91,
"signals": { "trace_depth_score": 1.0, "recency_score": 0.85, "centrality_score": 0.88 },
"file_path": "backend/worker.py",
"line_start": 150,
"line_end": 175,
"related_callers": ["pool.py::acquire"]
},
{
"node_id": "file::pool.py::fn::acquire",
"label": "acquire",
"type": "function",
"suspiciousness": 0.78,
"signals": { "trace_depth_score": 0.67, "recency_score": 0.72, "centrality_score": 0.65 },
"file_path": "backend/pool.py",
"line_start": 80,
"line_end": 110,
"related_callers": ["handler.py::handle_message"]
}
],
"co_change_suspects": [
{ "node_id": "file::config.py", "label": "config.py", "modified_at": 1710295000.0, "reason": "Modified within 2h of top suspect" }
],
"causal_chain": ["worker.py::submit", "pool.py::acquire", "handler.py::handle_message"],
"fix_scope": {
"files_to_inspect": ["backend/worker.py", "backend/pool.py", "backend/config.py"],
"estimated_blast_radius": 23,
"risk_level": "medium"
},
"unmapped_frames": [],
"elapsed_ms": 3.5
}
Suspiciousness Scoring
| Signal | Weight | Description |
|---|---|---|
trace_depth_score | High | 1.0 = deepest frame (most specific); decays linearly |
recency_score | Medium | Exponential decay from last modification time |
centrality_score | Medium | Normalized PageRank centrality |
When to Use
- Bug investigation – paste an error and get ranked suspects
- Root cause analysis – the causal chain shows the error propagation path
- Fix scoping –
fix_scopetells you which files to inspect and the risk level
Related Tools
m1nd.hypothesize– test a hypothesis about the root causem1nd.impact– assess the blast radius of a fixm1nd.validate_plan– validate your fix plan before implementing
m1nd.timeline
Git-based temporal history for a node. Returns the change history, co-change partners, velocity, stability score, and churn data for a specific file or module.
Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
node | string | Yes | – | Node external_id. Example: "file::backend/handler.py". |
agent_id | string | Yes | – | Calling agent identifier. |
depth | string | No | "30d" | Time depth. Values: "7d", "30d", "90d", "all". |
include_co_changes | boolean | No | true | Include co-changed files with coupling scores. |
include_churn | boolean | No | true | Include lines added/deleted churn data. |
top_k | integer | No | 10 | Max co-change partners to return. |
Example Request
{
"jsonrpc": "2.0",
"id": 5,
"method": "tools/call",
"params": {
"name": "m1nd.timeline",
"arguments": {
"agent_id": "orchestrator",
"node": "file::backend/handler.py",
"depth": "30d",
"top_k": 5
}
}
}
Example Response
{
"node": "file::backend/handler.py",
"depth": "30d",
"changes": [
{
"date": "2026-03-10",
"commit": "a1b2c3d",
"author": "cosmophonix",
"delta": "+45/-12",
"co_changed": ["parser.py", "chat_routes.py"]
},
{
"date": "2026-03-05",
"commit": "e4f5g6h",
"author": "cosmophonix",
"delta": "+120/-30",
"co_changed": ["pool.py", "worker.py"]
}
],
"co_changed_with": [
{ "file": "parser.py", "times": 8, "coupling_degree": 0.72 },
{ "file": "chat_routes.py", "times": 6, "coupling_degree": 0.55 },
{ "file": "pool.py", "times": 4, "coupling_degree": 0.38 }
],
"velocity": "accelerating",
"stability_score": 0.35,
"pattern": "churning",
"total_churn": { "lines_added": 580, "lines_deleted": 120 },
"commit_count_in_window": 12,
"elapsed_ms": 45.0
}
Velocity Values
| Value | Meaning |
|---|---|
"accelerating" | Change frequency is increasing |
"decelerating" | Change frequency is decreasing |
"stable" | Consistent change rate |
Pattern Values
| Value | Meaning |
|---|---|
"expanding" | Growing (net positive churn) |
"shrinking" | Reducing (net negative churn) |
"churning" | High add+delete with little net growth |
"dormant" | Few or no changes in the window |
"stable" | Small, consistent changes |
When to Use
- Hotspot detection – find files that change too frequently (stability_score < 0.3)
- Co-change discovery – find files that always change together (coupling_degree > 0.6)
- Refactoring signals – churning files may need redesign
Related Tools
m1nd.diverge– structural drift across the whole codebasem1nd.predict– predict co-changes for a given modification
m1nd.federate
Ingest multiple repositories into a unified federated graph with automatic cross-repo edge detection. After federation, all existing query tools (activate, impact, why, etc.) traverse cross-repo edges automatically.
Node IDs in the federated graph use {repo_name}::file::path format.
Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
agent_id | string | Yes | – | Calling agent identifier. |
repos | object[] | Yes | – | List of repositories to federate. Each object has: name (string, required – namespace prefix), path (string, required – absolute path), adapter (string, default "code"). |
detect_cross_repo_edges | boolean | No | true | Auto-detect cross-repo edges: config, API, import, type, deployment, MCP contract. |
incremental | boolean | No | false | Only re-ingest repos that changed since last federation. |
Cross-Repo Edge Types
| Edge Type | Description |
|---|---|
shared_config | Two repos reference the same configuration key |
api_contract | One repo’s API client matches another’s API server |
package_dep | Direct package dependency |
shared_type | Same type/interface definition used across repos |
deployment_dep | Deployment configuration dependency |
mcp_contract | MCP tool consumer/provider relationship |
Example Request
{
"jsonrpc": "2.0",
"id": 6,
"method": "tools/call",
"params": {
"name": "m1nd.federate",
"arguments": {
"agent_id": "orchestrator",
"repos": [
{ "name": "backend", "path": "/project/backend" },
{ "name": "frontend", "path": "/project/frontend" },
{ "name": "mcp-server", "path": "/project/mcp-server", "adapter": "code" }
],
"detect_cross_repo_edges": true,
"incremental": false
}
}
}
Example Response
{
"repos_ingested": [
{ "name": "backend", "path": "/project/backend", "node_count": 9767, "edge_count": 26557, "from_cache": false, "ingest_ms": 910.0 },
{ "name": "frontend", "path": "/project/frontend", "node_count": 1200, "edge_count": 3500, "from_cache": false, "ingest_ms": 320.0 },
{ "name": "mcp-server", "path": "/project/mcp-server", "node_count": 250, "edge_count": 680, "from_cache": false, "ingest_ms": 85.0 }
],
"total_nodes": 11217,
"total_edges": 30737,
"cross_repo_edges": [
{
"source_repo": "frontend",
"target_repo": "backend",
"source_node": "frontend::file::src/lib/apiConfig.ts",
"target_node": "backend::file::main.py",
"edge_type": "api_contract",
"relation": "calls_api",
"weight": 0.85,
"causal_strength": 0.72
},
{
"source_repo": "mcp-server",
"target_repo": "backend",
"source_node": "mcp-server::file::src/mcp-server.js",
"target_node": "backend::file::main.py",
"edge_type": "mcp_contract",
"relation": "calls_api",
"weight": 0.78,
"causal_strength": 0.65
}
],
"cross_repo_edge_count": 18203,
"incremental": false,
"skipped_repos": [],
"elapsed_ms": 1315.0
}
When to Use
- Multi-repo projects – analyze dependencies across frontend, backend, and infrastructure repos
- Monorepo decomposition – understand how packages depend on each other
- API impact analysis – find which repos are affected by an API change
Related Tools
m1nd.ingest– single-repo ingestionm1nd.impact– blast radius analysis (works across federated repos)m1nd.why– path explanation (traverses cross-repo edges)
Perspective Tools
Twelve tools for stateful, navigable exploration of the code graph. Perspectives are m1nd’s primary interface for guided codebase exploration – they synthesize routes (weighted navigation suggestions) from a query, then let you follow, inspect, branch, and compare routes.
Core Concepts
Perspective: A stateful exploration session. Created from a query, it synthesizes a set of routes from the current graph state. The agent navigates by following routes, which moves the focus and generates new routes.
Route: A suggested direction of exploration. Each route points to a target node and carries a score, a family classification, and a path preview.
Focus: The current node the perspective is centered on. Following a route moves the focus. back restores the previous focus.
Mode: Either "anchored" (routes stay related to an anchor node) or "local" (routes synthesized purely from the current focus). Anchored mode degrades to local if navigation moves more than 8 hops from the anchor.
Route Set Version: A version counter for the current route set. Clients must pass this in subsequent calls for staleness detection. If the graph changes between calls, the version changes and stale requests are rejected.
Lens: Optional filtering configuration for perspectives (dimension weights, node type filters, etc.).
m1nd.perspective.start
Enter a perspective: creates a navigable route surface from a query. Returns the initial set of routes.
Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
agent_id | string | Yes | – | Calling agent identifier. |
query | string | Yes | – | Seed query for route synthesis. |
anchor_node | string | No | – | Anchor to a specific node. If provided, activates anchored mode where all routes maintain relevance to this node. |
lens | object | No | – | Starting lens configuration. Controls dimension weights, node type filters, etc. |
Example Request
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "m1nd.perspective.start",
"arguments": {
"agent_id": "orchestrator",
"query": "session management and connection pooling",
"anchor_node": "file::pool.py"
}
}
}
Example Response
{
"perspective_id": "persp_jimi_001",
"mode": "anchored",
"anchor_node": "file::pool.py",
"focus_node": "file::pool.py",
"routes": [
{
"route_id": "R_a1b2c3",
"index": 1,
"target_node": "file::worker.py",
"target_label": "worker.py",
"family": "structural_neighbor",
"score": 0.89,
"path_preview": ["pool.py", "worker.py"],
"reason": "High structural coupling with anchor"
},
{
"route_id": "R_d4e5f6",
"index": 2,
"target_node": "file::process_manager.py",
"target_label": "process_manager.py",
"family": "causal_downstream",
"score": 0.76,
"path_preview": ["pool.py", "process_manager.py"],
"reason": "Causal dependency via imports"
}
],
"total_routes": 12,
"page": 1,
"total_pages": 2,
"route_set_version": 100,
"cache_generation": 42,
"suggested": "inspect R_a1b2c3 for structural details"
}
When to Use
- Guided exploration – when you want to explore a topic interactively
- Codebase navigation – start from a known file and discover related code
- Investigation – anchor to a suspicious file and follow routes to find related issues
Related Tools
m1nd.activate– one-shot query without navigation statem1nd.perspective.routes– paginate through routesm1nd.perspective.follow– follow a route to navigate
m1nd.perspective.routes
Browse the current route set with pagination.
Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
agent_id | string | Yes | – | Calling agent identifier. |
perspective_id | string | Yes | – | Perspective to browse. |
page | integer | No | 1 | Page number (1-based). |
page_size | integer | No | 6 | Routes per page. Clamped to [1, 10]. |
route_set_version | integer | No | – | Version from previous response for staleness check. |
Example Request
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "m1nd.perspective.routes",
"arguments": {
"agent_id": "orchestrator",
"perspective_id": "persp_jimi_001",
"page": 2,
"page_size": 6,
"route_set_version": 100
}
}
}
Example Response
{
"perspective_id": "persp_jimi_001",
"mode": "anchored",
"mode_effective": "anchored",
"anchor": "file::pool.py",
"focus": "file::pool.py",
"lens_summary": "all dimensions, no type filter",
"page": 2,
"total_pages": 2,
"total_routes": 12,
"route_set_version": 100,
"cache_generation": 42,
"routes": [
{
"route_id": "R_g7h8i9",
"index": 7,
"target_node": "file::config.py",
"target_label": "config.py",
"family": "configuration",
"score": 0.42,
"path_preview": ["pool.py", "config.py"],
"reason": "Shared configuration key"
}
],
"suggested": null,
"page_size_clamped": false
}
When to Use
- Pagination – browse all available routes when the first page was not enough
- Full route survey – see the complete landscape before deciding which route to follow
Related Tools
m1nd.perspective.inspect– expand a specific route for detailm1nd.perspective.follow– follow a route to navigate
m1nd.perspective.inspect
Expand a route with fuller path, metrics, provenance, affinity candidates, and score breakdown. Does not change the perspective state – purely informational.
Specify the route by either route_id (stable, content-addressed) or route_index (1-based position on the current page). Exactly one must be provided.
Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
agent_id | string | Yes | – | Calling agent identifier. |
perspective_id | string | Yes | – | Perspective containing the route. |
route_id | string | No | – | Stable content-addressed route ID. |
route_index | integer | No | – | 1-based page-local position. |
route_set_version | integer | Yes | – | Route set version for staleness check. |
Example Request
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "m1nd.perspective.inspect",
"arguments": {
"agent_id": "orchestrator",
"perspective_id": "persp_jimi_001",
"route_id": "R_a1b2c3",
"route_set_version": 100
}
}
}
Example Response
{
"route_id": "R_a1b2c3",
"route_index": 1,
"family": "structural_neighbor",
"target_node": "file::worker.py",
"target_label": "worker.py",
"target_type": "file",
"path_preview": ["pool.py", "worker.py", "worker.py"],
"family_explanation": "Direct structural neighbor via import chain",
"score": 0.89,
"score_breakdown": {
"local_activation": 0.92,
"path_coherence": 0.88,
"novelty": 0.75,
"anchor_relevance": 0.95,
"continuity": 0.82
},
"provenance": {
"source_path": "backend/worker.py",
"line_start": 1,
"line_end": 312,
"namespace": null,
"provenance_stale": false
},
"peek_available": true,
"affinity_candidates": [
{ "node_id": "file::fast_pool.py", "label": "fast_pool.py", "strength": 0.72, "reason": "Similar pool pattern" }
],
"response_chars": 450
}
When to Use
- Before following – understand what a route leads to before committing
- Score analysis – see why a route was ranked high or low
- Provenance check – verify the source file exists and is not stale
Related Tools
m1nd.perspective.peek– extract actual code contentm1nd.perspective.follow– navigate to the route targetm1nd.perspective.affinity– deeper affinity analysis
m1nd.perspective.peek
Extract a small relevant code or documentation slice from a route’s target node. Reads the actual source file and returns the relevant excerpt. Security-checked: only reads files within the ingested graph scope.
Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
agent_id | string | Yes | – | Calling agent identifier. |
perspective_id | string | Yes | – | Perspective containing the route. |
route_id | string | No | – | Stable content-addressed route ID. |
route_index | integer | No | – | 1-based page-local position. |
route_set_version | integer | Yes | – | Route set version for staleness check. |
Example Request
{
"jsonrpc": "2.0",
"id": 4,
"method": "tools/call",
"params": {
"name": "m1nd.perspective.peek",
"arguments": {
"agent_id": "orchestrator",
"perspective_id": "persp_jimi_001",
"route_index": 1,
"route_set_version": 100
}
}
}
Example Response
{
"route_id": "R_a1b2c3",
"route_index": 1,
"target_node": "file::worker.py",
"content": {
"excerpt": "class WorkerPool:\n \"\"\"Manages a pool of CLI subprocess workers.\"\"\"\n def __init__(self, max_workers=8):\n self.pool = {}\n self.max_workers = max_workers\n ...",
"file_path": "backend/worker.py",
"line_start": 15,
"line_end": 45,
"truncated": true
}
}
When to Use
- Quick preview – see the actual code at a route target without leaving m1nd
- Decision making – peek at code to decide whether to follow a route
- Investigation – read suspicious code without opening a file
Related Tools
m1nd.perspective.inspect– structural information about the route (no code content)m1nd.perspective.follow– navigate to the route target
m1nd.perspective.follow
Follow a route: move focus to the target node and synthesize new routes from the new position. This is the primary navigation action in a perspective. Updates the navigation history for back.
Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
agent_id | string | Yes | – | Calling agent identifier. |
perspective_id | string | Yes | – | Perspective to navigate within. |
route_id | string | No | – | Stable content-addressed route ID. |
route_index | integer | No | – | 1-based page-local position. |
route_set_version | integer | Yes | – | Route set version for staleness check. |
Example Request
{
"jsonrpc": "2.0",
"id": 5,
"method": "tools/call",
"params": {
"name": "m1nd.perspective.follow",
"arguments": {
"agent_id": "orchestrator",
"perspective_id": "persp_jimi_001",
"route_id": "R_a1b2c3",
"route_set_version": 100
}
}
}
Example Response
{
"perspective_id": "persp_jimi_001",
"previous_focus": "file::pool.py",
"new_focus": "file::worker.py",
"mode": "anchored",
"mode_effective": "anchored",
"routes": [
{
"route_id": "R_j1k2l3",
"index": 1,
"target_node": "file::process_manager.py",
"target_label": "process_manager.py",
"family": "causal_downstream",
"score": 0.85,
"path_preview": ["worker.py", "process_manager.py"],
"reason": "Direct caller of worker pool submit"
},
{
"route_id": "R_m4n5o6",
"index": 2,
"target_node": "file::worker.py",
"target_label": "worker.py",
"family": "structural_neighbor",
"score": 0.71,
"path_preview": ["worker.py", "worker.py"],
"reason": "Co-manages subprocess lifecycle"
}
],
"total_routes": 8,
"page": 1,
"total_pages": 2,
"route_set_version": 101,
"cache_generation": 42,
"suggested": "inspect R_j1k2l3 -- high causal relevance"
}
When to Use
- Navigation – this is the primary way to move through the graph
- Guided exploration – follow high-scoring routes to discover related code
- Investigation – follow causal chains to trace a bug
Related Tools
m1nd.perspective.back– undo a followm1nd.perspective.branch– fork before following to explore alternatives
m1nd.perspective.suggest
Get the next best move suggestion based on navigation history. Analyzes the routes, the current focus, and the history of followed routes to recommend what to do next.
Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
agent_id | string | Yes | – | Calling agent identifier. |
perspective_id | string | Yes | – | Perspective to get a suggestion for. |
route_set_version | integer | Yes | – | Route set version for staleness check. |
Example Request
{
"jsonrpc": "2.0",
"id": 6,
"method": "tools/call",
"params": {
"name": "m1nd.perspective.suggest",
"arguments": {
"agent_id": "orchestrator",
"perspective_id": "persp_jimi_001",
"route_set_version": 101
}
}
}
Example Response
{
"perspective_id": "persp_jimi_001",
"suggestion": {
"action": "follow",
"route_id": "R_j1k2l3",
"reason": "High-scoring causal route not yet explored. process_manager.py is a hub node connecting to 47 downstream modules.",
"confidence": 0.82
}
}
When to Use
- Undecided – when you are not sure which route to follow
- Systematic exploration – let m1nd guide you through the most informative path
- Investigation – follow the suggestion trail to efficient root cause discovery
Related Tools
m1nd.perspective.follow– follow the suggested routem1nd.perspective.inspect– inspect the suggested route first
m1nd.perspective.affinity
Discover probable connections a route target might have. Returns affinity candidates – nodes that are not directly connected to the target but show structural affinity (similar neighborhoods, shared patterns).
Epistemic notice: these are probable connections, not verified graph edges.
Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
agent_id | string | Yes | – | Calling agent identifier. |
perspective_id | string | Yes | – | Perspective containing the route. |
route_id | string | No | – | Stable content-addressed route ID. |
route_index | integer | No | – | 1-based page-local position. |
route_set_version | integer | Yes | – | Route set version for staleness check. |
Example Request
{
"jsonrpc": "2.0",
"id": 7,
"method": "tools/call",
"params": {
"name": "m1nd.perspective.affinity",
"arguments": {
"agent_id": "orchestrator",
"perspective_id": "persp_jimi_001",
"route_id": "R_j1k2l3",
"route_set_version": 101
}
}
}
Example Response
{
"route_id": "R_j1k2l3",
"target_node": "file::process_manager.py",
"notice": "Probable connections, not verified edges.",
"candidates": [
{ "node_id": "file::recovery.py", "label": "recovery.py", "strength": 0.68, "reason": "Similar lifecycle management pattern" },
{ "node_id": "file::autonomous_daemon.py", "label": "autonomous_daemon.py", "strength": 0.52, "reason": "Shared subprocess management trait" }
]
}
When to Use
- Discovery – find non-obvious connections that the graph does not yet have
- Architecture analysis – identify modules that should be connected but are not
- Ghost edge validation – affinity candidates often become confirmed edges after investigation
Related Tools
m1nd.missing– finds structural holes (broader scope)m1nd.perspective.inspect– affinity candidates are also included in inspect output
m1nd.perspective.branch
Fork the current navigation state into a new perspective branch. The branch starts at the same focus with the same route set, but future navigation in the branch is independent. Useful for exploring alternatives without losing your place.
Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
agent_id | string | Yes | – | Calling agent identifier. |
perspective_id | string | Yes | – | Perspective to branch from. |
branch_name | string | No | – | Optional branch name. Auto-generated if not provided. |
Example Request
{
"jsonrpc": "2.0",
"id": 8,
"method": "tools/call",
"params": {
"name": "m1nd.perspective.branch",
"arguments": {
"agent_id": "orchestrator",
"perspective_id": "persp_jimi_001",
"branch_name": "auth-path"
}
}
}
Example Response
{
"perspective_id": "persp_jimi_001",
"branch_perspective_id": "persp_jimi_002",
"branch_name": "auth-path",
"branched_from_focus": "file::worker.py"
}
When to Use
- Alternative exploration – explore two different paths from the same point
- Comparative analysis – branch, follow different routes, then compare
- Safe investigation – branch before following a risky route
Related Tools
m1nd.perspective.compare– compare the branch with the originalm1nd.perspective.follow– navigate within the branch
m1nd.perspective.back
Navigate back to the previous focus, restoring the checkpoint state. Like browser back navigation. Pops the latest navigation event from the history stack.
Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
agent_id | string | Yes | – | Calling agent identifier. |
perspective_id | string | Yes | – | Perspective to navigate back in. |
Example Request
{
"jsonrpc": "2.0",
"id": 9,
"method": "tools/call",
"params": {
"name": "m1nd.perspective.back",
"arguments": {
"agent_id": "orchestrator",
"perspective_id": "persp_jimi_001"
}
}
}
Example Response
{
"perspective_id": "persp_jimi_001",
"restored_focus": "file::pool.py",
"restored_mode": "anchored",
"routes": [
{
"route_id": "R_a1b2c3",
"index": 1,
"target_node": "file::worker.py",
"target_label": "worker.py",
"family": "structural_neighbor",
"score": 0.89,
"path_preview": ["pool.py", "worker.py"],
"reason": "High structural coupling with anchor"
}
],
"total_routes": 12,
"page": 1,
"total_pages": 2,
"route_set_version": 102,
"cache_generation": 42
}
When to Use
- Undo navigation – go back after following a wrong route
- Breadth-first exploration – follow a route, come back, follow another
Related Tools
m1nd.perspective.follow– the forward navigation thatbackundoesm1nd.perspective.branch– alternative: branch instead of back+follow
m1nd.perspective.compare
Compare two perspectives on shared/unique nodes and dimension deltas. Both perspectives must belong to the same agent (V1 restriction).
Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
agent_id | string | Yes | – | Calling agent identifier. |
perspective_id_a | string | Yes | – | First perspective ID. |
perspective_id_b | string | Yes | – | Second perspective ID. |
dimensions | string[] | No | [] | Dimensions to compare on. Empty = all dimensions. |
Example Request
{
"jsonrpc": "2.0",
"id": 10,
"method": "tools/call",
"params": {
"name": "m1nd.perspective.compare",
"arguments": {
"agent_id": "orchestrator",
"perspective_id_a": "persp_jimi_001",
"perspective_id_b": "persp_jimi_002"
}
}
}
Example Response
{
"perspective_id_a": "persp_jimi_001",
"perspective_id_b": "persp_jimi_002",
"shared_nodes": ["worker.py", "process_manager.py", "pool.py"],
"unique_to_a": ["worker.py", "config.py"],
"unique_to_b": ["auth_discovery.py", "middleware.py"],
"dimension_deltas": [
{ "dimension": "structural", "score_a": 0.82, "score_b": 0.75, "delta": 0.07 },
{ "dimension": "semantic", "score_a": 0.65, "score_b": 0.88, "delta": -0.23 },
{ "dimension": "causal", "score_a": 0.71, "score_b": 0.45, "delta": 0.26 }
],
"response_chars": 380
}
When to Use
- Branch comparison – compare a branch perspective with the original
- Multi-approach analysis – see how two different starting queries reach different parts of the graph
- Investigation correlation – find shared nodes between two independent investigations
Related Tools
m1nd.perspective.branch– create branches to comparem1nd.trail.merge– merge investigation trails (deeper than compare)
m1nd.perspective.list
List all perspectives for an agent. Returns compact summaries with status, focus, route count, and memory usage.
Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
agent_id | string | Yes | – | Calling agent identifier. |
Example Request
{
"jsonrpc": "2.0",
"id": 11,
"method": "tools/call",
"params": {
"name": "m1nd.perspective.list",
"arguments": {
"agent_id": "orchestrator"
}
}
}
Example Response
{
"agent_id": "orchestrator",
"perspectives": [
{
"perspective_id": "persp_jimi_001",
"mode": "anchored",
"focus_node": "file::worker.py",
"route_count": 12,
"nav_event_count": 3,
"stale": false,
"created_at_ms": 1710300000000,
"last_accessed_ms": 1710300500000
},
{
"perspective_id": "persp_jimi_002",
"mode": "anchored",
"focus_node": "file::auth_discovery.py",
"route_count": 8,
"nav_event_count": 1,
"stale": false,
"created_at_ms": 1710300200000,
"last_accessed_ms": 1710300400000
}
],
"total_memory_bytes": 24576
}
When to Use
- Session overview – see all active perspectives
- Memory management – check total memory usage and close stale perspectives
- Multi-perspective work – switch between perspectives
Related Tools
m1nd.perspective.close– close perspectives you no longer need
m1nd.perspective.close
Close a perspective and release associated locks. Frees memory and stops route caching for this perspective. Cascade-releases any locks that were associated with this perspective.
Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
agent_id | string | Yes | – | Calling agent identifier. |
perspective_id | string | Yes | – | Perspective to close. |
Example Request
{
"jsonrpc": "2.0",
"id": 12,
"method": "tools/call",
"params": {
"name": "m1nd.perspective.close",
"arguments": {
"agent_id": "orchestrator",
"perspective_id": "persp_jimi_002"
}
}
}
Example Response
{
"perspective_id": "persp_jimi_002",
"closed": true,
"locks_released": ["lock_jimi_003"]
}
When to Use
- Cleanup – close perspectives after investigation is complete
- Memory pressure – close unused perspectives to free memory
- Session end – close all perspectives before ending a session
Related Tools
m1nd.perspective.list– find perspectives to close
Full Workflow Example
A complete perspective exploration session, from start to close:
1. START a perspective from a query
m1nd.perspective.start(query="authentication and session security")
-> persp_jimi_001, 12 routes, anchored to auth_discovery.py
2. BROWSE routes (page 1 already returned by start)
Routes: R01 auth_discovery.py, R02 middleware.py, R03 principal_registry.py, ...
3. INSPECT a high-scoring route before following
m1nd.perspective.inspect(route_id="R01", route_set_version=100)
-> score_breakdown, provenance, affinity_candidates
4. FOLLOW the route to navigate
m1nd.perspective.follow(route_id="R01", route_set_version=100)
-> new focus: auth_discovery.py, 8 new routes, version=101
5. ASK for a suggestion on what to do next
m1nd.perspective.suggest(route_set_version=101)
-> "follow R_x: principal_registry.py, high causal relevance"
6. BRANCH before exploring a risky path
m1nd.perspective.branch(branch_name="session-path")
-> new persp_jimi_002, same focus
7. FOLLOW different routes in each branch
Branch 1: follow toward principal_registry.py
Branch 2: follow toward pool.py
8. COMPARE the two branches
m1nd.perspective.compare(perspective_id_a="persp_jimi_001", perspective_id_b="persp_jimi_002")
-> shared: [auth_discovery.py], unique_to_a: [principal_registry.py], unique_to_b: [pool.py]
9. BACK to undo the last follow in branch 1
m1nd.perspective.back()
-> restored focus: auth_discovery.py
10. CLOSE both perspectives when done
m1nd.perspective.close(perspective_id="persp_jimi_001")
m1nd.perspective.close(perspective_id="persp_jimi_002")
This workflow demonstrates the full exploration cycle: start with a query, navigate through the graph by following routes, branch to explore alternatives, compare branches, and close when done.
Lifecycle & Lock Tools
Eight tools for graph ingestion, health monitoring, plan validation, and subgraph locking with change detection.
m1nd.ingest
Ingest or re-ingest a codebase, descriptor, or memory corpus into the graph. This is the primary way to load data into m1nd. Supports three adapters (code, json, memory) and two modes (replace, merge).
Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
path | string | Yes | – | Filesystem path to the source root or memory corpus. |
agent_id | string | Yes | – | Calling agent identifier. |
incremental | boolean | No | false | Incremental ingest (code adapter only). Only re-processes files that changed since the last ingest. |
adapter | string | No | "code" | Adapter to use for parsing. Values: "code" (source code – Python, Rust, TypeScript, etc.), "json" (graph snapshot JSON), "memory" (markdown memory corpus). |
mode | string | No | "replace" | How to handle the existing graph. Values: "replace" (clear and rebuild), "merge" (add new nodes/edges into existing graph). |
namespace | string | No | – | Optional namespace tag for non-code nodes. Used by memory and json adapters to prefix node external_ids. |
Example Request
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "m1nd.ingest",
"arguments": {
"agent_id": "orchestrator",
"path": "/path/to/your/project",
"adapter": "code",
"mode": "replace",
"incremental": false
}
}
}
Example Response
{
"files_processed": 335,
"nodes_created": 9767,
"edges_created": 26557,
"languages": { "python": 335 },
"elapsed_ms": 910.0
}
Adapters
| Adapter | Input | Node Types | Edge Types |
|---|---|---|---|
code | Source code directory | file, class, function, struct, module | imports, calls, registers, configures, tests, inherits |
json | Graph snapshot JSON | (preserved from snapshot) | (preserved from snapshot) |
memory | Markdown files | document, concept, entity | references, relates_to |
Mode Behavior
| Mode | Behavior |
|---|---|
replace | Clears the existing graph, ingests fresh, finalizes (PageRank + CSR). All perspectives and locks are invalidated. |
merge | Adds new nodes and edges into the existing graph. Existing nodes are updated if they share the same external_id. Graph is re-finalized after merge. |
When to Use
- Session start – ingest the codebase if the graph is empty or stale
- After code changes – re-ingest incrementally to update the graph
- Multi-source – merge a memory corpus into a code graph for cross-domain queries
- Federation preparation – use
m1nd.federateinstead for multi-repo ingestion
Side Effects
- replace mode: clears all graph state, invalidates all perspectives and locks, marks lock baselines as stale
- merge mode: adds to graph, increments graph generation, triggers watcher events on affected locks
Related Tools
m1nd.health– check graph status before deciding to ingestm1nd.drift– see what changed since last sessionm1nd.federate– multi-repo ingestion
m1nd.health
Server health and statistics. Returns node/edge counts, query count, uptime, memory usage, plasticity state, and active sessions.
Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
agent_id | string | Yes | – | Calling agent identifier. |
Example Request
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "m1nd.health",
"arguments": {
"agent_id": "orchestrator"
}
}
}
Example Response
{
"status": "healthy",
"node_count": 9767,
"edge_count": 26557,
"queries_processed": 142,
"uptime_seconds": 3600.5,
"memory_usage_bytes": 52428800,
"plasticity_state": "active",
"last_persist_time": "2026-03-13T10:30:00Z",
"active_sessions": [
{ "agent_id": "orchestrator", "last_active": "2026-03-13T11:25:00Z" }
]
}
When to Use
- Session start – first tool to call; verify the server is alive and the graph is loaded
- Monitoring – periodic health checks in long sessions
- Debugging – check memory usage and query counts
Related Tools
m1nd.ingest– load data if the graph is emptym1nd.drift– check what changed since last session
m1nd.validate_plan
Validate a proposed modification plan against the code graph. Detects gaps (affected files missing from the plan), risk level, test coverage, and suggested additions. Designed to be called before implementing a plan.
Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
agent_id | string | Yes | – | Calling agent identifier. |
actions | object[] | Yes | – | Ordered list of planned actions. Each object has: action_type (string, required – "modify", "create", "delete", "rename", "test"), file_path (string, required – relative path), description (string, optional), depends_on (string[], optional – other file_paths this action depends on). |
include_test_impact | boolean | No | true | Analyze test coverage for modified files. |
include_risk_score | boolean | No | true | Compute composite risk score. |
Example Request
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "m1nd.validate_plan",
"arguments": {
"agent_id": "orchestrator",
"actions": [
{ "action_type": "modify", "file_path": "backend/pool.py", "description": "Add connection timeout" },
{ "action_type": "modify", "file_path": "backend/worker.py", "description": "Update pool acquire to use timeout" },
{ "action_type": "test", "file_path": "backend/tests/test_pool.py" }
]
}
}
}
Example Response
{
"actions_analyzed": 3,
"actions_resolved": 3,
"actions_unresolved": 0,
"gaps": [
{
"file_path": "backend/config.py",
"node_id": "file::config.py",
"reason": "imported by modified file pool.py -- timeout config likely needed here",
"severity": "warning",
"signal_strength": 0.72
},
{
"file_path": "backend/process_manager.py",
"node_id": "file::process_manager.py",
"reason": "in blast radius of worker.py -- calls worker.submit",
"severity": "info",
"signal_strength": 0.45
}
],
"risk_score": 0.52,
"risk_level": "medium",
"test_coverage": {
"modified_files": 2,
"tested_files": 1,
"untested_files": ["backend/worker.py"],
"coverage_ratio": 0.5
},
"suggested_additions": [
{ "action_type": "modify", "file_path": "backend/config.py", "reason": "Timeout configuration likely needed" },
{ "action_type": "test", "file_path": "backend/tests/test_worker.py", "reason": "Modified file has no test action in plan" }
],
"blast_radius_total": 47,
"elapsed_ms": 35.0
}
Risk Levels
| Level | Score Range | Meaning |
|---|---|---|
"low" | < 0.3 | Small, well-tested change |
"medium" | 0.3 – 0.6 | Moderate scope, some gaps |
"high" | 0.6 – 0.8 | Large scope or missing tests |
"critical" | >= 0.8 | Very high risk – review carefully |
When to Use
- Before implementing – validate your plan catches all affected files
- PR review – validate that a PR’s changes are complete
- Planning – estimate risk and scope before committing to a plan
- Quality gate – reject plans with risk_score > threshold
Related Tools
m1nd.impact– blast radius for a single nodem1nd.predict– co-change prediction for a single nodem1nd.trace– validate a fix plan for a specific error
m1nd.lock.create
Pin a subgraph region and capture a baseline snapshot for change monitoring. Locks are used to track what changes in a region of the graph while you work. The baseline is compared against the current state when you call lock.diff.
Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
agent_id | string | Yes | – | Calling agent identifier. Lock is owned by this agent. |
scope | string | Yes | – | Scope type. Values: "node" (single nodes only), "subgraph" (BFS expansion from roots), "query_neighborhood" (nodes matching a query), "path" (ordered node list). |
root_nodes | string[] | Yes | – | Root nodes for the lock scope. Non-empty. Matched by external_id (exact), then label, then substring. |
radius | integer | No | – | BFS radius for subgraph scope. Range: 1 to 4. Required for subgraph scope. |
query | string | No | – | Query string for query_neighborhood scope. |
path_nodes | string[] | No | – | Ordered node list for path scope. |
Scope Types
| Scope | Description |
|---|---|
node | Lock only the specified root nodes |
subgraph | BFS expansion from root nodes up to radius hops |
query_neighborhood | Nodes matching a query activation |
path | An ordered list of nodes forming a path |
Example Request
{
"jsonrpc": "2.0",
"id": 4,
"method": "tools/call",
"params": {
"name": "m1nd.lock.create",
"arguments": {
"agent_id": "orchestrator",
"scope": "subgraph",
"root_nodes": ["file::handler.py"],
"radius": 2
}
}
}
Example Response
{
"lock_id": "lock_jimi_001",
"scope": "subgraph",
"baseline_nodes": 1639,
"baseline_edges": 707,
"graph_generation": 42,
"created_at_ms": 1710300000000
}
Limits
- Max locks per agent: configurable (default: 10)
- Max baseline nodes: configurable (default: 5000)
- Max baseline edges: configurable (default: 10000)
- Total memory budget shared with perspectives
When to Use
- Change monitoring – lock a region before making changes, then diff to see what changed
- Multi-agent coordination – lock regions to detect when other agents’ changes affect your work
- Regression detection – lock a stable region and watch for unexpected changes
Related Tools
m1nd.lock.watch– set automatic change detectionm1nd.lock.diff– compute what changed since baselinem1nd.lock.release– release the lock when done
m1nd.lock.watch
Set a watcher strategy on a lock. Watchers determine when the lock automatically detects changes. Without a watcher, you must manually call lock.diff.
Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
agent_id | string | Yes | – | Calling agent identifier. Must own the lock. |
lock_id | string | Yes | – | Lock to set the watcher on. |
strategy | string | Yes | – | Watcher strategy. Values: "manual" (no automatic detection), "on_ingest" (detect after every ingest), "on_learn" (detect after every learn call). Note: "periodic" is not supported in V1. |
Example Request
{
"jsonrpc": "2.0",
"id": 5,
"method": "tools/call",
"params": {
"name": "m1nd.lock.watch",
"arguments": {
"agent_id": "orchestrator",
"lock_id": "lock_jimi_001",
"strategy": "on_ingest"
}
}
}
Example Response
{
"lock_id": "lock_jimi_001",
"strategy": "on_ingest",
"previous_strategy": null
}
When to Use
- Automatic monitoring – set
on_ingestto detect changes after every code re-ingest - Learning feedback – set
on_learnto detect when learning shifts edge weights in your region - Manual control – set
manualto disable automatic detection
Related Tools
m1nd.lock.diff– manually trigger a diff (always available regardless of strategy)m1nd.lock.create– create the lock first
m1nd.lock.diff
Compute what changed in a locked region since the baseline was captured. Returns new/removed nodes, new/removed edges, weight changes, and watcher event counts. Fast-path: if the graph generation has not changed, returns immediately with no_changes: true.
Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
agent_id | string | Yes | – | Calling agent identifier. Must own the lock. |
lock_id | string | Yes | – | Lock to diff. |
Example Request
{
"jsonrpc": "2.0",
"id": 6,
"method": "tools/call",
"params": {
"name": "m1nd.lock.diff",
"arguments": {
"agent_id": "orchestrator",
"lock_id": "lock_jimi_001"
}
}
}
Example Response (changes detected)
{
"diff": {
"lock_id": "lock_jimi_001",
"no_changes": false,
"new_nodes": ["file::handler.py::fn::new_method"],
"removed_nodes": [],
"new_edges": ["handler.py|new_method->parser.py|calls"],
"removed_edges": [],
"boundary_edges_added": [],
"boundary_edges_removed": [],
"weight_changes": [
{ "edge_key": "handler.py|parser.py|imports", "old_weight": 0.5, "new_weight": 0.72 }
],
"baseline_stale": false,
"elapsed_ms": 0.08
},
"watcher_events_drained": 2,
"rebase_suggested": null
}
Example Response (no changes)
{
"diff": {
"lock_id": "lock_jimi_001",
"no_changes": true,
"new_nodes": [],
"removed_nodes": [],
"new_edges": [],
"removed_edges": [],
"boundary_edges_added": [],
"boundary_edges_removed": [],
"weight_changes": [],
"baseline_stale": false,
"elapsed_ms": 0.001
},
"watcher_events_drained": 0,
"rebase_suggested": null
}
When to Use
- After ingest – check if your locked region was affected
- After learning – check if feedback shifted weights in your region
- Periodic check – poll for changes during long sessions
- Before committing – verify no unexpected changes in the region
Related Tools
m1nd.lock.rebase– re-capture baseline after acknowledging changesm1nd.lock.watch– set automatic change detection
m1nd.lock.rebase
Re-capture the lock baseline from the current graph without releasing the lock. Use this after calling lock.diff and acknowledging the changes – the new baseline becomes the reference for future diffs.
Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
agent_id | string | Yes | – | Calling agent identifier. Must own the lock. |
lock_id | string | Yes | – | Lock to rebase. |
Example Request
{
"jsonrpc": "2.0",
"id": 7,
"method": "tools/call",
"params": {
"name": "m1nd.lock.rebase",
"arguments": {
"agent_id": "orchestrator",
"lock_id": "lock_jimi_001"
}
}
}
Example Response
{
"lock_id": "lock_jimi_001",
"previous_generation": 42,
"new_generation": 45,
"baseline_nodes": 1645,
"baseline_edges": 712,
"watcher_preserved": true
}
When to Use
- After acknowledging changes – rebase after reviewing a diff to reset the baseline
- After stale warning – when
lock.diffreturnsbaseline_stale: true, rebase to fix it - Periodic refresh – rebase periodically in long sessions to keep baselines current
Related Tools
m1nd.lock.diff– the diff that triggers a rebasem1nd.lock.create– creating a new lock is an alternative to rebasing
m1nd.lock.release
Release a lock and free its resources. Removes the lock state, cleans up pending watcher events, and frees memory. Irreversible.
Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
agent_id | string | Yes | – | Calling agent identifier. Must own the lock. |
lock_id | string | Yes | – | Lock to release. |
Example Request
{
"jsonrpc": "2.0",
"id": 8,
"method": "tools/call",
"params": {
"name": "m1nd.lock.release",
"arguments": {
"agent_id": "orchestrator",
"lock_id": "lock_jimi_001"
}
}
}
Example Response
{
"lock_id": "lock_jimi_001",
"released": true
}
When to Use
- Done monitoring – release when you no longer need change detection
- Memory pressure – locks consume memory proportional to baseline size
- Session end – release all locks before ending a session
- Automatic: locks are also cascade-released when their associated perspective is closed via
perspective.close
Related Tools
m1nd.lock.create– create a new lockm1nd.perspective.close– cascade-releases associated locks
Quick Start
Five minutes from zero to your first query.
Prerequisites
- Rust toolchain: 1.75+ (install via rustup)
- A codebase: Any project with Python, Rust, TypeScript/JavaScript, Go, or Java files. Other languages use a generic fallback extractor.
Installation
From source (recommended)
git clone https://github.com/cosmophonix/m1nd.git
cd m1nd
cargo build --release
The binary is at ./target/release/m1nd-mcp (~8MB). Copy it wherever you want:
cp target/release/m1nd-mcp /usr/local/bin/
From crates.io
cargo install m1nd-mcp
Verify the build
m1nd-mcp --help
The binary should start and wait for JSON-RPC input on stdin. Press Ctrl+C to exit.
Configuration
m1nd is an MCP server that communicates over stdio using JSON-RPC. You configure it in your AI client’s MCP settings.
Claude Code
Add to your Claude Code MCP configuration (.mcp.json in your project root, or ~/.claude/mcp.json globally):
{
"mcpServers": {
"m1nd": {
"command": "/path/to/m1nd-mcp",
"env": {
"M1ND_GRAPH_SOURCE": "/tmp/m1nd-graph.json",
"M1ND_PLASTICITY_STATE": "/tmp/m1nd-plasticity.json"
}
}
}
}
Cursor
In Cursor settings, navigate to MCP Servers and add:
{
"m1nd": {
"command": "/path/to/m1nd-mcp",
"env": {
"M1ND_GRAPH_SOURCE": "/tmp/m1nd-graph.json",
"M1ND_PLASTICITY_STATE": "/tmp/m1nd-plasticity.json"
}
}
}
Windsurf / Cline / Roo Code / Continue
Any MCP-compatible client works. The pattern is the same: point the client at the m1nd-mcp binary and optionally set the environment variables for persistence.
Environment Variables
| Variable | Purpose | Default |
|---|---|---|
M1ND_GRAPH_SOURCE | Path to persist graph state between sessions | In-memory only (lost on exit) |
M1ND_PLASTICITY_STATE | Path to persist learned edge weights | In-memory only (lost on exit) |
Recommendation: Always set both variables. Without persistence, every restart discards learned weights and you lose the graph’s accumulated intelligence.
Advanced Configuration
The server accepts these configuration parameters (set via environment):
| Parameter | Default | Purpose |
|---|---|---|
learning_rate | 0.08 | How aggressively the graph learns from feedback |
decay_rate | 0.005 | Rate of edge weight decay over time |
xlr_enabled | true | Enable XLR noise cancellation |
auto_persist_interval | 50 | Persist state every N queries |
max_concurrent_reads | 32 | Maximum concurrent read operations |
domain | “code” | Domain preset: code, music, memory, or generic |
First Run: Ingest a Project
Once your MCP client is configured and m1nd is running, ingest your codebase. This is the foundation for everything else.
If you are using m1nd through an MCP client like Claude Code, the client sends the JSON-RPC calls for you when you invoke the tools. The raw JSON-RPC is shown here for clarity.
Step 1: Ingest
// Ingest your project
{
"method": "tools/call",
"params": {
"name": "m1nd.ingest",
"arguments": {
"path": "/path/to/your/project",
"agent_id": "dev"
}
}
}
Expected response (times will vary by project size):
{
"files_processed": 335,
"nodes_created": 9767,
"edges_created": 26557,
"languages": {"python": 335},
"elapsed_ms": 910
}
What happened: m1nd parsed every file, extracted structural elements (modules, classes, functions, imports), resolved references between them, built a compressed graph, and computed PageRank centrality for every node.
Step 2: Verify with Health Check
{
"method": "tools/call",
"params": {
"name": "m1nd.health",
"arguments": {
"agent_id": "dev"
}
}
}
Expected response:
{
"status": "ok",
"nodes": 9767,
"edges": 26557,
"finalized": true,
"plasticity_records": 0,
"agents_seen": ["dev"],
"queries_served": 1,
"uptime_seconds": 12
}
If you see "nodes": 0, re-check the path you passed to ingest.
Step 3: Your First Query
{
"method": "tools/call",
"params": {
"name": "m1nd.activate",
"arguments": {
"query": "authentication",
"agent_id": "dev",
"top_k": 5
}
}
}
Expected response:
{
"activated": [
{
"node_id": "file::auth.py",
"score": 0.89,
"dimension_scores": {
"structural": 0.92,
"semantic": 0.95,
"temporal": 0.78,
"causal": 0.71
}
},
{"node_id": "file::middleware.py", "score": 0.72},
{"node_id": "file::session.py", "score": 0.61}
],
"ghost_edges": [
{"from": "file::auth.py", "to": "file::rate_limiter.py", "confidence": 0.34}
]
}
What happened: m1nd fired a signal into the graph from nodes matching “authentication” and let it propagate across structural, semantic, temporal, and causal dimensions. Noise was cancelled via XLR differential processing. The results are ranked by multi-dimensional activation score, not just text matching.
What Next
You now have a working m1nd instance. From here:
- First Query Tutorial: Step-by-step walkthrough of the full activate-learn-activate cycle, counterfactual simulation, and structural hole detection.
- Multi-Agent Tutorial: How to use m1nd with multiple agents sharing one graph.
- FAQ: Common questions and answers.
Troubleshooting
“No graph snapshot found, starting fresh” – This is normal on first run. The graph is empty until you call ingest.
Ingest returns 0 files – Check that the path is correct and contains supported source files. m1nd currently has extractors for Python (.py), Rust (.rs), TypeScript/JavaScript (.ts/.js/.tsx/.jsx), Go (.go), and Java (.java). Other files use a generic fallback extractor.
MCP client does not see m1nd tools – Verify the binary path is correct and the binary is executable (chmod +x m1nd-mcp). Check your client’s MCP server logs for connection errors.
Permission denied – Ensure the M1ND_GRAPH_SOURCE and M1ND_PLASTICITY_STATE paths are writable.
First Query: The Full Cycle
This tutorial walks you through the core m1nd workflow: ingest, activate, learn, and observe the graph getting smarter. Then we explore structural holes and counterfactual simulation.
Prerequisites: You have completed the Quick Start and have m1nd running with a codebase ingested.
All examples use the JSON-RPC wire format. If you are working through an MCP client (Claude Code, Cursor, etc.), the client sends these calls for you when you invoke the tools by name.
Step 1: Ingest Your Codebase
If you have not already ingested, do it now:
{
"method": "tools/call",
"params": {
"name": "m1nd.ingest",
"arguments": {
"path": "/your/project",
"agent_id": "dev"
}
}
}
Response:
{
"files_processed": 335,
"nodes_created": 9767,
"edges_created": 26557,
"languages": {"python": 335},
"elapsed_ms": 910
}
The graph now contains structural nodes (files, classes, functions) and edges (imports, calls, inheritance, co-change patterns). PageRank has been computed, giving each node a centrality score.
Step 2: First Activation
Ask the graph about session pool management:
{
"method": "tools/call",
"params": {
"name": "m1nd.activate",
"arguments": {
"query": "session pool management",
"agent_id": "dev",
"top_k": 5
}
}
}
Response:
{
"activated": [
{
"node_id": "file::pool.py",
"score": 0.89,
"dimension_scores": {
"structural": 0.92,
"semantic": 0.95,
"temporal": 0.78,
"causal": 0.71
}
},
{"node_id": "file::pool.py::class::ConnectionPool", "score": 0.84},
{"node_id": "file::worker.py", "score": 0.61},
{"node_id": "file::pool.py::fn::acquire", "score": 0.58},
{"node_id": "file::process_manager.py", "score": 0.45}
],
"ghost_edges": [
{
"from": "file::pool.py",
"to": "file::recovery.py",
"confidence": 0.34
}
]
}
Reading the results:
score: Combined 4-dimensional activation score (0.0 to 1.0)dimension_scores: Breakdown by structural (graph distance, PageRank), semantic (token overlap), temporal (co-change history), and causal (suspiciousness)ghost_edges: Connections the graph inferred but that are not explicit in code. Here,pool.pyandrecovery.pyare structurally unconnected but co-activate together – a hidden dependency worth investigating.
Note the scores. We will come back to this query after teaching the graph.
Step 3: Teach the Graph (Hebbian Learning)
The top two results (pool.py and the ConnectionPool class) were exactly what we needed. Tell the graph:
{
"method": "tools/call",
"params": {
"name": "m1nd.learn",
"arguments": {
"query": "session pool management",
"agent_id": "dev",
"feedback": "correct",
"node_ids": [
"file::pool.py",
"file::pool.py::class::ConnectionPool"
],
"strength": 0.2
}
}
}
Response:
{
"edges_strengthened": 740,
"edges_weakened": 0,
"plasticity_records": 740,
"learning_type": "hebbian_ltp"
}
What happened: Hebbian Long-Term Potentiation (LTP) strengthened 740 edges along paths connecting the confirmed-useful nodes. “Neurons that fire together wire together.” The next time anyone queries this region of the graph, those paths carry more signal.
Now suppose worker.py (score 0.61) was not actually relevant. Mark it wrong:
{
"method": "tools/call",
"params": {
"name": "m1nd.learn",
"arguments": {
"query": "session pool management",
"agent_id": "dev",
"feedback": "wrong",
"node_ids": ["file::worker.py"],
"strength": 0.2
}
}
}
Response:
{
"edges_strengthened": 0,
"edges_weakened": 312,
"plasticity_records": 1052,
"learning_type": "hebbian_ltd"
}
Long-Term Depression (LTD) weakened 312 edges leading to worker.py from this query region. The graph now knows: for session pool queries, worker.py is noise.
Step 4: Activate Again – See the Improvement
Run the exact same query:
{
"method": "tools/call",
"params": {
"name": "m1nd.activate",
"arguments": {
"query": "session pool management",
"agent_id": "dev",
"top_k": 5
}
}
}
Expected changes:
{
"activated": [
{"node_id": "file::pool.py", "score": 0.93},
{"node_id": "file::pool.py::class::ConnectionPool", "score": 0.88},
{"node_id": "file::pool.py::fn::acquire", "score": 0.65},
{"node_id": "file::process_manager.py", "score": 0.47},
{"node_id": "file::recovery.py", "score": 0.39}
]
}
Compare with Step 2:
| Node | Before | After | Change |
|---|---|---|---|
pool.py | 0.89 | 0.93 | +0.04 (strengthened) |
ConnectionPool class | 0.84 | 0.88 | +0.04 (strengthened) |
worker.py | 0.61 | dropped | Pushed below top-5 (weakened) |
recovery.py | ghost only | 0.39 | Promoted from ghost to main results |
The graph learned. worker.py fell out of the top results. recovery.py, previously only a ghost edge, got promoted because the strengthened paths through pool.py now carry more signal to its neighborhood.
This is the core value proposition of m1nd: every interaction makes the graph smarter. No other code intelligence tool does this.
Step 5: Structural Hole Detection
Ask the graph what is missing around a topic:
{
"method": "tools/call",
"params": {
"name": "m1nd.missing",
"arguments": {
"query": "database connection pooling",
"agent_id": "dev"
}
}
}
Response:
{
"holes": [
{
"region": "connection lifecycle",
"adjacent_nodes": 4,
"description": "No dedicated connection pool abstraction"
},
{
"region": "pool metrics",
"adjacent_nodes": 3,
"description": "No pool health monitoring"
},
{
"region": "graceful drain",
"adjacent_nodes": 2,
"description": "No connection drain on shutdown"
}
],
"total_holes": 9
}
What happened: m1nd activated the “database connection pooling” region of the graph and looked for gaps – areas where the graph’s structure predicts a node should exist but none does. These are structural holes: places where other codebases of similar shape would have components but yours does not.
This is not a linter or rule-based checker. It is topology-based gap detection. The graph’s shape implies these components should exist, based on the relationships between the nodes that do exist.
Step 6: Counterfactual Simulation
Before deleting or rewriting a module, simulate the consequences:
{
"method": "tools/call",
"params": {
"name": "m1nd.counterfactual",
"arguments": {
"node_ids": ["file::worker.py"],
"agent_id": "dev"
}
}
}
Response:
{
"cascade": [
{"depth": 1, "affected": 23},
{"depth": 2, "affected": 456},
{"depth": 3, "affected": 3710}
],
"total_affected": 4189,
"orphaned_count": 0,
"pct_activation_lost": 0.41
}
Reading the results:
- Depth 1: 23 nodes directly depend on
worker.py - Depth 2: 456 more nodes depend on those 23
- Depth 3: 3,710 more – a cascade explosion
- Total: 4,189 nodes affected out of ~9,767 (42.9% of the graph)
- Activation lost: 41% of the graph’s total activation capacity would be disrupted
Compare this with removing config.py:
{
"method": "tools/call",
"params": {
"name": "m1nd.counterfactual",
"arguments": {
"node_ids": ["file::config.py"],
"agent_id": "dev"
}
}
}
Response:
{
"cascade": [
{"depth": 1, "affected": 89},
{"depth": 2, "affected": 1234},
{"depth": 3, "affected": 1208}
],
"total_affected": 2531,
"orphaned_count": 3,
"pct_activation_lost": 0.28
}
Despite config.py having more direct dependents (89 vs 23), its total cascade is smaller (2,531 vs 4,189). worker.py sits at a structural chokepoint where downstream nodes have more transitive dependencies. This insight is impossible to get from grep or import analysis alone – it requires full graph traversal.
Step 7: Hypothesis Testing (Bonus)
Test a structural claim against the graph:
{
"method": "tools/call",
"params": {
"name": "m1nd.hypothesize",
"arguments": {
"claim": "worker depends on messaging at runtime",
"agent_id": "dev"
}
}
}
Response:
{
"verdict": "likely_true",
"confidence": 0.72,
"paths_explored": 25015,
"evidence": [
{
"path": [
"file::worker.py",
"file::process_manager.py::fn::cancel",
"file::messaging.py"
],
"hops": 2
}
],
"note": "2-hop dependency via cancel function -- invisible to grep"
}
The hypothesis engine explored 25,015 paths in 58ms and found a 2-hop dependency that no text search could reveal: the worker pool reaches the WhatsApp manager through a cancel function in the process manager. This is the kind of hidden coupling that causes production incidents.
Summary: The Learning Loop
The core m1nd workflow is a feedback loop:
ingest --> activate --> use results --> learn --> activate again
| |
| (graph gets smarter) |
+----------------------------------------------+
Every learn call shifts edge weights. Every subsequent activate benefits from accumulated learning. Over sessions, the graph adapts to how your team thinks about your codebase.
Additional tools layer on top of this foundation:
| Tool | When to Use |
|---|---|
missing | Before designing new features – find what your codebase lacks |
counterfactual | Before deleting or rewriting – simulate the blast radius |
hypothesize | When debugging – test assumptions about hidden dependencies |
impact | Before modifying a file – understand the blast radius |
predict | After modifying a file – which other files probably need changes too |
trace | When an error occurs – map stacktraces to structural root causes |
Next: Multi-Agent Tutorial – how multiple agents share one graph.
Multi-Agent Usage
m1nd is designed for multi-agent systems. One m1nd instance serves many agents simultaneously. This tutorial covers agent identity, concurrent access, perspective isolation, trail sharing, and a real-world example of how a production system uses m1nd with a fleet of agents.
How It Works
m1nd runs as a single process with a single shared graph. Multiple MCP clients connect to the same instance (or multiple instances reading from the same persisted state). Every tool call includes an agent_id parameter that identifies the caller.
Agent A (Claude Code) ----+
|
Agent B (Cursor) ----+----> m1nd-mcp (single graph)
|
Agent C (custom agent) ----+
The graph is shared. Learning by one agent benefits all agents. Perspectives are isolated per agent. Trails can be shared across agents.
Agent ID Conventions
Every m1nd tool requires an agent_id parameter. This is a free-form string, but consistent naming matters:
agent_id: "orchestrator" -- orchestrator
agent_id: "auditor-1" -- security hardening agent
agent_id: "builder-api" -- API building agent
agent_id: "analyzer-core" -- performance analysis agent
Recommended convention: {archetype}-{task} for short-lived task agents, simple names for persistent agents.
Rules:
- Agent IDs are case-sensitive
- Use lowercase with hyphens
- m1nd tracks all agent IDs it has seen (visible in
healthoutput) - Agent ID determines perspective ownership and trail ownership
Shared Graph, Individual Learning
When Agent A calls learn with feedback, the edge weight changes are visible to all agents immediately:
// Agent A: "pool.py was useful for my auth investigation"
{
"method": "tools/call",
"params": {
"name": "m1nd.learn",
"arguments": {
"query": "authentication flow",
"agent_id": "agent-a",
"feedback": "correct",
"node_ids": ["file::pool.py"]
}
}
}
// -> 740 edges strengthened
// Agent B: immediately benefits from stronger pool.py edges
{
"method": "tools/call",
"params": {
"name": "m1nd.activate",
"arguments": {
"query": "session management",
"agent_id": "agent-b",
"top_k": 5
}
}
}
// -> pool.py scores higher than it would have before Agent A's feedback
This is by design. The graph represents collective intelligence about the codebase. Every agent’s learning contributes to the whole.
Perspective Isolation
Perspectives are m1nd’s navigation system – a stateful exploration session anchored to a node, with a route surface, breadcrumb history, and focus tracking.
Perspectives are isolated per agent. Agent A’s perspectives are not visible to Agent B, and navigation in one perspective does not affect another.
Agent A: Start a Perspective
{
"method": "tools/call",
"params": {
"name": "m1nd.perspective.start",
"arguments": {
"agent_id": "agent-a",
"query": "authentication middleware",
"anchor_node": "file::middleware.py"
}
}
}
Response:
{
"perspective_id": "persp-a1b2c3",
"focus": "file::middleware.py",
"routes": [
{"route_id": "r-001", "target": "file::auth.py", "label": "imports", "score": 0.92},
{"route_id": "r-002", "target": "file::session.py", "label": "calls", "score": 0.78}
],
"route_set_version": 1
}
Agent B: Has Its Own Perspectives
{
"method": "tools/call",
"params": {
"name": "m1nd.perspective.start",
"arguments": {
"agent_id": "agent-b",
"query": "worker pool scaling"
}
}
}
Agent B gets a completely independent perspective. Its routes, focus, and history are separate from Agent A’s.
Listing Perspectives
Each agent only sees its own:
// Agent A sees only its perspectives
{
"method": "tools/call",
"params": {
"name": "m1nd.perspective.list",
"arguments": {"agent_id": "agent-a"}
}
}
// -> [{"perspective_id": "persp-a1b2c3", "focus": "file::middleware.py", ...}]
// Agent B sees only its perspectives
{
"method": "tools/call",
"params": {
"name": "m1nd.perspective.list",
"arguments": {"agent_id": "agent-b"}
}
}
// -> [{"perspective_id": "persp-d4e5f6", "focus": "file::worker.py", ...}]
Comparing Perspectives Across Agents
You can explicitly compare two perspectives from different agents using perspective.compare. This is useful for discovering where two independent investigations overlap:
{
"method": "tools/call",
"params": {
"name": "m1nd.perspective.compare",
"arguments": {
"agent_id": "agent-a",
"perspective_id_a": "persp-a1b2c3",
"perspective_id_b": "persp-d4e5f6"
}
}
}
Response:
{
"shared_nodes": ["file::process_manager.py", "file::config.py"],
"unique_to_a": ["file::auth.py", "file::middleware.py"],
"unique_to_b": ["file::worker.py", "file::worker.py"],
"dimension_deltas": {
"structural": 0.12,
"semantic": 0.34,
"temporal": 0.08
}
}
Lock System for Concurrent Access
When multiple agents might modify the same region of the codebase simultaneously, the lock system prevents conflicts.
Agent A: Lock the Auth Region
{
"method": "tools/call",
"params": {
"name": "m1nd.lock.create",
"arguments": {
"agent_id": "agent-a",
"scope": "subgraph",
"root_nodes": ["file::auth.py"],
"radius": 2
}
}
}
Response:
{
"lock_id": "lock-xyz789",
"nodes_locked": 156,
"edges_locked": 423,
"scope": "subgraph",
"radius": 2
}
Set a Watch Strategy
{
"method": "tools/call",
"params": {
"name": "m1nd.lock.watch",
"arguments": {
"agent_id": "agent-a",
"lock_id": "lock-xyz789",
"strategy": "on_ingest"
}
}
}
Now, whenever any agent triggers an ingest that touches the locked region, the lock records the changes.
Check for Changes
After Agent B modifies some code and re-ingests:
{
"method": "tools/call",
"params": {
"name": "m1nd.lock.diff",
"arguments": {
"agent_id": "agent-a",
"lock_id": "lock-xyz789"
}
}
}
Response (in 0.08 microseconds):
{
"new_nodes": ["file::auth.py::fn::validate_token_v2"],
"removed_nodes": [],
"weight_changes": 3,
"structural_changes": true
}
Agent A now knows exactly what changed in its locked region, without scanning the entire graph.
Rebase or Release
// Accept changes and update baseline
{"method":"tools/call","params":{"name":"m1nd.lock.rebase","arguments":{
"agent_id":"agent-a","lock_id":"lock-xyz789"
}}}
// Or release when done
{"method":"tools/call","params":{"name":"m1nd.lock.release","arguments":{
"agent_id":"agent-a","lock_id":"lock-xyz789"
}}}
Trail Sharing
Trails are investigation snapshots: visited nodes, hypotheses, conclusions, open questions, and activation boosts. They persist across sessions and can be shared between agents.
Agent A: Save an Investigation
{
"method": "tools/call",
"params": {
"name": "m1nd.trail.save",
"arguments": {
"agent_id": "agent-a",
"label": "auth-token-leak-investigation",
"hypotheses": [
{
"statement": "Auth tokens leak through session pool",
"confidence": 0.7,
"status": "investigating"
},
{
"statement": "Rate limiter missing from auth chain",
"confidence": 0.9,
"status": "confirmed"
}
],
"open_questions": [
"Does the healing manager observe token lifecycle?",
"Is there a token rotation policy?"
],
"tags": ["security", "auth", "session"]
}
}
}
Response:
{
"trail_id": "trail-abc123",
"nodes_captured": 47,
"hypotheses_saved": 2,
"activation_boosts_saved": 12
}
Agent B: Resume Agent A’s Trail
{
"method": "tools/call",
"params": {
"name": "m1nd.trail.resume",
"arguments": {
"agent_id": "agent-b",
"trail_id": "trail-abc123"
}
}
}
Response:
{
"trail_id": "trail-abc123",
"nodes_reactivated": 47,
"stale_nodes": 2,
"hypotheses_restored": 2,
"hypotheses_downgraded": 0
}
Agent B now has Agent A’s exact cognitive context: the same nodes are activated, the same hypotheses are loaded, and any stale nodes (changed since the trail was saved) are flagged.
Merging Trails from Multiple Agents
When two agents investigate independently and you want to combine findings:
{
"method": "tools/call",
"params": {
"name": "m1nd.trail.merge",
"arguments": {
"agent_id": "orchestrator",
"trail_ids": ["trail-abc123", "trail-def456"],
"label": "combined-auth-investigation"
}
}
}
Response:
{
"merged_trail_id": "trail-merged-789",
"total_nodes": 83,
"shared_nodes": 12,
"conflicts": [
{
"node": "file::auth.py",
"trail_a_hypothesis": "token leak source",
"trail_b_hypothesis": "not involved"
}
],
"conflict_count": 3
}
The merge automatically detects where independent investigations converged (12 shared nodes) and where they conflict (3 disagreements). This is essential for synthesizing multi-agent research.
Browsing Trails
{
"method": "tools/call",
"params": {
"name": "m1nd.trail.list",
"arguments": {
"agent_id": "orchestrator",
"filter_tags": ["security"]
}
}
}
Real-World Example: Multi-Agent Production System
a production system is a multi-agent orchestration system that uses m1nd as its shared code intelligence layer. Here is how it works in production:
Architecture
orchestrator
|
+-- auditor-1 (security agent) --+
+-- builder-api (API builder) --+-- All share one m1nd instance
+-- analyzer-core (performance) --+
+-- sentinel-files (file watcher) --+
+-- critic-quality (code reviewer) --+
+-- ... 14 more agents --+
One m1nd instance serves all agents. The graph covers a 335-file Python backend (~52K lines), a React frontend, and MCP server infrastructure.
Orchestrator Boot Sequence
When the orchestrator starts a session:
// Step 1: Check m1nd health
{"method":"tools/call","params":{"name":"m1nd.health","arguments":{"agent_id":"orchestrator"}}}
// Step 2: Check what changed since last session
{"method":"tools/call","params":{"name":"m1nd.drift","arguments":{"agent_id":"orchestrator","since":"last_session"}}}
// Step 3: Re-ingest if the graph is stale
{"method":"tools/call","params":{"name":"m1nd.ingest","arguments":{
"path":"/project/backend","agent_id":"orchestrator","incremental":true
}}}
Task Delegation with Graph Context
When the orchestrator delegates a security hardening task:
// Before spawning the security agent, get blast radius context
{"method":"tools/call","params":{"name":"m1nd.impact","arguments":{
"node_id":"file::auth.py","agent_id":"orchestrator"
}}}
// Warm up the graph for the security task
{"method":"tools/call","params":{"name":"m1nd.warmup","arguments":{
"task_description":"harden authentication token validation","agent_id":"orchestrator"
}}}
// The security agent then uses the primed graph
{"method":"tools/call","params":{"name":"m1nd.activate","arguments":{
"query":"token validation vulnerabilities","agent_id":"auditor-1"
}}}
Collective Learning
After each agent completes its task, it provides feedback:
// Security agent found useful results
{"method":"tools/call","params":{"name":"m1nd.learn","arguments":{
"query":"token validation vulnerabilities",
"agent_id":"auditor-1",
"feedback":"correct",
"node_ids":["file::auth.py","file::middleware.py","file::pool.py"]
}}}
// Performance agent found different useful results
{"method":"tools/call","params":{"name":"m1nd.learn","arguments":{
"query":"connection pool bottleneck",
"agent_id":"analyzer-core",
"feedback":"correct",
"node_ids":["file::worker.py","file::process_manager.py"]
}}}
Over a session with many agents, the graph accumulates thousands of learning signals. Each agent benefits from every other agent’s discoveries.
Investigation Handoff
When Agent A finds something that Agent B needs to investigate:
// Agent A saves its investigation
{"method":"tools/call","params":{"name":"m1nd.trail.save","arguments":{
"agent_id":"auditor-1",
"label":"session-hijack-vector",
"tags":["security","critical"]
}}}
// Orchestrator merges with Agent B's independent findings
{"method":"tools/call","params":{"name":"m1nd.trail.merge","arguments":{
"agent_id":"orchestrator",
"trail_ids":["trail-hacker-001","trail-analyst-002"]
}}}
Best Practices
-
Use consistent agent IDs. The same agent should always use the same ID across sessions. This enables drift detection and trail continuity.
-
Learn after every useful activation. The more feedback the graph gets, the smarter it becomes. Make
learncalls automatic in your agent loop. -
Use locks for overlapping work. If two agents might modify the same code region, lock it first. Lock diffs are essentially free (0.08 microseconds).
-
Save trails at investigation checkpoints. Trails are cheap to save and invaluable for handoff, resume, and post-mortem analysis.
-
Merge trails for synthesis. When multiple agents investigate the same area independently, merge their trails to find convergence and conflicts.
-
Warm up before focused tasks.
warmupprimes the graph for a specific task, boosting relevant regions before the agent starts querying. -
Use
driftat session start. After any period of inactivity, check what changed. This recovers context efficiently.
Frequently Asked Questions
General
What is m1nd?
m1nd is a neuro-symbolic connectome engine for code intelligence. It parses your codebase into a weighted graph and provides 43 MCP tools for querying, learning, and navigating it. Built in Rust, it runs locally as an MCP server and works with any MCP-compatible AI agent (Claude Code, Cursor, Windsurf, Zed, etc.).
The key differentiator: the graph learns. When you tell m1nd which results were useful and which were not, it adjusts edge weights via Hebbian plasticity. Over time, the graph adapts to how your team thinks about your codebase.
How is m1nd different from grep / ripgrep?
Grep finds text. m1nd finds structure and relationships.
Grep tells you which files contain the word “authentication”. m1nd tells you which modules are structurally connected to authentication, which ones are likely co-changed when auth changes, which hidden dependencies exist between auth and seemingly unrelated modules, and what would break if you removed the auth module.
Grep is fast and essential. m1nd answers questions that grep cannot even formulate.
How is m1nd different from RAG?
RAG (Retrieval-Augmented Generation) embeds code chunks into vectors and retrieves the top-K most similar chunks for each query. Each retrieval is independent – RAG has no memory of previous queries and no understanding of relationships between results.
m1nd maintains a persistent graph where relationships are first-class citizens. The graph learns from feedback, remembers investigations across sessions, and can answer structural questions (“what breaks if I delete X?”, “does A depend on B at runtime?”) that RAG cannot.
RAG is useful for semantic similarity search. m1nd is useful for structural reasoning. They are complementary, not competing.
How is m1nd different from static analysis tools (tree-sitter, ast-grep, Sourcegraph)?
Static analysis tools parse code into ASTs and compute call graphs, type hierarchies, and cross-references. These are accurate but frozen – they represent the code at a single point in time and cannot answer “what if?” questions.
m1nd uses similar structural information as a starting point but adds three things static analysis cannot: (1) Hebbian learning that adapts the graph based on usage, (2) temporal intelligence from co-change history, and (3) simulation engines for hypotheses and counterfactuals.
Sourcegraph is a search engine. m1nd is a reasoning engine.
Do I need an LLM to use m1nd?
No. m1nd makes zero LLM calls. It is pure Rust computation: graph algorithms, spreading activation, Hebbian plasticity. No API keys, no network calls, no token costs.
m1nd is designed to work alongside LLMs (as an MCP tool that agents call), but the graph engine itself is completely self-contained.
What languages does m1nd support?
m1nd has dedicated extractors for:
- Python (.py)
- Rust (.rs)
- TypeScript (.ts, .tsx)
- JavaScript (.js, .jsx)
- Go (.go)
- Java (.java)
All other file types use a generic fallback extractor that identifies functions, classes, and imports through heuristic pattern matching. The generic extractor produces a less detailed graph but still captures useful structure.
Tree-sitter integration is planned, which would add support for 64+ languages.
Is m1nd open source?
Yes. MIT license. Source at github.com/cosmophonix/m1nd.
Installation
What platforms does m1nd support?
Any platform where Rust compiles. Tested on:
- macOS (ARM64 and x86_64)
- Linux (x86_64, ARM64)
- Windows (x86_64)
What is the minimum Rust version?
Rust 1.75 or later. The project uses the 2021 edition.
How large is the binary?
Approximately 8MB for a release build. No runtime dependencies, no shared libraries. The binary is fully self-contained.
Can I install from crates.io?
Yes: cargo install m1nd-mcp
Usage
How many files can m1nd handle?
Tested up to 10,000+ files. At ~2MB of memory for 10,000 nodes, a 100K-file codebase would need roughly 20MB for the graph. This is well within modern machine capacity.
At 400K+ files, the in-memory graph starts to become a consideration (~80MB), but it still works. m1nd was optimized for codebases in the 100 to 50,000 file range.
Does it work with non-code files?
Yes. m1nd has a json adapter for structured data and a memory adapter for text corpora. You can also use the generic fallback extractor for any text file. The graph is not limited to source code – it is a general-purpose knowledge graph that happens to have excellent code extractors.
How much memory does m1nd use?
The graph itself is compact: ~2MB for a 10,000-node graph with 26,000 edges (Compressed Sparse Row format). The MCP server process uses additional memory for the JSON-RPC layer, perspective state, lock baselines, and trail storage. A typical production instance serving a 335-file codebase uses under 50MB total.
Can I ingest multiple codebases into one graph?
Yes, through the federate tool. It ingests multiple repositories into a unified graph with automatic cross-repository edge detection:
{"method":"tools/call","params":{"name":"m1nd.federate","arguments":{
"agent_id":"dev",
"repos":[
{"name":"backend","path":"/project/backend"},
{"name":"frontend","path":"/project/frontend"}
]
}}}
Can I do incremental ingests?
Yes. Pass "incremental": true to the ingest tool. Incremental ingest only re-processes files that changed since the last ingest, preserving learned edge weights for unchanged regions.
Architecture
Where is the graph stored?
In memory during runtime. Optionally persisted to JSON files on disk via the M1ND_GRAPH_SOURCE and M1ND_PLASTICITY_STATE environment variables.
Without persistence configured, the graph is lost when the process exits. Always configure persistence for production use.
How often does it persist?
By default, every 50 queries. Also on shutdown. The auto_persist_interval configuration parameter controls this.
Can I export the graph?
The persisted graph state is a JSON file (graph_snapshot.json). You can read, copy, and process it with standard JSON tools. The format includes all nodes, edges, PageRank scores, and metadata.
The plasticity state is a separate JSON file (plasticity_state.json) containing per-edge synaptic records (learned weights).
What is the graph format?
Compressed Sparse Row (CSR) with forward and reverse adjacency lists. Each node has an external ID (e.g., file::auth.py), a type (file, class, function, module), metadata, and a PageRank score. Each edge has a type (imports, calls, inherits, co_change), a base weight, and an optional plasticity weight.
What is XLR noise cancellation?
Borrowed from professional audio engineering. Like a balanced XLR cable, m1nd transmits the activation signal on two inverted channels and subtracts common-mode noise at the receiver. This reduces false positives in activation queries by cancelling out signals that propagate through generic hub nodes (like config.py or utils.py) rather than through meaningful structural paths.
XLR is enabled by default. Pass "xlr": false to activate to disable it for a single query.
MCP Protocol
What MCP clients work with m1nd?
Any client that speaks MCP over stdio. Tested and verified:
- Claude Code
- Cursor
- Windsurf
- Zed
- Cline
- Roo Code
- Continue
- OpenCode
- GitHub Copilot (MCP mode)
- Amazon Q Developer
Can I use m1nd without MCP?
The server speaks JSON-RPC over stdio, which is the MCP transport. You can send raw JSON-RPC from any program that can write to stdin and read from stdout. No MCP client library is required – the protocol is just JSON over pipes.
What MCP protocol version does m1nd implement?
Protocol version 2024-11-05. The server reports this in the initialize response.
Does m1nd support MCP notifications?
Yes. The server silently ignores incoming notifications per the MCP specification.
Performance
How fast is spreading activation?
31-77ms for a 9,767-node graph, returning 15-20 ranked results. The variance depends on query specificity and graph density around the activated region.
How fast is ingest?
910ms for 335 Python files producing 9,767 nodes and 26,557 edges. This includes parsing, reference resolution, edge creation, and PageRank computation.
How fast is learning?
Sub-millisecond. A learn call with feedback on 2-3 nodes adjusts hundreds of edges in under 1ms.
What about 100K files?
Ingest would take roughly 30-45 seconds (linear scaling from the 335-file benchmark). Activation would be 100-200ms. The graph would occupy ~20MB in memory. These are estimates – actual performance depends on code density and language.
What is the lock diff speed?
0.08 microseconds (80 nanoseconds). Lock diff is a constant-time operation – it compares fingerprints, not individual nodes. It is essentially free.
Full benchmark table
All numbers from real execution against a production Python backend (335 files, ~52K lines):
| Operation | Time | Scale |
|---|---|---|
| Full ingest | 910ms | 335 files, 9,767 nodes, 26,557 edges |
| Spreading activation | 31-77ms | 15 results from 9,767 nodes |
| Blast radius (depth=3) | 5-52ms | Up to 4,271 affected nodes |
| Stacktrace analysis | 3.5ms | 5 frames, 4 suspects ranked |
| Plan validation | 10ms | 7 files, 43,152 blast radius |
| Counterfactual cascade | 3ms | Full BFS on 26,557 edges |
| Hypothesis testing | 58ms | 25,015 paths explored |
| Pattern scan (all 8) | 38ms | 335 files, 50 findings per pattern |
| Multi-repo federation | 1.3s | 11,217 nodes, 18,203 cross-repo edges |
| Lock diff | 0.08us | 1,639-node subgraph comparison |
| Trail merge | 1.2ms | 5 hypotheses, 3 conflicts detected |
Plasticity and Learning
How does Hebbian learning work?
“Neurons that fire together wire together.” When you call learn with feedback: "correct" and a list of node IDs, m1nd identifies all edges on paths between those nodes and increases their weights (Long-Term Potentiation, LTP). When you call with feedback: "wrong", it decreases weights on paths leading to the marked nodes (Long-Term Depression, LTD).
The strength parameter (default 0.2) controls how aggressively weights shift. The learning_rate server configuration (default 0.08) provides a global scaling factor.
Can I reset learned weights?
Yes. Delete the plasticity_state.json file and restart the server. Alternatively, re-ingest the codebase, which rebuilds the graph from scratch (but does not clear the plasticity state file – you need to delete it separately or pass "incremental": false and delete the plasticity file).
Does the graph overfit?
Hebbian learning includes homeostatic normalization to prevent runaway weight amplification. Edge weights are bounded and periodically normalized so that heavily-used paths do not completely dominate. The decay_rate parameter (default 0.005) provides a slow decay toward baseline weights over time.
In practice, overfitting requires thousands of feedback signals consistently reinforcing the same narrow paths. Normal usage produces a well-distributed weight landscape.
What is “partial” feedback?
The learn tool accepts three feedback types:
correct– strengthen paths (LTP)wrong– weaken paths (LTD)partial– mixed signal. Applies a mild strengthening (half the LTP strength) to acknowledged nodes while slightly weakening peripheral paths.
Perspectives
What are perspectives?
A perspective is a stateful navigation session through the graph. You start one with a query or anchor node, and m1nd synthesizes a “route surface” – a ranked set of paths you can follow. As you navigate (follow, back, branch), the perspective maintains breadcrumb history and updates the route surface.
Think of it as a browser for the code graph. You have a current “page” (focus node), links (routes), history (back button), and bookmarks (branches).
How many perspectives can be open simultaneously?
There is no hard limit. Each perspective occupies a small amount of memory (proportional to the number of visited nodes). In practice, 10-20 concurrent perspectives per agent is typical. Close perspectives you are done with to free memory.
Do perspectives persist across sessions?
Perspectives are in-memory only and are lost when the server restarts. For persistent investigation state, use the trail system (trail.save / trail.resume).
Can I branch a perspective?
Yes. perspective.branch forks the current navigation state into a new independent perspective. Both the original and the branch have the same history up to the branch point, and diverge afterward. This is useful for exploring “what if I go this way instead?” without losing your current position.
Comparison
m1nd vs tree-sitter
tree-sitter is a parser. It produces ASTs from source code. m1nd uses structural information similar to what tree-sitter extracts, but builds a weighted, learning graph on top of it. tree-sitter tells you what the code is. m1nd tells you what the code means in context, what changed, what might break, and what is missing.
They are complementary. tree-sitter integration is planned for m1nd to expand language support.
m1nd vs ast-grep
ast-grep is a structural search tool – it finds code patterns using AST matching. m1nd does not do pattern matching on syntax trees. Instead, it does graph-level reasoning: spreading activation, hypothesis testing, counterfactual simulation, learning. ast-grep answers “where does this pattern appear?” m1nd answers “what is connected to this, what breaks if it changes, and what am I missing?”
m1nd vs RAG (Retrieval-Augmented Generation)
RAG embeds code into vectors and retrieves similar chunks. Each retrieval is independent and stateless. m1nd maintains a persistent graph with relationships, learning, and investigation state. RAG cannot answer structural questions, simulate deletions, or learn from feedback.
RAG costs LLM tokens per query. m1nd costs zero tokens per query.
m1nd vs Sourcegraph / CodeGraph / SCIP
Sourcegraph provides cross-repository code search and navigation based on SCIP (Source Code Intelligence Protocol) indexing. It produces accurate, language-server-quality code intelligence.
m1nd is not a code search engine. It is a reasoning engine. Sourcegraph tells you “function X is defined here and called here.” m1nd tells you “if you change function X, here are the 4,189 nodes in the cascade, and the graph predicts these 3 files probably need changes too.”
Sourcegraph is a hosted SaaS product. m1nd is a local binary with zero cost per query.
m1nd vs GitHub Copilot context
Copilot uses a mix of embeddings and heuristics to select context files for the LLM. It does not maintain a persistent graph, does not learn from feedback, and does not support structural queries.
m1nd can be used alongside Copilot – through MCP, Copilot can call m1nd tools to get smarter context before generating code.
Contributing
How do I contribute?
See CONTRIBUTING.md. Fork the repo, create a branch, make your changes with tests, and open a PR.
What is the test suite?
cargo test --all
Tests cover the core graph engine, plasticity, spreading activation, hypothesis engine, all MCP tool handlers, and the JSON-RPC protocol layer.
What are the code style requirements?
cargo fmtbefore committingcargo clippy --all -- -D warningsmust pass- No
unsafewithout an explanatory comment - All new code needs tests
What areas need the most help?
- Language extractors: Adding tree-sitter integration or new language-specific extractors
- Graph algorithms: Community detection, better decay functions, embedding-based semantic scoring
- Benchmarks: Running m1nd on diverse codebases and reporting real-world performance numbers
- Documentation: Tutorials, examples, and translations
Where do I report bugs?
GitHub Issues at github.com/cosmophonix/m1nd/issues. Use the bug label.
Benchmarks
All numbers in this document are from real execution against a production Python backend: 335 files, approximately 52,000 lines of code, producing a graph of 9,767 nodes and 26,557 edges.
No synthetic benchmarks. No cherry-picked runs. These are the numbers you get when you run m1nd against a real codebase.
Core Operations
| Operation | Time | Scale |
|---|---|---|
| Full ingest | 910ms | 335 files -> 9,767 nodes, 26,557 edges |
| Spreading activation | 31-77ms | 15-20 results from 9,767 nodes |
| Blast radius (depth=3) | 5-52ms | Up to 4,271 affected nodes |
| Stacktrace analysis | 3.5ms | 5 frames -> 4 suspects ranked |
| Plan validation | 10ms | 7 files -> 43,152 blast radius |
| Counterfactual cascade | 3ms | Full BFS on 26,557 edges |
| Hypothesis testing | 58ms | 25,015 paths explored |
| Pattern scan (all 8 patterns) | 38ms | 335 files, 50 findings per pattern |
| Multi-repo federation | 1.3s | 11,217 nodes, 18,203 cross-repo edges |
| Lock diff | 0.08us | 1,639-node subgraph comparison |
| Trail merge | 1.2ms | 5 hypotheses, 3 conflicts detected |
| Hebbian learn | <1ms | 740 edges adjusted |
| Health check | <1ms | Statistics only |
| Seek (semantic search) | 10-15ms | 20 results |
| Warmup (task priming) | 82-89ms | 50 seed nodes primed |
| Resonate (standing wave) | 37-52ms | Harmonic analysis |
| Fingerprint (twin detection) | 1-107ms | Topology comparison |
| Why (path explanation) | 5-6ms | Shortest path between two nodes |
| Drift (weight changes) | 23ms | Since last session |
| Timeline (temporal history) | ~1ms | Node change history |
Comparison Table
m1nd vs grep / ripgrep
| Dimension | ripgrep | m1nd |
|---|---|---|
| Query type | Text pattern (regex) | Natural language intent |
| Returns | Lines matching pattern | Ranked nodes with 4D scores |
| Relationships | None | Full graph traversal |
| Learning | None | Hebbian plasticity |
| “What if?” queries | Not possible | Counterfactual, hypothesis, impact |
| Speed (simple query) | ~5ms | 31-77ms |
| Speed (structural query) | Not possible | 3-58ms |
| Memory | ~10MB | ~50MB |
| Cost per query | Zero | Zero |
ripgrep is faster for simple text matching and always will be. m1nd answers questions ripgrep cannot ask.
m1nd vs RAG
| Dimension | RAG | m1nd |
|---|---|---|
| Retrieval method | Embedding similarity (top-K) | Spreading activation (4D) |
| Statefulness | Stateless per query | Persistent graph + learning |
| Relationships | Not tracked | First-class edges |
| Learning | None | Hebbian feedback loop |
| Investigation memory | None | Trail save/resume/merge |
| Structural queries | Not possible | Impact, counterfactual, hypothesis |
| Setup cost | Embedding computation | 910ms ingest |
| Cost per query | LLM tokens for embedding | Zero |
| Typical query latency | 200-500ms (includes API call) | 31-77ms (local) |
m1nd vs Static Analysis (Sourcegraph, SCIP, LSP)
| Dimension | Static Analysis | m1nd |
|---|---|---|
| Accuracy | Language-server precise | Structural heuristic |
| Learning | None | Hebbian plasticity |
| Temporal intelligence | git blame only | Co-change velocity + decay |
| “What if?” simulation | Not possible | Counterfactual cascade |
| Hypothesis testing | Not possible | Bayesian path analysis |
| Investigation state | Not tracked | Trail system |
| Multi-agent | Read-only sharing | Shared graph + isolated perspectives |
| Cost | Hosted SaaS or self-hosted infra | Single binary, zero cost |
| Setup | Minutes to hours (indexing) | 910ms (ingest) |
Summary: Use the Right Tool
| Task | Best Tool |
|---|---|
| Find text in files | ripgrep |
| Find semantically similar code | RAG |
| Go-to-definition, find references | LSP / Sourcegraph |
| Understand blast radius of a change | m1nd |
| Simulate module removal | m1nd |
| Test structural hypotheses | m1nd |
| Learn from agent feedback | m1nd |
| Persist investigation state | m1nd |
| Multi-agent shared code intelligence | m1nd |
Cost Comparison: Tokens Saved
In a typical AI agent coding session, the agent calls grep/ripgrep 20-50 times and reads 10-30 files to navigate a codebase. Each file read costs tokens (the file content is sent to the LLM).
m1nd replaces many of these exploratory reads with graph queries that return ranked results rather than raw file contents. The agent reads fewer files because it reads the right files first.
Estimated Token Savings
Assumptions: 335-file Python backend, 8-hour agent workday, agent using Claude Opus.
| Without m1nd | With m1nd |
|---|---|
| ~40 grep calls/hour | ~15 grep calls/hour |
| ~20 file reads/hour | ~8 file reads/hour |
| ~150K tokens/hour (context) | ~60K tokens/hour (context) |
| ~1.2M tokens/day | ~480K tokens/day |
Estimated savings: ~720K tokens/day (60% reduction in context tokens).
These are estimates based on production usage in the a multi-agent system. Your mileage varies with codebase size, task type, and agent behavior.
The key insight: m1nd does not replace search. It focuses search. The agent still uses grep and reads files, but it starts from a much better position because m1nd told it where to look.
Memory and CPU Usage
Memory
| Component | Size |
|---|---|
| Graph (9,767 nodes, 26,557 edges) | ~2MB |
| Plasticity state | ~500KB |
| Perspective state (per active perspective) | ~100KB |
| Lock baselines (per lock) | ~200KB |
| Trail storage (per saved trail) | ~50KB |
| JSON-RPC server overhead | ~5MB |
| Typical total | ~50MB |
Memory scales linearly with graph size. A 100K-node graph would use approximately 20MB for the graph alone, with similar overhead for the server.
CPU
m1nd is single-threaded for graph operations (no lock contention, deterministic results). Ingest uses Rayon for parallel file parsing. During query serving, CPU usage is negligible between queries and spikes briefly during activation (31-77ms of computation).
On an Apple M2, the server at idle uses <0.1% CPU. During a burst of queries, it peaks at ~5% of a single core.
Scaling Characteristics
Ingest Time vs Codebase Size
Ingest scales linearly with file count. Reference resolution is roughly O(n log n) where n is the number of cross-file references.
| Files | Estimated Ingest Time | Estimated Nodes |
|---|---|---|
| 100 | ~270ms | ~3,000 |
| 335 | 910ms (measured) | 9,767 (measured) |
| 1,000 | ~2.7s | ~29,000 |
| 10,000 | ~27s | ~290,000 |
| 100,000 | ~4.5min | ~2,900,000 |
Activation Time vs Graph Size
Spreading activation is bounded by the number of edges traversed, which depends on graph density and query specificity rather than total graph size. Activation in a 100K-node graph is estimated at 100-200ms.
Persistence Time vs State Size
JSON serialization scales linearly with state size. A 10K-node graph persists in under 100ms. A 100K-node graph would take approximately 1 second.
Reproducibility
To reproduce these benchmarks:
git clone https://github.com/cosmophonix/m1nd.git
cd m1nd
cargo build --release
# Start the server
./target/release/m1nd-mcp
Then send the JSON-RPC calls from the Examples document against your own codebase. Times will vary based on:
- Hardware (CPU speed, memory bandwidth)
- Codebase size and language
- Graph density (codebases with many cross-references produce denser graphs)
- Plasticity state (learned weights affect activation propagation paths)
Report your benchmarks via GitHub Issues with the benchmark label.
Changelog
All notable changes to m1nd are documented here. This project uses Semantic Versioning.
v0.1.0 – Initial Release
The first public release of m1nd: a neuro-symbolic connectome engine with Hebbian plasticity, spreading activation, and 43 MCP tools. Built in Rust.
Core Engine (m1nd-core)
- Compressed Sparse Row (CSR) graph with forward and reverse adjacency
- PageRank computation on ingest
- 4-dimensional spreading activation: structural, semantic, temporal, causal
- Hebbian plasticity: Long-Term Potentiation (LTP), Long-Term Depression (LTD), homeostatic normalization
- XLR differential processing: noise cancellation inspired by balanced audio cables
- Hypothesis engine: claim testing with Bayesian confidence on graph paths
- Counterfactual engine: module removal simulation with cascade analysis
- Structural hole detection: topology-based gap analysis
- Resonance analysis: standing wave computation for structural hub identification
- Fingerprint engine: activation fingerprinting for structural twin detection
- Trail system: investigation state persistence, resume, and multi-trail merge with conflict detection
- Lock system: subgraph pinning with sub-microsecond diff (0.08us)
- Temporal engine: co-change history, velocity scoring, decay functions
- Domain configurations: code, music, memory, generic presets with tuned decay half-lives
Ingest Layer (m1nd-ingest)
- Language extractors: Python, Rust, TypeScript/JavaScript, Go, Java
- Generic fallback extractor: heuristic-based for unsupported languages
- JSON adapter: structured data ingestion
- Memory adapter: text corpus ingestion
- Reference resolver: cross-file import and call resolution
- Incremental ingest: re-process only changed files
- Multi-repo federation: unified graph with automatic cross-repo edge detection
MCP Server (m1nd-mcp)
- 43 MCP tools across 7 layers:
- Foundation (13): activate, impact, missing, why, learn, drift, health, seek, scan, timeline, diverge, warmup, federate
- Perspective Navigation (12): start, routes, follow, back, peek, inspect, suggest, affinity, branch, compare, list, close
- Lock System (5): create, watch, diff, rebase, release
- Superpowers (13): hypothesize, counterfactual, predict, fingerprint, resonate, trace, validate_plan, differential, trail.save, trail.resume, trail.merge, trail.list, seek
- JSON-RPC over stdio: compatible with MCP protocol version 2024-11-05
- Dual transport: framed (Content-Length headers) and line-delimited JSON-RPC
- Auto-persistence: configurable interval (default: every 50 queries) + on shutdown
- Multi-agent support: agent ID tracking, perspective isolation, shared graph
- Tool name normalization: underscores automatically converted to dots (e.g.,
m1nd_activate->m1nd.activate)
Performance (measured on 335-file Python backend, ~52K lines)
- Full ingest: 910ms (9,767 nodes, 26,557 edges)
- Spreading activation: 31-77ms
- Blast radius: 5-52ms
- Counterfactual: 3ms
- Hypothesis testing: 58ms (25,015 paths)
- Lock diff: 0.08us
- Trail merge: 1.2ms
- Memory footprint: ~50MB typical
Known Limitations
- Semantic scoring uses trigram matching, not neural embeddings (planned for v0.2)
- No tree-sitter integration yet (planned for v0.2)
- 6 languages with dedicated extractors; others use generic fallback
- Graph is fully in-memory; very large codebases (400K+ files) need ~80MB
- No dataflow or taint analysis (out of scope; use dedicated SAST tools)
Planned: v0.2.0
- Tree-sitter integration for 64+ language support
- Optional embedding-based semantic scoring
- Graph partitioning for very large codebases
- Community detection algorithms
- Performance optimizations for 100K+ node graphs
- MCP Streamable HTTP transport (in addition to stdio)
Planned: v0.3.0
- Distributed graph (multi-machine federation)
- Real-time file watcher integration
- Plugin system for custom extractors and tools
- Graph visualization export (DOT, D3.js, Mermaid)
- Metrics and observability (Prometheus, OpenTelemetry)