How Ormah Works
Data Model
All data models live in src/ormah/models/ and use Pydantic for validation and serialization.
MemoryNode
The core entity. Every memory is a MemoryNode (models/node.py).
Fields
# Identity
id: UUID # Unique identifier
short_id: str # First UUID segment (e.g., "97acbe8e") - property
# Classification
type: NodeType # What kind of memory (see below)
tier: Tier # Priority level: core | working | archival
space: str | None # Project namespace (e.g., "ormah", None = global)
# Content
title: str | None # Optional title (10x weight in FTS search)
content: str # The actual memory text
tags: list[str] # Categorization tags (e.g., ["about_self", "location"])
source: str # Origin: "agent:claude-code", "system:self", etc.
# Graph
connections: list[Connection] # Outgoing edges to other nodes
# Scoring
confidence: float # [0.0-1.0] How certain we are (default: 1.0)
importance: float # [0.0-1.0] Computed by importance_scorer job; see 05 - Background Jobs
access_count: int # Times this node has been accessed/recalled
# Temporal
created: datetime # When first stored (UTC)
updated: datetime # Last modification (UTC)
last_accessed: datetime # Last recall/search hit (UTC)
last_review: datetime | None # Last FSRS review (spaced repetition)
valid_until: datetime | None # Expiry date (set by mark_outdated)
# FSRS (Spaced Repetition)
stability: float # Days until ~37% retrievability (default: 1.0)
Connection
A directed, typed, weighted edge from one node to another:
class Connection(BaseModel):
target: str # Target node UUID
edge: EdgeType # Relationship type (default: related_to)
weight: float # Strength 0.0-1.0 (default: 0.5)
Node Types (10 types)
Defined as NodeType enum in models/node.py:
| Type | Purpose | Example |
|---|---|---|
fact |
Static, verifiable information | "Lives in Dublin, Ireland" |
decision |
A choice + its reasoning | "Chose SQLite over Postgres for local-first" |
preference |
How the user likes to work | "Prefers map/filter over for loops" |
event |
Time-bound occurrence | "AI Tinkerers presentation - April 2026" |
person |
An individual | "Rishikesh Chirammel Ajit" |
project |
Project metadata | "Ormah - AI agent memory system" |
concept |
Abstract idea or pattern | "Three-tier memory model" |
procedure |
Step-by-step process | "How to deploy ormah to production" |
goal |
An objective | "Make ormah frictionless to install" |
observation |
A surprising finding | "BGE query prefix is net-neutral for recall" |
Three Tiers
Defined as Tier enum. These map conceptually to human memory systems:
graph LR
subgraph "Core (max 50 nodes)"
C1[Identity facts]
C2[Key preferences]
C3[Critical decisions]
end
subgraph "Working (default tier)"
W1[Active project context]
W2[Recent conversations]
W3[Current goals]
end
subgraph "Archival (deep storage)"
A1[Old decisions]
A2[Completed events]
A3[Superseded facts]
end
W1 -->|"promote<br/>(manual)"| C1
W1 -->|"decay<br/>(FSRS auto)"| A1
A1 -->|"promote<br/>(manual)"| W1
| Tier | Cap | Decay | Search Boost | Purpose |
|---|---|---|---|---|
| core | 50 nodes | Protected from decay | +0.10 | Always-relevant: identity, key preferences, critical decisions |
| working | Unlimited | FSRS-based auto-demotion | +0.00 | Active context: current project info, recent decisions |
| archival | Unlimited | No further decay | -0.10 | Historical: still searchable but deprioritized |
Core cap enforcement (engine/tier_manager.py:enforce_core_cap()): When core exceeds 50 nodes, the least important ones (by importance score, not raw access count) are demoted to working, skipping protected nodes like the self node. See Background Jobs for how importance is calculated.
Edge Types (8 types)
Defined as EdgeType enum. Each has a spreading activation factor used during graph traversal:
| Edge Type | Activation Factor | Created By | Purpose |
|---|---|---|---|
supports |
1.0 | Auto-linker (LLM) | Strong supporting evidence |
part_of |
1.0 | Auto-linker (LLM) | Hierarchical containment |
depends_on |
1.0 | Auto-linker (LLM) | Logical dependency |
defines |
1.0 | System (identity) | Links self node to identity facts |
derived_from |
1.0 | Consolidator | Lineage after consolidation |
evolved_from |
0.8 | Conflict detector | Belief changed over time |
related_to |
0.7 | Auto-linker (LLM) | Generic semantic connection |
contradicts |
0.4 | Conflict detector | Active contradiction (low weight intentional) |
The activation factors are defined in engine/memory_engine.py:64-73 and control how strongly a neighbor's relevance propagates during spreading activation search.
Identity System
Users have a self node -- a special person-type, core-tier node that represents the user themselves.
graph TD
SELF["Self Node<br/>(type: person, tier: core)<br/>'Alice Example'"]
SELF -->|defines| F1["Lives in [city], [country]<br/>(fact, core)"]
SELF -->|defines| F2["Prefers dark mode<br/>(preference, core)"]
SELF -->|defines| F3["Spouse: [name]<br/>(fact, core)"]
SELF -->|defines| F4["Works at [company]<br/>(fact, working)"]
style SELF fill:#74b3a5,color:#000
style F1 fill:#4d8a7e,color:#fff
style F2 fill:#4d8a7e,color:#fff
style F3 fill:#4d8a7e,color:#fff
style F4 fill:#4d8a7e,color:#fff
Key design decision: identity membership is represented structurally in the graph, not inferred from tier labels.
In practice, Ormah starts from the self node and follows defines edges to find memories that describe the user. That means:
tieranswers "how important / always-loaded / demotable is this memory?"definesanswers "is this memory part of the user's identity?"
Those are different concerns. A memory can be identity-related and still live in core, working, or archival. For example, if "Works at company X" is demoted from core to working, Ormah can still find it as identity because the graph still says:
Self --defines--> Works at company X
This avoids a brittle design where identity retrieval would break whenever tier rules change.
Important nuance for the current implementation: the graph edge path is the identity anchor, but whisper does not rely on graph traversal alone. For identity prompts, it still runs search as well, because search can surface user-related facts that are not directly reachable from the self node.
When a memory is created with about_self=True:
- The node gets an
about_selftag - A
definesedge is created from the self node to the new node (memory_engine.py:_link_to_self()) - The consolidator preserves these edges when merging identity-related clusters
Identity nodes are tagged with about_self which also triggers special FTS behavior -- queries containing "me", "I", "my" get about_self injected into the search terms (index/graph.py:_sanitize_fts_query()).
Request/Response Models
CreateNodeRequest (models/node.py)
Used by the remember tool:
class CreateNodeRequest(BaseModel):
content: str # Required: the memory text
type: NodeType = "fact" # Default: fact
tier: Tier = "working" # Default: working
source: str = "agent:unknown"
space: str | None = None
tags: list[str] = []
connections: list[Connection] = []
title: str | None = None
about_self: bool = False # Create defines edge from self node
confidence: float = 1.0
SearchQuery (models/search.py)
Used by the recall tool:
class SearchQuery(BaseModel):
query: str # Search text
limit: int = 10 # 1-100
types: list[NodeType] | None # Filter by node type
tiers: list[Tier] | None # Filter by tier
spaces: list[str] | None # Filter by project space
tags: list[str] | None # Filter by tag
created_after: str | None # ISO date filter
created_before: str | None # ISO date filter
session_id: str | None # For session-aware whisper
Proposal (models/proposals.py)
Used by background maintenance jobs:
class Proposal(BaseModel):
id: UUID
type: ProposalType # merge | conflict | decay
status: ProposalStatus # pending | approved | rejected
source_nodes: list[str] # Node IDs involved
proposed_action: str # What to do
reason: str | None # Why
created: datetime
resolved: datetime | None
Walkthrough Example: Creating a Memory
Imagine you tell Claude: "Remember that I prefer using TypeScript over JavaScript"
- MCP adapter receives
remembercall withcontent="User prefers TypeScript over JavaScript",about_self=True - API route creates a
CreateNodeRequestwithtype=preference,tier=working - MemoryEngine.remember() (
engine/memory_engine.py):- Creates a
MemoryNodewith UUID, timestamps, default scores FileStore.save()writespreference_prefers-typescript_97acbe8e.mdatomicallyIndexBuilder.index_single()inserts into SQLite FTS5 + tagsVectorStore.upsert()stores the BGE embedding (768-dim)_link_to_self()creates adefinesedge from self node → this node_auto_link_node()searches for similar nodes, creates edges if similarity > 0.65TierManager.enforce_core_cap()checks if core tier is over 50 nodes
- Creates a
- Result: Node stored on disk, indexed in SQLite, linked in graph, ready for whisper recall