26 KiB
		
	
	
	
	
	
	
	
			
		
		
	
	
			26 KiB
		
	
	
	
	
	
	
	
LLMemory - AI Agent Memory System
Overview
LLMemory is a persistent memory/journal system for AI agents, providing grep-like search with fuzzy matching for efficient knowledge retrieval across sessions.
Core Requirements
Storage
- Store memories with metadata: 
created_at,entered_by,expires_at,tags - Local SQLite database (no cloud dependencies)
 - Content limit: 10KB per memory
 - Tag-based organization with normalized schema
 
Retrieval
- Grep/ripgrep-like query syntax (familiar to AI agents)
 - Fuzzy matching with configurable threshold
 - Relevance ranking (BM25 + edit distance + recency)
 - Metadata filtering (tags, dates, agent)
 - Token-efficient: limit results, prioritize quality over quantity
 
Interface
- Global CLI tool: 
memory [command] - Commands: 
store,search,list,prune,stats,export,import --agent-contextflag for comprehensive agent documentation- Output formats: plain text, JSON, markdown
 
Integration
- OpenCode plugin architecture
 - Expose API for programmatic access
 - Auto-extraction of 
*Remember*patterns from agent output 
Implementation Strategy
Phase 1: MVP (Simple LIKE Search)
Goal: Ship in 2-3 days, validate concept with real usage
Features:
- Basic schema (memories, tags tables)
 - Core commands (store, search, list, prune)
 - Simple LIKE-based search with wildcards
 - Plain text output
 - Tag filtering
 - Expiration handling
 
Success Criteria:
- Can store and retrieve memories
 - Search works for exact/prefix matches
 - Tags functional
 - Performance acceptable for <500 memories
 
Database:
CREATE TABLE memories (
  id INTEGER PRIMARY KEY,
  content TEXT NOT NULL CHECK(length(content) <= 10000),
  created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
  entered_by TEXT,
  expires_at INTEGER
);
CREATE TABLE tags (
  id INTEGER PRIMARY KEY,
  name TEXT UNIQUE COLLATE NOCASE
);
CREATE TABLE memory_tags (
  memory_id INTEGER,
  tag_id INTEGER,
  PRIMARY KEY (memory_id, tag_id),
  FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE CASCADE,
  FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
);
Search Logic:
// Simple case-insensitive LIKE with wildcards
WHERE LOWER(content) LIKE LOWER('%' || ? || '%')
AND (expires_at IS NULL OR expires_at > strftime('%s', 'now'))
ORDER BY created_at DESC
Phase 2: FTS5 Migration
Trigger: Dataset > 500 memories OR query latency > 500ms
Features:
- Add FTS5 virtual table
 - Migrate existing data
 - Implement BM25 ranking
 - Support boolean operators (AND/OR/NOT)
 - Phrase queries with quotes
 - Prefix matching with 
* 
Database Addition:
CREATE VIRTUAL TABLE memories_fts USING fts5(
  content,
  content='memories',
  content_rowid='id',
  tokenize='porter unicode61 remove_diacritics 2'
);
-- Triggers to keep in sync
CREATE TRIGGER memories_ai AFTER INSERT ON memories BEGIN
  INSERT INTO memories_fts(rowid, content) VALUES (new.id, new.content);
END;
-- ... (update/delete triggers)
Search Logic:
// FTS5 match with BM25 ranking
SELECT m.*, mf.rank
FROM memories_fts mf
JOIN memories m ON m.id = mf.rowid
WHERE memories_fts MATCH ?
ORDER BY mf.rank
Phase 3: Fuzzy Layer
Goal: Handle typos and inexact matches
Features:
- Trigram indexing
 - Levenshtein distance calculation
 - Intelligent cascade: exact (FTS5) → fuzzy (trigram)
 - Combined relevance scoring
 - Configurable threshold (default: 0.7)
 
Database Addition:
CREATE TABLE trigrams (
  trigram TEXT NOT NULL,
  memory_id INTEGER NOT NULL,
  position INTEGER NOT NULL,
  FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE CASCADE
);
CREATE INDEX idx_trigrams_trigram ON trigrams(trigram);
Search Logic:
// 1. Try FTS5 exact match
let results = ftsSearch(query);
// 2. If <5 results, try fuzzy
if (results.length < 5) {
  const fuzzyResults = trigramSearch(query, threshold);
  results = mergeAndDedupe(results, fuzzyResults);
}
// 3. Re-rank by combined score
results.forEach(r => {
  r.score = 0.4 * bmr25Score 
          + 0.3 * trigramSimilarity
          + 0.2 * editDistanceScore
          + 0.1 * recencyScore;
});
Architecture
Technology Stack
- Language: Node.js (JavaScript/TypeScript)
 - Database: SQLite with better-sqlite3
 - CLI Framework: Commander.js
 - Output Formatting: chalk (colors), marked-terminal (markdown)
 - Date Parsing: date-fns
 - Testing: Vitest
 
Directory Structure
llmemory/
├── src/
│   ├── cli.js              # CLI entry point
│   ├── commands/
│   │   ├── store.js
│   │   ├── search.js
│   │   ├── list.js
│   │   ├── prune.js
│   │   ├── stats.js
│   │   └── export.js
│   ├── db/
│   │   ├── connection.js   # Database setup
│   │   ├── schema.js       # Schema definitions
│   │   ├── migrations.js   # Migration runner
│   │   └── queries.js      # Prepared statements
│   ├── search/
│   │   ├── like.js         # Phase 1: LIKE search
│   │   ├── fts.js          # Phase 2: FTS5 search
│   │   ├── fuzzy.js        # Phase 3: Fuzzy matching
│   │   └── ranking.js      # Relevance scoring
│   ├── utils/
│   │   ├── dates.js
│   │   ├── tags.js
│   │   ├── formatting.js
│   │   └── validation.js
│   └── extractors/
│       └── remember.js     # Auto-extract *Remember* patterns
├── test/
│   ├── search.test.js
│   ├── fuzzy.test.js
│   ├── integration.test.js
│   └── fixtures/
├── docs/
│   ├── ARCHITECTURE.md
│   ├── AGENT_GUIDE.md      # For --agent-context
│   ├── CLI_REFERENCE.md
│   └── API.md
├── bin/
│   └── memory              # Executable
├── package.json
├── SPECIFICATION.md        # This file
├── IMPLEMENTATION_PLAN.md
└── README.md
CLI Interface
Commands
# Store a memory
memory store <content> [options]
  --tags <tag1,tag2>      Comma-separated tags
  --expires <date>        Expiration date (ISO 8601 or natural language)
  --entered-by <agent>    Agent/user identifier
  --file <path>           Read content from file
# Search memories
memory search <query> [options]
  --tags <tag1,tag2>      Filter by tags (AND)
  --any-tag <tag1,tag2>   Filter by tags (OR)
  --after <date>          Created after date
  --before <date>         Created before date
  --entered-by <agent>    Filter by creator
  --limit <n>             Max results (default: 10)
  --offset <n>            Pagination offset
  --fuzzy                 Enable fuzzy matching (default: auto)
  --no-fuzzy              Disable fuzzy matching
  --threshold <0-1>       Fuzzy match threshold (default: 0.7)
  --json                  JSON output
  --markdown              Markdown output
# List recent memories
memory list [options]
  --limit <n>             Max results (default: 20)
  --offset <n>            Pagination offset
  --tags <tags>           Filter by tags
  --sort <field>          Sort by: created, expires, content
  --order <asc|desc>      Sort order (default: desc)
# Prune expired memories
memory prune [options]
  --dry-run               Show what would be deleted
  --force                 Skip confirmation
  --before <date>         Delete before date (even if not expired)
# Show statistics
memory stats [options]
  --tags                  Show tag frequency
  --agents                Show memories per agent
# Export/import
memory export <file>      Export to JSON
memory import <file>      Import from JSON
# Global options
--agent-context           Display agent documentation
--db <path>               Custom database location
--verbose                 Detailed logging
--quiet                   Suppress non-error output
Query Syntax
# Basic
memory search "docker compose"           # Both terms (implicit AND)
memory search "docker AND compose"       # Explicit AND
memory search "docker OR podman"         # Either term
memory search "docker NOT swarm"         # Exclude term
memory search '"exact phrase"'           # Phrase search
memory search "docker*"                  # Prefix matching
# With filters
memory search "docker" --tags devops,networking
memory search "error" --after "2025-10-01"
memory search "config" --entered-by investigate-agent
# Fuzzy (automatic typo tolerance)
memory search "dokcer"                   # Finds "docker"
memory search "kuberntes"                # Finds "kubernetes"
Data Schema
Complete Schema (All Phases)
-- Core tables
CREATE TABLE memories (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  content TEXT NOT NULL CHECK(length(content) <= 10000),
  created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
  entered_by TEXT,
  expires_at INTEGER,
  CHECK(expires_at IS NULL OR expires_at > created_at)
);
CREATE INDEX idx_memories_created ON memories(created_at DESC);
CREATE INDEX idx_memories_expires ON memories(expires_at) WHERE expires_at IS NOT NULL;
CREATE INDEX idx_memories_entered_by ON memories(entered_by);
CREATE TABLE tags (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  name TEXT NOT NULL UNIQUE COLLATE NOCASE,
  created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
);
CREATE INDEX idx_tags_name ON tags(name);
CREATE TABLE memory_tags (
  memory_id INTEGER NOT NULL,
  tag_id INTEGER NOT NULL,
  PRIMARY KEY (memory_id, tag_id),
  FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE CASCADE,
  FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
);
CREATE INDEX idx_memory_tags_tag ON memory_tags(tag_id);
-- Phase 2: FTS5
CREATE VIRTUAL TABLE memories_fts USING fts5(
  content,
  content='memories',
  content_rowid='id',
  tokenize='porter unicode61 remove_diacritics 2'
);
CREATE TRIGGER memories_ai AFTER INSERT ON memories BEGIN
  INSERT INTO memories_fts(rowid, content) VALUES (new.id, new.content);
END;
CREATE TRIGGER memories_ad AFTER DELETE ON memories BEGIN
  DELETE FROM memories_fts WHERE rowid = old.id;
END;
CREATE TRIGGER memories_au AFTER UPDATE ON memories BEGIN
  DELETE FROM memories_fts WHERE rowid = old.id;
  INSERT INTO memories_fts(rowid, content) VALUES (new.id, new.content);
END;
-- Phase 3: Trigrams
CREATE TABLE trigrams (
  trigram TEXT NOT NULL,
  memory_id INTEGER NOT NULL,
  position INTEGER NOT NULL,
  FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE CASCADE
);
CREATE INDEX idx_trigrams_trigram ON trigrams(trigram);
CREATE INDEX idx_trigrams_memory ON trigrams(memory_id);
-- Metadata
CREATE TABLE metadata (
  key TEXT PRIMARY KEY,
  value TEXT NOT NULL
);
INSERT INTO metadata (key, value) VALUES ('schema_version', '1');
INSERT INTO metadata (key, value) VALUES ('created_at', strftime('%s', 'now'));
-- Useful view
CREATE VIEW memories_with_tags AS
SELECT 
  m.id,
  m.content,
  m.created_at,
  m.entered_by,
  m.expires_at,
  GROUP_CONCAT(t.name, ',') as tags
FROM memories m
LEFT JOIN memory_tags mt ON m.id = mt.memory_id
LEFT JOIN tags t ON mt.tag_id = t.id
GROUP BY m.id;
Search Algorithm Details
Phase 1: LIKE Search
function searchWithLike(query, filters = {}) {
  const { tags = [], after, before, enteredBy, limit = 10 } = filters;
  
  let sql = `
    SELECT DISTINCT m.id, m.content, m.created_at, m.entered_by, m.expires_at,
           GROUP_CONCAT(t.name, ',') as tags
    FROM memories m
    LEFT JOIN memory_tags mt ON m.id = mt.memory_id
    LEFT JOIN tags t ON mt.tag_id = t.id
    WHERE LOWER(m.content) LIKE LOWER(?)
      AND (m.expires_at IS NULL OR m.expires_at > strftime('%s', 'now'))
  `;
  
  const params = [`%${query}%`];
  
  // Tag filtering
  if (tags.length > 0) {
    sql += ` AND m.id IN (
      SELECT memory_id FROM memory_tags
      WHERE tag_id IN (SELECT id FROM tags WHERE name IN (${tags.map(() => '?').join(',')}))
      GROUP BY memory_id
      HAVING COUNT(*) = ?
    )`;
    params.push(...tags, tags.length);
  }
  
  // Date filtering
  if (after) {
    sql += ' AND m.created_at >= ?';
    params.push(after);
  }
  if (before) {
    sql += ' AND m.created_at <= ?';
    params.push(before);
  }
  
  // Agent filtering
  if (enteredBy) {
    sql += ' AND m.entered_by = ?';
    params.push(enteredBy);
  }
  
  sql += ' GROUP BY m.id ORDER BY m.created_at DESC LIMIT ?';
  params.push(limit);
  
  return db.prepare(sql).all(...params);
}
Phase 2: FTS5 Search
function searchWithFTS5(query, filters = {}) {
  const ftsQuery = buildFTS5Query(query);
  
  let sql = `
    SELECT m.id, m.content, m.created_at, m.entered_by, m.expires_at,
           GROUP_CONCAT(t.name, ',') as tags,
           mf.rank as relevance
    FROM memories_fts mf
    JOIN memories m ON m.id = mf.rowid
    LEFT JOIN memory_tags mt ON m.id = mt.memory_id
    LEFT JOIN tags t ON mt.tag_id = t.id
    WHERE memories_fts MATCH ?
      AND (m.expires_at IS NULL OR m.expires_at > strftime('%s', 'now'))
  `;
  
  const params = [ftsQuery];
  
  // Apply filters (same as Phase 1)
  // ...
  
  sql += ' GROUP BY m.id ORDER BY mf.rank LIMIT ?';
  params.push(limit);
  
  return db.prepare(sql).all(...params);
}
function buildFTS5Query(query) {
  // Handle quoted phrases
  if (query.includes('"')) {
    return query; // Already FTS5 compatible
  }
  
  // Handle explicit operators
  if (/\b(AND|OR|NOT)\b/i.test(query)) {
    return query.toUpperCase();
  }
  
  // Implicit AND between terms
  const terms = query.split(/\s+/).filter(t => t.length > 0);
  return terms.join(' AND ');
}
Phase 3: Fuzzy Search
function searchWithFuzzy(query, threshold = 0.7, limit = 10) {
  const queryTrigrams = extractTrigrams(query);
  
  if (queryTrigrams.length === 0) return [];
  
  // Find candidates by trigram overlap
  const sql = `
    SELECT 
      m.id,
      m.content,
      m.created_at,
      m.entered_by,
      m.expires_at,
      COUNT(DISTINCT tr.trigram) as trigram_matches
    FROM memories m
    JOIN trigrams tr ON tr.memory_id = m.id
    WHERE tr.trigram IN (${queryTrigrams.map(() => '?').join(',')})
      AND (m.expires_at IS NULL OR m.expires_at > strftime('%s', 'now'))
    GROUP BY m.id
    HAVING trigram_matches >= ?
    ORDER BY trigram_matches DESC
    LIMIT ?
  `;
  
  const minMatches = Math.ceil(queryTrigrams.length * threshold);
  const candidates = db.prepare(sql).all(...queryTrigrams, minMatches, limit * 2);
  
  // Calculate edit distance and combined score
  const scored = candidates.map(c => {
    const editDist = levenshtein(query.toLowerCase(), c.content.toLowerCase().substring(0, query.length * 3));
    const trigramSim = c.trigram_matches / queryTrigrams.length;
    const normalizedEditDist = 1 - (editDist / Math.max(query.length, c.content.length));
    
    return {
      ...c,
      relevance: 0.6 * trigramSim + 0.4 * normalizedEditDist
    };
  });
  
  return scored
    .filter(r => r.relevance >= threshold)
    .sort((a, b) => b.relevance - a.relevance)
    .slice(0, limit);
}
function extractTrigrams(text) {
  const normalized = text
    .toLowerCase()
    .replace(/[^\w\s]/g, ' ')
    .replace(/\s+/g, ' ')
    .trim();
  
  if (normalized.length < 3) return [];
  
  const padded = `  ${normalized}  `;
  const trigrams = [];
  
  for (let i = 0; i < padded.length - 2; i++) {
    const trigram = padded.substring(i, i + 3);
    if (trigram.trim().length === 3) {
      trigrams.push(trigram);
    }
  }
  
  return [...new Set(trigrams)]; // Deduplicate
}
function levenshtein(a, b) {
  if (a.length === 0) return b.length;
  if (b.length === 0) return a.length;
  
  let prevRow = Array(b.length + 1).fill(0).map((_, i) => i);
  
  for (let i = 0; i < a.length; i++) {
    let curRow = [i + 1];
    for (let j = 0; j < b.length; j++) {
      const cost = a[i] === b[j] ? 0 : 1;
      curRow.push(Math.min(
        curRow[j] + 1,        // deletion
        prevRow[j + 1] + 1,   // insertion
        prevRow[j] + cost     // substitution
      ));
    }
    prevRow = curRow;
  }
  
  return prevRow[b.length];
}
Intelligent Cascade
function search(query, filters = {}) {
  const { fuzzy = 'auto', threshold = 0.7 } = filters;
  
  // Phase 2 or Phase 3 installed?
  const hasFTS5 = checkTableExists('memories_fts');
  const hasTrigrams = checkTableExists('trigrams');
  
  let results;
  
  // Try FTS5 if available
  if (hasFTS5) {
    results = searchWithFTS5(query, filters);
  } else {
    results = searchWithLike(query, filters);
  }
  
  // If too few results and fuzzy available, try fuzzy
  if (results.length < 5 && hasTrigrams && (fuzzy === 'auto' || fuzzy === true)) {
    const fuzzyResults = searchWithFuzzy(query, threshold, filters.limit);
    results = mergeResults(results, fuzzyResults);
  }
  
  return results;
}
function mergeResults(exact, fuzzy) {
  const seen = new Set(exact.map(r => r.id));
  const merged = [...exact];
  
  for (const result of fuzzy) {
    if (!seen.has(result.id)) {
      merged.push(result);
      seen.add(result.id);
    }
  }
  
  return merged;
}
Memory Format Guidelines
Good Memory Examples
# Technical discovery with context
memory store "Docker Compose: Use 'depends_on' with 'condition: service_healthy' to ensure dependencies are ready. Prevents race conditions in multi-container apps." \
  --tags docker,docker-compose,best-practices
# Configuration pattern
memory store "Nginx reverse proxy: Set 'proxy_set_header X-Real-IP \$remote_addr' to preserve client IP through proxy. Required for rate limiting and logging." \
  --tags nginx,networking,security
# Error resolution
memory store "Node.js ENOSPC: Increase inotify watch limit with 'echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p'. Affects webpack, nodemon." \
  --tags nodejs,linux,troubleshooting
# Version-specific behavior
memory store "TypeScript 5.0+: 'const' type parameters preserve literal types. Example: 'function id<const T>(x: T): T'. Better inference for generic functions." \
  --tags typescript,types
# Temporary info with expiration
memory store "Staging server: https://staging.example.com:8443. Credentials in 1Password. Valid through Q1 2025." \
  --tags staging,infrastructure \
  --expires "2025-04-01"
Anti-Patterns to Avoid
# Too vague
❌ memory store "Fixed Docker issue"
✅ memory store "Docker: Use 'docker system prune -a' to reclaim space. Removes unused images, containers, networks."
# Widely known
❌ memory store "Git is a version control system"
✅ memory store "Git worktree: 'git worktree add -b feature ../feature' creates parallel working dir without cloning."
# Sensitive data
❌ memory store "DB password: hunter2"
✅ memory store "Production DB credentials stored in 1Password vault 'Infrastructure'"
# Multiple unrelated facts
❌ memory store "Docker uses namespaces. K8s has pods. Nginx is fast."
✅ memory store "Docker container isolation uses Linux namespaces: PID, NET, MNT, UTS, IPC."
Auto-Extraction: Remember Pattern
When agents output text containing *Remember*: [fact], automatically extract and store:
function extractRememberPatterns(text, context = {}) {
  const rememberRegex = /\*Remember\*:?\s+(.+?)(?=\n\n|\*Remember\*|$)/gis;
  const matches = [...text.matchAll(rememberRegex)];
  
  return matches.map(match => {
    const content = match[1].trim();
    const tags = autoExtractTags(content, context);
    const expires = autoExtractExpiration(content);
    
    return {
      content,
      tags,
      expires,
      entered_by: context.agentName || 'auto-extract'
    };
  });
}
function autoExtractTags(content, context) {
  const tags = new Set();
  
  // Technology patterns
  const techPatterns = {
    'docker': /docker|container|compose/i,
    'kubernetes': /k8s|kubernetes|kubectl/i,
    'git': /\bgit\b|github|gitlab/i,
    'nodejs': /node\.?js|npm|yarn/i,
    'postgresql': /postgres|postgresql/i,
    'nixos': /nix|nixos|flake/i
  };
  
  for (const [tag, pattern] of Object.entries(techPatterns)) {
    if (pattern.test(content)) tags.add(tag);
  }
  
  // Category patterns
  if (/error|bug|fix/i.test(content)) tags.add('troubleshooting');
  if (/performance|optimize/i.test(content)) tags.add('performance');
  if (/security|vulnerability/i.test(content)) tags.add('security');
  
  return Array.from(tags);
}
function autoExtractExpiration(content) {
  const patterns = [
    { re: /valid (through|until) (\w+ \d{4})/i, parse: m => new Date(m[2]) },
    { re: /expires? (on )?([\d-]+)/i, parse: m => new Date(m[2]) },
    { re: /temporary|temp/i, parse: () => addDays(new Date(), 90) },
    { re: /Q([1-4]) (\d{4})/i, parse: m => quarterEnd(m[1], m[2]) }
  ];
  
  for (const { re, parse } of patterns) {
    const match = content.match(re);
    if (match) {
      try {
        return parse(match).toISOString();
      } catch {}
    }
  }
  
  return null;
}
Migration Strategy
Phase 1 → Phase 2 (LIKE → FTS5)
async function migrateToFTS5(db) {
  console.log('Migrating to FTS5...');
  
  // Create FTS5 table
  db.exec(`
    CREATE VIRTUAL TABLE memories_fts USING fts5(
      content,
      content='memories',
      content_rowid='id',
      tokenize='porter unicode61 remove_diacritics 2'
    );
  `);
  
  // Populate from existing data
  db.exec(`
    INSERT INTO memories_fts(rowid, content)
    SELECT id, content FROM memories;
  `);
  
  // Create triggers
  db.exec(`
    CREATE TRIGGER memories_ai AFTER INSERT ON memories BEGIN
      INSERT INTO memories_fts(rowid, content) VALUES (new.id, new.content);
    END;
    
    CREATE TRIGGER memories_ad AFTER DELETE ON memories BEGIN
      DELETE FROM memories_fts WHERE rowid = old.id;
    END;
    
    CREATE TRIGGER memories_au AFTER UPDATE ON memories BEGIN
      DELETE FROM memories_fts WHERE rowid = old.id;
      INSERT INTO memories_fts(rowid, content) VALUES (new.id, new.content);
    END;
  `);
  
  // Update schema version
  db.prepare('UPDATE metadata SET value = ? WHERE key = ?').run('2', 'schema_version');
  
  console.log('FTS5 migration complete!');
}
Phase 2 → Phase 3 (Add Trigrams)
async function migrateToTrigrams(db) {
  console.log('Adding trigram support...');
  
  // Create trigrams table
  db.exec(`
    CREATE TABLE trigrams (
      trigram TEXT NOT NULL,
      memory_id INTEGER NOT NULL,
      position INTEGER NOT NULL,
      FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE CASCADE
    );
    
    CREATE INDEX idx_trigrams_trigram ON trigrams(trigram);
    CREATE INDEX idx_trigrams_memory ON trigrams(memory_id);
  `);
  
  // Populate from existing memories
  const memories = db.prepare('SELECT id, content FROM memories').all();
  const insertTrigram = db.prepare('INSERT INTO trigrams (trigram, memory_id, position) VALUES (?, ?, ?)');
  
  const insertMany = db.transaction((memories) => {
    for (const memory of memories) {
      const trigrams = extractTrigrams(memory.content);
      trigrams.forEach((trigram, position) => {
        insertTrigram.run(trigram, memory.id, position);
      });
    }
  });
  
  insertMany(memories);
  
  // Update schema version
  db.prepare('UPDATE metadata SET value = ? WHERE key = ?').run('3', 'schema_version');
  
  console.log('Trigram migration complete!');
}
Performance Targets
Latency
- Phase 1 (LIKE): <50ms for <500 memories
 - Phase 2 (FTS5): <100ms for 10K memories
 - Phase 3 (Fuzzy): <200ms for 10K memories with fuzzy
 
Storage
- Base: ~500 bytes per memory (average)
 - FTS5 index: +30% overhead (~150 bytes)
 - Trigrams: +200% overhead (~1KB) - prune common trigrams
 
Scalability
- Phase 1: Up to 500 memories
 - Phase 2: Up to 50K memories
 - Phase 3: Up to 100K+ memories
 
Testing Strategy
Unit Tests
- Search algorithms (LIKE, FTS5, fuzzy)
 - Trigram extraction
 - Levenshtein distance
 - Tag filtering
 - Date parsing
 - Relevance scoring
 
Integration Tests
- Store → retrieve flow
 - Search with various filters
 - Expiration pruning
 - Export/import
 - Migration Phase 1→2→3
 
Performance Tests
- Benchmark with 1K, 10K, 100K memories
 - Query latency measurement
 - Index size monitoring
 - Memory usage profiling
 
OpenCode Integration
Plugin Structure
// plugin.js - OpenCode plugin entry point
export default {
  name: 'llmemory',
  version: '1.0.0',
  description: 'Persistent memory system for AI agents',
  
  commands: {
    'memory': './src/cli.js'
  },
  
  api: {
    store: async (content, options) => {
      const { storeMemory } = await import('./src/db/queries.js');
      return storeMemory(content, options);
    },
    
    search: async (query, options) => {
      const { search } = await import('./src/search/index.js');
      return search(query, options);
    },
    
    extractRemember: async (text, context) => {
      const { extractRememberPatterns } = await import('./src/extractors/remember.js');
      return extractRememberPatterns(text, context);
    }
  },
  
  onInstall: async () => {
    const { initDatabase } = await import('./src/db/connection.js');
    await initDatabase();
    console.log('LLMemory installed! Try: memory --agent-context');
  }
};
Usage from Other Plugins
import llmemory from '@opencode/llmemory';
// Store a memory
await llmemory.api.store(
  'NixOS: flake.lock must be committed for reproducible builds',
  { tags: ['nixos', 'build-system'], entered_by: 'investigate-agent' }
);
// Search
const results = await llmemory.api.search('nixos builds', {
  tags: ['nixos'],
  limit: 5
});
// Auto-extract from agent output
const memories = await llmemory.api.extractRemember(agentOutput, {
  agentName: 'optimize-agent',
  currentTask: 'performance-tuning'
});
Next Steps
- ✅ Create project directory and documentation
 - Implement MVP (Phase 1): Basic CLI, LIKE search, core commands
 - Test with real usage: Validate concept, collect metrics
 - Migrate to FTS5 (Phase 2): When dataset > 500 or latency issues
 - Add fuzzy layer (Phase 3): For production-quality search
 - OpenCode integration: Plugin API and auto-extraction
 - Documentation: Complete agent guide, CLI reference, API docs
 
Success Metrics
- Usability: Agents can store/retrieve memories intuitively
 - Quality: Search returns relevant results, not noise
 - Performance: Queries complete in <100ms for typical datasets
 - Adoption: Agents use memory system regularly in workflows
 - Token Efficiency: Results are high-quality, limited in quantity