Design document for extending Recall to work with OpenCode alongside Claude Code. v4 — Phase 4 verified against a live OpenCode 1.18.5 server. The event contract, the measured
session.idlefrequency, and plugin discovery are pinned byscripts/e2e-opencode-runtime.ts; the extraction pipeline is pinned byscripts/e2e-opencode.ts. See Phase 4 Evidence for which script proves what.
Executive Summary
Recall’s core (SQLite DB, MCP server, CLI) is already platform-agnostic thanks to MCP. Both Claude Code and OpenCode are MCP clients — the recall-memory MCP server works identically on both without any changes to src/.
The entire integration is three thin adapter layers:
- Registration — where the MCP server config entry goes
- Extraction — how conversation transcripts are captured
- Instructions — what file the agent reads for guidance
Architecture Diagram
┌──────────────────────────────────────────────────────────────────┐
│ PLATFORM-AGNOSTIC CORE │
│ (zero changes needed) │
│ │
│ ┌─────────────┐ ┌──────────────────┐ ┌────────────────────┐ │
│ │ recall.db │ │ MCP Server │ │ CLI (recall) │ │
│ │ (SQLite) │←─│ (recall-memory) │ │ search/add/stats │ │
│ │ FTS5 + Vec │ │ 9 tools, stdio │ │ import/export │ │
│ └──────┬───────┘ └────────┬─────────┘ └────────────────────┘ │
│ │ │ │
│ │ ┌─────────────┴──────────────┐ │
│ │ │ src/lib/memory.ts │ │
│ │ │ src/lib/embeddings.ts │ │
│ │ │ src/db/connection.ts │ │
│ │ └────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
│
┌───────────────┴───────────────┐
│ │
┌───────────┴───────────┐ ┌─────────────┴──────────────┐
│ Claude Code Adapter │ │ OpenCode Adapter │
│ │ │ │
│ Registration: │ │ Registration: │
│ ~/.claude/settings.json│ │ ~/.config/opencode/ │
│ mcpServers.recall-memory │ │ opencode.json │
│ │ │ mcp.recall-memory │
│ Extraction: │ │ │
│ hooks/RecallExtract │ │ Extraction: │
│ (.ts, Stop hook) │ │ plugins/RecallExtract.ts │
│ reads JSONL transcript│ │ `event` hook → │
│ │ │ `opencode export` JSON │
│ Instructions: │ │ → markdown drop for batch │
│ FOR_CLAUDE.md │ │ │
│ → ~/.claude/ │ │ Instructions: │
│ Recall_GUIDE.md │ │ FOR_OPENCODE.md │
│ │ │ → ~/.config/opencode/ │
│ Agent Config: │ │ Recall_GUIDE.md │
│ CLAUDE.md snippet │ │ │
│ │ │ Agent Config: │
└────────────────────────┘ │ agents/recall-memory.md │
│ │
│ Context Injection: │
│ plugins/RecallPreCompact.ts │
│ pushes to output.context[] │
└─────────────────────────────┘New Files
atlas-recall/
├── src/ # UNCHANGED
├── hooks/
│ └── RecallExtract.ts # UNCHANGED (Claude Code adapter)
│ └── RecallBatchExtract.ts # scans MEMORY/*-sessions/
├── hosts/opencode/ # OpenCode adapter
│ ├── RecallExtract.ts # Plugin: session extraction via CLI export
│ ├── RecallPreCompact.ts # Plugin: context injection during compaction
│ ├── lib/session-export.ts # Shared helpers — nested so OpenCode does not
│ │ # glob them as plugin factories
│ └── recall-memory.md # Agent: memory-aware agent definition
├── docs/
│ ├── hosts/FOR_CLAUDE.md # guide for Claude Code agents
│ ├── hosts/FOR_OPENCODE.md # guide for OpenCode agents
│ └── OPENCODE_INTEGRATION.md # THIS FILE
├── scripts/
│ ├── e2e-opencode.ts # Pipeline e2e (export → drop → extract → search)
│ ├── e2e-opencode-runtime.ts # Live-server e2e (discovery, idle frequency)
│ └── lib/opencode-runtime.ts # Shared CLI pin, stub provider, event stream
├── packaging/install.sh # detect + register for both platforms
└── package.json # MODIFIED — adds the isolated OpenCode e2e scriptsComponent Details
1. MCP Server Registration (opencode.json)
Example installer registration in ~/.config/opencode/opencode.json (paths resolve on the installing machine):
{
"mcp": {
"recall-memory": {
"type": "local",
"command": ["/Users/username/.bun/bin/bun", "run", "/Users/username/.bun/bin/recall-mcp"],
"enabled": true,
"environment": {
"RECALL_DB_PATH": "/Users/username/.agents/Recall/recall.db"
}
}
}
}Key decisions:
- Resolves
bunandrecall-mcpwithwhich, falling back to~/.bun/bin/, and writes those absolute paths - Uses
bun runto execute that binary - The installer pins
RECALL_DB_PATHusing the shared resolver; see database environment variables. - Same binary, same tools — zero platform-specific MCP code
2. Session Extraction Plugin (hosts/opencode/RecallExtract.ts)
Current strategy: The plugin uses OpenCode’s current event hook, filters
session.idle, runs opencode export <session-id> for JSON, normalizes the
full { info, messages } response to markdown, and only records the session
after the drop write succeeds. RecallBatchExtract.ts then owns extraction
quality, SQLite dual-write, and searchable retrieval.
Implementation details and regression coverage live in
hosts/opencode/RecallExtract.ts and
tests/opencode-integration.test.ts.
Why this approach:
opencode exportis the verified current CLI command — not an assumed APIctx.$Bun shell is confirmed in plugin docs with examples- Persistent dedup via JSON file at
~/.agents/Recall/MEMORY/opencode-sessions/.extracted.json— survives plugin restarts. It maps session ID to a digest of the markdown last written, so a grown session re-exports and an unchanged one does not - The current payload is
event.properties.sessionID; defensive fallbacks keep older property spellings harmless - Drop directory pattern:
RecallBatchExtract.tsalready runs every 30 minutes and can scan this directory - No new CLI commands needed —
RecallBatchExtract.tsreads markdown files the same way it reads JSONL
Measured runtime contract (OpenCode 1.18.5). session.idle fires once per
assistant turn, not once per session. Measured by
scripts/e2e-opencode-runtime.ts against a live server: three turns of one
session produced exactly three session.idle events (delta 1 per turn), counted
from OpenCode’s own GET /event stream inside a single long-lived process so
the number cannot be an artifact of process teardown.
That number drives the adapter’s design, so it is asserted, not just recorded — the runtime e2e fails if the frequency ever changes.
Two consequences:
- Debouncing would be the wrong remedy. Earlier drafts of this document hedged toward adding it. Because idle marks the end of each turn rather than the end of the session, the adapter must RE-export on later idles; suppressing them freezes the drop on turn 1 and silently discards the rest of the conversation. That was a real defect: a three-turn session left a 154-byte drop holding only the first turn.
- The tracker records a content digest, not “this session is finished”. A
grown session overwrites its earlier, shorter drop; an unchanged one costs no
write.
RecallBatchExtractalready re-extracts a drop that grows, so the fuller transcript reaches memory without new machinery — but only once it grows pastGROWTH_THRESHOLD = 0.5(hooks/RecallBatchExtract.ts): a drop extracted at size S is not re-extracted until it exceeds 1.5 × S, so the tail of a long session can sit in the drop file without reaching the database. The drop itself does hold every turn; it is the drop-to-SQLite step that is threshold-gated. Tracked in #250.
Plugin module contract. OpenCode calls every export of a top-level
plugins/*.ts as a plugin factory. A plugin entry point must therefore export
nothing but its factory — shared helpers live in hosts/opencode/lib/session-export.ts,
which OpenCode does not glob. When the helpers sat beside the factory, OpenCode
called exportSession() with its plugin context and logged
failed to load plugin ... "Object is not a function" on every launch.
PATH requirement. The plugin shells out to opencode export <id>, so the
opencode CLI must be resolvable by name from the environment OpenCode runs in.
If it is not, the export fails and the drop is never written; the failure is
reported on stderr and retried on the next idle.
3. Compaction Context Injection (hosts/opencode/RecallPreCompact.ts)
The compaction plugin handles
experimental.session.compacting(input, output) by appending Recall search
context to output.context. It leaves output.prompt unchanged, uses a
five-second CLI timeout, and skips injection if the context array is missing
or the CLI fails. Its real compaction behavior remains unverified at runtime.
4. Agent Definition (hosts/opencode/recall-memory.md)
---
description: Memory-aware agent with persistent Recall context
mode: all
tools:
recall-memory_*: true
---
You have access to persistent memory via the recall-memory MCP server.
Before asking the user to repeat information, search memory first using
recall-memory_memory_search or recall-memory_memory_hybrid_search. For keyword
search, use `table` when you need only one record type; use `bias_type` when
that type should rank first while preserving other matching context.
When important decisions are made, record them with recall-memory_memory_add.
Before delegating to subagents via @agent mentions, call
recall-memory_context_for_agent to prepare relevant memory context.5. FOR_OPENCODE.md (Agent Guide)
Equivalent to FOR_CLAUDE.md but with:
- OpenCode-specific tool name prefixes (
recall-memory_*) @agentdelegation syntax instead of Claude Code’s Task tool- Reference to
opencode export <session-id>for manual session capture
6. Installer Changes
# Detection
detect_opencode() {
if command -v opencode &>/dev/null; then
OPENCODE_DETECTED=true
OPENCODE_CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/opencode"
fi
}
detect_claude_code() {
if [[ -d "$HOME/.claude" ]] || command -v claude &>/dev/null; then
CLAUDE_CODE_DETECTED=true
fi
}
# MCP registration and database-path selection use the shared installer helper.
recall_configure_opencode_mcp
# Plugin, agent, and guide installation
recall_install_opencode_plugins # hosts/opencode/*.ts -> canonicals, symlinked into plugins/
recall_install_opencode_agent # hosts/opencode/recall-memory.md -> agents/
recall_install_opencode_guide # docs/hosts/FOR_OPENCODE.md -> Recall_GUIDE.mdThe live installer delegates to lib/install-lib.sh’s shared
_recall_jsonc_merge_mcp_entry helper and the bundled dependency-free
lib/jsonc-mcp.ts parser. It preserves comments, trailing commas, sibling MCP
entries, and custom Recall entry fields; malformed or unwritable configs fail
before writing. Uninstall uses the matching surgical removal helper, leaves a
malformed OpenCode config unchanged, and continues the remaining integrations.
Database Schema Change
Add a source column to sessions table (the natural place — messages FK to sessions):
-- Migration v3: add source tracking
ALTER TABLE sessions ADD COLUMN source TEXT DEFAULT 'claude-code';The extraction pipeline tags OpenCode sessions with source: 'opencode'. The historical migration preserves claude-code on legacy records; fresh host-neutral sessions default to unknown when no adapter supplies a source.
No changes to MCP tools — they return results from all sources. The source field is metadata for provenance, not filtering.
RecallBatchExtract.ts
RecallBatchExtract.ts already scans MEMORY/*-sessions/ through hooks/lib/markdown-drop.ts, so the OpenCode drop at ~/.agents/Recall/MEMORY/opencode-sessions/ is picked up with the other markdown drops. It does not have a separate OpenCode-only finder.
The extraction prompt already handles markdown input — it processes conversation text regardless of format.
DB Location Strategy
OpenCode shares the CLI and hooks’ database. Defaults, overrides, and path resolution are defined in Installation: Environment Variables.
Tool Name Mapping
OpenCode connects to the same MCP server as every other host and prefixes each tool with
the server name plus an underscore: memory_search becomes recall-memory_memory_search.
Apply that one rule to any tool in mcp-tools.md, which documents all
nine with their parameters and return shapes.
Implementation Phases
Phase 1: Core Integration (MCP + Installer)
- Installer detection for both platforms
- MCP registration in
opencode.jsonwith absolute paths - JSONC-safe config parsing
FOR_OPENCODE.mdwith correct tool names- Agent definition
recall-memory.md sourcecolumn migration (schema v3)
Phase 2: Extraction Plugin
RecallExtract.tsplugin using theopencode exportJSON CLI via$shell- Persistent dedup tracker (
.extracted.json, session ID → content digest) - Defensive
session.idleevent property access - Drop directory at
~/.agents/Recall/MEMORY/opencode-sessions/ RecallBatchExtract.tsupdated to scan drop directory
Phase 3: Context Injection
RecallPreCompact.tsplugin with verified(input, output)signature- Pushes to
output.context[]array - 5s timeout on
recallCLI calls
Phase 4: Testing + Polish — complete
- Isolated e2e: OpenCode session → JSON export → markdown drop → RecallBatchExtract → search
- Concurrent Claude and OpenCode writers against one disposable WAL database
- Installer/update idempotence, JSONC-preserving OpenCode uninstall, and the
documented
install.sh restorerollback plus collision-backup preservation - Live-server verification: plugin discovery from the installed path, zero
plugin load errors, the
session.idlepayload, and the measured one-idle-per -turn frequency
Red Team Findings (v1 → v2 Changes)
| Flaw | Severity | Fix Applied |
|---|---|---|
client.session.messages() not on plugin client |
BLOCKER | Replaced with opencode export JSON CLI |
| In-memory Set dedup | HIGH | Persistent JSON file at .extracted.json |
session.idle wrong property name |
MEDIUM-HIGH | Current event hook reads properties.sessionID; legacy spellings remain defensive fallbacks |
| Compacting return type fabricated | MEDIUM | Corrected to output.context.push() |
| Tilde in RECALL_DB_PATH | MEDIUM | Shared resolver; see database environment variables |
recall import --source doesn’t exist |
LOW-MEDIUM | Eliminated — uses drop dir + RecallBatchExtract |
| Bun not guaranteed | LOW-MEDIUM | Documented as requirement (same as Claude Code) |
| JSONC parsing | LOW | Bundled dependency-free parser performs surgical merge/removal, preserves comments, and refuses malformed writes |
Phase 4 Evidence
Two scripts cover this, and they prove different things. Read the distinction before citing either as evidence.
scripts/e2e-opencode.ts — pipeline. Imports the adapter directly and
supplies its own $ and event payload. It proves Recall’s handler ACCEPTS
{type:'session.idle', properties:{sessionID}} and that export -> drop ->
RecallBatchExtract -> search works, plus export-failure retry, concurrent WAL
writers, and installer/uninstall JSONC rollback. It does NOT prove OpenCode
emits that payload, and it cannot detect a plugin OpenCode never loads.
scripts/e2e-opencode-runtime.ts — runtime. Drives a live opencode serve
and closes exactly those gaps:
- Discovery. Installs via the real
recall_install_opencode_platform, then observes OpenCode load the plugin from that path (the factory’s drop directory appears under aRECALL_HOMEonly this server knows) and asserts zerofailed to load pluginerrors. - Emission and frequency. Counts
session.idlefrom OpenCode’s ownGET /eventstream across three turns of one session in one long-lived process: 3 events, 1.00 per assistant turn, payloadproperties.sessionID. - Completeness. Asserts the drop carries every turn, and that
opencode export <session-id>returns the whole conversation.
opencode export <session-id> is the supported JSON export command; Recall owns
JSON-to-markdown normalization because the CLI does not expose the old
-f markdown -o session-export interface.
Both scripts run in disposable HOME / XDG config, data, state, cache /
RECALL_HOME / database roots and fail if production DB metadata changes. The
runtime script serves the model from a local OpenAI-compatible stub: the subject
under test is OpenCode’s session lifecycle, so a hosted model would only add
flakiness. The pinned version lives once, in
scripts/lib/opencode-runtime.ts.
Not yet verified at runtime
hosts/opencode/RecallPreCompact.ts (compaction context injection) is covered only by
unit tests of its (input, output) contract. No test drives a real OpenCode
compaction, so its runtime behaviour is unproven — the README roadmap says so
rather than implying otherwise.
Non-Goals
- Rewriting the MCP server for OpenCode
- Real-time sync between platforms (WAL + shared DB is sufficient)
- OpenCode-specific MCP tools (same tools serve both)
- Direct DB writes from plugin (CLI/drop-dir keeps plugin decoupled)