951 lines
		
	
	
		
			26 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
			
		
		
	
	
			951 lines
		
	
	
		
			26 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
# 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-context` flag 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:**
 | 
						|
```sql
 | 
						|
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:**
 | 
						|
```javascript
 | 
						|
// 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:**
 | 
						|
```sql
 | 
						|
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:**
 | 
						|
```javascript
 | 
						|
// 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:**
 | 
						|
```sql
 | 
						|
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:**
 | 
						|
```javascript
 | 
						|
// 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
 | 
						|
 | 
						|
```bash
 | 
						|
# 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
 | 
						|
 | 
						|
```bash
 | 
						|
# 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)
 | 
						|
 | 
						|
```sql
 | 
						|
-- 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
 | 
						|
 | 
						|
```javascript
 | 
						|
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
 | 
						|
 | 
						|
```javascript
 | 
						|
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
 | 
						|
 | 
						|
```javascript
 | 
						|
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
 | 
						|
 | 
						|
```javascript
 | 
						|
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
 | 
						|
 | 
						|
```bash
 | 
						|
# 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
 | 
						|
 | 
						|
```bash
 | 
						|
# 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:
 | 
						|
 | 
						|
```javascript
 | 
						|
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)
 | 
						|
 | 
						|
```javascript
 | 
						|
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)
 | 
						|
 | 
						|
```javascript
 | 
						|
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
 | 
						|
 | 
						|
```javascript
 | 
						|
// 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
 | 
						|
 | 
						|
```javascript
 | 
						|
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
 | 
						|
 | 
						|
1. ✅ Create project directory and documentation
 | 
						|
2. **Implement MVP (Phase 1)**: Basic CLI, LIKE search, core commands
 | 
						|
3. **Test with real usage**: Validate concept, collect metrics
 | 
						|
4. **Migrate to FTS5 (Phase 2)**: When dataset > 500 or latency issues
 | 
						|
5. **Add fuzzy layer (Phase 3)**: For production-quality search
 | 
						|
6. **OpenCode integration**: Plugin API and auto-extraction
 | 
						|
7. **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
 |