How It Works¶
What Happens Automatically¶
| Event | What memsearch does |
|---|---|
| Plugin loads | Detects memsearch CLI, derives collection name, ensures default ONNX config |
| Session starts | Starts capture daemon, runs initial index, injects recent memories via system.transform |
| Conversation continues | Capture daemon polls SQLite for new turns, summarizes, saves to .md, re-indexes |
| LLM needs history | Calls memory_search, memory_get, or memory_transcript tools |
Architecture¶
graph TB
subgraph "Capture"
SQLITE[("OpenCode SQLite<br/>~/.local/share/opencode/opencode.db")] --> DAEMON["capture-daemon.py<br/>(background poller, 10s interval)"]
DAEMON --> SUMMARIZE["opencode run<br/>(isolated XDG_CONFIG_HOME)"]
SUMMARIZE --> MD["memory/YYYY-MM-DD.md"]
end
subgraph "Index"
MD --> INDEX["memsearch index<br/>(triggered after each capture batch)"]
INDEX --> MIL[(Milvus)]
end
subgraph "Recall"
TOOLS["memory_search<br/>memory_get<br/>memory_transcript"] --> MIL
TOOLS --> SQLITE
end
subgraph "Cold Start"
INJECT["system.transform hook"] --> RECENT["Recent memories<br/>injected into system prompt"]
end
style SQLITE fill:#2a3a5c,stroke:#d66b6b,color:#a8b2c1
style MD fill:#2a3a5c,stroke:#e0976b,color:#a8b2c1
style MIL fill:#2a3a5c,stroke:#6ba3d6,color:#a8b2c1
style DAEMON fill:#2a3a5c,stroke:#7bc67e,color:#a8b2c1
Capture Daemon¶
Unlike Claude Code and Codex (which use hook-based capture), OpenCode uses a background Python daemon (capture-daemon.py). This design choice exists because OpenCode's plugin hooks don't support the kind of external capture that Claude Code's Stop hook enables -- there's no hook that fires after each response with access to the conversation transcript.
Why a Daemon?¶
OpenCode stores all conversations in a SQLite database (~/.local/share/opencode/opencode.db). The daemon polls this database directly, which means:
- No hook limitations -- capture works regardless of which hooks OpenCode exposes
- Reliable detection -- new turns are detected by tracking
last_msg_time, not by fragile event timing - Crash resilience -- state is persisted to
.memsearch/.last_msg_time, so daemon restarts don't re-capture old turns
Daemon Flow¶
sequenceDiagram
participant DB as OpenCode SQLite
participant Daemon as capture-daemon.py
participant LLM as opencode run
participant File as YYYY-MM-DD.md
participant Index as memsearch index
loop Every 10 seconds
Daemon->>DB: Query sessions for project_dir
DB->>Daemon: Messages newer than last_msg_time
alt New turns found
Daemon->>Daemon: Group into user+assistant pairs
Daemon->>LLM: Summarize turn (isolated config)
LLM->>Daemon: 2-6 bullet points
Daemon->>File: Append with session anchor
Daemon->>Daemon: Update last_msg_time (persist to disk)
Daemon->>Index: memsearch index (background)
end
end
Step by step:
- Poll SQLite -- queries the
sessionandmessagetables for the current project directory, looking for messages newer thanlast_msg_time - Group into turns -- pairs consecutive
user+assistantmessages into turns - Extract text -- reads message
parts(text content, tool calls with names/paths) into a readable format - Summarize -- calls
opencode runwith the turn text and a third-person summarization prompt - Write to memory -- appends the summary to
.memsearch/memory/YYYY-MM-DD.mdwith<!-- session:ID source:opencode-sqlite -->anchors - Persist state -- writes
last_msg_timeto.memsearch/.last_msg_timeso restarts don't re-capture - Re-index -- triggers
memsearch indexin the background
LLM Summarization with Isolation¶
The daemon summarizes turns via opencode run -- but it must avoid triggering the memsearch plugin recursively. It achieves this with XDG isolation:
result = subprocess.run(
["opencode", "run", "-m", small_model, prompt],
env={
"XDG_CONFIG_HOME": "/tmp/opencode-memsearch-summarize",
"XDG_DATA_HOME": "/tmp/opencode-memsearch-summarize/data",
"MEMSEARCH_NO_WATCH": "1",
},
)
The isolated XDG_CONFIG_HOME contains a copy of opencode.json (for provider/model config) but no plugins/ directory -- so the memsearch plugin doesn't load in the summarization subprocess. The MEMSEARCH_NO_WATCH env var provides an additional guard.
The daemon also reads small_model from opencode.json config, using a lighter model for summarization when available.
Daemon Self-Management¶
- PID file --
.memsearch/.capture.pidensures only one daemon runs per project - Stale PID cleanup -- on startup, the plugin checks if the PID is still alive; dead PIDs are cleaned up
- Signal handling -- daemon cleans up its PID file on SIGTERM/SIGINT
- Auto-start -- the TypeScript plugin starts the daemon on plugin load and on each tool invocation (ensuring it's running even after a crash)
Cold-Start Context¶
On session start, the experimental.chat.system.transform hook injects recent memories into the system prompt:
"experimental.chat.system.transform": async (_input, output) => {
const context = getRecentMemories(memoryDir);
if (context) {
output.system.push(
`[memsearch] Memory available. You have access to memory_search, ` +
`memory_get, and memory_transcript tools for recalling past sessions.\n\n${context}`
);
}
}
This reads the last 15 lines from the 2 most recent daily .md files, extracting bullet points and role-labeled lines. The injected context serves two purposes:
- Immediate awareness -- the LLM knows what happened recently without needing to search
- Tool discovery -- the message explicitly tells the LLM about the available memory tools
Memory Files¶
Example Memory File¶
# 2026-03-26
## Session 14:30
### 14:30
<!-- session:ses_abc123 source:opencode-sqlite -->
- User asked about authentication flow in the Express API
- OpenCode explained the OAuth2 implementation in auth.ts
- OpenCode modified token refresh logic in refresh.ts to handle expired tokens
- Added error handling for revoked refresh tokens
### 15:15
<!-- session:ses_abc123 source:opencode-sqlite -->
- User reported 500 error on /api/users endpoint
- OpenCode traced the issue to a missing null check in userController.ts
- OpenCode added optional chaining and a 404 response for missing users
- [Tool: bash `npm test`] — all tests pass
## Session 17:00
### 17:00
<!-- session:ses_def456 source:opencode-sqlite -->
- User asked to refactor the middleware chain for better error handling
- OpenCode created a centralized error handler in middleware/errorHandler.ts
- Removed try/catch blocks from individual route handlers
- Added structured error logging with request ID correlation
The <!-- session:... source:opencode-sqlite --> anchors are used by the memory_transcript tool to query the original conversation from OpenCode's SQLite database.
Differences from Other Plugins¶
| Aspect | OpenCode | Claude Code | OpenClaw | Codex |
|---|---|---|---|---|
| Capture | SQLite daemon (polling) | Stop hook (event-driven) | llm_output hook (event-driven) | Stop hook (event-driven) |
| Summarizer | opencode run (isolated) |
claude -p --model haiku |
openclaw agent --local |
codex exec (isolated) |
| L3 source | OpenCode SQLite DB | Claude Code JSONL | OpenClaw JSONL | Codex rollout JSONL |
| Recall trigger | Tool-based (LLM decides) | Skill in forked subagent (context: fork) |
Tool-based (LLM decides) | Skill-based (main context) |
| Install | npm + opencode.json | Plugin marketplace | openclaw plugins install |
install.sh + hooks.json |
| Recursion prevention | XDG_CONFIG_HOME isolation | CLAUDECODE= env var |
MEMSEARCH_NO_WATCH flag |
Isolated CODEX_HOME |
Plugin Files¶
plugins/opencode/
├── package.json # npm package with @opencode-ai/plugin peer dep
├── index.ts # Main plugin: tools, hooks, daemon management
├── install.sh # Installation script
├── skills/
│ └── memory-recall/
│ └── SKILL.md # Memory recall skill
└── scripts/
├── derive-collection.sh # Per-project collection name
├── capture-daemon.py # Background SQLite poller + summarizer
└── parse-transcript.py # SQLite session reader for L3 drill-down
| File | Purpose |
|---|---|
index.ts |
Main plugin. Registers 3 tools, system.transform hook, daemon lifecycle management |
capture-daemon.py |
Background Python daemon. Polls OpenCode's SQLite, summarizes turns via opencode run, writes to daily .md, triggers re-indexing |
parse-transcript.py |
SQLite session reader for L3 drill-down. Reads original messages from OpenCode's database by session ID |
derive-collection.sh |
Generates deterministic per-project Milvus collection names from project paths |
install.sh |
Installation script: symlinks plugin, copies skill to ~/.agents/skills/, installs dependencies |