Files
Trip 68fddd418e feat: OpenCode plugin with 22 auto-capture hooks (closes #236, #244) (#237)
* 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>
2026-05-19 00:22:42 +01:00

10 KiB

OpenCode  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.

53 MCP tools 22 hooks 2 slash commands 95.2% R@5


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:

  1. Memory context (once per session): calls /agentmemory/context and 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.

  2. File enrichment (every turn with stashed files): calls /agentmemory/enrich with files stashed by tool.execute.before, file.edited, and message.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/sync endpoint serializes agentmemory observations into a MEMORY.md file in the project root
  • Claude Code reads MEMORY.md on 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.transform calls /context at runtime and pushes the response directly into output.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.md is 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.