Canonical plan for eliminating duplication, extracting atomic modules, and hardening reliability.
A2 (atomic dedup + module extraction) → A5 (open-source readiness) → A6 (memory fixes) → A7+ (further)
A4 is done — unified updateStatus() with session-scoped status files (#69, #70).
A3 is obsolete — no blocked-directory scanning bug.
A1 is merged into A2 — library extraction now part of the atomic module plan.
Priority: HIGH. Eliminates duplication, extracts reusable npm packages, makes the codebase modular.
Brain currently has two types of duplication:
- opencode-native duplication — spinner array, hardcoded colors (can use opencode's built-in components)
- Monolith architecture — single package with 4 engines (ingest, search, memory, knowledge) + TUI + hooks, making it hard to reuse individual engines
| # | Task | Current | Target |
|---|---|---|---|
| A2.1 | Replace file polling with TuiEventBus | setInterval(poll, 200ms) reading status JSON file |
api.event.on('brain:status', handler) — push-based |
| A2.2 | Use opencode <Spinner> |
Handwritten braille ["⠋","⠙","⠹","⠸","⠼","⠴","⠦","⠧","⠇","⠏"] + manual spin++ |
Import <Spinner> from opencode TUI |
| A2.3 | Use api.theme colors |
Hardcoded setFg(GREEN), setFg(RED), setFg(YELLOW) |
theme().success, theme().error, theme().warning |
Each engine becomes its own npm package under @four-bytes/ scope:
@four-bytes/brain-core — DB schema init, LRU cache, logger, shared utils
@four-bytes/brain-ingest — File walker, content-hash dedup, chunker, embed pipeline
@four-bytes/brain-search — FTS5+vec0 hybrid search, query parser, FTS sanitizer
@four-bytes/brain-memory — Memory CRUD (add/search/list/forget) + diary
@four-bytes/brain-knowledge — KB entries, confidence gating, REGATE lifecycle
@four-bytes/brain-hooks — System prompt generator, auto-capture triggers
@four-bytes/brain-tui — SolidJS BrainStatusBar component, spinner/color integration
@four-bytes/opencode-plugin-lib — Toast wrapper (createToast), shared plugin utilities
The main @four-bytes/four-opencode-brain becomes a thin composer that imports all modules and wires them together:
// four-opencode-brain.ts (post-extraction)
import { initBrainDatabase } from "@four-bytes/brain-core";
import { ingestPath } from "@four-bytes/brain-ingest";
import { brainSearch } from "@four-bytes/brain-search";
import { memoryAdd, memorySearch } from "@four-bytes/brain-memory";
import { kbAdd, kbSearch } from "@four-bytes/brain-knowledge";
import { brainSystemPrompt } from "@four-bytes/brain-hooks";
import { createToast } from "@four-bytes/opencode-plugin-lib";
import { BrainStatusBar } from "@four-bytes/brain-tui";1. opencode-plugin-lib → Toast wrapper (no deps)
2. brain-core → Schema, cache, logger, shared (no deps)
3. brain-ingest → Depends on core (DB, cache)
4. brain-search → Depends on core (DB)
5. brain-memory → Depends on core (DB)
6. brain-knowledge → Depends on core (DB)
7. brain-hooks → Depends on memory + knowledge
8. brain-tui → Depends on core (project-agnostic)
9. four-opencode-brain → Composer (depends on all)
- In
src/status.ts: publish event on bus instead ofwriteFileSync() - In
src/tui.tsx: subscribe to event bus instead ofsetInterval(poll, 200ms) - Remove
POLL_MS,setInterval,onCleanup(clearInterval)polling loop - Remove file-based
writeFileSync/readFilestatus mechanism - Verification: Status bar updates in real time. Note: file I/O retained as opencode runs server/TUI in separate Worker contexts — event bus is fast-path within same context, file is cross-context fallback.
- Remove
const SPINNER = ["⠋","⠙",…]+spinvariable fromtui.tsx - Use opencode's
<Spinner />component (verify import path from@opentui/solid) - Wire spinner visibility to
data.phase === 'busy'or equivalent - Verification: Spinner animates identically to opencode's native spinners
- Replace all
setFg(GREEN)/setFg(RED)/setFg(YELLOW)with theme equivalents - Map: GREEN →
theme().success, RED →theme().error, YELLOW/ORANGE →theme().warning - Map: MUTED →
theme().textMuted, accent pulse →theme().accent - Remove hardcoded color constants
- Verification: Colors match the user's opencode theme
- A2.4a — Extract
@four-bytes/opencode-plugin-lib(toast wrapper) - A2.4b — Extract
@four-bytes/brain-core(schema, cache, logger, shared) - A2.4c — Extract
@four-bytes/brain-ingest(walker, chunker, embed, dedup) - A2.4d — Extract
@four-bytes/brain-search(FTS5+vec0, query parser) - A2.4e — Extract
@four-bytes/brain-memory(CRUD + diary) - A2.4f — Extract
@four-bytes/brain-knowledge(KB, confidence, REGATE) - A2.4g — Extract
@four-bytes/brain-hooks(system prompt, auto-capture) - A2.4h — Extract
@four-bytes/brain-tui(BrainStatusBar + spinner) - A2.4i — Refactor
@four-bytes/four-opencode-brainas composer - Verification: All tests pass; plugin behavior identical; each module independently publishable
- No handwritten spinner array in brain code
- No hardcoded color constants — uses
api.theme - No polling loop in TUI — uses TuiEventBus
- All 8 sub-packages extracted and independently buildable
-
four-opencode-braincomposer passes all tests - Each package has its own
package.json,tsconfig.json, build script
DONE (#69, #70). Single
updateStatus()function with directory-scoped status files.What was built:
src/status.ts—updateStatus(state, opts?)supporting busy/success/warning/error/ready- Session-scoped status files via
getBrainStatusFile(directory)(MD5-hash)- Wired to all 10 tool paths (ingest, search, reindex, memory, KB operations)
- TUI polls per-directory file via
api.state.path.directory
Priority: MEDIUM. Required before public announcement.
- Project description: "Unified brain plugin — SQLite DB for RAG search, memory, and knowledge base"
- Installation:
npm install @four-bytes/four-opencode-brainor opencode plugin marketplace - Quick start:
/brain ingest,/brain search, etc. - Tool reference table (all 10 brain tools)
- Architecture diagram (three engines: ingest, search, memory)
- Configuration (env vars:
BRAIN_AUTO_INGEST,BRAIN_DEBUG) - Requirements (Bun, opencode, vec0 extension)
- Branch workflow: Issue → Branch → PR → Merge
- Conventional commits:
feat:,fix:,refactor: - Build discipline:
bun run buildafter every change - Testing:
bun test - PR template checklist
-
.github/ISSUE_TEMPLATE/bug_report.md -
.github/ISSUE_TEMPLATE/feature_request.md -
.github/PULL_REQUEST_TEMPLATE.md
- Repository description, topics, website
- About section with key features
- License badge, npm version badge
- README is complete and welcoming
- CONTRIBUTING.md covers full workflow
- Issue templates guide quality reports
- Repository looks professional and discoverable
Priority: HIGH. Memory (diary, important updates, store) currently silently broken.
| # | Bug | File | Fix |
|---|---|---|---|
| 1 | memories_dedup_bi trigger silently ignores repeat inserts; memoryAdd() returns success with fake ID |
schema.ts:574–582, store.ts:85–116 |
Check result.changes after INSERT; return error if 0 |
| 2 | No data migration from ~/.four-mem/ to SQLite |
Both repos | One-time migration script (personal use only) |
| 3 | memorySearch returns [] without error when no query |
store.ts:123 |
Return explicit error like old plugin |
| 4 | Diary auto-capture depends on external API call that can fail silently | four-opencode-brain.ts:738–767 |
Add fallback — if API fails, create diary from event properties |
| 5 | memoryAdd() never verifies INSERT succeeded |
store.ts:85–116 |
Check result.changes and connect to updateStatus |
| 6 | onSessionIdle creates knowledge entries, not memory entries |
auto-capture.ts:207–362 |
Ensure memory patterns also create memoryAdd entries |
| 7 | Diary API requires subMode: "add" (non-obvious) |
four-opencode-brain.ts:486–499 |
Improve API: auto-detect add vs get |
| 8 | Tool renamed from memory → brain_memory |
N/A | No fix needed — no code references old name |
- In
memoryAdd(): captureconst result = db.run("INSERT INTO memories …") - If
result.changes === 0, throw error:"Memory not stored: duplicate content detected" - Wire to
updateStatus('warning', { text: 'Duplicate', toast: 'Memory already exists' }) - Apply same pattern to
diaryAdd(),kbAdd(), and any other INSERT
- In
memorySearch(): if!opts.query, return structured error{ error: "query required" } - Match old plugin's behavior
- In
eventhook handler:- If
client.session.messages()fails → fall back toeventInput.event.propertiestext - Log warning but still attempt
onSessionIdle
- If
- Ensure diary entries are always created on session idle
- In
onSessionIdle/onChatMessage: after creating knowledge entries, also creatememoryAddentries - Pattern: decisions →
memoryAddwithtype: "decision"ANDkbAddwithkind: "decision" - Ensure both stores are populated
- Simplify:
diaryGetreturns today if no date specified -
diaryAddbecomes simpler:brain_memory({ mode: "diary", title: "...", content: "..." })auto-detects add vs get - Backward compatible with current API
- Create
scripts/migrate-from-four-mem.ts - Reads
~/.four-mem/MEMORY.md→ parses entries → inserts into SQLite - Reads
~/.four-mem/diary/*.md→ parses entries → inserts intodiary_entries - One-time use, documented in README
- Note: Personal script — not part of plugin runtime
- Re-adding the same memory returns a clear error, not fake success
-
brain_memory({ mode: "search" })without query returns error - Diary entries created on every session idle
- Memory entries created alongside knowledge entries on auto-capture
- Diary API is intuitive (auto-detect add vs get)
- Migration script available for old data
A2 (atomic dedup + modules)
├─► A5 (open-source readiness) — independent, can run in parallel with A2 phase 1
└─► A6 (memory fixes) — uses modules from A2
- A2 Phase 1 → Replace spinner, colors, polling with opencode-native APIs
- A2 Phase 2 → Extract 8 atomic npm packages + refactor composer
- A5 → README, CONTRIBUTING, issue templates, repo polish (parallelizable with A2 phase 2)
- A6 → Fix memory dedup, search, diary, migration script
| Wave | Status | Issue |
|---|---|---|
| A2 | 🔄 In Progress | #71, #76, #98 |
| A4 | ✅ Done | #69, #70 |
| A5 | ✅ Done | #85 |
| A6 | ✅ Done | #86 |
| Wave | Status | Issue |
|---|---|---|
| A1 | Merged into A2 | #49 |
| A3 | Obsolete | — |
Priority: HIGH. Automated quality gates + AI code review for every PR.
Per P48 — Open Source Github Review Automation: CI, linters, and tests are already solid. Adding an AI reviewer + quality gate pattern on top ensures every PR is automatically reviewed before merge.
- ✅
.github/workflows/ci.yml— builds + runs 247 tests on every PR - Trigger:
pull_request(opened, reopened, synchronize, ready_for_review) +push: main
- Install cubic GitHub App on the repo
- Automatic PR review per commit, inline comments, summary
- Model: same as dagu reference implementation
- Require:
Build & Test(CI workflow) — blocking - Require:
cubic AI review— blocking from start - Require: at least 1 human review
- Require: conversation resolution before merge
- CI workflow runs on every PR (build + test)
- cubic AI reviewer installed and active
- Branch protection enforces both CI + AI review
- No PR merges without passing gates
| Wave | Status | Issue |
|---|---|---|
| A7 | ✅ Done | #87 |
Status: 🔄 In Progress (EPIC #117). Replaces node-llama-cpp (in-process) with a Go sidecar binary that manages
llama-serveras a separate HTTP process. Split into 3 waves:
- Wave 1 (#118): Go embed-sidecar binary
- Wave 2 (#119): Plugin integration
- Wave 3 (#120): Remove node-llama-cpp
- Two modes only: hash-based (default) / Go sidecar (opt-in via
BRAIN_EMBED_SIDECAR). No in-process node-llama-cpp. - Non-blocking:
embed()returns hash result immediately; sidecar loads async. - Go binary: Bundled via GitHub Releases prebuilt, extracted by
build.ts(same pattern asvec0.so). - Self-bootstrapping: Go binary downloads
llama-server+ GGUF model on first run (cached in~/.cache/four-opencode-brain/). - Port: 8666, configurable via
opencode.jsonandBRAIN_EMBED_SIDECAR_PORT. - Single-instance: Port bind detection — 2nd instance connects to existing sidecar.
- Idle timeout: 30min without requests → auto-shutdown.
- Rich
/status:{phase, progress, queue_depth}— always responsive even during model loading.
OpenCode communicates with a local llama.cpp server via HTTP (OpenAI-compatible /v1/embeddings) instead of embedding node-llama-cpp in the main process. This eliminates init-race conditions, separates CPU-heavy embedding from the main event loop, and allows the sidecar to outlive individual OpenCode sessions.
| Problem | Sidecar Fix |
|---|---|
initialize() race — multiple callers trigger parallel getLlama() |
Single start via Promise-lock + cross-process lockfile |
| Ingest batch embeddings block search queries | Separate process → separate CPU core; search uses its own HTTP connection |
| node-llama-cpp addon conflicts in Worker threads | No addon in main process at all |
| Process crash takes down embeddings | Sidecar is detached — survives parent crash/restart |
| Component | File | Responsibility |
|---|---|---|
| EmbeddingSidecarManager | src/embed/sidecar/EmbeddingSidecarManager.ts |
Process lifecycle: spawn, health poll, restart, stop |
| LlamaCppEmbeddingClient | src/embed/sidecar/LlamaCppEmbeddingClient.ts |
HTTP client for /v1/embeddings + /health |
| Lockfile | src/embed/sidecar/lockfile.ts |
Cross-process start guard with PID-based stale detection |
| Integration | src/embed/embeddingService.ts |
Mode switch: OPENCODE_EMBED_SIDECAR=true → HTTP; else legacy node-llama-cpp |
┌─────────────────────────────────┐
│ OpenCode Process (Bun) │
│ ┌───────────────────────────┐ │
│ │ EmbeddingService │ │
│ │ ├─ Promise-lock init │ │
│ │ ├─ Priority queue │ │
│ │ └─ HTTP client ─────────┼──┼──► POST /v1/embeddings
│ └───────────────────────────┘ │ GET /health
│ ┌───────────────────────────┐ │
│ │ SidecarManager │ │
│ │ ├─ spawn (detached) │ │
│ │ ├─ lockfile guard │ │
│ │ └─ health poll │ │
│ └───────────────────────────┘ │
└─────────────────────────────────┘
│ spawn + lock
▼
┌─────────────────────────────────┐
│ llama.cpp server (sidecar) │
│ Port: 8091 (configurable) │
│ ├─ /health → 200/503 │
│ └─ /v1/embeddings → vectors │
│ Model: all-MiniLM-L6-v2.Q8_0 │
└─────────────────────────────────┘
| State | /health response | Meaning |
|---|---|---|
live |
Any HTTP response | Process is running, TCP port open |
ready |
HTTP 200 | Model loaded, embeddings available |
loading |
HTTP 503 + "loading model" |
Live but not ready |
down |
Connection refused / timeout | Process not reachable |
| Config | Behavior |
|---|---|
off (default) |
Always CPU — -ngl 0 (no GPU layers offloaded) |
on |
Try GPU first (-ngl 999), fallback to CPU on failure |
auto |
Detect GPU availability; if uncertain, prefer CPU |
CPU always works. GPU is a bonus path with mandatory fallback.
- Path:
/tmp/opencode-embeddings-sidecar.lock - Content:
{ pid, createdAt, port, binary, model } - Stale detection: PID dead OR lock > 30s old → acquire
- Release: only if our PID matches
| Variable | Default | Description |
|---|---|---|
OPENCODE_EMBED_SIDECAR |
false |
Enable sidecar mode |
OPENCODE_EMBED_HOST |
127.0.0.1 |
Sidecar bind address |
OPENCODE_EMBED_PORT |
8091 |
Sidecar port |
OPENCODE_EMBED_MODEL |
~/.cache/.../all-MiniLM-L6-v2.Q8_0.gguf |
Model path |
OPENCODE_EMBED_LLAMA_SERVER |
llama-server from PATH |
Binary path |
OPENCODE_EMBED_GPU |
auto |
GPU mode |
OPENCODE_EMBED_GPU_LAYERS |
999 |
GPU layers to offload |
OPENCODE_EMBED_START_TIMEOUT_MS |
15000 |
Max wait for process start |
OPENCODE_EMBED_READY_TIMEOUT_MS |
120000 |
Max wait for model load |
OPENCODE_EMBED_LOG |
/tmp/opencode-embed-sidecar.log |
Sidecar log file |
- EmbeddingService gains
sidecarandsidecarClientfields initialize(): whenOPENCODE_EMBED_SIDECAR=true, create SidecarManager + Client instead of loading node-llama-cppembedDirect(): swapthis.ctx.getEmbeddingFor()→sidecarClient.embed([text])dispose(): addsidecar.stop()or leave running (detached)- Legacy mode (node-llama-cpp) preserved as default until sidecar is stable
- Unit: Lockfile stale detection, status parsing, start-guard Promise dedup
- Smoke: Start sidecar → poll /health → POST /v1/embeddings → verify dimensions
- Manual: CPU-only test, simulated crash recovery, optional GPU test
- Sidecar starts on first embed call, survives parent restart
- Search queries are never blocked by ingest (separate HTTP connections)
- Lockfile prevents duplicate sidecar processes
- GPU failure gracefully falls back to CPU
- Legacy mode continues working unchanged