> **Quick Summary**: Build `opencode-memory`, a standalone Opencode plugin that replaces mem0+qdrant with a unified SQLite-based hybrid memory system. Indexes markdown files from the user's Obsidian vault (`~/CODEX/80-memory/`) and Opencode session transcripts into a SQLite database with FTS5 (BM25 keyword search) and vec0 (vector cosine similarity). Provides auto-recall on session start, auto-capture on session idle, and three agent tools (memory_search, memory_store, memory_get). Architecture inspired by Openclaw's battle-tested 1590-line MemoryIndexManager.
"I want to implement a memory system for my Opencode Agent. A project named Openclaw has a very nice memory system and I would like to make something similar." User has mem0+qdrant running with Obsidian vault integration. Wants persistent, reliable memory with hybrid search. Open to replacing the existing architecture if something better exists.
### Interview Summary
**Key Discussions**:
- **Architecture**: User chose full SQLite replacement (drop mem0) — the most reliable approach. Single source of truth (markdown), derived index (SQLite).
- **Embedding Provider**: OpenAI text-embedding-3 (user's explicit choice over Gemini and local).
- **Plugin Location**: Separate git repo (not in AGENTS repo). Own npm package/Nix input.
- **Test Strategy**: TDD with bun test. New repo needs full test infrastructure setup.
- **Session Indexing**: Yes, full transcripts. Read from `~/.local/share/opencode/storage/`.
- **Deployment**: Global via Nix home-manager. Plugin registered in `opencode.json`.
- **Deduplication/expiration**: Deferred to Phase 2. Scope locked.
- **Multi-project scope**: Global search by default. Configurable later. Phase 2.
---
## Work Objectives
### Core Objective
Build a standalone Opencode plugin that provides persistent, reliable, hybrid (vector + keyword) memory for all agent sessions, powered by SQLite+FTS5+vec0 over Obsidian markdown files.
### Concrete Deliverables
-`opencode-memory/` — Standalone TypeScript repo with bun
-`src/index.ts` — Opencode plugin entry point (hooks + tools)
- Create `package.json` with `"type": "module"`, scripts for test/build
**Must NOT do**:
- Don't implement any real logic — stubs only
- Don't configure Nix packaging yet (Task 14)
- Don't create README or documentation files
**Recommended Agent Profile**:
- **Category**: `unspecified-high`
- Reason: Repo scaffolding with critical platform verification (vec0). Not purely visual or algorithmic, but requires careful setup.
- **Skills**: none needed
- **Skills Evaluated but Omitted**:
-`frontend-ui-ux`: No UI involved
**Parallelization**:
- **Can Run In Parallel**: NO (foundation task)
- **Parallel Group**: Wave 1 (solo)
- **Blocks**: Tasks 2, 3, 4
- **Blocked By**: None
**References**:
**Pattern References** (existing code to follow):
-`/home/m3tam3re/p/AI/openclaw/src/memory/manager.ts:1-50` — Imports and dependency list (shows what Openclaw uses: better-sqlite3, tiktoken, chokidar, etc.)
-`/home/m3tam3re/p/AI/openclaw/src/memory/types.ts` — TypeScript type definitions for memory system
- SQLite vec0: `https://github.com/asg017/sqlite-vec` — vec0 extension for vector search in SQLite
- better-sqlite3: `https://github.com/WiseLibs/better-sqlite3` — Synchronous SQLite3 for Node.js
- Opencode plugin docs: `https://opencode.ai/docs/plugins/` — Plugin API and lifecycle
**Acceptance Criteria**:
**TDD (setup verification):**
- [ ]`bun test` runs and passes at least 1 example test
- [ ]`better-sqlite3` imports successfully
- [ ] vec0 extension loads or alternative documented
**Agent-Executed QA Scenarios:**
```
Scenario: Repo initializes and tests pass
Tool: Bash
Preconditions: ~/p/AI/opencode-memory/ does not exist
Steps:
1. ls ~/p/AI/opencode-memory/ → should not exist
2. After task: ls ~/p/AI/opencode-memory/src/ → should list all stub files
3. bun test (in opencode-memory/) → 1 test passes, 0 failures
4. bun run -e "import Database from 'better-sqlite3'; const db = new Database(':memory:'); console.log('SQLite OK:', db.pragma('journal_mode', { simple: true }))"
→ prints "SQLite OK: memory" (or "wal")
Expected Result: Repo exists, tests pass, SQLite works
Evidence: Terminal output captured
Scenario: vec0 extension availability check
Tool: Bash
Preconditions: opencode-memory/ initialized with better-sqlite3
Steps:
1. bun run -e "import Database from 'better-sqlite3'; const db = new Database(':memory:'); try { db.loadExtension('vec0'); console.log('vec0: AVAILABLE') } catch(e) { console.log('vec0: NOT AVAILABLE -', e.message) }"
2. If NOT AVAILABLE: try `bun add sqlite-vec` and test with that package's loading mechanism
3. Document result in src/db.ts as comment
Expected Result: vec0 status determined (available or alternative found)
Evidence: Terminal output + documented in code comment
```
**Commit**: YES
- Message: `feat(scaffold): initialize opencode-memory repo with test infrastructure`
CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT);
CREATE TABLE IF NOT EXISTS files (path TEXT PRIMARY KEY, source TEXT NOT NULL, hash TEXT NOT NULL, indexed_at INTEGER NOT NULL);
CREATE TABLE IF NOT EXISTS chunks (id TEXT PRIMARY KEY, file_path TEXT NOT NULL REFERENCES files(path), start_line INTEGER, end_line INTEGER, content_hash TEXT NOT NULL, model TEXT NOT NULL, text TEXT NOT NULL, embedding BLOB);
CREATE TABLE IF NOT EXISTS embedding_cache (content_hash TEXT NOT NULL, model TEXT NOT NULL, embedding BLOB NOT NULL, created_at INTEGER NOT NULL, PRIMARY KEY (content_hash, model));
CREATE VIRTUAL TABLE IF NOT EXISTS chunks_fts USING fts5(text, content='chunks', content_rowid='rowid');
CREATE VIRTUAL TABLE IF NOT EXISTS chunks_vec USING vec0(embedding float[1536]);
- Don't add indexes beyond what schema requires (premature optimization)
- Don't implement any search logic (Task 8, 9)
**Recommended Agent Profile**:
- **Category**: `unspecified-high`
- Reason: SQLite schema with extensions (FTS5, vec0) requires careful handling. Extension loading may need platform-specific workarounds.
- **Skills**: none
- **Skills Evaluated but Omitted**:
- All: Pure database schema work, no domain-specific skill needed
**Parallelization**:
- **Can Run In Parallel**: YES
- **Parallel Group**: Wave 2 (with Tasks 2, 4)
- **Blocks**: Tasks 5, 6, 7, 8, 9
- **Blocked By**: Task 1 (needs vec0 findings)
**References**:
**Pattern References**:
-`/home/m3tam3re/p/AI/openclaw/src/memory/memory-schema.ts` — EXACT schema to follow (adapt table names/columns). This is the primary reference — copy the structure closely.
Scenario: Database creates all tables and extensions
Tool: Bash (bun run)
Preconditions: Task 1 complete
Steps:
1. bun test src/__tests__/db.test.ts
2. Assert: all tests pass
3. bun run -e "import { initDatabase } from './src/db'; const db = initDatabase(':memory:'); console.log(db.pragma('journal_mode', {simple:true})); console.log(JSON.stringify(db.prepare('SELECT name FROM sqlite_master WHERE type=\"table\"').all()))"
4. Assert: journal_mode = "wal"
5. Assert: tables include "meta", "files", "chunks", "embedding_cache"
Expected Result: Schema created correctly with WAL mode
Evidence: Terminal output captured
Scenario: FTS5 virtual table is functional
Tool: Bash (bun run)
Preconditions: Database module implemented
Steps:
1. Create in-memory db, insert test chunk with text "TypeScript is my preferred language"
2. Query: SELECT * FROM chunks_fts WHERE chunks_fts MATCH 'TypeScript'
-`chunkText(text: string, config: ChunkConfig): Chunk[]` — split text by token count using tiktoken (cl100k_base encoding, matching Openclaw). Each chunk gets start_line/end_line and content_hash.
- Don't parse YAML frontmatter (just treat as text for now)
- Don't handle binary files (filter by extension)
**Recommended Agent Profile**:
- **Category**: `unspecified-low`
- Reason: Straightforward file I/O and text processing. No complex algorithms.
- **Skills**: none
**Parallelization**:
- **Can Run In Parallel**: YES
- **Parallel Group**: Wave 2 (with Tasks 2, 3)
- **Blocks**: Tasks 6, 7
- **Blocked By**: Task 1
**References**:
**Pattern References**:
-`/home/m3tam3re/p/AI/openclaw/src/memory/internal.ts` — File discovery functions, chunking logic, hash computation. This is the PRIMARY reference for chunking algorithm.
-`/home/m3tam3re/p/AI/openclaw/src/memory/manager.ts:200-350` — How files are discovered and processed
**External References**:
- tiktoken: `https://github.com/openai/tiktoken` — Token counting for chunking
- Node.js crypto: Built-in `crypto.createHash('sha256')` for hashing
**Acceptance Criteria**:
**TDD:**
- [ ] Test file: `src/__tests__/discovery.test.ts`
- [ ]`bun test src/__tests__/discovery.test.ts` → PASS (all 12 tests)
- [ ] Uses tiktoken cl100k_base encoding for token counting
**Agent-Executed QA Scenarios:**
```
Scenario: Discover files in test fixture directory
Tool: Bash
Preconditions: Create test fixture dir with 3 .md files and 1 .png
- Reason: API integration with retry logic, binary serialization, caching. Moderate complexity.
- **Skills**: none
**Parallelization**:
- **Can Run In Parallel**: YES (parallel with Task 7)
- **Parallel Group**: Wave 3 (first in wave — 6 depends on this)
- **Blocks**: Task 6
- **Blocked By**: Task 3 (needs db for cache table)
**References**:
**Pattern References**:
-`/home/m3tam3re/p/AI/openclaw/src/memory/embeddings.ts` — Multi-provider embedding system. Focus on the OpenAI provider implementation and the cache logic. Copy the binary serialization (Float32Array ↔ Buffer).
-`/home/m3tam3re/p/AI/openclaw/src/memory/manager.ts:400-500` — How embeddings are called and cached during indexing
-`/home/m3tam3re/p/AI/openclaw/src/memory/manager.ts:350-600` — `syncFiles()` method: the EXACT pattern for indexing. Shows hash checking, chunk insertion, FTS5/vec0 population, transaction wrapping. This is the PRIMARY reference.
-`/home/m3tam3re/p/AI/openclaw/src/memory/manager.ts:600-800` — How file removal and re-indexing works
**Acceptance Criteria**:
**TDD:**
- [ ] Test file: `src/__tests__/indexer.test.ts`
- [ ]`bun test src/__tests__/indexer.test.ts` → PASS (all 11 tests)
- [ ] Unchanged files are skipped (hash check)
- [ ] Changed files replace old chunks (not append)
- [ ] Deleted files are removed from index
**Agent-Executed QA Scenarios:**
```
Scenario: Index a directory of markdown files
Tool: Bash
Preconditions: DB module, discovery, embeddings all working. Test fixtures exist.
Steps:
1. Create 3 test .md files in /tmp/test-vault/
2. bun run -e "import { indexDirectory } from './src/indexer'; import { initDatabase } from './src/db'; import { EmbeddingProvider } from './src/embeddings'; const db = initDatabase(':memory:'); const ep = new EmbeddingProvider({db, model:'text-embedding-3-small'}); await indexDirectory('/tmp/test-vault', 'memory', db, ep, defaultConfig); const files = db.prepare('SELECT * FROM files').all(); const chunks = db.prepare('SELECT * FROM chunks').all(); console.log('files:', files.length, 'chunks:', chunks.length)"
- Test: `indexSessions(storagePath, db, embedder, config)` indexes all session transcripts
- Test: already-indexed sessions (by file hash) are skipped
- Test: new sessions since last index are added
- **GREEN**: Implement `src/sessions.ts`:
-`discoverSessions(storagePath: string): SessionDir[]` — find all `message/{session_id}/` directories under storage path. Also check project-specific dirs in `session/{hash}/`.
-`parseSession(sessionDir: string): ParsedSession` — read all msg_*.json files, sort by timestamp, extract role + content fields. Handle missing/corrupt files with try/catch.
-`sessionToText(session: ParsedSession): string` — format as:
- Reason: Parsing JSON files from unknown directory structure, handling corruption, integrating with indexer pipeline. Moderate complexity.
- **Skills**: none
**Parallelization**:
- **Can Run In Parallel**: YES (parallel with Task 5)
- **Parallel Group**: Wave 3
- **Blocks**: Task 11
- **Blocked By**: Tasks 3, 4
**References**:
**Pattern References**:
-`/home/m3tam3re/p/AI/openclaw/src/memory/session-files.ts` — Session transcript conversion. Shows how JSONL transcripts are converted to searchable text. Adapt for Opencode's JSON format.
-`/home/m3tam3re/p/AI/openclaw/src/memory/manager.ts:800-1000` — How session sources are handled alongside memory sources
**API/Type References**:
- Opencode session JSON format (discovered during research):
-`session-files.ts`: Exact pattern for converting conversation transcripts to text format suitable for chunking and embedding.
- Session JSON format: Needed to parse the actual message content from Opencode's storage.
**Acceptance Criteria**:
**TDD:**
- [ ] Test file: `src/__tests__/sessions.test.ts`
- [ ]`bun test src/__tests__/sessions.test.ts` → PASS (all 8 tests)
- [ ] Handles corrupt JSON without crashing
**Agent-Executed QA Scenarios:**
```
Scenario: Parse real Opencode session transcripts
Tool: Bash
Preconditions: Opencode storage exists at ~/.local/share/opencode/storage/
Steps:
1. bun run -e "import { discoverSessions } from './src/sessions'; const sessions = discoverSessions(process.env.HOME + '/.local/share/opencode/storage'); console.log('found sessions:', sessions.length)"
2. Assert: sessions.length > 0
3. Parse first session and verify text output contains "User:" and "Assistant:" markers
Expected Result: Real session transcripts parseable
Evidence: Terminal output (first 200 chars of transcript)
Scenario: Corrupt JSON file doesn't crash parser
Tool: Bash
Preconditions: Test fixture with corrupt JSON
Steps:
1. Create test dir with valid session JSON + one corrupt msg file (invalid JSON)
2. bun run -e "import { parseSession } from './src/sessions'; const s = parseSession('/tmp/test-session'); console.log('messages:', s.messages.length)"
3. Assert: no error thrown, corrupt message skipped
- Don't implement re-ranking with a separate model
**Recommended Agent Profile**:
- **Category**: `ultrabrain`
- Reason: Score merging, deduplication by ID, weighted combination, edge case handling (degraded modes). Requires careful algorithmic thinking.
- **Skills**: none
**Parallelization**:
- **Can Run In Parallel**: NO (depends on 8 and 9)
- **Parallel Group**: Wave 4 (after Tasks 8 + 9)
- **Blocks**: Task 11
- **Blocked By**: Tasks 8, 9
**References**:
**Pattern References**:
-`/home/m3tam3re/p/AI/openclaw/src/memory/hybrid.ts` — THE reference for hybrid search. This entire file is the pattern. Shows score normalization, weighted combination, merging, deduplication, filtering, sorting. Copy the algorithm closely.
**WHY This Reference Matters**:
- This is the heart of Openclaw's memory system. The hybrid search combiner determines recall quality. The weighting, merging, and filtering logic must be correct.
**Acceptance Criteria**:
**TDD:**
- [ ] Test file: `src/__tests__/search-hybrid.test.ts`
- [ ]`bun test src/__tests__/search-hybrid.test.ts` → PASS (all 9 tests)
- [ ] Degraded mode works (FTS-only, vector-only)
- [ ] Duplicate chunks merged correctly
**Agent-Executed QA Scenarios:**
```
Scenario: Hybrid search combines vector and keyword results
Tool: Bash (bun test)
Preconditions: DB with indexed chunks containing diverse content
Steps:
1. bun test src/__tests__/search-hybrid.test.ts
2. Assert: hybrid results include chunks found by BOTH methods
- Auto-recall logic: On session.created, search for memories related to the project directory and recent context. Format top results within token budget. Inject via system prompt addition.
- Auto-capture logic: On session.idle, analyze recent messages. Use LLM (or simple heuristics) to extract key facts, decisions, preferences. Store as markdown via memoryStoreTool.
- Compaction injection: On session.compacting, search for relevant memories and include in compaction context.
- Error wrapping: ALL hooks wrapped in try/catch → log error, never crash Opencode
- File watcher: Start chokidar watcher on vault directory for live file changes → re-index changed files
**Must NOT do**:
- Don't implement complex LLM-based extraction for auto-capture (use simple heuristic or minimal prompt — Phase 2 can enhance)
-`/home/m3tam3re/p/AI/openclaw/src/agents/tools/memory-tool.ts` — How tools are registered and how search results are formatted for agent consumption
-`/home/m3tam3re/p/AI/openclaw/src/agents/system-prompt.ts` — How memory instructions are injected into system prompt. Shows the "Before answering, search memory..." pattern.
- Opencode custom tools: `https://opencode.ai/docs/custom-tools/` — Tool schema format with Zod
**Documentation References**:
-`/home/m3tam3re/.config/opencode/opencode.json:128-132` — Existing plugin registration pattern (shows how plugins are listed)
**WHY Each Reference Matters**:
-`system-prompt.ts`: Shows the exact memory instruction pattern that makes agents reliably use memory tools. Without this, agents may ignore the tools.
- Plugin docs: The exact API surface for ctx.tool() and ctx.on(). Critical for correct integration.
-`manager.ts:1300-1590`: Shows chokidar file watcher setup, debouncing, and cleanup.
**Acceptance Criteria**:
**TDD:**
- [ ] Test file: `src/__tests__/plugin.test.ts`
- [ ]`bun test src/__tests__/plugin.test.ts` → PASS (all 9 tests)
- [ ] All hooks wrapped in try/catch
- [ ] Token budget respected
**Agent-Executed QA Scenarios:**
```
Scenario: Plugin loads in opencode without errors
Tool: interactive_bash (tmux)
Preconditions: Plugin built, registered in opencode.json
Steps:
1. Add "opencode-memory" to plugin list in opencode.json (or use local path)
2. Start opencode in tmux session
3. Wait for initialization (5s)
4. Check opencode logs for "opencode-memory" → no errors
5. Verify memory_search tool is available (try calling it)