ICM Technical Architecture
March 6, 2026 · View on GitHub
Overview
ICM is a Rust workspace of 4 crates that compile into a single binary. No runtime dependencies, no external services.
icm (binary)
├── icm-core Types, traits, embedder
├── icm-store SQLite + FTS5 + sqlite-vec
├── icm-mcp MCP server (JSON-RPC 2.0 over stdio)
└── icm-cli CLI, config, extraction, benchmarks
Crate Dependency Graph
icm-cli ──────► icm-core
│ ▲
├──► icm-store ─┘
│ ▲
└──► icm-mcp ──► icm-store
│
└──► icm-core
icm-core
Foundation crate. No I/O, no database — only types and traits.
Data Types
Memory
pub struct Memory {
pub id: String, // ULID
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub last_accessed: DateTime<Utc>,
pub access_count: u32,
pub weight: f32, // 1.0 at creation, decays over time
pub topic: String,
pub summary: String,
pub raw_excerpt: Option<String>,
pub keywords: Vec<String>,
pub importance: Importance, // Critical | High | Medium | Low
pub source: MemorySource, // ClaudeCode | Conversation | Manual
pub related_ids: Vec<String>,
pub embedding: Option<Vec<f32>>, // 384/768/1024d depending on model
}
Importance
pub enum Importance {
Critical, // decay: 0.0 (never), prune: never
High, // decay: 0.5x rate, prune: never
Medium, // decay: 1.0x rate, prune: when weight < threshold
Low, // decay: 2.0x rate, prune: when weight < threshold
}
Memoir (Knowledge Graph)
pub struct Memoir {
pub id: String,
pub name: String, // unique
pub description: String,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub consolidation_threshold: u32, // default: 50
}
pub struct Concept {
pub id: String,
pub memoir_id: String,
pub name: String, // unique within memoir
pub definition: String,
pub labels: Vec<Label>, // namespace:value pairs
pub confidence: f32, // 0.0-1.0, grows with refinement
pub revision: u32, // incremented on refine
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub source_memory_ids: Vec<String>,
}
pub struct ConceptLink {
pub id: String,
pub source_id: String,
pub target_id: String,
pub relation: Relation, // 9 types (see below)
pub weight: f32,
pub created_at: DateTime<Utc>,
}
Relations
pub enum Relation {
PartOf, // A is part of B
DependsOn, // A requires B
RelatedTo, // A is associated with B
Contradicts, // A conflicts with B
Refines, // A is a more precise version of B
AlternativeTo, // A can replace B
CausedBy, // A is caused by B
InstanceOf, // A is an instance of B
SupersededBy, // A is replaced by B (marks obsolescence)
}
Parsing accepts both snake_case and camelCase (depends_on = dependson). Self-links (source == target) are rejected at the database level via CHECK constraint.
Traits
MemoryStore
pub trait MemoryStore {
fn store(&self, memory: Memory) -> IcmResult<String>;
fn get(&self, id: &str) -> IcmResult<Option<Memory>>;
fn update(&self, memory: &Memory) -> IcmResult<()>;
fn delete(&self, id: &str) -> IcmResult<()>;
fn search_by_keywords(&self, keywords: &[&str], limit: usize) -> IcmResult<Vec<Memory>>;
fn search_fts(&self, query: &str, limit: usize) -> IcmResult<Vec<Memory>>;
fn search_by_embedding(&self, embedding: &[f32], limit: usize) -> IcmResult<Vec<(Memory, f32)>>;
fn search_hybrid(&self, query: &str, embedding: &[f32], limit: usize) -> IcmResult<Vec<(Memory, f32)>>;
fn update_access(&self, id: &str) -> IcmResult<()>;
fn apply_decay(&self, decay_factor: f32) -> IcmResult<usize>;
fn prune(&self, weight_threshold: f32) -> IcmResult<usize>;
fn get_by_topic(&self, topic: &str) -> IcmResult<Vec<Memory>>;
fn list_topics(&self) -> IcmResult<Vec<(String, usize)>>;
fn consolidate_topic(&self, topic: &str, consolidated: Memory) -> IcmResult<()>;
fn count(&self) -> IcmResult<usize>;
fn count_by_topic(&self, topic: &str) -> IcmResult<usize>;
fn stats(&self) -> IcmResult<StoreStats>;
fn topic_health(&self, topic: &str) -> IcmResult<TopicHealth>;
}
MemoirStore
pub trait MemoirStore {
fn create_memoir(&self, memoir: Memoir) -> IcmResult<String>;
fn get_memoir(&self, id: &str) -> IcmResult<Option<Memoir>>;
fn get_memoir_by_name(&self, name: &str) -> IcmResult<Option<Memoir>>;
fn update_memoir(&self, memoir: &Memoir) -> IcmResult<()>;
fn delete_memoir(&self, id: &str) -> IcmResult<()>; // CASCADE: deletes concepts + links
fn list_memoirs(&self) -> IcmResult<Vec<Memoir>>;
fn add_concept(&self, concept: Concept) -> IcmResult<String>;
fn get_concept(&self, id: &str) -> IcmResult<Option<Concept>>;
fn get_concept_by_name(&self, memoir_id: &str, name: &str) -> IcmResult<Option<Concept>>;
fn update_concept(&self, concept: &Concept) -> IcmResult<()>;
fn delete_concept(&self, id: &str) -> IcmResult<()>;
fn list_concepts(&self, memoir_id: &str) -> IcmResult<Vec<Concept>>;
fn search_concepts_fts(&self, memoir_id: &str, query: &str, limit: usize) -> IcmResult<Vec<Concept>>;
fn search_concepts_by_label(&self, memoir_id: &str, label: &Label, limit: usize) -> IcmResult<Vec<Concept>>;
fn search_all_concepts_fts(&self, query: &str, limit: usize) -> IcmResult<Vec<Concept>>;
fn refine_concept(&self, id: &str, new_definition: &str, new_source_ids: &[String]) -> IcmResult<()>;
fn add_link(&self, link: ConceptLink) -> IcmResult<String>;
fn get_links_from(&self, concept_id: &str) -> IcmResult<Vec<ConceptLink>>;
fn get_links_to(&self, concept_id: &str) -> IcmResult<Vec<ConceptLink>>;
fn delete_link(&self, id: &str) -> IcmResult<()>;
fn get_neighbors(&self, concept_id: &str, relation: Option<Relation>) -> IcmResult<Vec<Concept>>;
fn get_neighborhood(&self, concept_id: &str, depth: usize) -> IcmResult<(Vec<Concept>, Vec<ConceptLink>)>;
fn memoir_stats(&self, memoir_id: &str) -> IcmResult<MemoirStats>;
}
Embedder
pub trait Embedder: Send + Sync {
fn embed(&self, text: &str) -> IcmResult<Vec<f32>>;
fn embed_batch(&self, texts: &[&str]) -> IcmResult<Vec<Vec<f32>>>;
fn dimensions(&self) -> usize;
}
Feature-gated (embeddings). Uses fastembed v4 with lazy initialization via OnceLock + Mutex double-check pattern (because OnceLock::get_or_try_init is unstable).
Default model: intfloat/multilingual-e5-base (768d, 100+ languages). Configurable via config.toml.
Error Types
pub enum IcmError {
NotFound(String), // get/delete with unknown ID
Database(String), // SQLite errors
Serialization(serde_json::Error), // JSON serialization
Config(String), // Configuration errors
Embedding(String), // Embedding model errors
}
pub type IcmResult<T> = Result<T, IcmError>;
icm-store
SQLite implementation of MemoryStore + MemoirStore via rusqlite 0.34 (synchronous, not async).
Constructors
SqliteStore::new(path: &Path) -> IcmResult<Self> // default 384d
SqliteStore::with_dims(path: &Path, dims: usize) -> IcmResult<Self> // custom dimensions
SqliteStore::in_memory() -> IcmResult<Self> // for tests
Schema (8 tables)
| Table | Type | Purpose |
|---|---|---|
memories | regular | Main memory storage (id, topic, summary, weight, embedding, ...) |
memories_fts | FTS5 virtual | Full-text search on id, topic, summary, keywords |
vec_memories | vec0 virtual | Cosine similarity search via sqlite-vec |
memoirs | regular | Named knowledge containers |
concepts | regular | Knowledge graph nodes with UNIQUE(memoir_id, name) |
concepts_fts | FTS5 virtual | Full-text search on concept id, name, definition, labels |
concept_links | regular | Typed edges with CHECK(source_id != target_id) |
icm_metadata | regular | Key-value store (embedding_dims, last_decay_at) |
FTS tables are synchronized via AFTER INSERT/UPDATE/DELETE triggers.
Auto-Migrations
On startup, the schema is checked and migrated if needed:
- Missing columns (
updated_at,embedding) →ALTER TABLE ADD COLUMN - Missing FTS/vec tables → created if absent
- Dimension change (model switch) → drops
vec_memories, clears all embeddings, recreates with new dimensions, stores new dim inicm_metadata
Search Pipeline
Query arrives
│
├─ Has embedder? ──► Hybrid search
│ ├─ FTS5 BM25 (30% weight)
│ ├─ Cosine similarity via sqlite-vec (70% weight)
│ └─ Merge + deduplicate by memory ID
│
└─ No embedder ──► FTS5 search
│
└─ No FTS results? ──► Keyword LIKE fallback
FTS queries are sanitized: special characters (-, *, :, etc.) are stripped and each token is quoted to prevent FTS5 syntax injection.
Decay Model
Decay runs automatically on recall if >24h since last run. Stored in icm_metadata.last_decay_at.
effective_rate = base_decay × importance_multiplier / (1 + access_count × 0.1)
importance_multiplier:
Critical = 0.0 (no decay ever)
High = 0.5 (half speed)
Medium = 1.0 (normal)
Low = 2.0 (double speed)
new_weight = weight × (1 - effective_rate)
Prune: only Medium and Low importance memories with weight < threshold are deleted.
sqlite-vec
Loaded via sqlite3_auto_extension with transmute (required by C extension API). Initialization runs once per process via std::sync::Once.
Vector table: distance_metric=cosine (L2 is the default but gives negative similarities for normalized vectors).
Dedup
On store via MCP, if an existing memory in the same topic has >85% hybrid search similarity, the existing memory is updated instead of creating a duplicate.
Cascade Delete
DELETE memoir → cascades to all concepts → cascades to all links (via ON DELETE CASCADE).
icm-mcp
MCP server implementing JSON-RPC 2.0 over stdio. 18 tools.
Protocol Flow
Client ICM Server
│ │
├── initialize ───────────────────────►│
│◄── capabilities + instructions ──────┤
│ │
├── tools/list ───────────────────────►│
│◄── 18 tool definitions ─────────────┤
│ │
├── tools/call {name, arguments} ─────►│
│◄── ToolResult {content, isError} ───┤
│ │
└── (stdin closes) ──────────────────►│ exit
Tool Dispatch
| Tool | Required args | Optional args |
|---|---|---|
icm_memory_store | topic, content | importance, keywords[], raw_excerpt |
icm_memory_recall | query | topic, keyword, limit |
icm_memory_update | id, content | importance, keywords[] |
icm_memory_forget | id | — |
icm_memory_consolidate | topic, summary | — |
icm_memory_list_topics | — | — |
icm_memory_stats | — | — |
icm_memory_health | — | topic |
icm_memory_embed_all | — | topic |
icm_memoir_create | name | description |
icm_memoir_list | — | — |
icm_memoir_show | name | — |
icm_memoir_add_concept | memoir, name, definition | labels |
icm_memoir_refine | memoir, name, definition | — |
icm_memoir_search | memoir, query | label, limit |
icm_memoir_search_all | query | limit |
icm_memoir_link | memoir, from, to, relation | — |
icm_memoir_inspect | memoir, name | depth |
Store Nudge
The server tracks consecutive non-store tool calls. After 10 calls without icm_memory_store, it appends a hint to the response:
[ICM: 12 tool calls since last store. Consider saving important context.]
Counter resets on every icm_memory_store.
Compact Mode
icm serve --compact produces shorter responses:
- Store:
ok:<id>instead ofStored memory: <id> - Recall:
[topic] summary\nper line instead of multi-line verbose format
Saves ~40% tokens on recall output.
Auto-behaviors
- Auto-dedup:
icm_memory_storechecks hybrid similarity >85% in same topic → updates existing instead of duplicating - Auto-decay:
icm_memory_recallruns decay if >24h since last run - Consolidation hint:
icm_memory_storewarns when topic has >7 entries - Auto-embed: if embedder is available, memories are embedded on store/update
icm-cli
Binary entrypoint. All commands:
icm store Store a memory
icm recall Search memories
icm forget Delete a memory by ID
icm topics List all topics
icm stats Global statistics
icm health Per-topic hygiene report
icm decay Apply temporal decay
icm prune Delete low-weight memories
icm consolidate Merge topic into single summary
icm embed Backfill embeddings
icm extract Rule-based fact extraction from stdin/text
icm recall-context Format recalled memories for prompt injection
icm memoir Subcommands: create, show, add-concept, refine, search, search-all, link, inspect, list
icm init Auto-configure 14 AI tools (mcp, cli, skill, hook modes)
icm serve Start MCP server (--compact for shorter output)
icm config Show active configuration
icm bench Storage performance benchmark
icm bench-recall Knowledge retention benchmark
icm bench-agent Multi-session agent efficiency benchmark
Extraction (Layer 0)
Pattern-based scoring. Each sentence gets a score from keyword matches:
| Signal | Example keywords | Score boost |
|---|---|---|
| Architecture | uses, architecture, pattern, algorithm | +3 |
| Error/Fix | error, fixed, bug, workaround | +3 |
| Decision | decided, chose, prefer, switched to | +4 |
| Config | configured, setup, installed, enabled | +2 |
| Dev signals | commit, deploy, migrate, refactor | +2 |
Sentences below threshold are dropped. Dedup via Jaccard similarity (>0.6 = skip).
Build
cargo build --release # Full build with embeddings
cargo build --release --no-default-features # Without embeddings (fast, small)
The embeddings feature adds fastembed + ort (~2GB debug build). Use --no-default-features for fast iteration on non-embedding code.
Testing
cargo test # 110 tests across all crates
cargo clippy # Lint (CI uses -D warnings)
cargo fmt --check # Format check
Test categories:
| Category | Count | What's tested |
|---|---|---|
| Unit | ~50 | Core CRUD, FTS, vector search, schema migrations, memoirs, concepts, links, graph traversal |
| Security | ~10 | SQL injection (topic, summary, keywords, FTS), null bytes, unicode, XSS via MCP, large inputs |
| Performance | 7 | 1000 stores, 100 FTS/vector/hybrid searches, decay on 1000 memories, 1000 gets (all with time assertions) |
| UX | ~15 | Missing params, unknown tools, empty states, compact output, protocol serialization |
| Integration | ~28 | MCP tool dispatch, store+recall roundtrip, consolidation, topic filtering, dedup |
Configuration
# ~/.config/icm/config.toml
[embeddings]
model = "intfloat/multilingual-e5-base" # Any fastembed model code
Environment variables:
ICM_CONFIG— override config file pathICM_DB— override database file pathICM_LOG— set log level (debug, info, warn, error)
Security
- All SQL queries use parameterized statements (no string interpolation)
- FTS5 queries are sanitized (special chars stripped, tokens quoted)
- No network access for storage (local SQLite only)
- Embedding model runs locally (no API calls unless explicitly configured)
- Self-links rejected via SQL CHECK constraint
- Tested against: SQL injection, FTS injection, null bytes, unicode boundaries, 500KB payloads