Skip to main content

Memory Architecture

OWL's memory system provides persistent context across sessions.

Three-Layer Design

┌─────────────────────────────────────────────────────────────┐
│ Memory Architecture │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Layer 1: Memory File (~/.owl/memory.md) │ │
│ │ • Human-readable markdown │ │
│ │ • Always included in context │ │
│ │ • Preferences, notes, decisions │ │
│ └────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Layer 2: SQLite Database (~/.owl/memory/owl.db) │ │
│ │ • Structured storage │ │
│ │ • Conversations by session/project │ │
│ │ • Auto-extracted learnings │ │
│ └────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Layer 3: Session Summaries │ │
│ │ • Compressed old conversations │ │
│ │ • Prevents context overflow │ │
│ │ • Maintains continuity │ │
│ └────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘

Memory File

Implementation

# memory/memory_file.py
class MemoryFile:
def __init__(self, path: Path):
self.path = path

def read(self) -> str:
"""Read entire memory file."""
if self.path.exists():
return self.path.read_text()
return ""

def remember(self, text: str, category: str = None) -> bool:
"""Add to memory with auto-categorization."""
if category is None:
category = self._categorize(text)

content = self.read()
# Find category section and append
# ...
self.path.write_text(new_content)
return True

def forget(self, text: str) -> bool:
"""Remove from memory by substring match."""
content = self.read()
# Find and remove matching line
# ...

File Format

# Preferences
- I prefer tabs over spaces
- Use pytest for testing

# Notes
- API uses JWT authentication
- Database is PostgreSQL

# Decisions
- Chose FastAPI for performance

SQLite Store

Schema

-- Conversations
CREATE TABLE conversations (
id INTEGER PRIMARY KEY,
session_id TEXT NOT NULL,
project_path TEXT,
role TEXT NOT NULL, -- user, assistant, tool
content TEXT NOT NULL,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
metadata JSON
);

CREATE INDEX idx_conv_session ON conversations(session_id);
CREATE INDEX idx_conv_project ON conversations(project_path);

-- Learnings
CREATE TABLE learnings (
id INTEGER PRIMARY KEY,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
category TEXT NOT NULL, -- preference, project, style, etc.
observation TEXT NOT NULL,
learning TEXT NOT NULL,
project_path TEXT,
source TEXT DEFAULT 'auto', -- user, auto
confidence REAL DEFAULT 0.8,
scope TEXT DEFAULT 'global' -- global, project
);

-- Sessions
CREATE TABLE sessions (
id TEXT PRIMARY KEY,
started_at DATETIME DEFAULT CURRENT_TIMESTAMP,
last_active DATETIME,
message_count INTEGER DEFAULT 0,
summary TEXT
);

Implementation

# memory/store.py
class MemoryStore:
def __init__(self, db_path: Path):
self.conn = sqlite3.connect(str(db_path))
self._init_schema()

def add_message(
self,
session_id: str,
role: str,
content: str,
project_path: str = None,
metadata: dict = None
):
"""Store a conversation message."""
self.conn.execute("""
INSERT INTO conversations
(session_id, project_path, role, content, metadata)
VALUES (?, ?, ?, ?, ?)
""", (session_id, project_path, role, content, json.dumps(metadata)))
self.conn.commit()

def get_conversation(
self,
session_id: str,
limit: int = 20
) -> List[Message]:
"""Get recent messages for session."""
cursor = self.conn.execute("""
SELECT role, content, timestamp
FROM conversations
WHERE session_id = ?
ORDER BY timestamp DESC
LIMIT ?
""", (session_id, limit))
return [Message(*row) for row in reversed(cursor.fetchall())]

def get_conversation_for_project(
self,
session_id: str,
project_path: str,
limit: int = 20
) -> List[Message]:
"""Get messages scoped to a project."""
cursor = self.conn.execute("""
SELECT role, content, timestamp
FROM conversations
WHERE session_id = ? AND project_path = ?
ORDER BY timestamp DESC
LIMIT ?
""", (session_id, project_path, limit))
return [Message(*row) for row in reversed(cursor.fetchall())]

Summarizer

Purpose

Prevent context overflow by compressing old conversations.

Configuration

SUMMARIZE_THRESHOLD = 20  # When to trigger
SUMMARIZE_BATCH = 10 # How many to summarize
KEEP_RECENT = 10 # Always keep fresh

Implementation

# memory/summarizer.py
class Summarizer:
def __init__(self, llm: LLMClient):
self.llm = llm

def summarize_async(self, session_id: str):
"""Trigger async summarization if needed."""
thread = threading.Thread(
target=self._summarize,
args=(session_id,)
)
thread.start()

def _summarize(self, session_id: str):
messages = self.memory.get_conversation(session_id)

if len(messages) < SUMMARIZE_THRESHOLD:
return

# Get oldest batch to summarize
to_summarize = messages[:SUMMARIZE_BATCH]

# Generate summary using LLM
prompt = f"Summarize this conversation in 2-4 sentences:\n{format_messages(to_summarize)}"
summary = self.llm.simple_query(prompt)

# Store summary
self.memory.update_session_summary(session_id, summary)

# Delete summarized messages
for msg in to_summarize:
self.memory.delete_message(msg.id)

Summary Format

Session Summary:
- Discussed Python testing approaches, settled on pytest
- Reviewed authentication flow, identified security issue
- Implemented user registration with email verification

Learnings

Extraction

Learnings are extracted during reflection:

# soul/reflector.py
class SoulReflector:
def reflect_async(self, session_id: str, project_path: str = None):
thread = threading.Thread(
target=self._reflect,
args=(session_id, project_path)
)
thread.start()

def _reflect(self, session_id: str, project_path: str):
# Get recent conversation
messages = self.memory.get_conversation(session_id, limit=6)

# Ask LLM to extract learnings
prompt = """
Extract learnings from this conversation.
Format as JSON array:
[{"category": "preference|project|style|technical",
"observation": "what happened",
"learning": "what to remember",
"scope": "global|project"}]
"""

result = self.llm.simple_query(prompt + format_messages(messages))
learnings = json.loads(result)

# Store learnings
for learning in learnings:
self.memory.add_learning(
category=learning["category"],
observation=learning["observation"],
learning=learning["learning"],
project_path=project_path if learning["scope"] == "project" else None,
scope=learning["scope"]
)

Categories

CategoryScopeExample
preferenceglobal"User prefers functional style"
projectproject"Uses PostgreSQL database"
styleglobal"Prefers descriptive names"
technicalproject"API uses JWT authentication"
feedbackglobal"Likes concise responses"

Context Building

How Memory is Used

# llm/context.py
class ContextBuilder:
def build(self, user_message: str, history: List[Message]) -> List[Message]:
sections = []

# Memory file (always included)
memory_content = self.memory_file.read()
if memory_content:
sections.append(f"## MY MEMORY\n{memory_content}")

# Session summary (if exists)
summary = self.memory.get_session_summary(self.session_id)
if summary:
sections.append(f"## PREVIOUS CONTEXT\n{summary}")

# Build system prompt
system_prompt = "\n\n".join(sections)

return [
Message(role="system", content=system_prompt),
*history,
Message(role="user", content=user_message)
]

Data Lifecycle

User Message

├──→ Stored in SQLite (conversations)

├──→ Every 3 exchanges → Reflection
│ │
│ └──→ Learnings extracted → SQLite (learnings)
│ │
│ └──→ Every 10 learnings → Evolution eligible

└──→ Every 20 messages → Summarization

└──→ Old messages compressed

└──→ Summary stored in sessions table