* feat: OpenCode plugin with 22 auto-capture hooks (closes #156) - 22 hook handlers across session lifecycle, messages, tool lifecycle, parts, files, permissions, tasks, commands, and config - Two-layer enrichment pipeline: /context + /enrich via system.transform - Two slash commands: /recall and /remember - Full Claude Code hook parity documented with gap analysis * fix(plugin): use prompt_submit hookType so sessions get firstPrompt mem::observe checks hookType === "prompt_submit" to extract raw.userPrompt and set session.firstPrompt. The plugin was using "user_prompt_submit" which didn't match, so sessions were never named. * fix: address CodeRabbit review feedback on OpenCode plugin - Use ctx.worktree for projectPath instead of opaque project.id - Add Array.isArray guards before output.system/.context.push() - Only delete stashed files after successful enrich POST - Defensive JSON.stringify for undefined tool inputs - Per-session Map-based dedup sets to prevent unbounded growth - Fix negative duration_ms when time.completed is unset - Validate props.file and enforce MAX_STASHED_FILES in file.edited - Deduplicate /summarize call on session idle - Buffer early config events until session.created flushes them - Move contextInjectedSessions.add after successful context fetch - Add DEBUG-gated error logging to network helpers - Guard config input.agent/mcp/provider against non-object types - Fix MCP badge count 44→51 in plugin README * fix: add language identifier to fenced code block in plugin README * fix: store OpenCode session title as summary/firstPrompt on creation * fix(plugin): add session instruction injection and consolidation pipeline (closes #233) Three gaps from the Claude Code plugin port sweep: - Inject agentmemory usage instructions (memory_save, memory_recall, etc.) into the system prompt on first turn via experimental.chat.system.transform, replacing the skills mechanism that OpenCode lacks - Call /crystals/auto and /consolidate-pipeline on session.deleted, mirroring Claude's CONSOLIDATION_ENABLED behavior - Document MEMORY.md vs AGENTS.md architecture comparison (two-hop file bridge vs one-hop direct injection) Gap A (SubagentStop) is unfixable — OpenCode's SubtaskPart type has no completion/result fields. Gap C (Claude MEMORY.md bridge) is intentionally skipped — OpenCode uses direct injection. * fix(plugin): address adversarial code review — 7 critical/high fixes - Guard instructions push with Array.isArray check + fix TOCTOU race by moving contextInjectedSessions.add() before the await - Session-scope stashedFiles via stashFor() helper (was process-global, could cross-contaminate concurrent sessions) - Fix tool prefix in instructions (agentmemory_memory_ not agentmemory_) - Check typeof string before pushing .context into system arrays - Change olderThanDays: 0 → 7 in consolidation fire-and-forget - Increase fire-and-forget timeout from 5s to 30s (consolidation takes minutes, 5s was guaranteed to abort) * fix(plugin): scope stashedFiles.delete(sid) not .clear() on session.deleted * fix(plugin): address round 2 adversarial review — 3 critical fixes - Fix session cross-contamination: file.edited and tool.execute.before now use props.sessionID/input.sessionID fallback instead of only activeSessionId, preventing subagent hijack of parent stash - Fix contextInjectedSessions regression from round 1: move add(sid) to after instructions push (synchronous) but before context fetch (async), so failed /context calls don't permanently skip injection - Fix Map memory leak: prune session entries (stashedFiles, seenSubtaskIds, seenToolCallIds) when session.status goes idle, preventing unbounded growth for crash-killed sessions * fix(plugin): address round 2 remaining medium issues - Enforce MAX_STASHED_FILES cap on chat.message (was missing, could grow unbounded from 50+ file-ref messages) - Remove activeSessionId fallback from session.deleted; log warning when both info.id and sessionID are missing instead of guessing - Add subtaskSetFor/toolCallSetFor lazy-init wrappers matching stashFor pattern; fixes dedup failures for subagents spawned without a preceding session.created event - Guard chat.params against missing input.model (TypeError crash) * fix(plugin): address round 3 adversarial review — 10 HIGH/MEDIUM fixes - Add safeSlice() helper replacing all unsafe (v as string || "").slice() calls; handles objects/BigInt/circular refs via try/catch JSON.stringify - Restore activeSessionId fallback on session.deleted (if both info.id and sessionID missing, fall back like every other handler) - Fix message.updated to check info.id before info.sessionID (matches session.created/session.updated pattern) - Add props.sessionID to message.part.updated fallback chain - Guard undefined callID and subtask ID (prevent dedup silently dropping all subsequent undefined-ID events) - Cap todo.updated at 100 entries (match session.diff's slice pattern) - Fix duration_ms || → ?? (0ms genuine duration no longer falsifies) - Fix extractErrorMessage || → ?? (0/false error values preserved) - Replace extractErrorMessage.slice with safeSlice in retry handler * fix(plugin): address round 4 adversarial review — 6 CRITICAL/HIGH fixes CRITICAL: - Revert message.updated session ID resolution: info.id is the message UUID, not session ID. In message.updated, info is the message object (has role/tokens/modelID), not session info like in session.created. Use props.sessionID || info.sessionID || activeSessionId instead. - Fix post_tool_use_failure -> post_tool_failure: server types.ts defines "post_tool_failure" (no "use_"); compress-synthetic.ts classifies by that exact string. All error observations were misclassified as "other" instead of "error". - Fix contextInjected ordering: move add(sid) after the context fetch completes, not before. If /context times out, session is no longer permanently marked injected, allowing retry on next transform. HIGH: - Add process.cwd() fallback for projectPath (was null, causing silent 400s on every REST call when no workspace) - Guard session.start against null activeSessionId with early return (was sending sessionId: null to API) - Fix duration_ms: use typeof number checks instead of || 0 defaults; missing timing data now correctly reports null instead of 0ms * fix(plugin): address final adversarial blockers --------- Co-authored-by: xuli500177 <62830942+xuli500177@users.noreply.github.com> Co-authored-by: Trip <5579540+cl0ckt0wer@users.noreply.github.com> Co-authored-by: Rohit Ghumare <ghumare64@gmail.com>
10 KiB
agentmemory for OpenCode
Your OpenCode agents remember everything. No more re-explaining.
Persistent cross-session memory via agentmemory — 95.2% retrieval accuracy on LongMemEval-S.
Quick start
1. Start the agentmemory server
npx @agentmemory/agentmemory
The server starts on http://localhost:3111.
2. Configure the MCP server
Add to ~/.config/opencode/opencode.json or your project's .opencode/opencode.json:
{
"mcp": {
"agentmemory": {
"type": "local",
"command": ["npx", "-y", "@agentmemory/mcp"],
"enabled": true
}
}
}
3. Install the plugin
Add to ~/.config/opencode/opencode.json:
{
"plugin": ["./plugins/agentmemory-capture.ts"]
}
Copy the plugin file from this repo:
mkdir -p ~/.config/opencode/plugins
cp plugin/opencode/agentmemory-capture.ts ~/.config/opencode/plugins/
4. Add the slash commands
Copy the commands into your project or global .opencode/commands/ directory:
mkdir -p ~/.config/opencode/commands
cp plugin/opencode/commands/recall.md ~/.config/opencode/commands/
cp plugin/opencode/commands/remember.md ~/.config/opencode/commands/
Restart OpenCode or open a new session. The plugin auto-captures everything.
What gets captured
Session lifecycle
| Event | Hook | agentmemory API |
|---|---|---|
| Session start | session.created |
POST /session/start |
| Idle → summarize | session.idle + session.status (idle) |
POST /summarize |
| Status transitions | session.status (idle/busy/retry) |
POST /observe |
| Compaction | session.compacted |
POST /summarize + POST /observe |
| Metadata updates | session.updated |
POST /observe |
| Code change tracking | session.diff |
POST /observe |
| Session delete | session.deleted |
POST /session/end |
| Session error | session.error |
POST /observe |
Messages & prompts
| Event | Hook | agentmemory API |
|---|---|---|
| User prompt (rich) | chat.message |
POST /observe |
| User prompt metadata | message.updated (user) |
POST /observe |
| Assistant response | message.updated (assistant) |
POST /observe |
| Message removed (undo) | message.removed |
POST /observe |
Parts & steps
| Event | Hook | agentmemory API |
|---|---|---|
| Subagent start | message.part.updated (subtask) |
POST /observe |
| Tool completed | message.part.updated (tool completed) |
POST /observe |
| Tool error | message.part.updated (tool error) |
POST /observe |
| Step finish (cost/tokens) | message.part.updated (step-finish) |
POST /observe |
| Reasoning trace | message.part.updated (reasoning) |
POST /observe |
| Patch applied | message.part.updated (patch) |
POST /observe |
| Auto/manual compaction | message.part.updated (compaction) |
POST /observe |
| Agent selection | message.part.updated (agent) |
POST /observe |
| API retry | message.part.updated (retry) |
POST /observe |
File enrichment pipeline
| Event | Hook | agentmemory API |
|---|---|---|
| File tool params | tool.execute.before → stash paths |
— |
| File edited | file.edited → stash paths |
— |
| File part attached | message.part.updated (file) → stash paths |
— |
| Enrichment inject | experimental.chat.system.transform |
POST /enrich → output.system[] |
| Memory context inject | experimental.chat.system.transform |
POST /context → output.system[] |
Permissions
| Event | Hook | agentmemory API |
|---|---|---|
| Permission prompt | permission.updated |
POST /observe |
| Permission reply | permission.replied |
POST /observe |
Tasks & commands
| Event | Hook | agentmemory API |
|---|---|---|
| Task tracking (w/ priority) | todo.updated |
POST /observe |
| Command executed | command.executed |
POST /observe |
Model & config
| Event | Hook | agentmemory API |
|---|---|---|
| LLM parameters | chat.params |
POST /observe |
| Config loaded | config |
POST /observe |
| Compaction (WIP) | experimental.session.compacting |
POST /context → output.context[] |
File enrichment + memory injection (two-layer pipeline)
experimental.chat.system.transform fires before every LLM call and injects two layers of context:
-
Memory context (once per session): calls
/agentmemory/contextand injects project profile, recent session summaries, and important past observations into the system prompt. This is the OpenCode equivalent of Claude's MEMORY.md bridge — instead of syncing to a markdown file, context is injected directly into the system prompt. -
File enrichment (every turn with stashed files): calls
/agentmemory/enrichwith files stashed bytool.execute.before,file.edited, andmessage.part.updated(file parts). File-specific context (past observations, related bugs, semantic search) is injected into the system prompt.
System prompt = [OpenCode instructions] + [memory context] + [file enrichment] + [user message]
^ ^
first turn only every file-touching turn
Differences from Claude's PreToolUse:
| Dimension | Claude (PreToolUse) | OpenCode (two-hop pipeline) |
|---|---|---|
| Injection mechanism | stdout → context window | output.system[] → system prompt |
| Timing | Same turn (parallel with tool) | Next turn (before next LLM call) |
| File set | Per-tool (immediate) | Batched (all files since last enrichment) |
| Coverage | Edit/Write/Read/Glob/Grep only | Edit/Write/Read/Glob/Grep only |
| What gets injected | <agentmemory-file-context> + bug memories |
Identical /enrich response |
MEMORY.md vs AGENTS.md: how context flows
Claude Code and OpenCode take fundamentally different approaches to injecting memory context into the agent's system prompt.
Claude Code: file-backed bridge (two-hop)
agentmemory ──write──▶ MEMORY.md ──read──▶ Claude system prompt
- The
claude-bridge/syncendpoint serializes agentmemory observations into aMEMORY.mdfile in the project root - Claude Code reads
MEMORY.mdon session start and prepends it to the system prompt - Sync is periodic — sessions only get fresh context when the bridge last ran (session end, pre-compact)
- Coupling: memory data lives in a git-trackable file, visible to CI, team members, and other tools
OpenCode: direct injection (one-hop)
agentmemory ──push──▶ OpenCode system prompt
experimental.chat.system.transformcalls/contextat runtime and pushes the response directly intooutput.system[]- Always current — context is fetched at session start (once) and before file-touching turns (per-batch)
- No file intermediary — no stale copies, no merge conflicts, no disk I/O
AGENTS.mdis a static instruction file for project conventions, coding standards, and tool guidance — agentmemory does not read or write it
Tradeoffs
| Dimension | Claude (MEMORY.md bridge) | OpenCode (direct injection) |
|---|---|---|
| Freshness | Stale between syncs | Always current (fetched at call time) |
| Visibility | Human-readable file in repo | In-memory injection only |
| Simplicity | Two moving parts (bridge + file) | One step (API → system prompt) |
| Team sharing | File is git-trackable, CI-friendly | Memory shared via agentmemory server API |
| Integration | Any tool can read MEMORY.md | Requires OpenCode plugin SDK |
Why OpenCode goes direct
agentmemory already persists everything in SQLite (data/state_store.db). Adding an intermediate MEMORY.md file would duplicate data, introduce sync lag, and require the model to re-parse structured context from markdown. Direct injection delivers the same data with lower latency and zero staleness — the agent always sees what agentmemory knows right now.
Slash commands
/recall <query>— Search past observations and lessons/remember <text>— Save an insight to long-term memory
Session instruction injection
Agentmemory usage instructions are injected into the system prompt on the first turn of every session via experimental.chat.system.transform (alongside memory context from /context). This is functionally equivalent to Claude Code's skills mechanism — the agent learns which agentmemory_memory_* tools to use and when, without needing separate skill invocations.
What's not covered (vs Claude Code plugin)
| Claude feature | Reason |
|---|---|
| SubagentStop | OpenCode's SubtaskPart type has no completion/result fields; subtask lifecycle ends are not exposed as distinct events in the OpenCode SDK |
| TaskCompleted | No team/teammate concept in OpenCode; todo.updated captures task state changes as a partial equivalent |
| Stop | session.compacted event handler exists; experimental.session.compacting injection hook defined in SDK but Go binary (v1.14.41) doesn't wire it — will auto-activate when upstream implements it |
| Skills (remember/recall/forget/session-history) | Covered by injected system instructions via experimental.chat.system.transform — agent receives usage guidance on first turn |
| Consolidation pipeline (crystals/auto + consolidate-pipeline) | Now called on session.deleted — mirrors Claude's CONSOLIDATION_ENABLED=true behavior |
| Claude MEMORY.md bridge | OpenCode-specific; OpenCode uses its own AGENTS.md mechanism, not Claude's MEMORY.md |
All other Claude Code hooks have direct or pipeline equivalents in this plugin. 12 of 12 Claude hook types covered.