# MCP Roo Memory — Concept
> **Concept**: Graph as a workspace with subgraphs, Qdrant vector index for semantic search.
> Reasoning chains preserved as graph paths — not linear logs.
>
> **Regression Search Pattern**: First meaning (vector), then context (graph),
> then specifics (project files).
---
## 1. Philosophy
Typical AI agent memory is a dump: flat "facts" + vector chunks with no structure.
This doesn't work for complex tasks because:
- **No structure** — can't see how facts relate
- **No navigation** — can't walk a reasoning chain
- **No decomposition** — complex tasks can't be broken down in memory
- **No context** — vectors don't know which graph they live in
- **No context window control** — graph grows unbounded, token window fills up
- **No mutation** — when decisions change, stale facts linger
**MCP Roo Memory** solves this with **fractal graph memory** and four key mechanisms:
1. **Fractal Graph** (SQLite) — workspace with subgraphs, navigation, relations
2. **Multi-layer Vector Index** (Qdrant) — Entity + Chunk + Fact for semantic search
3. **Desktop Viewport** — lazy loading, Hot/Cold/Archive tiers for context window control
4. **Mutation Strategy** — Update/Supersedes/Stale-cascade for knowledge evolution
5. **Temporal Layer** — time as a first-class citizen: chronological walks, session timelines, temporal vector filters
```
┌─────────────────────────────────────────────────────────────────────┐
│ DESKTOP VIEWPORT │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ HOT (always in agent context, 3-10 nodes) │ │
│ │ ┌──────────┐ ┌──────────┐ ┌─────────────────────────┐ │ │
│ │ │ Task: │ │ Entity: │ │ Fact: JWT tokens │ │ │
│ │ │ Auth │ │ JWT Auth │ │ expire in 24h │ │ │
│ │ └──────────┘ └──────────┘ └─────────────────────────┘ │ │
│ └────────────────────────────────────────────────────────┘ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ COLD (titles only, available via focus/search, 10-100) │ │
│ │ Task: DB Schema | Task: API Design | Subtask: OAuth... │ │
│ └────────────────────────────────────────────────────────┘ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ ARCHIVE (metadata only, 100+, via search) │ │
│ │ 47 archived nodes (oldest: 2026-04-28) │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ Vector Index (Qdrant) — multi-layer search │
│ Layer 1: Entities | Layer 2: Chunks | Layer 3: Facts/Decisions │
└─────────────────────────────────────────────────────────────────────┘
```
---
## 2. System Architecture
### 2.1 Components
```mermaid
graph TB
subgraph "MCP Server mcp-cortex"
MC[Main Controller]
GM[Graph Manager
SQLite + JSON]
VM[Vector Manager
Qdrant Client + fastembed]
DM[Desktop Manager
Navigation + History + Viewport]
end
subgraph "External Dependencies"
QD[(Qdrant
Vector Index)]
SQL[(SQLite
Graph + Metadata)]
end
subgraph "Roo Code"
RA[Roo Agent]
end
RA <-->|MCP Protocol| MC
MC --> GM
MC --> VM
MC --> DM
GM --> SQL
VM --> QD
GM <--> VM
DM --> GM
```
### 2.2 Regression Search Pattern
```mermaid
graph TB
L1[LEVEL 1: Meaning
Vector search in Qdrant] -->|"query text"| Q1[Qdrant returns
Entity Chunk Fact]
L1 --> L2[LEVEL 2: Context
Graph traversal from found nodes]
L2 -->|relations| CTX[Parent task
Related facts
Decision chain
Fileref links]
L2 --> L3[LEVEL 3: Specifics
Targeted file reading]
L3 -->|"now I know what to look for"| FILES["src/auth/jwt.py
actual code"]
style L1 fill:#4af,stroke:#333
style L2 fill:#4fa,stroke:#333
style L3 fill:#fa4,stroke:#333
```
---
## 3. Data Model
### 3.1 Node Taxonomy (17 types)
#### Vectorized (Qdrant — semantic search)
| Type | Description | Example |
|------|-------------|---------|
| `entity` | **Concept/entity** — semantic center of gravity | "JWT Access Token", "Rate Limiter" |
| `fact` | **Knowledge/statement** | "Refresh token lives 7 days" |
| `decision` | **Architectural decision** | "Use RS256 instead of HS256" |
| `chunk` | **Fragment** of documentation/discussion/chat | "From PyJWT docs: paragraph about RS256" |
| `thought` | **Reasoning step/thought** | "Check if performance drops" |
| `question` | **Question** arising during work | "What TTL for refresh token?" |
| `hypothesis` | **Assumption/guess** | "Maybe the bottleneck is in DB indexes" |
| `action` | **Roo action** | "Changed auth/jwt.py: replaced HS256 with RS256" |
| `error` | **Error/bug** | "JWT ExpiredSignature exception" |
| `note` | **General note** | "API docs are in /docs/api.md" |
| `pattern` | **Architectural pattern** | "Repository Pattern", "CQRS" |
| `goal` | **Goal/requirement** | "System must handle 10k RPS" |
| `constraint` | **Constraint** | "Cannot use Neo4j, only SQLite" |
#### Graph-only (SQLite — not vectorized)
| Type | Description | Example |
|------|-------------|---------|
| `session` | Workspace/session root | `sess_abc` |
| `task` | Task | "Implement auth module" |
| `subtask` | Subtask | "Implement JWT" (task with parent_id) |
| `fileref` | **File reference** | `/src/auth/jwt.py` |
**Fileref** is a special node type. Contains path, file type, hash, description. Not vectorized because file search is path-based, not semantic. Connections from concepts to files go through `indexes`, `extracted_from`, `references` relations.
**Entity** — semantic concept of the project. Vectorized for meaning-based search. Connected to fileref via `indexes`.
### 3.2 Relation Taxonomy (22 types)
#### Hierarchical (tree)
| Relation | From → To | Meaning |
|----------|-----------|---------|
| `contains` | Session → Task, Task → Subtask, Task → Fact | Parent-child (fractality) |
| `decomposes_to` | Task → Subtask | Task decomposition |
| `belongs_to` | Fileref → Task | File used in task |
#### Semantic (knowledge)
| Relation | From → To | Meaning |
|----------|-----------|---------|
| `derives_from` | Fact → Chunk/Reference | Fact derived from source |
| `supports` | Fact → Decision | Fact supports decision |
| `contradicts` | Fact ↔ Fact | Facts contradict each other |
| `related_to` | Any ↔ Any | Semantically related (weak) |
| `questions` | Question → Entity/Task/Fact | Question relates to something |
| `answers` | Fact/Decision → Question | Answer to a question |
#### Index (graph as index)
| Relation | From → To | Meaning |
|----------|-----------|---------|
| `indexes` | Entity → Fileref | Concept described by file |
| `extracted_from` | Fact/Chunk → Fileref | Knowledge extracted from file |
| `references` | Decision → Fileref | Decision implemented in file |
| `implements` | Action → Fileref | Action modified file |
| `relates_to_file` | Any → Fileref | Universal file link |
#### Chronological (history)
| Relation | From → To | Meaning |
|----------|-----------|---------|
| `sequel_to` | Thought → Thought, Action → Action | Chain in time |
| `supersedes` | Decision → Decision (stale) | New decision replaced old |
| `leads_to` | Thought → Decision | Thought led to decision |
| `resolves` | Action → Error | Action fixed error |
| `triggers` | Error → Action | Error triggered action |
#### Dependency
| Relation | From → To | Meaning |
|----------|-----------|---------|
| `depends_on` | Task → Task, Subtask → Subtask | One task depends on another |
| `blocks` | Error → Task | Error blocks task |
| `constrained_by` | Decision → Constraint | Decision constrained by requirement |
### 3.3 SQLite Schema
```sql
-- Graph nodes
CREATE TABLE nodes (
id TEXT PRIMARY KEY, -- UUID v4
type TEXT NOT NULL, -- entity|fact|decision|chunk|thought|question|hypothesis|
-- action|error|note|pattern|goal|constraint|
-- session|task|subtask|fileref
workspace_id TEXT NOT NULL, -- workspace isolation
parent_id TEXT, -- parent node (for fractality)
data JSON NOT NULL DEFAULT '{}',-- flexible metadata
status TEXT DEFAULT 'active', -- active|stale|archived
created_at TEXT NOT NULL, -- ISO 8601
updated_at TEXT NOT NULL -- ISO 8601
);
-- Graph edges
CREATE TABLE relations (
id TEXT PRIMARY KEY, -- UUID v4
from_id TEXT NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
to_id TEXT NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
type TEXT NOT NULL, -- contains|derives_from|references|indexes|...
weight REAL DEFAULT 1.0, -- connection strength 0.0-1.0
data JSON DEFAULT '{}', -- relation metadata
created_at TEXT NOT NULL
);
-- Navigation history (Desktop Manager)
CREATE TABLE navigation_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
workspace_id TEXT NOT NULL,
node_id TEXT NOT NULL,
action TEXT NOT NULL, -- focus|branch|traverse|search|open
context JSON DEFAULT '{}', -- what led to this step
created_at TEXT NOT NULL
);
-- Indexes
CREATE INDEX idx_nodes_workspace ON nodes(workspace_id);
CREATE INDEX idx_nodes_parent ON nodes(parent_id);
CREATE INDEX idx_nodes_type ON nodes(type);
CREATE INDEX idx_nodes_status ON nodes(status);
CREATE INDEX idx_relations_from ON relations(from_id);
CREATE INDEX idx_relations_to ON relations(to_id);
CREATE INDEX idx_relations_type ON relations(type);
CREATE INDEX idx_nav_workspace ON navigation_history(workspace_id);
```
### 3.4 Qdrant Collection
**Collection**: `cortex_memory`
**Parameters**:
- `vectors_config`: `fastembed` auto-configure (paraphrase-multilingual-MiniLM-L12-v2, 384d)
- `sparse_vectors_config`: enabled for hybrid search (BM25-style)
- `distance`: Cosine
**Payload** (vector metadata):
```json
{
"node_id": "uuid-of-graph-node",
"workspace_id": "workspace-identifier",
"node_type": "entity|fact|chunk|thought|...",
"layer": "entity|chunk|fact",
"tags": ["auth", "jwt", "security"],
"status": "active|stale",
"created_at": "2026-05-12T20:00:00Z"
}
```
**Indexing Strategy**:
- Entity, Fact, Decision, Chunk, Thought, Question, Hypothesis, Action, Error, Note, Pattern, Goal, Constraint — vectorized
- Session, Task, Subtask, Fileref — not vectorized
- When a node is deleted from the graph, its vector is removed from Qdrant
- When node text is updated, the old vector is replaced
---
## 4. Fractality: Subgraphs
Fractality is implemented through the `contains` relation:
```mermaid
graph TB
S[Session: workspace_1]
S -->|contains| T1[Task: Auth Module]
S -->|contains| G1[Goal: 10k RPS]
T1 -->|decomposes_to| ST1[Subtask: JWT]
T1 -->|decomposes_to| ST2[Subtask: OAuth]
T1 -->|depends_on| DB[Task: DB Schema]
ST1 -->|contains| ENT1[Entity: Access Token]
ST1 -->|contains| F1[Fact: JWT 24h]
ST1 -->|contains| D1[Decision: RS256]
ST1 -->|contains| Q1[Question: What TTL?]
D1 -->|answers| Q1
D1 -->|references| FR1[Fileref: jwt.py]
D1 -->|constrained_by| C1[Constraint: no external key service]
ENT1 -->|indexes| FR1
F1 -->|extracted_from| FR1
F1 -->|supports| D1
ACT1[Action: changed jwt.py] -->|implements| FR1
ACT1 -->|resolves| ER1[Error: ExpiredSignature]
```
**Fractality Rules**:
1. Any node can be a parent (have `parent_id`)
2. Subgraph = all nodes reachable via `contains` from root
3. Nesting depth is unlimited
4. A node can belong to multiple subgraphs (via different relations)
---
## 5. Temporal Layer — Time as a First-Class Citizen
**Decision**: [ADR-008 Temporal Layer](plans/ADR-008-temporal-layer.md)
Before the temporal layer, timestamps were passive metadata — written once, never queried. The graph had no time axis; the agent could not answer "what happened in what order?" without manually parsing every node's `created_at`.
### 5.1 What Changed
| Mechanism | Before | After |
|-----------|--------|-------|
| **Relation tracking** | `updated_at` was missing | `updated_at` on every relation, auto-set |
| **Temporal indexes** | None | `(workspace_id, created_at)` and `(workspace_id, updated_at)` |
| **Chronological walk** | Manual sort by agent | `temporal_walk()` — graph nodes sorted by `created_at` ASC |
| **Session timeline** | Multiple `graph_get_node` calls | `session_timeline()` — flat event log, one call |
| **Temporal vector search** | Impossible | `time_from` / `time_to` params → Qdrant `DatetimeRange` filter |
| **Anchor** | 5 last focuses, no weight | Single deterministic `(last_focus_node_id, timestamp)` |
### 5.2 New MCP Tools
| Tool | Description |
|------|-------------|
| `temporal_walk` | Traverse graph along time axis (chronological order) |
| `session_timeline` | Flat timeline of all events in a session |
### 5.3 What It Solves
1. **Anchor** — agent knows exactly where it stopped last time. `desktop_open` returns the deterministic entry point.
2. **Sequence recall** — `temporal_walk("Show decisions leading to current architecture")` returns chronological chain.
3. **Session overview** — `session_timeline("What happened in this session?")` returns one flat list.
4. **Temporal pruning** — `vector_search(query, time_from="2026-05-14", time_to="2026-05-16")` returns only results from that window.
### 5.4 Migration
Backwards-compatible: `ALTER TABLE relations ADD COLUMN updated_at`. Old data works without changes.
---
## 6. Context Window Strategy (Desktop Viewport)
### 6.1 Three Access Tiers
| Tier | What | Size | When loaded | MCP response content |
|------|------|------|-------------|----------------------|
| **Hot** | Current focus + direct relations | 3-10 nodes | Always in `desktop_open` response | Full node data + relations |
| **Cold** | Other session nodes | 10-100 nodes | Only via `desktop_focus(node_id)` or `vector_search()` | Titles + relations, no full text |
| **Archive** | Old tasks (>7 days without focus) | 100+ nodes | Only via `vector_search()` | Metadata only: "N archived nodes, use search" |
### GC for Archiving
```python
def archive_stale_nodes(workspace_id: str, days_threshold: int = 7):
"""Moves old nodes to archive"""
stale_nodes = sqlite.query("""
SELECT n.id FROM nodes n
LEFT JOIN navigation_history h ON h.node_id = n.id
WHERE n.workspace_id = ? AND n.status = 'active'
GROUP BY n.id
HAVING MAX(h.created_at) < datetime('now', ?)
""", (workspace_id, f'-{days_threshold} days'))
for node in stale_nodes:
sqlite.update("UPDATE nodes SET status = 'archived' WHERE id = ?", node.id)
```
---
## 7. Mutation Strategy
### Three Update Strategies
| Situation | Strategy | What happens |
|-----------|----------|--------------|
| Fixed a typo in a fact | **A: Update** | `data.text` changed, `updated_at` refreshed, vector re-indexed in Qdrant |
| Radically changed approach | **B: Supersedes** | Old node marked `status: stale`, new node created with `supersedes` relation |
| Reworked task — subtasks outdated | **C: Stale-cascade** | Child nodes get `status: stale`, on focus Roo sees a warning |
### Vector Synchronization
```python
def update_node(node_id: str, new_data: dict):
with sqlite.transaction():
old_node = sqlite.get("SELECT * FROM nodes WHERE id = ?", node_id)
sqlite.update("""
UPDATE nodes
SET data = ?, updated_at = datetime('now')
WHERE id = ?
""", (json.dumps(new_data), node_id))
# If text changed — re-index vector
if old_node.data.get("text") != new_data.get("text"):
text = new_data.get("text", "")
if text:
qdrant.delete(collection="cortex_memory",
points_selector=Filter(must=[FieldCondition(
key="node_id", match=MatchValue(value=node_id))]))
qdrant.add(collection="cortex_memory",
documents=[text],
metadata=[{"node_id": node_id, "workspace_id": old_node.workspace_id}])
```
---
## 8. MCP Protocol
### 8.1 Tools
#### Temporal
| Tool | Description | Parameters |
|------|-------------|------------|
| `temporal_walk` | Chronological graph traversal | `workspace_id: str, from_time: str, to_time: str, relation_type: str, limit: int=50` |
| `session_timeline` | Flat timeline of all events in a session | `workspace_id: str, from_time: str, to_time: str, limit: int=50` |
#### Graph
| Tool | Description | Parameters |
|------|-------------|------------|
| `graph_add_node` | Add a node | `parent_id: str, type: str, data: dict, workspace_id: str` |
| `graph_get_node` | Get node with relations | `node_id: str, depth: int=2` |
| `graph_add_relation` | Add a relation | `from_id: str, to_id: str, type: str, weight: float` |
| `graph_traverse` | Traverse graph from node | `start_id: str, relation: str, depth: int=3` |
| `graph_walk` | Walk along reasoning chain | `start_id: str, steps: int=5` |
| `graph_decompose` | Decompose a task | `task_id: str, subtasks: list[dict]` |
| `graph_update_node` | Update node (Strategy A: Update) | `node_id: str, data: dict` |
| `graph_supersede` | Replace node (Strategy B: Supersedes) | `old_id: str, new_data: dict` |
| `graph_delete_node` | Delete node and its vectors | `node_id: str, cascade: bool` |
| `graph_search` | Hybrid: vector + subgraph expansion | `query: str, workspace_id: str` |
#### Vector
| Tool | Description | Parameters |
|------|-------------|------------|
| `vector_store` | Store text with automatic vectorization | `text: str, metadata: dict` |
| `vector_search` | Semantic search (meaning-based) | `query: str, top_k: int=10, workspace_id: str, time_from: str, time_to: str` |
| *(no separate `vector_hybrid_search` — use `graph_search` instead)* | | |
#### Desktop
| Tool | Description | Parameters |
|------|-------------|------------|
| `desktop_open` | Open workspace session, return Hot/Cold/Archive viewport | `workspace_id: str` |
| `desktop_focus` | Focus on a node, expand its subgraph | `node_id: str, workspace_id: str` |
| `desktop_history` | Navigation history | `workspace_id: str, limit: int=20` |
### 8.2 Resources
| URI | Description |
|-----|-------------|
| `cortex://graph/{workspace_id}` | Full session graph (via `desktop_open`) |
| `cortex://node/{node_id}` | Specific node with context (via `get_subgraph`) |
| `cortex://desktop/{workspace_id}` | Current desktop viewport (via `desktop_open`) |
| `cortex://search/{query}` | Hybrid search results (via `graph_search`) |
---
## 9. Usage Scenarios
### 9.1 Starting a Session
```mermaid
sequenceDiagram
participant Roo as Roo Agent
participant M as mcp-cortex
Roo->>M: desktop_open(workspace_id="project_alpha")
M->>M: Check if session root exists
alt No session
M->>M: graph_init(workspace_id)
M-->>Roo: { session_id, graph: empty }
else Session exists
M->>M: Load viewport: hot + cold + archive
M-->>Roo: { session_id, hot: [...], cold: [...], archive: {...} }
end
```
### 9.2 Regression Search
```mermaid
sequenceDiagram
participant Roo as Roo Agent
participant Q as Qdrant
participant G as SQLite
participant FS as Files
Roo->>Q: vector_search("JWT authentication")
Q-->>Roo: [entity: "JWT Auth", fact: "PyJWT RS256", chunk: "..."]
Roo->>G: get_subgraph([entity_id, fact_id])
G-->>Roo: results
Note over G,Roo: entity indexes → fileref jwt.py
fact extracted_from → fileref jwt.py
fact supports → decision "RS256"
Roo->>FS: read("src/auth/jwt.py")
FS-->>Roo: ...code...
```
### 9.3 Mutation (Decision Change)
```mermaid
sequenceDiagram
participant Roo as Roo Agent
participant M as mcp-cortex
participant Q as Qdrant
Note over Roo,Q: Decision changed: HS256 -> RS256
Roo->>M: graph_supersede(old_id=dec_001, new_data={text: "RS256"})
M->>M: old dec_001 -> status: stale
M->>M: create dec_002 -> status: active
M->>M: relation dec_002 -> supersedes -> dec_001
M->>Q: delete vector for dec_001
M->>Q: upsert vector for dec_002
M-->>Roo: { new_id: dec_002, old_id: dec_001, status: "superseded" }
```
---
## 10. Workspace Isolation
### 10.1 Concept
Each project can have its own **workspace** — an isolated memory space. The workspace ID is set via the `--workspace` CLI argument:
```bash
python -m src.cortex --workspace mcp-roo-memory
```
### 10.2 Resolution Chain
Workspace ID is resolved in this order ([`config.py:resolve_workspace_id()`](src/cortex/config.py)):
1. **Explicit argument** — from tool call parameters
2. **`CORTEX_WORKSPACE_ID`** environment variable (set by `--workspace` CLI arg)
3. **CWD basename** — only works in native mode (inside Docker CWD is always `/workspace`)
4. **Fallback** `"default"`
### 10.3 Write vs Search
| Tool | Without `workspace_id` | With `workspace_id` |
|------|----------------------|-------------------|
| `desktop_open`, `graph_add_node`, `vector_store` | → **caller's project** (from `CORTEX_WORKSPACE_ID`) | → specified project |
| `vector_search`, `graph_search` | → **all projects** (cross-project) | → only in specified project |
This is implemented via two helpers in [`server.py`](src/cortex/server.py):
- **`_ws(args)`** — resolves workspace for WRITE tools (always returns a concrete ID)
- **`_ws_opt(args)`** — resolves workspace for SEARCH tools (returns `None` if not explicitly provided, meaning "search everywhere")
### 10.4 Per-Project Configuration
Each project that wants isolation adds its own `.roo/mcp.json`:
```json
{
"mcpServers": {
"cortex": {
"command": "docker",
"args": [
"exec", "-i", "cortex-mcp", "python3",
"-m", "src.cortex", "--workspace", "your-project-name"
]
}
}
}
```
This overrides the global MCP config **only for this project**. Projects without a local `.roo/mcp.json` share the default workspace.
### 10.5 Storage
All workspaces share the same SQLite database and Qdrant collection — isolation is by `workspace_id` field:
```python
# Graph: workspace_id column in nodes table
CREATE TABLE nodes (
workspace_id TEXT NOT NULL,
...
);
# Vector: workspace_id in Qdrant payload
payload = {
"workspace_id": "mcp-roo-memory",
...
}
```
---
## 11. ADR Index
| ADR | Decision |
|-----|----------|
| [ADR-001](plans/ADR-001-fractal-memory.md) | Custom MCP server instead of extending existing Memory MCP |
| [ADR-002](plans/ADR-002-sqlite-graph.md) | SQLite + JSON for graph instead of Neo4j/Cayley |
| [ADR-003](plans/ADR-003-qdrant-vectors.md) | Qdrant for vectors (existing) |
| [ADR-004](plans/ADR-004-fastembed.md) | fastembed for embeddings (paraphrase-multilingual-MiniLM-L12-v2) |
| [ADR-005](plans/ADR-005-desktop-viewport.md) | Desktop Viewport: Hot/Cold/Archive for context window control |
| [ADR-006](plans/ADR-006-mutation-strategy.md) | Mutation strategy: Update/Supersedes/Stale-cascade |
| [ADR-007](plans/ADR-007-regression-pattern.md) | Regression search pattern: meaning → context → specifics |
| [ADR-008](plans/ADR-008-temporal-layer.md) | Temporal layer: time as first-class citizen, chronological walks, session timelines |
---
## 12. Conclusion
**MCP Roo Memory** is not just another vector store.
It is a **fractal graph memory** where:
- **Graph** — workspace with subgraphs, walkable and navigable
- **Vector** — multi-layer index (Entity + Chunk + Fact) for semantic search
- **Fractality** — any task decomposes into a subgraph
- **Navigation** — reasoning chain preserved as a graph path
- **Viewport** — lazy loading with Hot/Cold/Archive for context window control
- **Mutation** — Update/Supersedes/Stale-cascade for knowledge evolution
- **Temporal** — time as a first-class citizen: chronological walks, session timelines, temporal vector filters
All of this works as an MCP server for Roo Code, using existing
tools (Qdrant) and adding the missing graph structure.