@josxa/opencode-recall
v1.1.0
Published
OpenCode plugin that adds semantic + lexical recall over your local session history, with cursor-paginated ChatML transcript reads.
Maintainers
Readme
@josxa/opencode-recall
🧠 Searchable memory for OpenCode. Ask "what did we figure out about X last month?" and the agent actually finds it.
Every OpenCode session is already saved locally. Recall makes them searchable, so the agent can pull the right slice of a past conversation back into context on demand, instead of you re-explaining yourself or dumping whole sessions into the prompt.
you ask you (or the agent) follow up
"what did we figure out "show me more around that one"
about rate limiting?" "what came right before?"
│ │
▼ ▼
┌────────────────────┐ ┌──────────────────────────┐
│ search history │ ─ hits + cursors ──► │ ranked snippets │
└────────────────────┘ └────────────┬─────────────┘
│
pick a cursor
│
▼
┌──────────────────────────┐
┌──────►│ read transcript │
│ └────────────┬─────────────┘
│ │
│ transcript + cursors
│ │
│ ▼
│ ┌──────────────────────────┐
next page
└──────►│ pull surrounding │
│ messages (ChatML) │
└──────────────────────────┘Why Recall?
You've already solved this problem. You debugged this exact error six weeks ago in another project. You worked out the deploy steps in a session you can't find anymore. The knowledge is there, sitting in opencode.db, but the agent can't see it.
Recall fixes that:
🔎 Find the right session in seconds. Hybrid lexical + semantic search across every project you've used OpenCode in.
📜 Read the actual conversation, not a summary. The agent pages a bounded window of messages around the match. Tool calls, patches, and file attachments render as structured ChatML, not a wall of JSON.
🎯 Explicit by design. Recall doesn't auto-inject memories. You (or the agent, when you ask) decide when to search. Your context window stays lean and you always know why something showed up in the prompt.
🔒 Local and read-only. Your opencode.db is never written to. The embedding index lives in a sidecar SQLite file you can delete any time.
What it looks like
Simple recall: "remind me how we did this."

Iterative recall: agent reformulates the search when the first pass is thin.

Install
opencode plugin @josxa/opencode-recall -gfThis installs the package and wires it into your global OpenCode config.
pnpm add -D @josxa/opencode-recallThen register the plugin in opencode.json or ~/.config/opencode/opencode.json:
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["@josxa/opencode-recall"]
}Set up embeddings (Ollama)
Semantic search uses Ollama running locally. Install it, that's it:
ollama --version # should print a version. install from https://ollama.com/download if not.Everything else is handled for you. The first history_search call will start ollama serve if it isn't running, pull the default embedding model (all-minilm, small and fast) if it isn't installed, and build the sidecar index incrementally. Subsequent calls reuse and sync it.
Lexical search still works without Ollama, but you lose paraphrase recall, which is half the value.
How to actually use it
Recall is explicit. Ask the dedicated subagent when you want old session context:
@recall how did we set up trusted publishing last time?
@recall find the session where we debugged recall lookup accuracy
@recall what did we decide about the rate limiter design?The recall subagent starts with fresh context, searches and reads past sessions, then returns only a concise answer to the parent agent. It reports useful source session ids, titles, and directories, but not internal msg_... cursors unless you explicitly ask for raw read anchors. That gives you the useful part of session-level querying without dumping raw transcript pages into the main conversation. If you ask a follow-up about the same recalled session, the parent should invoke @recall again instead of answering from memory.
The subagent only checks OpenCode history. It does not read current files, run commands, or verify whether old session facts are still true.
Some natural-language prompts that should trigger recall:
- "Recall how we set up the GitHub Actions release workflow."
- "Did we ever debug the Postgres connection pool exhaustion? Find that conversation."
- "Pull up the session where we discussed the rate limiter design."
- "Search my history for anything about Figma MCP and Azure."
- "What were the file names involved when we worked on the timeline component?"
The recall subagent runs history_search, picks a promising hit, calls history_read to load the surrounding messages, and can page forward or back if more context is needed.
Configuration
Recall creates a config file automatically on first use:
<opencode-config-base-path>/recall.jsoncOn a normal Linux/macOS setup that is ~/.config/opencode/recall.jsonc.
Most users never need to edit it. Open it when your OpenCode database lives somewhere unusual, you want the sidecar index somewhere else, or you want to try a different embedding model.
{
"$schema": "https://raw.githubusercontent.com/JosXa/opencode-recall/main/schema/config.schema.json",
"database": {
// OpenCode session database. Opened read-only; recall never writes here.
// Default: ~/.local/share/opencode/opencode.db
"path": "~/.local/share/opencode/opencode.db",
// Sidecar embedding index. Safe to delete; rebuilt on next search.
// Default: ~/.local/share/opencode/opencode-recall-index.db
"indexPath": "~/.local/share/opencode/opencode-recall-index.db"
},
"embeddings": {
// Ollama base URL.
// Default: http://127.0.0.1:11434
"ollamaUrl": "http://127.0.0.1:11434",
// Try "mxbai-embed-large" for higher quality at the cost of speed and memory.
// Default: all-minilm
"model": "all-minilm"
}
}Environment variables still work as overrides for CI, MCP, and temporary experiments: OPENCODE_DB_PATH, OPENCODE_RECALL_DB_PATH, OPENCODE_RECALL_OLLAMA_URL, and OPENCODE_RECALL_EMBED_MODEL.
Run pnpm run eval:embeddings to compare installed embedding models against the local regression cases in docs/real-history-regressions.md.
Choosing the recall subagent model
Recall integrates with the native OpenCode agent.recall configuration instead of replacing it. The plugin always supplies Recall's description, subagent mode, prompt, and history-only tool permissions. Other agent settings, including model, variant, and temperature, stay under your control.
For example, configure a small model for Recall independently of the parent agent:
// ~/.config/opencode/opencode.json
{
"$schema": "https://opencode.ai/config.json",
"agent": {
"recall": {
"model": "example-provider/recall-mini",
"variant": "low"
}
}
}Recall keeps this model when a parent agent uses a different one, matching native OpenCode subagent behavior. Each machine can select a provider and model available in its own OpenCode configuration. When agent.recall is unset, Recall uses OpenCode's normal default model resolution.
Tool reference
Recall exposes four history tools. They are intended to be called by the recall subagent, not by the main agent directly.
| Arg | Type | Notes |
| -------- | -------- | ------------------------------------------------------------------------------------------------------ |
| n | number | Max sessions (default 20, max 100). |
| title | string | Case-insensitive session title filter. |
| directory | string | Exact OpenCode session directory filter. |
| after | ISO date | Only sessions updated at or after this timestamp. |
| before | ISO date | Only sessions updated at or before this timestamp. Defaults to now - 30 s to exclude the live chat. |
Returns newest-first session rows with a ses_... cursor for history_read and quick usefulness signals:
[
{
"cursor": "ses_...",
"sid": "ses_...",
"title": "Release debugging notes",
"directory": "/Users/you/projects/foo",
"updated": "2026-04-12T08:21:44.000Z",
"messages": 84,
"turns": 18,
"assistantMessages": 32,
"toolMessages": 9,
"textParts": 76,
"approxContextChars": 118420
}
]Use this when you do not have a search term yet and want to spot substantial past sessions at a glance.
| Arg | Type | Notes |
| -------- | -------------------------------- | ----------------------------------------------- |
| cursor | string | Required. A ses_... session cursor only. |
| path | string | Required. Workspace-relative destination. |
| format | chatml | markdown | jsonl | Transcript encoding. Default chatml. |
Writes the full normalized session transcript and returns a compact receipt:
{
"path": "recall/ses_....chatml",
"bytes": 48321,
"messages": 94
}The destination is always overwritten and must stay inside the active workspace.
| Arg | Type | Notes |
| -------- | -------- | ------------------------------------------------------------------------------------------------------ |
| q | string | Required. Free-text query. |
| n | number | Max hits (default 8, max 25). |
| directory | string | Exact OpenCode session directory filter. |
| after | ISO date | Only messages at or after this timestamp. |
| before | ISO date | Only messages at or before this timestamp. Defaults to now − 30 s to exclude the live conversation. |
Returns a JSON array of compact hits:
[
{
"cursor": "msg_…", // opaque; pass to history_read
"sid": "ses_…", // OpenCode session id
"dir": "/Users/you/projects/foo",
"title": "Figma MCP server on Azure API Center",
"time": "2026-04-12T08:21:44.000Z",
"role": "assistant",
"score": 0.7421,
"text": "…snippet capped at 280 chars…"
}
]| Arg | Type | Notes |
| -------- | ------ | ---------------------------------------------------------------------------------------------- |
| cursor | string | Required. A msg_…, a ses_…, or an encoded cursor from history_search. |
| mode | string | around (default), next, prev, head, or tail. full is rejected; page instead. |
| n | number | Message count. Default 12, max 50. |
Returns a ChatML-like transcript window:
<hist sid="ses_…" dir="/…" mode="around" range="42-53" anchor="48" total="120" title="…">
<|im_start|>user name="msg_…" index="42" time="2026-04-12T08:21:44.000Z"
…text and tool_call blocks…
<|im_end|>
…more messages…
<nav cur="…" prev="…" head="…" next="…" tail="…" />
</hist>Tool calls, patches, and file attachments render as structured tags with explicit truncated / original_chars markers when content is capped. The <nav/> element gives the agent cursors to keep paging without re-searching. To read from the end upward, start with mode="tail", then pass the returned prev cursor back with mode="prev" until enough context is loaded. mode="full" is intentionally rejected because large transcript responses can still be truncated by the outer tool transport.
How it works
┌───────────────────┐ history_search ┌──────────────────────┐
│ OpenCode │ ───────────────────► │ Ranked anchors │
│ │ │ (opaque cursors) │
└───────────────────┘ ◄─────────────┐ └──────────┬───────────┘
│ history_read │ │
▼ │ ChatML window │
┌───────────────────┐ pagination │ │
│ ChatML window │ ◄─────────────┘ │
│ with <nav/> │ │
└───────────────────┘ │
▼
┌──────────────────────────────┐
│ opencode.db (read-only) │
│ + sidecar embedding index │
└──────────────────────────────┘- Source of truth.
opencode.dbis opened read-only. The plugin never writes to it. - Sidecar index. A separate SQLite database (
opencode-recall-index.db) stores text chunks, content hashes, andFloat32embedding blobs. Syntheticsession-title:<id>rows are indexed so proper-noun title queries beat noisy snippet matches. - Sync. Each
history_searchcall performs an incremental sync with a 30-minute overlap window and a lock so concurrent agents don't fight. Stale rows are pruned by comparing part ids. - Ranking. Lexical and semantic candidates are merged, scored with title/text/directory term ratios plus phrase boosts, filtered to require enough query-term overlap, and diversified to at most two hits per session. A small semantic-rescue lane admits paraphrase matches that miss lexical filters but have high embedding similarity.
- Reads. Windows are computed by
row_number()over(session_id, time_created, id), then parts are normalized intotext | tool | patch | fileand capped (tool input 2 000 chars, output 6 000 chars).
Development
pnpm install
pnpm run ai:check # biome + tsc type-check
pnpm test # deterministic ranking + cursor tests
pnpm run eval:real-history # regression suite against your local opencode.db
pnpm run build # emits dist/Code quality is enforced by biome and tsc --noEmit. See AGENTS.md for the style guide.
License
MIT © JosXa
