# Antigravity CLI Setup
Source: https://docs.claude-mem.ai/antigravity-cli/setup
Add persistent memory to Antigravity CLI with claude-mem
# Antigravity CLI Setup
> **Give Antigravity CLI persistent memory across sessions.**
Antigravity CLI (`agy`) is Google's standalone successor to Gemini CLI — it reuses Gemini CLI's `~/.gemini/` config tree and, per Google, "keeps the most critical features of Gemini CLI: Agent Skills, Hooks, Subagents, and Extensions." Claude-mem changes what happens across sessions by capturing observations, decisions, and patterns — then injecting relevant context into each new session.
**How it works:** Claude-mem installs lifecycle hooks into Antigravity CLI's `~/.gemini/config/hooks.json` that capture tool usage, agent responses, and session events. A local worker service extracts semantic observations and injects relevant history at session start — via `GEMINI.md`, an MCP server, and a rules file.
Antigravity CLI is a different product from the Antigravity **desktop IDE** (`antigravity` binary) — both share the same `~/.gemini/antigravity` namespace, but the CLI's own binary is `agy`. Detection checks for `agy` in your `PATH` (or an existing `~/.gemini/antigravity` directory).
## Prerequisites
* [Antigravity CLI](https://github.com/google-antigravity/antigravity-cli) (`agy`) installed — `curl -fsSL https://antigravity.google/cli/install.sh | bash`
* [Node.js](https://nodejs.org/) 18+
* The `~/.gemini` directory must exist (created by Antigravity CLI / Gemini CLI on first run)
## Installation
### Step 1: Install claude-mem
```bash theme={null}
npx claude-mem install --ide antigravity
```
The installer will:
1. Auto-detect Antigravity CLI (checks for `agy` in `PATH`, or an existing `~/.gemini/antigravity` directory)
2. Install the Antigravity CLI lifecycle hooks into `~/.gemini/config/hooks.json`
3. Inject context configuration into `~/.gemini/GEMINI.md`
4. Register claude-mem's MCP server in `~/.gemini/antigravity/mcp_config.json` **and** `~/.gemini/config/mcp_config.json`
5. Write a rules/context placeholder to `~/.agents/rules/claude-mem-context.md`
6. Start the worker service
### Step 2: Configure an AI provider
Claude-mem needs an AI provider to extract observations from your sessions. Choose one:
The simplest option — use Gemini's own API for observation extraction:
1. Create an API key in [Google AI Studio](https://aistudio.google.com/apikey)
2. Add it to your settings:
```bash theme={null}
mkdir -p ~/.claude-mem
cat > ~/.claude-mem/settings.json << 'EOF'
{
"CLAUDE_MEM_PROVIDER": "gemini",
"CLAUDE_MEM_GEMINI_API_KEY": "YOUR_API_KEY"
}
EOF
```
**No billing required:** You can create a Gemini API key without enabling billing. Enable billing for higher rate limits only after reviewing Google's current quota and billing terms.
If you have a Claude API key:
```bash theme={null}
mkdir -p ~/.claude-mem
cat > ~/.claude-mem/settings.json << 'EOF'
{
"CLAUDE_MEM_PROVIDER": "claude"
}
EOF
```
Set your API key via environment variable:
```bash theme={null}
export ANTHROPIC_API_KEY="your-key"
```
For access to 100+ models:
```bash theme={null}
mkdir -p ~/.claude-mem
cat > ~/.claude-mem/settings.json << 'EOF'
{
"CLAUDE_MEM_PROVIDER": "openrouter",
"CLAUDE_MEM_OPENROUTER_API_KEY": "YOUR_KEY"
}
EOF
```
### Step 3: Verify installation
```bash theme={null}
# Check worker is running
npx claude-mem status
# Check hooks are installed — look for claude-mem entries
cat ~/.gemini/config/hooks.json | grep claude-mem
# Check MCP registration in either candidate config path
cat ~/.gemini/antigravity/mcp_config.json | grep claude-mem
cat ~/.gemini/config/mcp_config.json | grep claude-mem
```
Open the worker URL printed on startup to see the memory viewer.
### Step 4: Start using Antigravity CLI
Launch Antigravity CLI normally. Claude-mem works in the background:
```bash theme={null}
agy
```
On session start, you'll see claude-mem context injected with your recent observations and project history.
## What gets captured
Claude-mem registers the five lifecycle hooks Antigravity CLI (`agy` 1.2.1) actually fires:
| Hook | Internal event | Purpose |
| - | - | - |
| **PreInvocation** | `context` | Injects memory context (via `injectSteps`) before the agent runs |
| **PreToolUse** | `observation` | Records tool intent, returns `{"decision":"allow"}` so the call proceeds |
| **PostToolUse** | `observation` | Captures tool results after execution |
| **PostInvocation** | `observation` | Records the agent's response (from the transcript's `PLANNER_RESPONSE` node) |
| **Stop** | `summarize` | Captures the session summary at turn/session end |
These are the real `agy` 1.2.1 event names. Earlier releases mistakenly registered Gemini CLI names (`SessionStart`, `BeforeAgent`, `AfterAgent`, `BeforeTool`, `AfterTool`) into `~/.gemini/settings.json` — none of which exist in the `agy` binary, so hooks never fired and no observations were recorded (issue [#4057](https://github.com/thedotmack/claude-mem/issues/4057)).
## MCP registration
Antigravity CLI has native MCP support, but which config path it reads was genuinely ambiguous at the time of writing — two real candidate paths exist on disk with no definitive documentation resolving which one `agy` loads. Claude-mem writes to **both**, safely and idempotently:
* `~/.gemini/antigravity/mcp_config.json`
* `~/.gemini/config/mcp_config.json`
This gives claude-mem's search tools (`search`, `smart_search`, `timeline`, etc.) a chance to register correctly regardless of which path Antigravity CLI actually reads.
## Future enhancement (not implemented in this release)
Antigravity CLI ships a first-class plugin-marketplace subcommand system: `agy plugin {list,import,install,uninstall,enable,disable,validate,link}`. Notably, `agy plugin import gemini|claude` suggests native cross-tool plugin migration — structurally similar to Codex CLI's `.codex-plugin/plugin.json` marketplace mechanism. This could eventually be a cleaner, more idiomatic way to bundle claude-mem's hooks + MCP + skills registration than hand-editing `hooks.json`. It isn't implemented here because its manifest schema isn't discoverable without running `agy plugin import`/`install` against a real manifest, which would mutate a user's live local plugin state. Tracked as a candidate follow-up.
## Troubleshooting
### Hooks not firing
1. Verify hooks exist in the hooks config:
```bash theme={null}
cat ~/.gemini/config/hooks.json
```
Each top-level key is a **hook name**. claude-mem writes a single
`"claude-mem"` key wrapping the event keys, with `PreToolUse`/`PostToolUse`
using the grouped `{ "matcher", "hooks" }` shape and
`PreInvocation`/`PostInvocation`/`Stop` using a flat array of handlers:
```json theme={null}
{
"claude-mem": {
"PreInvocation": [ { "type": "command", "command": "...", "timeout": 30 } ],
"PreToolUse": [ { "matcher": "*", "hooks": [ { "type": "command", "command": "...", "timeout": 30 } ] } ],
"PostToolUse": [ { "matcher": "*", "hooks": [ { "type": "command", "command": "...", "timeout": 30 } ] } ],
"PostInvocation": [ { "type": "command", "command": "...", "timeout": 30 } ],
"Stop": [ { "type": "command", "command": "...", "timeout": 30 } ]
}
}
```
`timeout` is in **seconds** (agy's default is 30). If you see event names at
the top level, handlers tagged with `"name": "claude-mem"`, or
`"timeout": 10000`, you are on a pre-fix install — re-run the installer
below, which migrates the old layout automatically.
2. Restart Antigravity CLI after installation.
3. Re-run the installer:
```bash theme={null}
npx claude-mem install --ide antigravity
```
### Worker not running
```bash theme={null}
# Check status
npx claude-mem status
# View logs
npx claude-mem logs
# Restart worker
npx claude-mem restart
```
### No context appearing at session start
1. Ensure the worker is running (`npm run worker:status`)
2. You need at least one previous session with observations for context to appear
3. Check your AI provider is configured in `~/.claude-mem/settings.json`
### Raw escape codes in output
If you see characters like `[31m` or `[0m` in the session context, your claude-mem version may need updating — the Antigravity CLI adapter strips ANSI color codes automatically:
```bash theme={null}
npx claude-mem install --ide antigravity
```
## Uninstalling
```bash theme={null}
npx claude-mem uninstall
```
This removes claude-mem's hooks from `~/.gemini/config/hooks.json` (and any legacy entries from `~/.gemini/settings.json`), cleans up the context section in `~/.gemini/GEMINI.md`, removes claude-mem's entry from both MCP config files, and removes the rules context section — while preserving everything else in those files.
## Next Steps
* [Gemini Provider](/usage/gemini-provider) — Configure the Gemini AI provider for observation extraction
* [Configuration](/configuration) — All settings options
* [Search Tools](/usage/search-tools) — Search your memory from within sessions
* [Troubleshooting](/troubleshooting) — Common issues and solutions
# Architecture Evolution
Source: https://docs.claude-mem.ai/architecture-evolution
How claude-mem evolved from v3 to v5+
# Architecture Evolution
## The Problem We Solved
**Goal:** Create a memory system that makes Claude smarter across sessions without the user noticing it exists.
**Challenge:** How do you observe AI agent behavior, compress it intelligently, and serve it back at the right time - all without slowing down or interfering with the main workflow?
This is the story of how claude-mem evolved from a simple idea to a production-ready system, and the key architectural decisions that made it work.
***
## v5.x: Maturity and User Experience
After establishing the solid v4 architecture, v5.x focused on user experience, visualization, and polish.
### v5.1.2: Theme Toggle (November 2025)
**What Changed**: Added light/dark mode theme toggle to viewer UI
**New Features**:
* User-selectable theme preference (light, dark, system)
* Persistent theme settings in localStorage
* Smooth theme transitions
* System preference detection
**Implementation**:
```typescript theme={null}
// Theme context with persistence
const ThemeProvider = ({ children }) => {
const [theme, setTheme] = useState<'light' | 'dark' | 'system'>(() => {
return localStorage.getItem('claude-mem-theme') || 'system';
});
useEffect(() => {
localStorage.setItem('claude-mem-theme', theme);
}, [theme]);
return (
{children}
);
};
```
**Why It Matters**: Users working in different lighting conditions can now customize the viewer for comfort.
### v5.1.1: Worker Startup Fix (November 2025) - Now Deprecated
**Note**: This section describes a historical PM2-based approach that has been replaced with Bun in later versions.
**The Problem**: Worker startup failed on Windows with ENOENT error when using PM2
**Historical Solution**: Used full path to PM2 binary instead of relying on PATH
**Current Approach**: The project now uses Bun for process management, which provides better cross-platform compatibility and eliminates these PATH-related issues.
**Impact**: Cross-platform compatibility restored, Windows users can now use claude-mem without issues.
### v5.1.0: Web-Based Viewer UI (October 2025)
**The Breakthrough**: Real-time visualization of memory stream
**What We Built**:
* React-based web UI at [http://localhost:37777](http://localhost:37777)
* Server-Sent Events (SSE) for real-time updates
* Infinite scroll pagination
* Project filtering
* Settings persistence (sidebar state, selected project)
* Auto-reconnection with exponential backoff
* GPU-accelerated animations
**New Worker Endpoints** (8 additions):
```
GET / # Serves viewer HTML
GET /stream # SSE real-time updates
GET /api/prompts # Paginated user prompts
GET /api/observations # Paginated observations
GET /api/summaries # Paginated session summaries
GET /api/stats # Database statistics
GET /api/settings # User settings
POST /api/settings # Save settings
```
**Database Enhancements**: SessionStore gained paginated queries for recent prompts, observations, and summaries (filterable by project), plus database stats and project listing to back the viewer endpoints. (These viewer-specific methods were later replaced by the unified search layer.)
**React Architecture**:
```
src/ui/viewer/
├── components/
│ ├── Header.tsx # Navigation + stats
│ ├── Sidebar.tsx # Project filter
│ ├── Feed.tsx # Infinite scroll
│ └── cards/
│ ├── ObservationCard.tsx
│ ├── PromptCard.tsx
│ ├── SummaryCard.tsx
│ └── SkeletonCard.tsx
├── hooks/
│ ├── useSSE.ts # Real-time events
│ ├── usePagination.ts # Infinite scroll
│ ├── useSettings.ts # Persistence
│ └── useStats.ts # Statistics
└── utils/
├── merge.ts # Data deduplication
└── format.ts # Display formatting
```
**Build Process**:
```typescript theme={null}
// esbuild bundles everything into single HTML file
esbuild.build({
entryPoints: ['src/ui/viewer/index.tsx'],
bundle: true,
outfile: 'plugin/ui/viewer.html',
loader: { '.tsx': 'tsx', '.woff2': 'dataurl' },
define: { 'process.env.NODE_ENV': '"production"' },
});
```
**Why It Matters**: Users can now see exactly what's being captured in real-time, making the memory system transparent and debuggable.
### v5.0.3: Smart Install Caching (October 2025)
**The Problem**: `npm install` ran on every SessionStart (2-5 seconds)
**The Insight**: Dependencies rarely change between sessions
**The Solution**: Version-based caching
```typescript theme={null}
// Check version marker before installing
const currentVersion = getPackageVersion();
const installedVersion = readFileSync('.install-version', 'utf-8');
if (currentVersion !== installedVersion) {
// Only install if version changed
await runNpmInstall();
writeFileSync('.install-version', currentVersion);
}
```
**Cached Check Logic**:
1. Does `node_modules` exist?
2. Does `.install-version` match `package.json` version?
3. Is `better-sqlite3` present? (Legacy: now uses bun:sqlite which requires no installation)
**Impact**:
* SessionStart hook: 2-5 seconds → 10ms (99.5% faster)
* Only installs on: first run, version change, missing deps
* Better Windows error messages with build tool help
### v5.0.2: Worker Health Checks (October 2025)
**What Changed**: More robust worker startup and monitoring
**New Features**:
```typescript theme={null}
// Health check endpoint
app.get('/health', (req, res) => {
res.json({
status: 'ok',
uptime: process.uptime(),
port: WORKER_PORT,
memory: process.memoryUsage(),
});
});
// Smart worker startup
async function ensureWorkerHealthy() {
const healthy = await isWorkerHealthy(1000);
if (!healthy) {
await startWorker();
await waitForWorkerHealth(10000);
}
}
```
**Benefits**:
* Graceful degradation when worker is down
* Auto-recovery from crashes
* Better error messages for debugging
### v5.0.1: Stability Improvements (October 2025)
**What Changed**: Various bug fixes and stability enhancements
**Key Fixes**:
* Fixed race conditions in observation queue processing
* Improved error handling in SDK worker
* Better cleanup of stale worker processes
* Enhanced logging for debugging
### v5.0.0: Hybrid Search Architecture (October 2025)
**The Evolution**: SQLite FTS5 + Chroma vector search
**What We Added**:
```
┌─────────────────────────────────────────────────────────┐
│ HYBRID SEARCH │
│ │
│ Text Query → SQLite FTS5 (keyword matching) │
│ ↓ │
│ Chroma Vector Search (semantic) │
│ ↓ │
│ Merge + Re-rank Results │
└─────────────────────────────────────────────────────────┘
```
**New Dependencies**:
* `chromadb` - Vector database for semantic search
* Python 3.8+ - Required by chromadb
**MCP Tools Enhancement**:
```typescript theme={null}
// Chroma-backed semantic search
search_observations({
query: "authentication bug",
useSemanticSearch: true // Uses Chroma
});
// Falls back to FTS5 if Chroma unavailable
```
**Why Hybrid**:
* FTS5: Fast keyword matching, no dependencies
* Chroma: Semantic understanding, finds related concepts
* Graceful degradation: Works without Chroma (FTS5 only)
**Trade-offs**:
* Added Python dependency (optional)
* Increased installation complexity
* Better search relevance
***
## MCP Architecture Simplification (December 2025)
### The Problem: Complex MCP Implementation
**Before:**
```
9+ MCP tools registered at session start:
- search_observations
- find_by_type
- find_by_file
- find_by_concept
- get_recent_context
- get_observation
- get_session
- get_prompt
- help
Problems:
- Overlapping operations (search_observations vs find_by_type)
- Complex parameter schemas (~2,500 tokens in tool definitions)
- No built-in workflow guidance
- High cognitive load for Claude (which tool to use?)
- Code size: ~2,718 lines in mcp-server.ts
```
**The Insight:** Progressive disclosure should be built into tool design itself, not something Claude has to remember.
### The Solution: 3-Layer Workflow
**After:**
```
4 MCP tools following 3-layer workflow:
1. important_workflow - Workflow documentation (always visible)
"3-LAYER WORKFLOW (ALWAYS FOLLOW):
1. search(query) → Get index with IDs
2. timeline(anchor=ID) → Get context
3. get_observations([IDs]) → Fetch details
NEVER fetch full details without filtering first."
2. search - Layer 1: Get index with IDs (~50-100 tokens/result)
3. timeline - Layer 2: Get chronological context
4. get_observations - Layer 3: Fetch full details (~500-1,000 tokens/result)
Benefits:
- Progressive disclosure enforced by tool structure
- No overlapping operations
- Simple schemas (additionalProperties: true)
- Clear workflow pattern
- Code size: ~312 lines in mcp-server.ts (88% reduction)
- ~10x token savings
```
### Migration: Skill-Based Search Removed
**Previously:** Used skill-based search
* mem-search skill invoked via natural language
* HTTP API called directly via curl
* Progressive disclosure through skill loading
* 17 skill documentation files
**Now:** Removed skill-based approach
* MCP-only architecture
* Native MCP protocol (better Claude integration)
* Works with both Claude Desktop and Claude Code
* Simpler to maintain (no skill files)
* All 19 mem-search skill files removed (\~2,744 lines)
### Key Architectural Changes
**MCP Server Refactor:**
Before:
```typescript theme={null}
// Complex parameter schemas
{
name: "search_observations",
inputSchema: {
type: "object",
properties: {
query: { type: "string", description: "..." },
type: { type: "array", items: { enum: [...] } },
format: { enum: ["index", "full"] },
limit: { type: "number", minimum: 1, maximum: 100 },
// ... many more parameters
}
}
}
```
After:
```typescript theme={null}
// Simple schemas with workflow guidance
{
name: "search",
description: "Step 1: Search memory. Returns index with IDs.",
inputSchema: {
type: "object",
properties: {},
additionalProperties: true // Accept any parameters
}
}
```
**Workflow Enforcement:**
Before: Claude had to remember progressive disclosure pattern
After: Tool structure makes it impossible to skip steps
* Can't get details without IDs from search
* Can't search without seeing important\_workflow reminder
* Timeline provides middle ground (context without full details)
### Impact
**Token Efficiency:**
```
Traditional: Fetch 20 observations upfront
→ 10,000-20,000 tokens
→ Only 2 observations relevant (90% waste)
3-Layer Workflow:
→ search (20 results): ~1,000-2,000 tokens
→ Review index, identify 3 relevant IDs
→ get_observations (3 IDs): ~1,500-3,000 tokens
→ Total: 2,500-5,000 tokens (50-75% savings)
```
**Code Simplicity:**
* MCP server: 2,718 lines → 312 lines (88% reduction)
* Removed: 19 skill files (\~2,744 lines)
* Net reduction: \~5,150 lines of code removed
**User Experience:**
* Same natural language interaction
* Better token efficiency
* Clearer architecture
* Works identically on Claude Desktop and Claude Code
### Design Philosophy
**Progressive Disclosure Through Structure:**
The 3-layer workflow embodies progressive disclosure at the architectural level:
1. **Layer 1 (Index)** - "What exists?" - Cheap survey of options
2. **Layer 2 (Timeline)** - "What was happening?" - Context around specific points
3. **Layer 3 (Details)** - "Tell me everything" - Full details only when justified
Each layer provides a decision point where Claude can:
* Stop if irrelevant
* Get more context if uncertain
* Dive deep if confident
This makes it structurally difficult to waste tokens.
***
## v1-v2: The Naive Approach
### The First Attempt: Dump Everything
**Architecture:**
```
PostToolUse Hook → Save raw tool outputs → Retrieve everything on startup
```
**What we learned:**
* ❌ Context pollution (thousands of tokens of irrelevant data)
* ❌ No compression (raw tool outputs are verbose)
* ❌ No search (had to scan everything linearly)
* ✅ Proved the concept: Memory across sessions is valuable
**Example of what went wrong:**
```
SessionStart loaded:
- 150 file read operations
- 80 grep searches
- 45 bash commands
- Total: ~35,000 tokens
- Relevant to current task: ~500 tokens (1.4%)
```
***
## v3: Smart Compression, Wrong Architecture
### The Breakthrough: AI-Powered Compression
**New idea:** Use Claude itself to compress observations
**Architecture:**
```
PostToolUse Hook → Queue observation → SDK Worker → AI compression → Store insights
```
**What we added:**
1. **Claude Agent SDK integration** - Use AI to compress observations
2. **Background worker** - Don't block main session
3. **Structured observations** - Extract facts, decisions, insights
4. **Session summaries** - Generate comprehensive summaries
**What worked:**
* ✅ Compression ratio: 10:1 to 100:1
* ✅ Semantic understanding (not just keyword matching)
* ✅ Background processing (hooks stayed fast)
* ✅ Search became useful
**What didn't work:**
* ❌ Still loaded everything upfront
* ❌ Session ID management was broken
* ❌ Aggressive cleanup interrupted summaries
* ❌ Multiple SDK sessions per Claude Code session
***
## The Key Realizations
### Realization 1: Progressive Disclosure
**Problem:** Even compressed observations can pollute context if you load them all.
**Insight:** Humans don't read everything before starting work. Why should AI?
**Solution:** Show an index first, fetch details on-demand.
```
❌ Old: Load 50 observations (8,500 tokens)
✅ New: Show index of 50 observations (800 tokens)
Agent fetches 2-3 relevant ones (300 tokens)
Total: 1,100 tokens vs 8,500 tokens
```
**Impact:**
* 87% reduction in context usage
* 100% relevance (only fetch what's needed)
* Agent autonomy (decides what's relevant)
### Realization 2: Session ID Chaos
**Problem:** SDK session IDs change on every turn.
**What we thought:**
```typescript theme={null}
// ❌ Wrong assumption
UserPromptSubmit → Capture session ID once → Use forever
```
**Reality:**
```typescript theme={null}
// ✅ Actual behavior
Turn 1: session_abc123
Turn 2: session_def456
Turn 3: session_ghi789
```
**Why this matters:**
* Can't resume sessions without tracking ID updates
* Session state gets lost between turns
* Observations get orphaned
**Solution:**
```typescript theme={null}
// Capture from system init message
for await (const msg of response) {
if (msg.type === 'system' && msg.subtype === 'init') {
sdkSessionId = msg.session_id;
await updateSessionId(sessionId, sdkSessionId);
}
}
```
### Realization 3: Graceful vs Aggressive Cleanup
**v3 approach:**
```typescript theme={null}
// ❌ Aggressive: Kill worker immediately
SessionEnd → DELETE /worker/session → Worker stops
```
**Problems:**
* Summary generation interrupted mid-process
* Pending observations lost
* Race conditions everywhere
**v4 approach:**
```typescript theme={null}
// ✅ Graceful: Let worker finish
SessionEnd → Mark session complete → Worker finishes → Exit naturally
```
**Benefits:**
* Summaries complete successfully
* No lost observations
* Clean state transitions
**Code:**
```typescript theme={null}
// v3: Aggressive
async function sessionEnd(sessionId: string) {
await fetch(`http://localhost:37777/sessions/${sessionId}`, {
method: 'DELETE'
});
}
// v4: Graceful
async function sessionEnd(sessionId: string) {
await db.run(
'UPDATE sdk_sessions SET completed_at = ? WHERE id = ?',
[Date.now(), sessionId]
);
}
```
### Realization 4: One Session, Not Many
**Problem:** We were creating multiple SDK sessions per Claude Code session.
**What we thought:**
```
Claude Code session → Create SDK session per observation → 100+ SDK sessions
```
**Reality should be:**
```
Claude Code session → ONE long-running SDK session → Streaming input
```
**Why this matters:**
* SDK maintains conversation state
* Context accumulates naturally
* Much more efficient
**Implementation:**
```typescript theme={null}
// ✅ Streaming Input Mode
async function* messageGenerator(): AsyncIterable {
// Initial prompt
yield {
role: "user",
content: "You are a memory assistant..."
};
// Then continuously yield observations
while (session.status === 'active') {
const observations = await pollQueue();
for (const obs of observations) {
yield {
role: "user",
content: formatObservation(obs)
};
}
await sleep(1000);
}
}
const response = query({
prompt: messageGenerator(),
options: { maxTurns: 1000 }
});
```
***
## v4: The Architecture That Works
### The Core Design
```
┌─────────────────────────────────────────────────────────┐
│ CLAUDE CODE SESSION │
│ User → Claude → Tools (Read, Edit, Write, Bash) │
│ ↓ │
│ PostToolUse Hook │
│ (queues observation) │
└─────────────────────────────────────────────────────────┘
↓ SQLite queue
┌─────────────────────────────────────────────────────────┐
│ SDK WORKER PROCESS │
│ ONE streaming session per Claude Code session │
│ │
│ AsyncIterable │
│ → Yields observations from queue │
│ → SDK compresses via AI │
│ → Parses XML responses │
│ → Stores in database │
└─────────────────────────────────────────────────────────┘
↓ SQLite storage
┌─────────────────────────────────────────────────────────┐
│ NEXT SESSION │
│ SessionStart Hook │
│ → Queries database │
│ → Returns progressive disclosure index │
│ → Agent fetches details via MCP │
└─────────────────────────────────────────────────────────┘
```
### The Five Hook Architecture
**Purpose:** Inject context from previous sessions
**Timing:** When Claude Code starts
**What it does:**
* Queries last 10 session summaries
* Formats as progressive disclosure index
* Injects into context via stdout
**Key change from v3:**
* ✅ Index format (not full details)
* ✅ Token counts visible
* ✅ MCP search instructions included
**Purpose:** Initialize session tracking
**Timing:** Before Claude processes prompt
**What it does:**
* Creates session record
* Saves raw user prompt (v4.2.0+)
* Starts worker if needed
**Key change from v3:**
* ✅ Stores raw prompts for search
* ✅ Auto-starts worker service
**Purpose:** Capture tool observations
**Timing:** After every tool execution
**What it does:**
* Enqueues observation in database
* Returns immediately
**Key change from v3:**
* ✅ Just enqueues (doesn't process)
* ✅ Worker handles all AI calls
**Purpose:** Generate session summaries
**Timing:** Worker-triggered (mid-session)
**What it does:**
* Gathers observations
* Sends to Claude for summarization
* Stores structured summary
**Key change from v3:**
* ✅ Multiple summaries per session
* ✅ Summaries are checkpoints, not endings
**Purpose:** Graceful cleanup
**Timing:** When session ends
**What it does:**
* Marks session complete
* Lets worker finish processing
**Key change from v3:**
* ✅ Graceful (not aggressive)
* ✅ No DELETE requests
* ✅ Worker finishes naturally
### Database Schema Evolution
**v3 schema:**
```sql theme={null}
-- Simple, flat structure
CREATE TABLE observations (
id INTEGER PRIMARY KEY,
session_id TEXT,
text TEXT,
created_at INTEGER
);
```
**v4 schema:**
```sql theme={null}
-- Rich, structured schema
CREATE TABLE observations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL,
project TEXT NOT NULL,
-- Progressive disclosure metadata
title TEXT NOT NULL,
subtitle TEXT,
type TEXT NOT NULL, -- decision, bugfix, feature, etc.
-- Content
narrative TEXT NOT NULL,
facts TEXT, -- JSON array
-- Searchability
concepts TEXT, -- JSON array of tags
files_read TEXT, -- JSON array
files_modified TEXT, -- JSON array
-- Timestamps
created_at TEXT NOT NULL,
created_at_epoch INTEGER NOT NULL,
FOREIGN KEY(session_id) REFERENCES sdk_sessions(id)
);
-- FTS5 for full-text search
CREATE VIRTUAL TABLE observations_fts USING fts5(
title, subtitle, narrative, facts, concepts,
content=observations
);
-- Auto-sync triggers
CREATE TRIGGER observations_ai AFTER INSERT ON observations BEGIN
INSERT INTO observations_fts(rowid, title, subtitle, narrative, facts, concepts)
VALUES (new.id, new.title, new.subtitle, new.narrative, new.facts, new.concepts);
END;
```
**What changed:**
* ✅ Structured fields (title, subtitle, type)
* ✅ FTS5 full-text search
* ✅ Project-scoped queries
* ✅ Rich metadata for progressive disclosure
### Worker Service Redesign
**v3 worker:**
```typescript theme={null}
// Multiple short SDK sessions
app.post('/process', async (req, res) => {
const response = await query({
prompt: buildPrompt(req.body),
options: { maxTurns: 1 }
});
for await (const msg of response) {
// Process single observation
}
res.json({ success: true });
});
```
**v4 worker:**
```typescript theme={null}
// ONE long-running SDK session
async function runWorker(sessionId: string) {
const response = query({
prompt: messageGenerator(), // AsyncIterable
options: { maxTurns: 1000 }
});
for await (const msg of response) {
if (msg.type === 'text') {
parseObservations(msg.content);
parseSummaries(msg.content);
}
}
}
```
**Benefits:**
* Maintains conversation state
* SDK handles context automatically
* More efficient (fewer API calls)
* Natural multi-turn flow
***
## Critical Fixes Along the Way
### Fix 1: Context Injection Pollution (v4.3.1)
**Problem:** SessionStart hook output polluted with npm install logs
```bash theme={null}
# Hook output contained:
npm WARN deprecated ...
npm WARN deprecated ...
{"hookSpecificOutput": {"additionalContext": "..."}}
```
**Why it broke:**
* Claude Code expects clean JSON or plain text
* stderr/stdout from npm install mixed with hook output
* Context didn't inject properly
**Solution:**
```json theme={null}
{
"command": "npm install --loglevel=silent && node context-hook.js"
}
```
**Result:** Clean JSON output, context injection works
### Fix 2: Double Shebang Issue (v4.3.1)
**Problem:** Hook executables had duplicate shebangs
```javascript theme={null}
#!/usr/bin/env node
#!/usr/bin/env node // ← Duplicate!
// Rest of code...
```
**Why it happened:**
* Source files had shebang
* esbuild added another shebang during build
**Solution:**
```typescript theme={null}
// Remove shebangs from source files
// Let esbuild add them during build
```
**Result:** Clean executables, no parsing errors
### Fix 3: FTS5 Injection Vulnerability (v4.2.3)
**Problem:** User input passed directly to FTS5 query
```typescript theme={null}
// ❌ Vulnerable
const results = db.query(
`SELECT * FROM observations_fts WHERE observations_fts MATCH '${userQuery}'`
);
```
**Attack:**
```typescript theme={null}
userQuery = "'; DROP TABLE observations; --"
```
**Solution:**
```typescript theme={null}
// ✅ Safe: Use parameterized queries
const results = db.query(
'SELECT * FROM observations_fts WHERE observations_fts MATCH ?',
[userQuery]
);
```
### Fix 4: NOT NULL Constraint Violation (v4.2.8)
**Problem:** Session creation failed when prompt was empty
```sql theme={null}
INSERT INTO sdk_sessions (claude_session_id, user_prompt, ...)
VALUES ('abc123', NULL, ...) -- ❌ user_prompt is NOT NULL
```
**Solution:**
```typescript theme={null}
// Allow NULL user_prompts
user_prompt: input.prompt ?? null
```
**Schema change:**
```sql theme={null}
-- Before
user_prompt TEXT NOT NULL
-- After
user_prompt TEXT -- Nullable
```
***
## Performance Improvements
### Optimization 1: Prepared Statements
**Before:**
```typescript theme={null}
for (const obs of observations) {
db.run(`INSERT INTO observations (...) VALUES (?, ?, ...)`, [obs.id, obs.text, ...]);
}
```
**After:**
```typescript theme={null}
const stmt = db.prepare(`INSERT INTO observations (...) VALUES (?, ?, ...)`);
for (const obs of observations) {
stmt.run([obs.id, obs.text, ...]);
}
stmt.finalize();
```
**Impact:** 5x faster bulk inserts
### Optimization 2: FTS5 Indexing
**Before:**
```typescript theme={null}
// Manual full-text search
const results = db.query(
`SELECT * FROM observations WHERE text LIKE '%${query}%'`
);
```
**After:**
```typescript theme={null}
// FTS5 virtual table
const results = db.query(
`SELECT * FROM observations_fts WHERE observations_fts MATCH ?`,
[query]
);
```
**Impact:** 100x faster searches on large datasets
### Optimization 3: Index Format Default
**Before:**
```typescript theme={null}
// Always return full observations
search_observations({ query: "hooks" });
// Returns: 5,000 tokens
```
**After:**
```typescript theme={null}
// Default to index format
search_observations({ query: "hooks", format: "index" });
// Returns: 200 tokens
// Fetch full only when needed
search_observations({ query: "hooks", format: "full", limit: 1 });
// Returns: 150 tokens
```
**Impact:** 25x reduction in average search result size
***
## What We Learned
### Lesson 1: Context is Precious
**Principle:** Every token you put in context window costs attention.
**Application:**
* Progressive disclosure reduces waste by 87%
* Index-first approach gives agent control
* Token counts make costs visible
### Lesson 2: Session State is Complicated
**Principle:** Distributed state is hard. SDK handles it better than we can.
**Application:**
* Use SDK's built-in session resumption
* Don't try to manually reconstruct state
* Track session IDs from init messages
### Lesson 3: Graceful Beats Aggressive
**Principle:** Let processes finish their work before terminating.
**Application:**
* Graceful cleanup prevents data loss
* Workers finish important operations
* Clean state transitions reduce bugs
### Lesson 4: AI is the Compressor
**Principle:** Don't compress manually. Let AI do semantic compression.
**Application:**
* 10:1 to 100:1 compression ratios
* Semantic understanding, not keyword extraction
* Structured outputs (XML parsing)
### Lesson 5: Progressive Everything
**Principle:** Show metadata first, fetch details on-demand.
**Application:**
* Progressive disclosure in context injection
* Index format in search results
* Layer 1 (titles) → Layer 2 (summaries) → Layer 3 (full details)
***
## The Road Ahead
### Planned: Adaptive Index Size
```typescript theme={null}
SessionStart({ source: "startup" }):
→ Show last 10 sessions (normal)
SessionStart({ source: "resume" }):
→ Show only current session (minimal)
SessionStart({ source: "compact" }):
→ Show last 20 sessions (comprehensive)
```
### Planned: Relevance Scoring
```typescript theme={null}
// Use embeddings to pre-sort index by semantic relevance
search_observations({
query: "authentication bug",
sort: "relevance" // Based on embeddings
});
```
### Planned: Multi-Project Context
```typescript theme={null}
// Cross-project pattern recognition
search_observations({
query: "API rate limiting",
projects: ["api-gateway", "user-service", "billing-service"]
});
```
### Planned: Collaborative Memory
```typescript theme={null}
// Team-shared observations (optional)
createObservation({
title: "Rate limit: 100 req/min",
scope: "team" // vs "user"
});
```
***
## Migration Guide: v3 → v5
### Step 1: Backup Database
```bash theme={null}
cp ~/.claude-mem/claude-mem.db ~/.claude-mem/claude-mem-v3-backup.db
```
### Step 2: Update Plugin
```bash theme={null}
cd ~/.claude/plugins/marketplaces/thedotmack
git pull
```
### Step 3: Update Plugin
```bash theme={null}
/plugin update claude-mem
```
**What happens automatically:**
* Dependencies update (including new ones like chromadb for v5.0.0+)
* Database schema migrations run automatically
* Worker service restarts with new code
* Smart install caching activates (v5.0.3+)
### Step 4: Test
```bash theme={null}
# Start Claude Code
claude
# Check that context is injected
# (Should see progressive disclosure index with v5 viewer link)
# Open viewer UI (v5.1.0+)
open http://localhost:37777
# Submit a prompt and watch real-time updates in viewer
```
### Step 5: Explore New Features
```bash theme={null}
# View memory stream in browser (v5.1.0+)
open http://localhost:37777
# Toggle theme (v5.1.2+)
# Click theme button in viewer header
# Check worker health
npm run worker:status
curl http://localhost:37777/health
```
***
## Key Metrics
### v3 Performance
| Metric | Value |
| - | - |
| Context usage per session | \~25,000 tokens |
| Relevant context | \~2,000 tokens (8%) |
| Hook execution time | \~200ms |
| Search latency | \~500ms (LIKE queries) |
### v4 Performance
| Metric | Value |
| - | - |
| Context usage per session | \~1,100 tokens |
| Relevant context | \~1,100 tokens (100%) |
| Hook execution time | \~45ms |
| Search latency | \~15ms (FTS5) |
### v5 Performance
| Metric | Value |
| - | - |
| Context usage per session | \~1,100 tokens |
| Relevant context | \~1,100 tokens (100%) |
| Hook execution time | \~10ms (cached install) |
| Search latency | \~12ms (FTS5) or \~25ms (hybrid) |
| Viewer UI load time | \~50ms (bundled HTML) |
| SSE update latency | \~5ms (real-time) |
**v3 → v4 Improvements:**
* 96% reduction in context waste
* 12x increase in relevance
* 4x faster hooks
* 33x faster search
**v4 → v5 Improvements:**
* 78% faster hooks (smart caching)
* Real-time visualization (viewer UI)
* Better search relevance (hybrid)
* Enhanced UX (theme toggle, persistence)
***
## Conclusion
The journey from v3 to v5 was about understanding these fundamental truths:
1. **Context is finite** - Progressive disclosure respects attention budget
2. **AI is the compressor** - Semantic understanding beats keyword extraction
3. **Agents are smart** - Let them decide what to fetch
4. **State is hard** - Use SDK's built-in mechanisms
5. **Graceful wins** - Let processes finish cleanly
The result is a memory system that's both powerful and invisible. Users never notice it working - Claude just gets smarter over time.
**v5 adds visibility**: Now users CAN see the memory system working if they want (via viewer UI), but it's still non-intrusive.
***
## Further Reading
* [Progressive Disclosure](progressive-disclosure) - The philosophy behind v4
* [Hooks Architecture](hooks-architecture) - How hooks power the system
* [Context Engineering](context-engineering) - Foundational principles
* [Worker Service](/architecture/worker-service) - Real-time visualization (v5.1.0+)
***
*This architecture evolution reflects hundreds of hours of experimentation, dozens of dead ends, and the invaluable experience of real-world usage. v5 is the architecture that emerged from understanding what actually works - and making it visible to users.*
# Database Architecture
Source: https://docs.claude-mem.ai/architecture/database
SQLite schema, FTS5 search, and data storage
# Database Architecture
Claude-Mem uses SQLite 3 with the bun:sqlite native module for persistent storage and FTS5 for full-text search.
## Database Location
**Path**: `~/.claude-mem/claude-mem.db`
The database uses SQLite's WAL (Write-Ahead Logging) mode for concurrent reads/writes.
## Database Implementation
**Primary Implementation**: bun:sqlite (native SQLite module)
* Used by: SessionStore and SessionSearch
* Format: Synchronous API with better performance
* **Note**: Database.ts (using bun:sqlite) is legacy code
## Core Tables
### 1. sdk\_sessions
Tracks active and completed sessions.
```sql theme={null}
CREATE TABLE sdk_sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sdk_session_id TEXT UNIQUE NOT NULL,
claude_session_id TEXT,
project TEXT NOT NULL,
prompt_counter INTEGER DEFAULT 0,
status TEXT NOT NULL DEFAULT 'active',
created_at TEXT NOT NULL,
created_at_epoch INTEGER NOT NULL,
completed_at TEXT,
completed_at_epoch INTEGER,
last_activity_at TEXT,
last_activity_epoch INTEGER
);
```
**Indexes**:
* `idx_sdk_sessions_claude_session` on `claude_session_id`
* `idx_sdk_sessions_project` on `project`
* `idx_sdk_sessions_status` on `status`
* `idx_sdk_sessions_created_at` on `created_at_epoch DESC`
### 2. observations
Individual tool executions with hierarchical structure.
```sql theme={null}
CREATE TABLE observations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL,
sdk_session_id TEXT NOT NULL,
claude_session_id TEXT,
project TEXT NOT NULL,
prompt_number INTEGER,
tool_name TEXT NOT NULL,
correlation_id TEXT,
-- Hierarchical fields
title TEXT,
subtitle TEXT,
narrative TEXT,
text TEXT,
facts TEXT,
concepts TEXT,
type TEXT,
files_read TEXT,
files_modified TEXT,
created_at TEXT NOT NULL,
created_at_epoch INTEGER NOT NULL,
FOREIGN KEY (sdk_session_id) REFERENCES sdk_sessions(sdk_session_id)
);
```
**Observation Types**:
* `decision` - Architectural or design decisions
* `bugfix` - Bug fixes and corrections
* `feature` - New features or capabilities
* `refactor` - Code refactoring and cleanup
* `discovery` - Learnings about the codebase
* `change` - General changes and modifications
**Indexes**:
* `idx_observations_session` on `session_id`
* `idx_observations_sdk_session` on `sdk_session_id`
* `idx_observations_project` on `project`
* `idx_observations_tool_name` on `tool_name`
* `idx_observations_created_at` on `created_at_epoch DESC`
* `idx_observations_type` on `type`
### 3. session\_summaries
AI-generated session summaries (multiple per session).
```sql theme={null}
CREATE TABLE session_summaries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sdk_session_id TEXT NOT NULL,
claude_session_id TEXT,
project TEXT NOT NULL,
prompt_number INTEGER,
-- Summary fields
request TEXT,
investigated TEXT,
learned TEXT,
completed TEXT,
next_steps TEXT,
notes TEXT,
created_at TEXT NOT NULL,
created_at_epoch INTEGER NOT NULL,
FOREIGN KEY (sdk_session_id) REFERENCES sdk_sessions(sdk_session_id)
);
```
**Indexes**:
* `idx_session_summaries_sdk_session` on `sdk_session_id`
* `idx_session_summaries_project` on `project`
* `idx_session_summaries_created_at` on `created_at_epoch DESC`
### 4. user\_prompts
Raw user prompts with FTS5 search (as of v4.2.0).
```sql theme={null}
CREATE TABLE user_prompts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sdk_session_id TEXT NOT NULL,
claude_session_id TEXT,
project TEXT NOT NULL,
prompt_number INTEGER,
prompt_text TEXT NOT NULL,
created_at TEXT NOT NULL,
created_at_epoch INTEGER NOT NULL,
FOREIGN KEY (sdk_session_id) REFERENCES sdk_sessions(sdk_session_id)
);
```
**Indexes**:
* `idx_user_prompts_sdk_session` on `sdk_session_id`
* `idx_user_prompts_project` on `project`
* `idx_user_prompts_created_at` on `created_at_epoch DESC`
### 5. tool\_uses
Durable backup index for raw tool I/O (schema v51). Written from the same ingest
choke point that feeds the observation generator, so it captures the same calls
without adding a second capture path.
```sql theme={null}
CREATE TABLE tool_uses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
tool_use_id TEXT NOT NULL,
content_session_id TEXT NOT NULL,
memory_session_id TEXT,
session_db_id INTEGER,
project TEXT NOT NULL,
platform_source TEXT NOT NULL DEFAULT 'claude',
tool_name TEXT NOT NULL,
tool_input TEXT,
tool_response TEXT,
cwd TEXT,
prompt_number INTEGER,
agent_type TEXT,
agent_id TEXT,
observation_id INTEGER,
or_generation_id TEXT,
or_session_id TEXT,
content_hash TEXT,
created_at TEXT NOT NULL,
created_at_epoch INTEGER NOT NULL,
UNIQUE(content_session_id, tool_use_id)
);
```
**Why it exists**: `pending_messages` is the generation *queue* — rows are
claimed, summarized, and deleted, so raw tool payloads were unrecoverable once
an observation existed. `tool_uses` is the durable side index that survives the
queue, and it is what the `get_tool_uses` MCP tool (progressive-disclosure layer
4\) reads.
**Not a transcript store**: the JSONL transcripts on disk and the transcript
watcher remain the spine for session history. This table is a by-reference index
for tool bodies, nothing more.
**Idempotency**: `UNIQUE(content_session_id, tool_use_id)` means a replayed
PostToolUse (hook retry, transcript re-scan) updates the row instead of
duplicating it. `observation_id` is linked *after* generation and is never
overwritten once set.
**Payload cap**: `tool_input` / `tool_response` are truncated at 64 KB with a
`…[truncated: N bytes]` marker. `content_hash` is computed over the original,
pre-truncation payload.
**Cost columns**: none, deliberately. `or_generation_id` and `or_session_id` are
nullable join keys back to an OpenRouter spend line; dollars live on that line,
never here.
**Indexes**: `project`, `memory_session_id`, `content_session_id`,
`session_db_id`, `tool_name`, `created_at_epoch`, `observation_id`,
`or_generation_id`
### Legacy Tables
* **sessions**: Legacy session tracking (v3.x)
* **memories**: Legacy compressed memory chunks (v3.x)
* **overviews**: Legacy session summaries (v3.x)
## FTS5 Full-Text Search
SQLite FTS5 (Full-Text Search) virtual tables enable fast full-text search across observations and session summaries. User prompts are searched by substring (`LIKE`) and have no FTS index.
### FTS5 Virtual Tables
#### observations\_fts
```sql theme={null}
CREATE VIRTUAL TABLE observations_fts USING fts5(
title,
subtitle,
narrative,
text,
facts,
concepts,
content='observations',
content_rowid='id'
);
```
#### session\_summaries\_fts
```sql theme={null}
CREATE VIRTUAL TABLE session_summaries_fts USING fts5(
request,
investigated,
learned,
completed,
next_steps,
notes,
content='session_summaries',
content_rowid='id'
);
```
### Automatic Synchronization
FTS5 tables stay in sync via triggers. The update triggers fire only when an indexed column is written, so bookkeeping updates (sync revision, project merges, content hashes) never touch the index. Each index update appends a delete marker plus a re-insert of the row's text, so an unscoped update trigger would grow the index on every such write:
```sql theme={null}
-- Insert trigger example
CREATE TRIGGER observations_ai AFTER INSERT ON observations BEGIN
INSERT INTO observations_fts(rowid, title, subtitle, narrative, text, facts, concepts)
VALUES (new.id, new.title, new.subtitle, new.narrative, new.text, new.facts, new.concepts);
END;
-- Update trigger example (fires only when an indexed column is written)
CREATE TRIGGER observations_au
AFTER UPDATE OF title, subtitle, narrative, text, facts, concepts ON observations BEGIN
INSERT INTO observations_fts(observations_fts, rowid, title, subtitle, narrative, text, facts, concepts)
VALUES('delete', old.id, old.title, old.subtitle, old.narrative, old.text, old.facts, old.concepts);
INSERT INTO observations_fts(rowid, title, subtitle, narrative, text, facts, concepts)
VALUES (new.id, new.title, new.subtitle, new.narrative, new.text, new.facts, new.concepts);
END;
-- Delete trigger example
CREATE TRIGGER observations_ad AFTER DELETE ON observations BEGIN
INSERT INTO observations_fts(observations_fts, rowid, title, subtitle, narrative, text, facts, concepts)
VALUES('delete', old.id, old.title, old.subtitle, old.narrative, old.text, old.facts, old.concepts);
END;
```
### FTS5 Query Syntax
FTS5 supports rich query syntax:
* **Simple**: `"error handling"`
* **AND**: `"error" AND "handling"`
* **OR**: `"bug" OR "fix"`
* **NOT**: `"bug" NOT "feature"`
* **Phrase**: `"'exact phrase'"`
* **Column**: `title:"authentication"`
### Security
As of v4.2.3, all FTS5 queries are properly escaped to prevent SQL injection:
* Double quotes are escaped: `query.replace(/"/g, '""')`
* Comprehensive test suite with 332 injection attack tests
## Database Classes
### SessionStore
CRUD operations for sessions, observations, summaries, and user prompts.
**Location**: `src/services/sqlite/SessionStore.ts`
**Methods**:
* `createSession()`
* `getSession()`
* `updateSession()`
* `createObservation()`
* `getObservations()`
* `createSummary()`
* `getSummaries()`
* `createUserPrompt()`
### SessionSearch
FTS5 full-text search with 8 specialized search methods.
**Location**: `src/services/sqlite/SessionSearch.ts`
**Methods**:
* `searchObservations()` - Full-text search across observations
* `searchSessions()` - Full-text search across summaries
* `searchUserPrompts()` - Full-text search across user prompts
* `findByConcept()` - Find by concept tags
* `findByFile()` - Find by file references
* `findByType()` - Find by observation type
* `getRecentContext()` - Get recent session context
* `advancedSearch()` - Combined filters
## Migrations
Database schema is managed via migrations in `src/services/sqlite/migrations.ts`.
**Migration History**:
* Migration 001: Initial schema (sessions, memories, overviews, diagnostics, transcript\_events)
* Migration 002: Hierarchical memory fields (title, subtitle, facts, concepts, files\_touched)
* Migration 003: SDK sessions and observations
* Migration 004: Session summaries
* Migration 005: Multi-prompt sessions (prompt\_counter, prompt\_number)
* Migration 006: FTS5 virtual tables and triggers
* Migration 007-010: Various improvements and user prompts table
* Migration 051: `tool_uses` durable raw tool I/O index
## Performance Considerations
* **Indexes**: All foreign keys and frequently queried columns are indexed
* **FTS5**: Full-text search is significantly faster than LIKE queries
* **Triggers**: Automatic synchronization has minimal overhead
* **Connection Pooling**: bun:sqlite reuses connections efficiently
* **Synchronous API**: bun:sqlite uses synchronous API for better performance
## Troubleshooting
See [Troubleshooting - Database Issues](../troubleshooting.md#database-issues) for common problems and solutions.
# Hook Lifecycle
Source: https://docs.claude-mem.ai/architecture/hooks
Complete guide to the 5-stage memory agent lifecycle for platform implementers
# Hook Lifecycle
Claude-Mem implements a **5-stage hook system** that captures development work across Claude Code sessions. This document provides a complete technical reference for developers implementing this pattern on other platforms.
## Architecture Overview
### System Architecture
This two-process architecture works in both Claude Code and VS Code:
```mermaid theme={null}
graph TB
subgraph EXT["Extension Process (runs in IDE)"]
direction TB
ACT[Extension Activation]
HOOKS[Hook Event Handlers]
ACT --> HOOKS
subgraph HOOK_HANDLERS["5 Lifecycle Hooks"]
H1[SessionStart
activate function]
H2[UserPromptSubmit
command handler]
H3[PostToolUse
middleware]
H4[Stop
idle timeout]
H5[SessionEnd
deactivate function]
end
HOOKS --> HOOK_HANDLERS
end
HOOK_HANDLERS -->|"HTTP
(fire-and-forget
2s timeout)"| HTTP[Worker HTTP API
Configured port]
subgraph WORKER["Worker Process (separate Node.js)"]
direction TB
HTTP --> API[Express Server]
API --> SESS[Session Manager]
API --> AGENT[SDK Agent]
API --> DB[Database Manager]
AGENT -->|Event-Driven| CLAUDE[Claude Agent SDK]
CLAUDE --> SQLITE[(SQLite + FTS5)]
CLAUDE --> CHROMA[(Chroma Vectors)]
end
style EXT fill:#e1f5ff
style WORKER fill:#fff4e1
style HOOK_HANDLERS fill:#f0f0f0
```
**Key Principles:**
* Extension process never blocks (fire-and-forget HTTP)
* Worker processes observations asynchronously
* Session state persists across IDE restarts
### VS Code Extension API Integration Points
For developers porting to VS Code, here's where to hook into the VS Code Extension API:
```mermaid theme={null}
graph LR
subgraph VSCODE["VS Code Extension API"]
direction TB
A["activate(context)"]
B["commands.registerCommand()"]
C["chat.createChatParticipant()"]
D["workspace.onDidSaveTextDocument()"]
E["window.onDidChangeActiveTextEditor()"]
F["deactivate()"]
end
subgraph HOOKS["Hook Equivalents"]
direction TB
G[SessionStart]
H[UserPromptSubmit]
I[PostToolUse]
J[Stop/Summary]
K[SessionEnd]
end
subgraph WORKER_API["Worker HTTP Endpoints"]
direction TB
L[GET /api/context/inject]
M[POST /sessions/init]
N[POST /sessions/observations]
O[POST /sessions/summarize]
P[POST /sessions/complete]
end
A --> G
B --> H
C --> H
D --> I
E --> I
F --> K
G --> L
H --> M
I --> N
J --> O
K --> P
style VSCODE fill:#007acc,color:#fff
style HOOKS fill:#f0f0f0
style WORKER_API fill:#4caf50,color:#fff
```
**Implementation Examples:**
```typescript theme={null}
const workerPort = process.env.CLAUDE_MEM_WORKER_PORT ?? ""
const workerBaseUrl = `http://127.0.0.1:${workerPort}`
// VS Code Extension - SessionStart Hook
export async function activate(context: vscode.ExtensionContext) {
const sessionId = generateSessionId()
const project = workspace.name || 'default'
// Fetch context from worker
const response = await fetch(`${workerBaseUrl}/api/context/inject?project=${project}`)
const context = await response.text()
// Inject into chat or UI panel
injectContextToChat(context)
}
// VS Code Extension - UserPromptSubmit Hook
const command = vscode.commands.registerCommand('extension.command', async (prompt) => {
await fetch(`${workerBaseUrl}/sessions/init`, {
method: 'POST',
body: JSON.stringify({ sessionId, project, userPrompt: prompt })
})
})
// VS Code Extension - PostToolUse Hook (middleware pattern)
workspace.onDidSaveTextDocument(async (document) => {
await fetch(`${workerBaseUrl}/api/sessions/observations`, {
method: 'POST',
body: JSON.stringify({
claudeSessionId: sessionId,
tool_name: 'FileSave',
tool_input: { path: document.uri.path },
tool_response: 'File saved successfully'
})
})
})
```
### Async Processing Pipeline
How observations flow from extension to database without blocking the IDE:
```mermaid theme={null}
graph TB
A["Extension: Tool Use Event"] --> B{"Skip List?
(TodoWrite, AskUserQuestion, etc.)"}
B -->|"Skip"| X["Discard"]
B -->|"Keep"| C["Strip Privacy Tags
<private>...</private>"]
C --> D["HTTP POST to Worker
Configured port"]
D --> E["2s timeout
fire-and-forget"]
E --> F["Extension continues
(non-blocking)"]
D -.Async Path.-> G["Worker: Queue Observation"]
G --> H["SDK Agent picks up
(event-driven)"]
H --> I["Call Claude API
(compress observation)"]
I --> J["Parse XML response"]
J --> K["Save to SQLite
(sdk_sessions table)"]
K --> L["Sync to Chroma
(vector embeddings)"]
style F fill:#90EE90,stroke:#2d6b2d,stroke-width:3px
style L fill:#87CEEB,stroke:#2d5f8d,stroke-width:3px
style E fill:#ffeb3b,stroke:#c6a700,stroke-width:2px
```
**Critical Pattern:** The extension's HTTP call has a 2-second timeout and doesn't wait for AI processing. The worker handles compression asynchronously using an event-driven queue.
## The 5 Lifecycle Stages
| Stage | Hook | Trigger | Purpose |
| - | - | - | - |
| **1. SessionStart** | `claude-mem hook claude-code context` | User opens Claude Code | Inject prior context silently |
| **2. UserPromptSubmit** | `claude-mem hook claude-code session-init` | User submits a prompt | Create/get session, save prompt, init worker |
| **3. PostToolUse** | `claude-mem hook claude-code observation` | Claude uses any tool | Queue observation for AI compression |
| **4. Stop** | `claude-mem hook claude-code summarize` | User stops asking questions | Generate session summary |
| **5. SessionEnd** | `claude-mem hook claude-code session-end` | Session closes | Mark session completed |
PreToolUse (matcher `Read` plus the search tools) also runs `claude-mem hook claude-code file-context` synchronously, to add a file's history before Claude reads it and let the [File Read Gate](/file-read-gate) block a whole-file Read. PostToolUseFailure runs the same `observation` hook as PostToolUse for a tool call that failed.
## Hook Configuration
Hooks are configured in `plugin/hooks/hooks.json` (generated by `scripts/build-hooks.js`). Every runtime hook is exec form: `command` + `args`, no `shell`. Claude Code resolves `claude-mem` on PATH and spawns it directly, with no bash in between:
```json theme={null}
{
"hooks": {
"Setup": [{
"matcher": "*",
"hooks": [{
"type": "command",
"shell": "bash",
"command": "… resolve the plugin root …; node \"$_P/scripts/version-check.js\"",
"timeout": 300
}]
}],
"SessionStart": [{
"matcher": "startup|resume|clear|compact",
"hooks": [
{ "type": "command", "command": "claude-mem", "args": ["start"], "timeout": 60, "async": true }
]
}, {
"matcher": "startup|clear|compact",
"hooks": [
{ "type": "command", "command": "claude-mem", "args": ["hook", "claude-code", "context"], "timeout": 60 }
]
}, {
"matcher": "startup",
"hooks": [
{ "type": "command", "command": "node", "args": ["${CLAUDE_PLUGIN_ROOT}/scripts/ensure-launcher.cjs"], "timeout": 180 }
]
}],
"UserPromptSubmit": [{
"hooks": [{ "type": "command", "command": "claude-mem", "args": ["hook", "claude-code", "session-init"], "timeout": 15 }]
}],
"PreToolUse": [{
"matcher": "Read|^Bash$|^Grep$|^Glob$|^memory_search$|^memory$|^mcp__.+__memory_search$",
"hooks": [{ "type": "command", "command": "claude-mem", "args": ["hook", "claude-code", "file-context"], "timeout": 15 }]
}],
"PostToolUse": [{
"matcher": "*",
"hooks": [{ "type": "command", "command": "claude-mem", "args": ["hook", "claude-code", "observation"], "timeout": 120, "async": true }]
}],
"PostToolUseFailure": [{
"matcher": "*",
"hooks": [{ "type": "command", "command": "claude-mem", "args": ["hook", "claude-code", "observation"], "timeout": 120, "async": true }]
}],
"Stop": [{
"hooks": [{ "type": "command", "command": "claude-mem", "args": ["hook", "claude-code", "summarize"], "timeout": 120, "async": true }]
}],
"SessionEnd": [{
"hooks": [{ "type": "command", "command": "claude-mem", "args": ["hook", "claude-code", "session-end"], "async": true }]
}]
}
}
```
* **`claude-mem`** is a small, version-stable compiled launcher, not the npm CLI. `npx claude-mem install` / `update` place it at `~/.local/bin` (POSIX) or `%LOCALAPPDATA%\claude-mem\bin` (Windows) and add that directory to PATH. It finds the plugin root, runs `worker-service.cjs` with the same arguments under Bun, and exits 0 on any failure.
* **`ensure-launcher.cjs`** (startup only) is the self-heal for marketplace-only installs: it places or refreshes the launcher when it is missing or out of date.
* **Setup** is the only entry that still uses bash. Claude Code fires it only for `claude --init`, `--init-only` and `-p --maintenance`, never on plugin install, update or a normal session start.
See [Hooks Architecture](/hooks-architecture#how-every-hook-is-launched) for the launcher's behavior and the error you see when it is missing.
***
## Stage 1: SessionStart
**Timing**: When user opens Claude Code or resumes session
**Hooks Triggered** (in order):
1. `claude-mem start` - Starts the worker service (async)
2. `claude-mem hook claude-code context` - Fetches and silently injects prior session context
3. `node ${CLAUDE_PLUGIN_ROOT}/scripts/ensure-launcher.cjs` - Startup only, synchronous: keeps the `claude-mem` launcher on PATH, and shows a `systemMessage` when the memory hooks are not active
(Runtime setup is handled by `npx claude-mem install` / `npx claude-mem repair`. The Setup hook runs `version-check.js` only for `claude --init` / `--maintenance`: it installs the plugin's dependencies when they are missing, records the `.install-version` marker once they are complete, and asks you to run `npx claude-mem@latest install` only if they are still missing.)
As of Claude Code 2.1.0 (ultrathink update), SessionStart hooks no longer display user-visible messages. Context is silently injected via `hookSpecificOutput.additionalContext`.
### Sequence Diagram
```mermaid theme={null}
sequenceDiagram
participant User
participant IDE as IDE/Extension
participant ContextHook as context-hook.js
participant Worker as Worker Service
participant DB as SQLite Database
User->>IDE: Opens workspace / resumes session
IDE->>ContextHook: Trigger SessionStart hook
ContextHook->>ContextHook: Generate/reuse session_id
ContextHook->>Worker: Health check (max 10s retry)
alt Worker Ready
ContextHook->>Worker: GET /api/context/inject?project=X
Worker->>DB: SELECT * FROM observations
WHERE project=X
ORDER BY created_at DESC
LIMIT 50
DB-->>Worker: Last 50 observations
Worker-->>ContextHook: Context markdown
ContextHook-->>IDE: hookSpecificOutput.additionalContext
IDE->>IDE: Inject context to Claude's prompt
IDE-->>User: Session ready with context
else Worker Not Ready
ContextHook-->>IDE: Empty context (graceful degradation)
IDE-->>User: Session ready without context
end
Note over User,DB: Total time: <300ms (with health check)
```
### Context Hook (`claude-mem hook claude-code context`)
**Purpose**: Inject context from previous sessions into Claude's initial context.
**Input** (via stdin):
```json theme={null}
{
"session_id": "claude-session-123",
"cwd": "/path/to/project",
"source": "startup"
}
```
**Processing**:
1. Wait for worker to be available (health check, max 10 seconds)
2. Call: `GET http://127.0.0.1:/api/context/inject?project={project}`
3. Return formatted context as `additionalContext` in `hookSpecificOutput`
**Output** (via stdout):
```json theme={null}
{
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": "<>"
}
}
```
**Implementation**: `src/cli/handlers/context.ts`
***
## Stage 2: UserPromptSubmit
**Timing**: When user submits any prompt in a session
**Hook**: `claude-mem hook claude-code session-init`
### Sequence Diagram
```mermaid theme={null}
sequenceDiagram
participant User
participant IDE as IDE/Extension
participant NewHook as new-hook.js
participant DB as Direct SQLite Access
participant Worker as Worker Service
User->>IDE: Submits prompt: "Add login feature"
IDE->>NewHook: Trigger UserPromptSubmit
{ session_id, cwd, prompt }
NewHook->>NewHook: Extract project = basename(cwd)
NewHook->>NewHook: Strip privacy tags
<private>...</private>
alt Prompt fully private (empty after stripping)
NewHook-->>IDE: Skip (don't save)
else Prompt has content
NewHook->>DB: INSERT OR IGNORE INTO sdk_sessions
(claude_session_id, project, first_user_prompt)
DB-->>NewHook: sessionDbId (new or existing)
NewHook->>DB: UPDATE sdk_sessions
SET prompt_counter = prompt_counter + 1
WHERE id = sessionDbId
DB-->>NewHook: promptNumber (e.g., 1 for first, 2 for continuation)
NewHook->>DB: INSERT INTO user_prompts
(session_id, prompt_number, prompt)
NewHook->>Worker: POST /sessions/{sessionDbId}/init
{ project, userPrompt, promptNumber }
(fire-and-forget, 2s timeout)
Worker-->>NewHook: 200 OK (or timeout)
NewHook-->>IDE: { continue: true, suppressOutput: true }
IDE-->>User: Prompt accepted
end
Note over NewHook,DB: Idempotent: Same session_id → same sessionDbId
```
**Key Pattern:** The `INSERT OR IGNORE` ensures the same `session_id` always maps to the same `sessionDbId`, enabling conversation continuations.
**Input** (via stdin):
```json theme={null}
{
"session_id": "claude-session-123",
"cwd": "/path/to/project",
"prompt": "User's actual prompt text"
}
```
**Processing Steps**:
```typescript theme={null}
// 1. Extract project name from working directory
project = path.basename(cwd)
// 2. Create or get database session (IDEMPOTENT)
sessionDbId = db.createSDKSession(session_id, project, prompt)
// INSERT OR IGNORE: Creates new row if first prompt, returns existing if continuation
// 3. Increment prompt counter
promptNumber = db.incrementPromptCounter(sessionDbId)
// Returns 1 for first prompt, 2 for continuation, etc.
// 4. Strip privacy tags
cleanedPrompt = stripMemoryTags(prompt)
// Removes ... and ...
// 5. Skip if fully private
if (!cleanedPrompt || cleanedPrompt.trim() === '') {
return // Don't save, don't call worker
}
// 6. Save user prompt to database
db.saveUserPrompt(session_id, promptNumber, cleanedPrompt)
// 7. Initialize session via worker HTTP
POST http://127.0.0.1:/sessions/{sessionDbId}/init
Body: { project, userPrompt, promptNumber }
```
**Output**:
```json theme={null}
{ "continue": true, "suppressOutput": true }
```
**Implementation**: `src/cli/handlers/session-init.ts`
The same `session_id` flows through ALL hooks in a conversation. The `createSDKSession` call is idempotent - it returns the existing session for continuation prompts.
***
## Stage 3: PostToolUse
**Timing**: After Claude uses any tool (Read, Bash, Grep, Write, etc.)
**Hook**: `claude-mem hook claude-code observation`
### Sequence Diagram
```mermaid theme={null}
sequenceDiagram
participant Claude as Claude AI
participant IDE as IDE/Extension
participant SaveHook as save-hook.js
participant Worker as Worker Service
participant Agent as SDK Agent
participant DB as SQLite + Chroma
Claude->>IDE: Uses tool: Read("/src/auth.ts")
IDE->>SaveHook: PostToolUse hook triggered
{ session_id, tool_name, tool_input, tool_response }
SaveHook->>SaveHook: Check skip list
(TodoWrite, AskUserQuestion, etc.)
alt Tool in skip list
SaveHook-->>IDE: Discard (low-value tool)
else Tool allowed
SaveHook->>SaveHook: Strip privacy tags from input/response
SaveHook->>SaveHook: Ensure worker running
(health check)
SaveHook->>Worker: POST /api/sessions/observations
{ claudeSessionId, tool_name, tool_input, tool_response, cwd }
(fire-and-forget, 2s timeout)
SaveHook-->>IDE: { continue: true, suppressOutput: true }
IDE-->>Claude: Tool execution complete
Note over Worker,DB: Async path (doesn't block IDE)
Worker->>Worker: createSDKSession(claudeSessionId)
→ returns sessionDbId
Worker->>Worker: Check if prompt was private
(skip if fully private)
Worker->>Agent: Queue observation for processing
Agent->>Agent: Call Claude SDK to compress
observation into structured format
Agent->>DB: Save compressed observation
to sdk_sessions table
Agent->>DB: Sync to Chroma vector DB
end
Note over SaveHook,DB: Total sync time: ~2ms
AI processing: 1-3s (async)
```
**Key Pattern:** The hook returns immediately after HTTP POST. AI compression happens asynchronously in the worker without blocking Claude's tool execution.
**Input** (via stdin):
```json theme={null}
{
"session_id": "claude-session-123",
"cwd": "/path/to/project",
"tool_name": "Read",
"tool_input": { "file_path": "/src/index.ts" },
"tool_response": "file contents..."
}
```
**Processing Steps**:
```typescript theme={null}
// 1. Check blocklist - skip low-value tools
const SKIP_TOOLS = {
'ListMcpResourcesTool', // MCP infrastructure noise
'SlashCommand', // Command invocation
'Skill', // Skill invocation
'TodoWrite', // Task management meta-tool
'AskUserQuestion' // User interaction
}
if (SKIP_TOOLS[tool_name]) return
// 2. Ensure worker is running
await ensureWorkerRunning()
// 3. Send to worker (fire-and-forget HTTP)
POST http://127.0.0.1:/api/sessions/observations
Body: {
claudeSessionId: session_id,
tool_name,
tool_input,
tool_response,
cwd
}
Timeout: 2000ms
```
**Worker Processing**:
1. Looks up or creates session: `createSDKSession(claudeSessionId, '', '')`
2. Gets prompt counter
3. Checks privacy (skips if user prompt was entirely private)
4. Strips memory tags from `tool_input` and `tool_response`
5. Queues observation for SDK agent processing
6. SDK agent calls Claude to compress into structured observation
7. Stores observation in database and syncs to Chroma
**Output**:
```json theme={null}
{ "continue": true, "suppressOutput": true }
```
**Implementation**: `src/cli/handlers/observation.ts`
***
## Stage 4: Stop
**Timing**: When user stops or pauses asking questions
**Hook**: `claude-mem hook claude-code summarize`
### Sequence Diagram
```mermaid theme={null}
sequenceDiagram
participant User
participant IDE as IDE/Extension
participant SummaryHook as summary-hook.js
participant Worker as Worker Service
participant Agent as SDK Agent
participant DB as SQLite Database
User->>IDE: Stops asking questions
(pause, idle, or explicit stop)
IDE->>SummaryHook: Stop hook triggered
{ session_id, cwd, transcript_path }
SummaryHook->>SummaryHook: Read transcript JSONL file
SummaryHook->>SummaryHook: Extract last user message
(type: "user")
SummaryHook->>SummaryHook: Extract last assistant message
(type: "assistant", filter <system-reminder>)
SummaryHook->>Worker: POST /api/sessions/summarize
{ claudeSessionId, last_user_message, last_assistant_message }
(fire-and-forget, 2s timeout)
SummaryHook-->>IDE: { continue: true, suppressOutput: true }
IDE-->>User: Session paused/stopped
Note over Worker,DB: Async path
Worker->>Worker: Lookup sessionDbId from claudeSessionId
Worker->>Agent: Queue summarization request
Agent->>Agent: Call Claude SDK with prompt:
"Summarize: request, investigated, learned, completed, next_steps"
Agent->>Agent: Parse XML response
Agent->>DB: INSERT INTO session_summaries
{ session_id, request, investigated, learned, completed, next_steps }
Agent->>DB: Sync to Chroma (for semantic search)
Note over SummaryHook,DB: Total sync time: ~2ms
AI summarization: 2-5s (async)
```
**Key Pattern:** The summary is generated asynchronously and doesn't block the user from resuming work or closing the session.
**Input** (via stdin):
```json theme={null}
{
"session_id": "claude-session-123",
"cwd": "/path/to/project",
"transcript_path": "/path/to/transcript.jsonl"
}
```
**Processing Steps**:
```typescript theme={null}
// 1. Extract last messages from transcript JSONL
const lines = fs.readFileSync(transcript_path, 'utf-8').split('\n')
// Find last user message (type: "user")
// Find last assistant message (type: "assistant", filter tags)
// 2. Ensure worker is running
await ensureWorkerRunning()
// 3. Send summarization request (fire-and-forget HTTP)
POST http://127.0.0.1:/api/sessions/summarize
Body: {
claudeSessionId: session_id,
last_user_message: string,
last_assistant_message: string
}
Timeout: 2000ms
```
The summary hook does not call `POST /api/processing`, and that route is not a spinner toggle. It is a localhost-only operator retry. `{ "isProcessing": false }` restarts the observer for every active session that holds buffered work and has no generator running. That includes pauses the automatic resume sweep leaves alone, such as a refused credential or a transport failure. The response's `scheduledSessions` reports how many sessions were retried. `{ "isProcessing": true }` is accepted and does nothing.
**Worker Processing**:
1. Queues summarization for SDK agent
2. Agent calls Claude to generate structured summary
3. Summary stored in database with fields: `request`, `investigated`, `learned`, `completed`, `next_steps`
**Output**:
```json theme={null}
{ "continue": true, "suppressOutput": true }
```
**Implementation**: `src/cli/handlers/summarize.ts`
***
## Stage 5: SessionEnd
**Timing**: When Claude Code session closes (exit, clear, logout, etc.)
**Hook**: `claude-mem hook claude-code session-end`
### Sequence Diagram
```mermaid theme={null}
sequenceDiagram
participant User
participant IDE as IDE/Extension
participant CleanupHook as cleanup-hook.js
participant Worker as Worker Service
participant DB as SQLite Database
participant SSE as SSE Clients (Viewer UI)
User->>IDE: Closes session
(exit, clear, logout)
IDE->>CleanupHook: SessionEnd hook triggered
{ session_id, cwd, transcript_path, reason }
CleanupHook->>Worker: POST /api/sessions/complete
{ claudeSessionId, reason }
(fire-and-forget, 2s timeout)
CleanupHook-->>IDE: { continue: true, suppressOutput: true }
IDE-->>User: Session closed
Note over Worker,SSE: Async path
Worker->>Worker: Lookup sessionDbId from claudeSessionId
Worker->>DB: UPDATE sdk_sessions
SET status = 'completed', completed_at = NOW()
WHERE claude_session_id = claudeSessionId
Worker->>SSE: Broadcast session completion event
(for live viewer UI updates)
SSE-->>SSE: Update UI to show session as completed
Note over CleanupHook,SSE: Total sync time: ~2ms
```
**Key Pattern:** Session completion is tracked for analytics and UI updates, but doesn't prevent the user from closing the IDE.
**Input** (via stdin):
```json theme={null}
{
"session_id": "claude-session-123",
"cwd": "/path/to/project",
"transcript_path": "/path/to/transcript.jsonl",
"reason": "exit"
}
```
**Processing Steps**:
```typescript theme={null}
// Send session complete (fire-and-forget HTTP)
POST http://127.0.0.1:/api/sessions/complete
Body: {
claudeSessionId: session_id,
reason: string // 'exit' | 'clear' | 'logout' | 'prompt_input_exit' | 'other'
}
Timeout: 2000ms
```
**Worker Processing**:
1. Finds session by `claudeSessionId`
2. Marks session as 'completed' in database
3. Broadcasts session completion event to SSE clients
**Output**:
```json theme={null}
{ "continue": true, "suppressOutput": true }
```
**Implementation**: `src/cli/handlers/session-end.ts`
***
## Session State Machine
Understanding session lifecycle and state transitions:
```mermaid theme={null}
stateDiagram-v2
[*] --> Initialized: SessionStart hook
(generate session_id)
Initialized --> Active: UserPromptSubmit
(first prompt)
Active --> Active: UserPromptSubmit
(continuation prompts)
promptNumber++
Active --> ObservationQueued: PostToolUse hook
(tool execution captured)
ObservationQueued --> Active: Observation processed
(async, non-blocking)
Active --> Summarizing: Stop hook
(user pauses/stops)
Summarizing --> Active: User resumes
(new prompt submitted)
Summarizing --> Completed: SessionEnd hook
(session closes)
Active --> Completed: SessionEnd hook
(session closes)
Completed --> [*]
note right of Active
session_id: constant (e.g., "claude-session-abc123")
sessionDbId: constant (e.g., 42)
promptNumber: increments (1, 2, 3, ...)
All operations use same sessionDbId
end note
note right of ObservationQueued
Fire-and-forget HTTP
AI compression happens async
IDE never blocks
end note
```
**Key Insights:**
* `session_id` never changes during a conversation
* `sessionDbId` is the database primary key for the session
* `promptNumber` increments with each user prompt
* State transitions are non-blocking (fire-and-forget pattern)
***
## Database Schema
The session-centric data model that enables cross-session memory:
```mermaid theme={null}
erDiagram
SDK_SESSIONS ||--o{ USER_PROMPTS : "has many"
SDK_SESSIONS ||--o{ OBSERVATIONS : "has many"
SDK_SESSIONS ||--o{ SESSION_SUMMARIES : "has many"
SDK_SESSIONS {
integer id PK "Auto-increment primary key"
text claude_session_id UK "From IDE (e.g., 'claude-session-123')"
text project "Project name from cwd basename"
text first_user_prompt "Initial prompt that started session"
integer prompt_counter "Increments with each UserPromptSubmit"
text status "initialized | active | completed"
datetime created_at
datetime completed_at
}
USER_PROMPTS {
integer id PK
integer session_id FK "References SDK_SESSIONS.id"
integer prompt_number "1, 2, 3, ... matches prompt_counter"
text prompt "User's actual prompt (tags stripped)"
datetime created_at
}
OBSERVATIONS {
integer id PK
integer session_id FK "References SDK_SESSIONS.id"
integer prompt_number "Which prompt this observation belongs to"
text tool_name "Read, Bash, Grep, Write, etc."
text tool_input_json "Stripped of privacy tags"
text tool_response_text "Stripped of privacy tags"
text compressed_observation "AI-generated structured observation"
datetime created_at
}
SESSION_SUMMARIES {
integer id PK
integer session_id FK "References SDK_SESSIONS.id"
text request "What user requested"
text investigated "What was explored"
text learned "What was discovered"
text completed "What was accomplished"
text next_steps "What remains to be done"
datetime created_at
}
```
**Idempotency Pattern:**
```sql theme={null}
-- This ensures same session_id always maps to same sessionDbId
INSERT OR IGNORE INTO sdk_sessions (claude_session_id, project, first_user_prompt)
VALUES (?, ?, ?)
RETURNING id;
-- If already exists, returns existing row
-- If new, creates and returns new row
```
**Foreign Key Cascade:**
All child tables (user\_prompts, observations, session\_summaries) use `session_id` foreign key referencing `SDK_SESSIONS.id`. This ensures:
* All data for a session is queryable by sessionDbId
* Session deletions cascade to child tables
* Efficient joins for context injection
Never generate your own session IDs. Always use the `session_id` provided by the IDE - this is the source of truth for linking all data together.
***
## Privacy & Tag Stripping
### Dual-Tag System
```typescript theme={null}
// User-Level Privacy Control (manual)
sensitive data
// System-Level Recursion Prevention (auto-injected)
...
```
### Processing Pipeline
**Location**: `src/utils/tag-stripping.ts`
```typescript theme={null}
// Called by: new-hook.js (user prompts) and save-hook.js (tool_input, tool_response)
stripMemoryTags(content: string): string
```
**Execution Order** (Edge Processing):
1. `new-hook.js` strips tags from user prompt before saving
2. `save-hook.js` strips tags from tool data before sending to worker
3. Worker strips tags again (defense in depth) before storing
***
## SDK Agent Processing
### Query Loop (Event-Driven)
**Location**: `src/services/worker/SDKAgent.ts`
```typescript theme={null}
async startSession(session: ActiveSession, worker?: any) {
// 1. Create event-driven message generator
const messageGenerator = this.createMessageGenerator(session)
// 2. Run Agent SDK query loop
const queryResult = query({
prompt: messageGenerator,
options: {
model: 'claude-sonnet-5',
disallowedTools: ['Bash', 'Read', 'Write', ...], // Observer-only
abortController: session.abortController
}
})
// 3. Process responses
for await (const message of queryResult) {
if (message.type === 'assistant') {
await this.processSDKResponse(session, text, worker)
}
}
}
```
### Message Types
The message generator yields three types of prompts:
1. **Initial Prompt** (prompt #1): Full instructions for starting observation
2. **Continuation Prompt** (prompt #2+): Context-only for continuing work
3. **Observation Prompts**: Tool use data to compress into observations
4. **Summary Prompts**: Session data to summarize
***
## Implementation Checklist
For developers implementing this pattern on other platforms:
### Hook Registration
* [ ] Define hook entry points in platform config
* [ ] 5 hook types: SessionStart (2 hooks), UserPromptSubmit, PostToolUse, Stop, SessionEnd
* [ ] Prefer exec-form hook entries (`command` + `args`, no shell) when the host supports them
* [ ] Pass `session_id`, `cwd`, and context-specific data
### Database Schema
* [ ] SQLite with WAL mode
* [ ] 4 main tables: `sdk_sessions`, `user_prompts`, `observations`, `session_summaries`
* [ ] Indices for common queries
### Worker Service
* [ ] HTTP server on configured worker port
* [ ] Bun runtime for process management
* [ ] 3 core services: SessionManager, SDKAgent, DatabaseManager
### Hook Implementation
* [ ] context-hook: `GET /api/context/inject` (with health check)
* [ ] new-hook: createSDKSession, saveUserPrompt, `POST /sessions/{id}/init`
* [ ] save-hook: Skip low-value tools, `POST /api/sessions/observations`
* [ ] summary-hook: Parse transcript, `POST /api/sessions/summarize`
* [ ] cleanup-hook: `POST /api/sessions/complete`
### Privacy & Tags
* [ ] Implement `stripMemoryTags()`
* [ ] Process tags at hook layer (edge processing)
* [ ] Max tag count = 100 (ReDoS protection)
### SDK Integration
* [ ] Call Claude Agent SDK to process observations/summaries
* [ ] Parse XML responses for structured data
* [ ] Store to database + sync to vector DB
***
## Key Design Principles
1. **Session ID is Source of Truth**: Never generate your own session IDs
2. **Idempotent Database Operations**: Use `INSERT OR IGNORE` for session creation
3. **Edge Processing for Privacy**: Strip tags at hook layer before data reaches worker
4. **Fire-and-Forget for Non-Blocking**: HTTP timeouts prevent IDE blocking
5. **Event-Driven, Not Polling**: Zero-latency queue notification to SDK agent
6. **Everything Saves Always**: No "orphaned" sessions
***
## Common Pitfalls
| Problem | Root Cause | Solution |
| - | - | - |
| Session ID mismatch | Different `session_id` used in different hooks | Always use ID from hook input |
| Duplicate sessions | Creating new session instead of using existing | Use `INSERT OR IGNORE` with `session_id` as key |
| Blocking IDE | Waiting for full response | Use fire-and-forget with short timeouts |
| Memory tags in DB | Stripping tags in wrong layer | Strip at hook layer, before HTTP send |
| Worker not found | Health check too fast | Add retry loop with exponential backoff |
***
## Related Documentation
* [Worker Service](/architecture/worker-service) - HTTP API and async processing
* [Database Schema](/architecture/database) - SQLite tables and FTS5 search
* [Privacy Tags](/usage/private-tags) - Using `` tags
* [Troubleshooting](/troubleshooting) - Common hook issues
# Architecture Overview
Source: https://docs.claude-mem.ai/architecture/overview
System components and data flow in Claude-Mem
# Architecture Overview
## System Components
Claude-Mem operates as a Claude Code plugin with the following core components:
1. **Plugin Hooks** - Lifecycle events (lifecycle hooks for SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, PostToolUseFailure, Stop and SessionEnd, plus a Setup version-check), each run as a shell-free `claude-mem hook claude-code ` command
2. **Worker Service** - Express HTTP API on a per-user port; processes observations via the Claude Agent SDK (or Gemini / OpenRouter)
3. **Database Layer** - SQLite + FTS5 (and optional Chroma for semantic search)
4. **Search Tools** - HTTP API + the `mem-search` skill / MCP server for progressive disclosure search
5. **Viewer UI** - React-based real-time memory stream served by the worker
## Technology Stack
| Layer | Technology |
| - | - |
| **Language** | TypeScript (ES2022, ESNext modules) |
| **Runtime** | Node.js 20+ and Bun ≥ 1.0 |
| **Database** | SQLite 3 with bun:sqlite driver |
| **Vector Store** | Chroma (optional, for semantic search) |
| **HTTP Server** | Express.js 5 |
| **Real-time** | Server-Sent Events (SSE) |
| **UI Framework** | React + TypeScript |
| **AI SDK** | @anthropic-ai/claude-agent-sdk (or Gemini / OpenRouter) |
| **Build Tool** | esbuild (bundles TypeScript) |
| **Process Manager** | Bun |
| **Testing** | `bun test` |
## Data Flow
### Memory Pipeline
```
Hook (stdin) → Database → Worker Service → SDK Processor → Database → Next Session Hook
```
1. **Input**: Claude Code sends tool execution data via stdin to hooks
2. **Storage**: Hooks write observations to SQLite database
3. **Processing**: Worker service reads observations, processes via SDK
4. **Output**: Processed summaries written back to database
5. **Retrieval**: Next session's context hook reads summaries from database
### Search Pipeline
```
User Query → MCP Tools Invoked → HTTP API → SessionSearch Service → FTS5 Database → Search Results → Claude
```
1. **User Query**: User asks naturally: "What bugs did we fix?"
2. **MCP Tools Invoked**: Claude recognizes intent and invokes MCP search tools
3. **HTTP API**: MCP tools call HTTP endpoint (e.g., `/api/search/observations`)
4. **SessionSearch**: Worker service queries FTS5 virtual tables
5. **Format**: Results formatted and returned via MCP
6. **Return**: Claude presents formatted results to user
Uses 3-layer progressive disclosure: search → timeline → get\_observations
## Session Lifecycle
```
┌─────────────────────────────────────────────────────────────────┐
│ 0. Install puts the claude-mem launcher on PATH │
│ npx claude-mem install/update, or the startup SessionStart │
│ self-heal (ensure-launcher.cjs) for marketplace-only installs│
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ 1. Session Starts → Worker-start, then Context Hook │
│ Starts Bun worker if needed, injects context from previous │
│ sessions (configurable observation count) │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ 2. User Types Prompt → UserPromptSubmit Hook Fires │
│ Creates session in database, saves raw user prompt for FTS5 │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ 3. Claude Uses Tools → PostToolUse Hook Fires (100+ times) │
│ Captures tool executions, sends to worker for AI compression │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ 4. Worker Processes → Claude Agent SDK Analyzes │
│ Extracts structured learnings via iterative AI processing │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ 5. Claude Stops → Summary Hook Fires │
│ Generates final summary with request, completions, learnings │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ 6. Session Ends → Cleanup Hook Fires │
│ Marks session complete (graceful, not DELETE), ready for │
│ next session context. Skips on /clear to preserve ongoing │
└─────────────────────────────────────────────────────────────────┘
```
## Directory Structure
```
claude-mem/
├── src/
│ ├── hooks/ # TypeScript hook implementations (built via esbuild)
│ ├── sdk/ # Claude Agent SDK integration
│ ├── services/
│ │ ├── worker-service.ts # Express HTTP + SSE service (worker entry point)
│ │ ├── sync/ChromaSync.ts # Optional Chroma vector index
│ │ └── sqlite/ # SQLite + FTS5 storage layer
│ ├── ui/viewer/ # React + TypeScript web viewer
│ ├── shared/ # Shared utilities (paths, settings defaults)
│ └── utils/ # Logging, platform, tag-stripping helpers
│
├── scripts/ # Build + utility scripts
│
├── plugin/ # Plugin distribution (synced to marketplace)
│ ├── .claude-plugin/plugin.json
│ ├── hooks/hooks.json # Hook registration (exec-form `claude-mem` hooks + Setup)
│ ├── scripts/ # Built executables
│ │ ├── claude-mem-launcher.cjs # Source of the on-PATH `claude-mem` hook launcher (compiled at install)
│ │ ├── ensure-launcher.cjs # Startup SessionStart self-heal: places/refreshes the launcher
│ │ ├── version-check.js # Setup hook (--init / --maintenance): dependency check + install marker
│ │ ├── bun-runner.js # Resolves Bun and runs worker-service.cjs (Codex hooks)
│ │ ├── worker-service.cjs # Worker daemon + lifecycle hook dispatcher
│ │ ├── worker-cli.js # CLI shim
│ │ ├── worker-wrapper.cjs # Process wrapper
│ │ ├── mcp-server.cjs # MCP search server
│ │ ├── statusline-counts.js
│ │ └── context-generator.cjs
│ ├── skills/ # Agent skills (mem-search, make-plan, do, etc.)
│ └── ui/viewer.html # Self-contained React bundle
│
├── tests/ # Test suite (`bun test`)
├── docs/ # Mintlify documentation
└── openclaw/ # OpenClaw integration plugin
```
## Component Details
### 1. Plugin Hooks
The plugin registers lifecycle hooks plus a Setup-phase `version-check.js`. Each lifecycle hook is one exec-form command, `claude-mem hook claude-code `, with no shell. `claude-mem` is a small compiled launcher on PATH: it finds the plugin root and runs `worker-service.cjs hook claude-code ` under Bun; the worker process is the single dispatcher for all hook logic. Events:
* **Setup** → `version-check.js` (installs missing plugin dependencies, then records the install marker). Still a bash entry, and Claude Code fires it only for `claude --init` / `--maintenance`.
* **SessionStart** → `claude-mem start` (worker, async), then `hook claude-code context` (context injection); at startup also `node ${CLAUDE_PLUGIN_ROOT}/scripts/ensure-launcher.cjs` (launcher self-heal)
* **UserPromptSubmit** → `hook claude-code session-init`
* **PreToolUse** (matcher `Read`) → `hook claude-code file-context`
* **PostToolUse** (matcher `*`) → `hook claude-code observation`
* **Stop** → `hook claude-code summarize` (summary generation)
* **SessionEnd** → `hook claude-code session-end`
The runtime install (Bun, uv, `bun install`, and compiling the launcher into `~/.local/bin` or `%LOCALAPPDATA%\claude-mem\bin`) is performed by `npx claude-mem install` / `npx claude-mem update` with a visible installer spinner. For plugin-marketplace installs, the startup self-heal places the launcher, and the Setup hook (on `claude --init` / `--maintenance`) runs `bun install` when the plugin's dependencies are missing.
See [Plugin Hooks](/architecture/hooks) for detailed hook documentation.
### 2. Worker Service
Express.js HTTP server on a per-user port (default `37700 + (uid % 100)`, override via `CLAUDE_MEM_WORKER_PORT`) with:
* Search HTTP API endpoints
* Viewer UI HTTP/SSE endpoints
* Async observation processing via the Claude Agent SDK (or Gemini / OpenRouter)
* Real-time updates via Server-Sent Events
* Auto-managed by Bun
See [Worker Service](/architecture/worker-service) for HTTP API and endpoints.
### 3. Database Layer
SQLite3 with bun:sqlite driver featuring:
* FTS5 virtual tables for full-text search
* SessionStore for CRUD operations
* SessionSearch for FTS5 queries
* Location: `~/.claude-mem/claude-mem.db`
See [Database Architecture](/architecture/database) for schema and FTS5 search.
### 4. mem-search Skill (v5.4.0+)
Skill-based search with progressive disclosure providing 10 search operations:
* Search observations, sessions, prompts (full-text FTS5)
* Filter by type, concept, file
* Get recent context, timeline, timeline by query
* API help documentation
**Token Savings**: \~2,250 tokens per session vs MCP approach
* Skill frontmatter: \~250 tokens (loaded at session start)
* Full instructions: \~2,500 tokens (loaded on-demand when invoked)
* HTTP API endpoints instead of MCP tools
**Skill Enhancement (v5.5.0)**: Renamed from "search" to "mem-search" for better scope differentiation. Effectiveness increased from 67% to 100% with enhanced triggers and comprehensive documentation.
See [Search Architecture](/architecture/search-architecture) for technical details and examples.
### 5. Viewer UI
React + TypeScript web interface served by the worker on its configured `CLAUDE_MEM_WORKER_PORT` featuring:
* Real-time memory stream via Server-Sent Events
* Infinite scroll pagination with automatic deduplication
* Project filtering and settings persistence
* GPU-accelerated animations
* Self-contained HTML bundle (viewer.html)
Built with esbuild into a single file deployment.
# PM2 to Bun Migration
Source: https://docs.claude-mem.ai/architecture/pm2-to-bun-migration
Complete technical documentation for the process management and database driver migration in v7.1.0
**Historical Migration Documentation**
This document describes the PM2 to Bun migration that occurred in v7.1.0 (December 2025). If you're installing claude-mem for the first time, this migration has already been completed and you can use the current Bun-based system documented in the main guides.
This documentation is preserved for users upgrading from versions older than v7.1.0.
# PM2 to Bun Migration: Complete Technical Documentation
**Version**: 7.1.0
**Date**: December 2025
**Migration Type**: Process Management (PM2 → Bun) + Database Driver (better-sqlite3 → bun:sqlite)
## Executive Summary
Claude-mem version 7.1.0 introduces two major architectural migrations:
1. **Process Management**: PM2 → Custom Bun-based ProcessManager
2. **Database Driver**: better-sqlite3 npm package → bun:sqlite runtime module
Both migrations are **automatic** and **transparent** to end users. The first time a hook fires after updating to 7.1.0+, the system performs a one-time cleanup of legacy PM2 processes and transitions to the new architecture.
### Key Benefits
* **Simplified Dependencies**: Removes PM2 and better-sqlite3 npm packages
* **Improved Cross-Platform Support**: Better Windows compatibility
* **Faster Installation**: No native module compilation required
* **Built-in Runtime**: Leverages Bun's built-in process management and SQLite
* **Reduced Complexity**: Custom ProcessManager is simpler than PM2 integration
### Migration Impact
* **Data Preservation**: User data, settings, and database remain unchanged
* **Automatic Cleanup**: Old PM2 processes automatically terminated (all platforms)
* **No User Action Required**: Migration happens automatically on first hook trigger
* **Backward Compatible**: SQLite database format unchanged (only driver changed)
## Architecture Comparison
### Old System (PM2-based)
**Component**: PM2 (Process Manager 2)
* **Package**: `pm2` npm dependency
* **Process Name**: `claude-mem-worker`
* **Management**: External PM2 daemon manages lifecycle
* **Discovery**: `pm2 list`, `pm2 describe` commands
* **Auto-restart**: PM2 automatically restarts on crash
* **Logs**: `~/.pm2/logs/claude-mem-worker-*.log`
* **PID File**: `~/.pm2/pids/claude-mem-worker.pid`
**Lifecycle Commands**:
```bash theme={null}
pm2 start