@eleboucher/opencode-memini
v0.7.36
Published
Automatic cross-session memory for opencode via memini — recall before each turn, capture after.
Maintainers
Readme
memini + opencode
opencode supports plugins
— JS/TS modules that hook into its lifecycle. memini ships a native plugin in
plugin/ that makes memory automatic: it recalls relevant memories
before each turn and captures completed turns afterward, with no tool calls
required from the model.
Recommended: the memory plugin
What it wires (two hooks):
chat.message— searches memini for the incoming user message and prepends the matches as a synthetic context part before the turn runs. It excludes this session's own captured turns (already in the live context), so they aren't echoed back as memory a turn behind; past sessions still recall.event(session.idle) — once the session goes idle, stores the completed user/assistant turn back into memini (tagged with the session id) so it can be recalled later.
Install
Add the package to the plugin array in your opencode.json — a project
opencode.json to scope it to one repo, or ~/.config/opencode/opencode.json
for every project:
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["@eleboucher/opencode-memini"]
}opencode installs it from npm with Bun at startup.
opencode v2 (beta)
opencode's v2 preview (opencode2) uses a different plugin system: a plugins
(plural) config array and a Plugin.define({ id, setup }) module — the v1
{ id, server } plugin above does not load under it. memini ships a v2 sibling
that wires the same recall / capture / memini_status behaviour to v2's
session hooks, public event stream, and tool transforms.
The v2 loader treats a plugins entry as either an npm package specifier or a
path. It cannot install this package's /v2 export subpath as an npm package,
so v2 ships under its own package name:
{
"$schema": "https://opencode.ai/config.json",
"plugins": ["@eleboucher/opencode-memini-v2"],
}A bare entry resolves to the latest dist-tag, so that is where this package
is published. opencode2 plugin add @eleboucher/opencode-memini-v2 writes the
entry for you, using whatever specifier you pass it.
Why a second package rather than one with two entrypoints? opencode resolves a plugin through
exports["./server"]before falling back tomain, and it uses that sameserverkind for v1-style plugins. Adding a./serverkey to@eleboucher/opencode-meminiwould therefore hand the v2 module to v1 hosts, which cannot load it.
To run from a checkout, use the local entrypoint. opencode does not install dependencies for path-loaded plugins; this one has none, so nothing else is needed:
{
"plugins": ["/absolute/path/to/memini/integrations/opencode/plugin/memini-v2.js"],
}For options, use the documented object form:
{
"plugins": [
{
"package": "@eleboucher/opencode-memini-v2",
"options": { "namespace": "my-project" },
},
],
}The same options and env vars below apply. Recall injects into the request's
system prompt (rather than a synthetic message part).
Beta status. Verified against
opencode2 v0.0.0-next-16502and re-checked throughv0.0.0-beta-18050: the working request hook iscontext(the docs call itrequest); user/assistant turns are captured fromsession.inbox.enqueued,session.text.ended, andsession.execution.*because these builds emit nosession.idleand expose no session-message list method.next-17444renamed the first event fromsession.input.admittedand moved its text frominput.datatoitem.payload; the plugin reads both shapes, and registers both hook names so it remains compatible with the documented spelling.
The v2 plugin sends diagnostics through the structured ctx.app.log logger when
available. Logging is best-effort and remains silent when that logger is absent
or unavailable.
Configure
Pass options inline via the [name, options] form:
{
"plugin": [["@eleboucher/opencode-memini", { "namespace": "my-project", "recall_limit": 8 }]]
}| Option | Env var | Default | Purpose |
| ------------------------- | -------------------------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| base_url | MEMINI_BASE_URL | http://localhost:8080 | memini REST base URL |
| namespace | MEMINI_NAMESPACE | server handshake | project the memory is scoped to (X-Memini-Namespace) |
| home | MEMINI_HOME | unset | caller's personal namespace, sent as X-Memini-Home; unset = no home leg |
| recall | MEMINI_RECALL | on | false disables recall-before-turn |
| capture | MEMINI_CAPTURE | on | false disables capture-after-turn |
| capture_child_sessions | MEMINI_CAPTURE_CHILD_SESSIONS | off | capture child sessions; off skips children and unknown ancestry (fail-closed), on records ancestry metadata |
| recall_limit | MEMINI_RECALL_LIMIT | 3 | max memories injected per turn |
| recall_max_tokens | MEMINI_INJECT_RECALL_MAX_TOK | 0 | hard ceiling on the recall-block tokens (0 = unbounded); the tail is dropped with a [… N item(s) truncated by token budget] footer |
| recall_min_score | MEMINI_INJECT_RECALL_MIN_SCORE | server default 0.5 (0 pre-handshake) | composite floor (>=) on the final ranked score, sent as min_rank_score (server-enforced; stripped + applied client-side on pre-min_rank_score servers, and for values ≥ 1) |
| inject_cooldown_ms | MEMINI_INJECT_COOLDOWN_MS | 1800000 | repeat-injection cooldown, time window (ms): an already-injected memory is held back this long before it may re-serve; 0 disables the time dimension |
| inject_cooldown_prompts | MEMINI_INJECT_COOLDOWN_PROMPTS | 3 | repeat-injection cooldown, prompt window (counted per user message); 0 disables the prompt dimension; both cooldown knobs 0 = suppress for the whole session |
| recall_budget_ms | MEMINI_RECALL_BUDGET_MS | 2000 | how long a turn waits for recall before proceeding without it (0 = wait for the full timeout_ms) |
| timeout_ms | MEMINI_TIMEOUT_MS | 30000 | per-request timeout for memini requests |
| fallback_on_error | MEMINI_FALLBACK | on | false surfaces errors instead of degrading silently |
| auto_update | MEMINI_AUTO_UPDATE | on | false disables npm auto-update checks (opencode never re-fetches cached plugins otherwise) |
| — | MEMINI_INJECT_LABELS | — | comma-separated label toggles for each bullet: tier, confidence, age, reason |
| — | MEMINI_API_KEY | — | bearer token, if memini needs auth (env only — secret) |
| — | MEMINI_REQUIRE_HTTPS | — | 1 refuses to send the token over plaintext HTTP |
opencode awaits chat.message before the model sees the message, so a slow or
unreachable memini would otherwise freeze the turn for the full timeout_ms.
Instead, recall races recall_budget_ms: if the search hasn't answered in time,
the turn proceeds without memories and eventual results are discarded. Errors
from the background request are still logged. The plugin also pings /healthz
once at startup to warm the connection, so the first recall doesn't pay the
DNS/TLS cold-start. Set recall_budget_ms: 0 to restore fully blocking
same-turn injection.
Automatic captures leave tier selection to the server. Before retrieving
messages, the plugin resolves session ancestry. Root sessions are captured by
default; child sessions and sessions whose ancestry cannot be resolved are
skipped by default. Set capture_child_sessions (or
MEMINI_CAPTURE_CHILD_SESSIONS=1) to opt in; captures then include
metadata.session_type (root, child, or unknown) and child captures also
include metadata.parent_session_id.
Repeat-injection cooldown
The two inject_cooldown_* knobs are the windowed repeat-injection
cooldown (shared with the Claude Code / hermes / openclaw integrations): an
already-injected memory is excluded from recall (server-side via
exclude_ids, with a client-side backstop) while it is inside either window
— the time window (inject_cooldown_ms) or the prompt window
(inject_cooldown_prompts, one prompt = one user message; in the v2 plugin,
one request-hook fire) — and is re-served once both have lapsed. A
memory whose content was updated in place re-injects immediately (the
content-hash bypass), so a correction is never withheld for the window.
Behavior change (was: suppress forever). Earlier versions suppressed an injected memory for the whole session. With the default windows (30 min / 3 prompts) a long session is now re-reminded of a still-relevant memory once enough of the conversation has moved past it. Set both knobs to
0to restore the old suppress-for-the-whole-session behavior.
Inline options win over the env vars. Secrets stay in the environment: set
MEMINI_API_KEY (sent as Authorization: Bearer …), and optionally
MEMINI_REQUIRE_HTTPS=1 to refuse plaintext HTTP, in the shell that launches
opencode — not in opencode.json.
Every option is optional, namespace included: ["@eleboucher/opencode-memini"]
runs with no config. On plugin load (and again every 10 minutes thereafter),
the plugin calls POST /v1/handshake with what it cheaply knows about the
project (the git remote/toplevel, when the worktree is a repo, plus the
worktree basename) and lets the server resolve the namespace and behavioral
settings (recall, capture, recall_limit, the recall-injection budget)
the same way every other memini client does. The call is fail-soft: any
error or a ~2.5s timeout falls back to purely local resolution below, so an
unreachable or older memini never breaks a turn.
Namespace resolution
In full, in order: the namespace option / MEMINI_NAMESPACE > the
server's handshake-resolved namespace > the git worktree basename > the
built-in default (opencode).
The option/env tier wins over the handshake outright and deliberately: a
namespace option in a global ~/.config/opencode/opencode.json, or a
globally exported MEMINI_NAMESPACE, is this integration's own explicit pin
and is honored as such rather than second-guessed by the server. Absent
either, the server's resolution (which can draw on a pin, the git remote, or
an operator's per-key default) wins over this plugin's own git
worktree/default fallback, which only applies when the handshake itself is
unavailable.
Each recall/capture setting follows the same shape: the plugin option beats
MEMINI_NAMESPACE's sibling env vars above beats the server's resolved
ClientSettings beats the built-in default baked into this plugin.
The memini_status tool
The plugin registers one tool, memini_status: read-only, no arguments. It
reports the namespace in force and where it came from (the namespace option,
MEMINI_NAMESPACE, the server's handshake, or the git worktree fallback),
what it would be without the env/option pin, the connection settings (the
API key fingerprinted, never printed), and warnings — a global
MEMINI_NAMESPACE pin, a bearer token crossing plaintext HTTP.
There is no /memini:status slash command: opencode's plugin contract registers
tools, not commands, and this plugin does not invent an API it does not have.
Tests
cd integrations/opencode/plugin && node --testAlternative: manual MCP wire (tools, not automatic)
Instead of the plugin you can expose memini's memory_* tools to the model and
let it call them on demand. opencode reads MCP servers from opencode.json under
mcp. This wire gives the model the full tool set, including memory_recall
(with tags / metadata filters), memory_list (query-less browse by tier /
tags / metadata category), and memory_remember. Set metadata.category on
writes to browse by subject later — see docs/categories.md.
Remote: merge opencode.json into your config.
Local (stdio): see opencode.local.json — opencode
spawns memini mcp.
Mind the context budget: memini adds a handful of tools, which is light, but
disable other large MCP servers you aren't using. Use the same
X-Memini-Namespace as your other agents to share memory. my-project is a
placeholder — replace it with your real project name, or drop the header and let
memini auto-resolve from its own working directory.
