From 524975537b1ebf3b813e40cdfd47c41b477bbe91 Mon Sep 17 00:00:00 2001 From: xuli500177 <62830942+xuli500177@users.noreply.github.com> Date: Thu, 7 May 2026 22:01:51 -0400 Subject: [PATCH 1/4] 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 --- README.md | 14 +- ROADMAP.md | 2 +- plugin/opencode/README.md | 182 +++++++++ plugin/opencode/agentmemory-capture.ts | 546 +++++++++++++++++++++++++ plugin/opencode/commands/recall.md | 19 + plugin/opencode/commands/remember.md | 19 + plugin/opencode/plugin.json | 12 + 7 files changed, 790 insertions(+), 4 deletions(-) create mode 100644 plugin/opencode/README.md create mode 100644 plugin/opencode/agentmemory-capture.ts create mode 100644 plugin/opencode/commands/recall.md create mode 100644 plugin/opencode/commands/remember.md create mode 100644 plugin/opencode/plugin.json diff --git a/README.md b/README.md index eff3339bb..0d98edfb7 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ agentmemory works with any agent that supports hooks, MCP, or REST API. All agen OpenCode
OpenCode
-MCP server +22 hooks + MCP + plugin Codex CLI
@@ -393,7 +393,7 @@ Then add the MCP config for your agent: | **Gemini CLI** | `gemini mcp add agentmemory npx -y @agentmemory/mcp --scope user` | | **Codex CLI** | `codex mcp add agentmemory -- npx -y @agentmemory/mcp` or add `[mcp_servers.agentmemory]` to `.codex/config.toml` | | **pi** | Copy [`integrations/pi`](integrations/pi/) to `~/.pi/agent/extensions/agentmemory` and restart pi | -| **OpenCode** | Add to `opencode.json`: `{"mcp": {"agentmemory": {"type": "local", "command": ["npx", "-y", "@agentmemory/mcp"], "enabled": true}}}` | +| **OpenCode** | Add to `opencode.json`: `{"mcp": {"agentmemory": {"type": "local", "command": ["npx", "-y", "@agentmemory/mcp"], "enabled": true}}}` + copy `plugin/opencode/` for 22 auto-capture hooks | | **Hermes Agent** | Add to `~/.hermes/config.yaml` with `memory.provider: agentmemory` or use the [memory provider plugin](integrations/hermes/) | | **Cline / Goose / Kilo Code** | Add MCP server in settings | | **Claude Desktop** | Add to `claude_desktop_config.json`: `{"mcpServers": {"agentmemory": {"command": "npx", "args": ["-y", "@agentmemory/mcp"]}}}` | @@ -712,10 +712,18 @@ OpenCode (`opencode.json`): "command": ["npx", "-y", "@agentmemory/mcp"], "enabled": true } - } + }, + "plugin": ["./plugins/agentmemory-capture.ts"] } ``` +Copy the plugin file from the repo: +```bash +mkdir -p ~/.config/opencode/plugins +cp plugin/opencode/agentmemory-capture.ts ~/.config/opencode/plugins/ +cp plugin/opencode/commands/*.md ~/.config/opencode/commands/ +``` + ---

Real-Time Viewer

diff --git a/ROADMAP.md b/ROADMAP.md index 66fb54673..f77f371d7 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -37,7 +37,7 @@ Anything not on this list that a contributor wants to pursue is welcome — open ### Planned - [ ] **GitHub connector** (`@agentmemory/github-watcher`) — sync issues, PRs, discussions as observations. Shares the `POST /agentmemory/observe` wire format with the filesystem connector. -- [ ] **OpenCode hook bus** (#156) — if upstream ships hook events, wire them; otherwise ship a REST-polling adapter. +- [x] **OpenCode hook bus** (#156) — wired 22 hooks covering all 12 Claude Code hook types: session lifecycle (create/idle/status/compacted/update/diff/delete/error), messages & prompts (chat.message, message.updated user+assistant, message.removed), tool capture (before + rich ToolPart lifecycle in message.part.updated), memory injection (context + enrich via system.transform), part tracking (subtask, step-finish, reasoning, file, patch, compaction, agent, retry), file enrichment pipeline (stash via tool.execute.before + file.edited + file parts), permissions (updated + replied), task tracking (todo.updated w/ priority), commands (command.executed), config & model tracking (config + chat.params). Plus 2 slash commands (recall/remember). See `plugin/opencode/`. - [ ] **Session replay UI** in the real-time viewer — scrub the timeline, inspect per-observation payloads. - [ ] **Benchmark harness in CI** — keep the 95.2% R@5 number honest across releases by re-running LongMemEval-S on every minor tag. diff --git a/plugin/opencode/README.md b/plugin/opencode/README.md new file mode 100644 index 000000000..cde13ba29 --- /dev/null +++ b/plugin/opencode/README.md @@ -0,0 +1,182 @@ +

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

+ +

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

+ +--- + +## Quick start + +### 1. Start the agentmemory server + +```bash +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`: + +```json +{ + "mcp": { + "agentmemory": { + "type": "local", + "command": ["npx", "-y", "@agentmemory/mcp"], + "enabled": true + } + } +} +``` + +### 3. Install the plugin + +Add to `~/.config/opencode/opencode.json`: + +```json +{ + "plugin": ["./plugins/agentmemory-capture.ts"] +} +``` + +Copy the plugin file from this repo: + +```bash +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: + +```bash +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 | `` + bug memories | Identical `/enrich` response | + +## Slash commands + +- `/recall ` — Search past observations and lessons +- `/remember ` — Save an insight to long-term memory + +## What's not covered (vs Claude Code plugin) + +| Claude feature | Reason | +|---|---| +| SubagentStop | No explicit subtask-completion event; stop boundary is inferred from the observation timeline by the memory system | +| 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 | + +All other Claude Code hooks have direct or pipeline equivalents in this plugin. 12 of 12 Claude hook types covered. diff --git a/plugin/opencode/agentmemory-capture.ts b/plugin/opencode/agentmemory-capture.ts new file mode 100644 index 000000000..8eff1eaf1 --- /dev/null +++ b/plugin/opencode/agentmemory-capture.ts @@ -0,0 +1,546 @@ +import type { Plugin } from "@opencode-ai/plugin"; + +const API = process.env.AGENTMEMORY_URL || "http://localhost:3111"; +const FILE_TOOLS = new Set(["Read", "Write", "Edit", "Glob", "Grep"]); +const FILE_KEYS = ["filePath", "file_path", "path", "file", "pattern"]; +const MAX_STASHED_FILES = 20; + +async function post(path: string, body: Record): Promise { + try { + await fetch(`${API}/agentmemory${path}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + signal: AbortSignal.timeout(5000), + }); + } catch {} +} + +async function postJson(path: string, body: Record): Promise { + try { + const res = await fetch(`${API}/agentmemory${path}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + signal: AbortSignal.timeout(5000), + }); + return res.ok ? res.json() : null; + } catch { + return null; + } +} + +async function observe( + sessionId: string, + hookType: string, + data: Record, +): Promise { + await post("/observe", { + hookType, + sessionId, + project: projectPath, + cwd: projectPath, + timestamp: new Date().toISOString(), + data, + }); +} + +let activeSessionId: string | null = null; +let projectPath: string | null = null; +const stashedFiles = new Set(); +const seenSubtaskIds = new Set(); +const contextInjectedSessions = new Set(); +const seenToolCallIds = new Set(); + +function extractFilePaths(args: Record): string[] { + const files: string[] = []; + for (const key of FILE_KEYS) { + const val = args[key]; + if (typeof val === "string" && val.length > 0) { + files.push(val); + } + } + return files; +} + +function extractErrorMessage(err: unknown): string { + if (typeof err === "string") return err; + if (err && typeof err === "object") { + const e = err as Record; + if (typeof e.message === "string") return e.message; + if (e.data && typeof e.data === "object") { + const d = e.data as Record; + if (typeof d.message === "string") return d.message; + } + if (typeof e.name === "string") return e.name; + try { return JSON.stringify(err); } catch { return ""; } + } + return String(err || ""); +} + +export const AgentmemoryCapturePlugin: Plugin = async (ctx) => { + projectPath = ctx.project?.id || ctx.worktree || null; + + return { + event: async ({ event }) => { + const type = event.type; + const props = (event as any).properties || {}; + + // ── session.created ── + if (type === "session.created") { + const info = props.info as Record | undefined; + activeSessionId = (info?.id as string) || props.sessionID || null; + stashedFiles.clear(); + seenSubtaskIds.clear(); + seenToolCallIds.clear(); + if (activeSessionId) { + contextInjectedSessions.delete(activeSessionId); + } + await post("/session/start", { + sessionId: activeSessionId, + title: info?.title ?? null, + parentID: info?.parentID ?? null, + version: info?.version ?? null, + project: projectPath, + cwd: projectPath, + }); + } + + // ── session.idle ── + if (type === "session.idle") { + const sid = props.sessionID || activeSessionId; + if (sid) { + await post("/summarize", { sessionId: sid }); + } + } + + // ── session.status ── + if (type === "session.status") { + const status = props.status as Record | undefined; + const sid = props.sessionID || activeSessionId; + if (!sid || !status) return; + if (status.type === "idle") { + await post("/summarize", { sessionId: sid }); + } + await observe(sid, "session_status", { + status_type: status.type, + attempt: status.attempt ?? null, + message: ((status.message as string) || "").slice(0, 2000), + }); + } + + // ── session.compacted ── + if (type === "session.compacted") { + const sid = props.sessionID || activeSessionId; + if (sid) { + await post("/summarize", { sessionId: sid }); + await observe(sid, "session_compacted", {}); + } + } + + // ── session.updated ── + if (type === "session.updated") { + const info = props.info as Record | undefined; + const sid = (info?.id as string) || props.sessionID || activeSessionId; + if (!sid) return; + await observe(sid, "session_updated", { + title: info?.title ?? null, + parentID: info?.parentID ?? null, + additions: (info?.summary as any)?.additions ?? null, + deletions: (info?.summary as any)?.deletions ?? null, + files: (info?.summary as any)?.files ?? null, + }); + } + + // ── session.diff ── + if (type === "session.diff") { + const sid = props.sessionID || activeSessionId; + if (!sid || !Array.isArray(props.diff)) return; + const diffs = props.diff as Array>; + await observe(sid, "session_diff", { + files: diffs.map(d => d.file), + additions: diffs.reduce((s, d) => s + ((d.additions as number) || 0), 0), + deletions: diffs.reduce((s, d) => s + ((d.deletions as number) || 0), 0), + diffs: diffs.slice(0, 50), + }); + } + + // ── session.deleted ── + if (type === "session.deleted") { + const sid = props.info?.id || props.sessionID || activeSessionId; + if (sid) { + await post("/session/end", { sessionId: sid }); + if (sid === activeSessionId) activeSessionId = null; + stashedFiles.clear(); + seenSubtaskIds.clear(); + seenToolCallIds.clear(); + contextInjectedSessions.delete(sid); + } + } + + // ── session.error ── + if (type === "session.error") { + const sid = props.sessionID || activeSessionId; + if (sid) { + await observe(sid, "post_tool_use_failure", { + tool_name: "session.error", + tool_input: "", + tool_output: extractErrorMessage(props.error).slice(0, 8000), + }); + } + } + + // ── message.updated ── + if (type === "message.updated") { + const info = props.info as Record | undefined; + if (!info) return; + + if (info.role === "user") { + const sid = (info.sessionID as string) || activeSessionId; + if (sid) { + await observe(sid, "user_prompt_submit", { + agent: info.agent ?? null, + model: info.model ?? null, + system: ((info.system as string) || "").slice(0, 1000), + tools: info.tools ?? null, + summary: info.summary ?? null, + }); + } + } + + if (info.role === "assistant") { + const sid = (info.sessionID as string) || activeSessionId; + if (!sid) return; + const tokens = info.tokens as Record | undefined; + const error = info.error ? extractErrorMessage(info.error) : null; + await observe(sid, "assistant_message", { + messageID: info.id, + parentID: info.parentID, + modelID: info.modelID, + providerID: info.providerID, + mode: info.mode, + cost: info.cost ?? 0, + tokens: { + input: tokens?.input ?? 0, + output: tokens?.output ?? 0, + reasoning: tokens?.reasoning ?? 0, + cache_read: (tokens?.cache as any)?.read ?? 0, + cache_write: (tokens?.cache as any)?.write ?? 0, + }, + finish: info.finish ?? null, + error, + duration_ms: info.time + ? ((info.time as any).completed || 0) - ((info.time as any).created || 0) + : null, + }); + } + } + + // ── message.removed ── + if (type === "message.removed") { + const sid = props.sessionID || activeSessionId; + if (sid) { + await observe(sid, "message_removed", { + messageID: props.messageID, + }); + } + } + + // ── message.part.updated ── + if (type === "message.part.updated") { + const part = props.part as Record | undefined; + if (!part) return; + const sid = (part.sessionID as string) || activeSessionId; + if (!sid) return; + + if (part.type === "subtask") { + if (seenSubtaskIds.has(part.id as string)) return; + seenSubtaskIds.add(part.id as string); + await observe(sid, "subagent_start", { + subtask_id: part.id, + agent: part.agent, + prompt: ((part.prompt as string) || "").slice(0, 4000), + description: ((part.description as string) || "").slice(0, 2000), + }); + return; + } + + if (part.type === "tool") { + const state = part.state as Record | undefined; + if (!state) return; + const callId = part.callID as string; + const toolName = part.tool as string; + + if (state.status === "completed") { + if (seenToolCallIds.has(callId)) return; + seenToolCallIds.add(callId); + const st = state as Record; + const startTime = ((st.time as any)?.start as number) || 0; + const endTime = ((st.time as any)?.end as number) || 0; + await observe(sid, "post_tool_use", { + tool_name: toolName, + call_id: callId, + tool_input: JSON.stringify(st.input).slice(0, 4000), + tool_output: ((st.output as string) || "").slice(0, 8000), + title: st.title ?? null, + metadata: st.metadata || {}, + duration_ms: endTime - startTime || null, + attachments: Array.isArray(st.attachments) + ? (st.attachments as Array>).map(a => a.filename || a.url) + : [], + }); + } else if (state.status === "error") { + if (seenToolCallIds.has(callId)) return; + seenToolCallIds.add(callId); + const st = state as Record; + const startTime = ((st.time as any)?.start as number) || 0; + const endTime = ((st.time as any)?.end as number) || 0; + await observe(sid, "post_tool_use_failure", { + tool_name: toolName, + call_id: callId, + tool_input: JSON.stringify(st.input).slice(0, 4000), + tool_output: ((st.error as string) || "").slice(0, 8000), + duration_ms: endTime - startTime || null, + }); + } + return; + } + + if (part.type === "step-finish") { + await observe(sid, "step_finish", { + messageID: part.messageID, + reason: part.reason ?? null, + cost: (part as any).cost ?? 0, + input_tokens: ((part as any).tokens?.input as number) ?? 0, + output_tokens: ((part as any).tokens?.output as number) ?? 0, + reasoning_tokens: ((part as any).tokens?.reasoning as number) ?? 0, + }); + return; + } + + if (part.type === "reasoning") { + await observe(sid, "reasoning", { + messageID: part.messageID, + text: ((part as any).text as string || "").slice(0, 4000), + }); + return; + } + + if (part.type === "file") { + const filename = (part as any).filename || (part as any).url || null; + if (filename) stashedFiles.add(filename); + return; + } + + if (part.type === "patch") { + await observe(sid, "patch_applied", { + messageID: part.messageID, + hash: (part as any).hash, + files: (part as any).files || [], + }); + return; + } + + if (part.type === "compaction") { + await observe(sid, "compaction_event", { + messageID: part.messageID, + auto: (part as any).auto ?? false, + }); + return; + } + + if (part.type === "agent") { + await observe(sid, "agent_selected", { + messageID: part.messageID, + name: (part as any).name, + }); + return; + } + + if (part.type === "retry") { + await observe(sid, "retry_attempt", { + messageID: part.messageID, + attempt: (part as any).attempt, + error: extractErrorMessage((part as any).error).slice(0, 2000), + }); + return; + } + } + + // ── file.edited ── + if (type === "file.edited") { + const sid = activeSessionId; + if (sid) { + stashedFiles.add(props.file as string); + } + } + + // ── permission.updated ── + if (type === "permission.updated") { + const sid = props.sessionID || activeSessionId; + if (!sid) return; + await observe(sid, "notification", { + notification_type: "permission_prompt", + permission: props.type || "unknown", + pattern: Array.isArray(props.pattern) + ? props.pattern.join(", ") + : (props.pattern || ""), + tool_call_id: props.callID || null, + title: props.title || props.type || "", + metadata: props.metadata || {}, + }); + } + + // ── permission.replied ── + if (type === "permission.replied") { + const sid = props.sessionID || activeSessionId; + if (!sid) return; + await observe(sid, "permission_replied", { + permission_id: props.permissionID || props.requestID || "", + response: props.response || props.reply || "", + }); + } + + // ── todo.updated ── + if (type === "todo.updated") { + const sid = props.sessionID || activeSessionId; + const todos = Array.isArray(props.todos) ? props.todos : []; + if (!sid || todos.length === 0) return; + const completed = todos.filter((t: any) => t.status === "completed"); + const active = todos.filter((t: any) => t.status !== "completed"); + await observe(sid, "task_completed", { + completed: completed.map((t: any) => ({ content: t.content, priority: t.priority })), + in_progress: active.map((t: any) => ({ content: t.content, priority: t.priority })), + total: todos.length, + }); + } + + // ── command.executed ── + if (type === "command.executed") { + const sid = props.sessionID || activeSessionId; + if (sid) { + await observe(sid, "command_executed", { + name: props.name, + arguments: props.arguments || "", + }); + } + } + }, + + // ── chat.message ── + "chat.message": async (input, output) => { + const sid = input.sessionID || activeSessionId; + if (!sid) return; + const parts = output.parts || []; + const files = parts + .filter((p: any) => p.type === "file") + .map((p: any) => p.filename || p.url) + .filter(Boolean); + for (const f of files) stashedFiles.add(f); + + const textParts = parts.filter((p: any) => p.type === "text" && !p.synthetic && !p.ignored); + const userText = textParts.map((p: any) => p.text || "").join("\n"); + + await observe(sid, "user_prompt_submit", { + agent: input.agent ?? null, + model: input.model ?? null, + variant: input.variant ?? null, + prompt: userText.slice(0, 8000), + files: files.slice(0, 20), + parts_summary: parts.map((p: any) => p.type).filter(Boolean), + }); + }, + + // ── chat.params ── + "chat.params": async (input, output) => { + const sid = input.sessionID || activeSessionId; + if (!sid) return; + await observe(sid, "llm_params", { + agent: input.agent, + model: `${input.model.providerID}/${input.model.id}`, + provider_url: input.model.api?.url ?? null, + temperature: output.temperature, + topP: output.topP, + max_output_tokens: input.model.limit?.output ?? null, + context_limit: input.model.limit?.context ?? null, + cost_1k_input: input.model.cost?.input ?? 0, + cost_1k_output: input.model.cost?.output ?? 0, + }); + }, + + // ── tool.execute.before ── + "tool.execute.before": async (input, output) => { + if (!FILE_TOOLS.has(input.tool)) return; + const args = output.args as Record | undefined; + if (!args) return; + for (const fp of extractFilePaths(args)) { + stashedFiles.add(fp); + } + if (stashedFiles.size > MAX_STASHED_FILES) { + const keep = [...stashedFiles].slice(-MAX_STASHED_FILES); + stashedFiles.clear(); + for (const f of keep) stashedFiles.add(f); + } + }, + + // ── experimental.chat.system.transform ── + "experimental.chat.system.transform": async (input, output) => { + const sid = input.sessionID || activeSessionId; + if (!sid) return; + + if (!contextInjectedSessions.has(sid)) { + contextInjectedSessions.add(sid); + const result = await postJson("/context", { + sessionId: sid, + project: projectPath, + }); + if (result && typeof result === "object" && (result as any).context) { + output.system.push((result as any).context); + } + } + + if (stashedFiles.size === 0) return; + const files = [...stashedFiles].slice(0, 10); + stashedFiles.clear(); + + const enrichResult = await postJson("/enrich", { + sessionId: sid, + files, + toolName: "enrich_inject", + }); + + if (enrichResult && typeof enrichResult === "object" && (enrichResult as any).context) { + output.system.push((enrichResult as any).context); + } + }, + + // ── experimental.session.compacting (WIP) ── + "experimental.session.compacting": async (input, output) => { + const sid = input.sessionID || activeSessionId; + if (!sid) return; + + const result = await postJson("/context", { + sessionId: sid, + project: projectPath, + }); + if (result && typeof result === "object" && (result as any).context) { + output.context.push((result as any).context); + } + }, + + // ── config ── + config: async (input) => { + if (activeSessionId) { + await observe(activeSessionId, "config_loaded", { + theme: input.theme ?? null, + model: input.model ?? null, + autoupdate: input.autoupdate ?? null, + agents: input.agent ? Object.keys(input.agent as Record) : [], + mcp_servers: input.mcp ? Object.keys(input.mcp as Record) : [], + providers: input.provider ? Object.keys(input.provider as Record) : [], + permission: input.permission ?? null, + }); + } + }, + }; +}; diff --git a/plugin/opencode/commands/recall.md b/plugin/opencode/commands/recall.md new file mode 100644 index 000000000..de01f0884 --- /dev/null +++ b/plugin/opencode/commands/recall.md @@ -0,0 +1,19 @@ +Search past session observations and lessons for relevant context. Wrap the `memory_smart_search` and `memory_lesson_recall` MCP tools. + +## Usage + +``` +/recall [query] +``` + +## Instructions + +1. Call `memory_smart_search` with the query and `limit: 10` (hybrid BM25 + vector + graph search). +2. Call `memory_lesson_recall` with the same query and `limit: 5` (lesson search). +3. Combine results and present to the user: + - Group by session + - Show type, title, and narrative for each observation + - Highlight high-importance (>= 7) observations + - Show lessons separately with confidence scores +4. If no results, suggest 2-3 alternative search terms. +5. **Never hallucinate results.** Only present what the MCP tools actually return. diff --git a/plugin/opencode/commands/remember.md b/plugin/opencode/commands/remember.md new file mode 100644 index 000000000..196fc0043 --- /dev/null +++ b/plugin/opencode/commands/remember.md @@ -0,0 +1,19 @@ +Explicitly save an insight, decision, or learning to agentmemory for future sessions. Wraps the `memory_save` MCP tool. + +## Usage + +``` +/remember [what to remember] +``` + +## Instructions + +1. Analyze what needs to be remembered — extract the core insight, decision, or fact. +2. Extract 2-5 searchable concepts (lowercased keyword phrases). Prefer specific terms ("jwt-refresh-rotation" over "auth"). +3. Extract relevant file paths the memory references. +4. Call `memory_save` with: + - `content` — full text to remember (preserve user's phrasing) + - `concepts` — extracted concept list + - `files` — extracted file list (empty array if none) + - `type` — choose from: pattern, preference, architecture, bug, workflow, fact +5. Confirm the save and show the concepts tagged so the user knows retrieval terms. diff --git a/plugin/opencode/plugin.json b/plugin/opencode/plugin.json new file mode 100644 index 000000000..a66e437a2 --- /dev/null +++ b/plugin/opencode/plugin.json @@ -0,0 +1,12 @@ +{ + "name": "agentmemory-capture", + "version": "0.9.4", + "description": "OpenCode plugin for agentmemory — full Claude Code hook parity: session lifecycle (create/idle/status/compacted/update/diff/delete/error), messages & prompts (chat.message, message.updated user+assistant, message.removed), tool lifecycle (ToolPart states with timing), part tracking (subtask, step-finish, reasoning, file, patch, compaction, agent, retry), file enrichment pipeline, permissions, task tracking (w/ priority), commands, config & model tracking. 22 hooks, 2 slash commands.", + "author": { + "name": "Rohit Ghumare", + "url": "https://github.com/rohitg00" + }, + "license": "Apache-2.0", + "homepage": "https://github.com/rohitg00/agentmemory", + "repository": "https://github.com/rohitg00/agentmemory" +} From da8b09e6643896f2d9c720bf4913060702c3394c Mon Sep 17 00:00:00 2001 From: Trip <5579540+cl0ckt0wer@users.noreply.github.com> Date: Thu, 7 May 2026 22:26:45 -0400 Subject: [PATCH 2/4] 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. --- plugin/opencode/agentmemory-capture.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin/opencode/agentmemory-capture.ts b/plugin/opencode/agentmemory-capture.ts index 8eff1eaf1..7db112c80 100644 --- a/plugin/opencode/agentmemory-capture.ts +++ b/plugin/opencode/agentmemory-capture.ts @@ -198,7 +198,7 @@ export const AgentmemoryCapturePlugin: Plugin = async (ctx) => { if (info.role === "user") { const sid = (info.sessionID as string) || activeSessionId; if (sid) { - await observe(sid, "user_prompt_submit", { + await observe(sid, "prompt_submit", { agent: info.agent ?? null, model: info.model ?? null, system: ((info.system as string) || "").slice(0, 1000), @@ -441,7 +441,7 @@ export const AgentmemoryCapturePlugin: Plugin = async (ctx) => { const textParts = parts.filter((p: any) => p.type === "text" && !p.synthetic && !p.ignored); const userText = textParts.map((p: any) => p.text || "").join("\n"); - await observe(sid, "user_prompt_submit", { + await observe(sid, "prompt_submit", { agent: input.agent ?? null, model: input.model ?? null, variant: input.variant ?? null, From 1dc684821431a5a00f53f46f067bca5ab1d149fd Mon Sep 17 00:00:00 2001 From: Trip <5579540+cl0ckt0wer@users.noreply.github.com> Date: Thu, 7 May 2026 23:25:22 -0400 Subject: [PATCH 3/4] fix(plugin): add session instruction injection and consolidation pipeline (closes #233) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- plugin/opencode/README.md | 49 +++++++++++++++++++++++++- plugin/opencode/agentmemory-capture.ts | 43 ++++++++++++++++++++++ 2 files changed, 91 insertions(+), 1 deletion(-) diff --git a/plugin/opencode/README.md b/plugin/opencode/README.md index cde13ba29..19518bdd1 100644 --- a/plugin/opencode/README.md +++ b/plugin/opencode/README.md @@ -166,17 +166,64 @@ System prompt = [OpenCode instructions] + [memory context] + [file enrichment] + | Coverage | Edit/Write/Read/Glob/Grep only | Edit/Write/Read/Glob/Grep only | | What gets injected | `` + 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 ` — Search past observations and lessons - `/remember ` — 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 | No explicit subtask-completion event; stop boundary is inferred from the observation timeline by the memory system | +| 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. diff --git a/plugin/opencode/agentmemory-capture.ts b/plugin/opencode/agentmemory-capture.ts index 7db112c80..aa8fd92a0 100644 --- a/plugin/opencode/agentmemory-capture.ts +++ b/plugin/opencode/agentmemory-capture.ts @@ -52,6 +52,46 @@ const seenSubtaskIds = new Set(); const contextInjectedSessions = new Set(); const seenToolCallIds = new Set(); +const AGENTMEMORY_INSTRUCTIONS = ` +You have access to agentmemory for persistent cross-session memory. Use these tools proactively. + +CORE TOOLS: + +memory_save — Save an insight, decision, or fact to long-term memory. + Required: content (text), concepts (2-5 comma-separated keywords), type (pattern/preference/architecture/bug/workflow/fact) + Optional: files (comma-separated paths) + Use when: user says "remember this", after discovering a bug, after making an architectural decision, after learning a project convention. + +memory_recall — Search past observations by keywords. + Use when: user says "recall", "what did we do", "do you remember", or needs context from past sessions. + +memory_smart_search — Hybrid semantic+keyword search with progressive disclosure. + Use when: you need the most relevant past context, fuzzy/conceptual searches, or recall doesn't find what you need. + +memory_sessions — List recent sessions with status and observation counts. + Use when: user asks about session/past history, "what did we work on". + +memory_file_history — Get past observations about specific files (across all sessions). + Use when: you're about to edit a file and want to know its history, common pitfalls, or past edits. + +memory_lesson_save — Save a lesson learned (what worked, what to avoid). + Use when: you discover a pattern that could help future sessions avoid mistakes. + +memory_lesson_recall — Search lessons by query. Returns lessons sorted by confidence. + Use when: before making a decision, check if past lessons apply. + +memory_governance_delete — Delete specific memories. Requires explicit user confirmation. + Use when: user says "forget this", "delete that memory". + +memory_patterns — Detect recurring patterns across sessions. + Use when: you want to understand project-level trends over time. + +memory_consolidate — Run the 4-tier memory consolidation pipeline. + Use when: you want to compress and organize accumulated session observations. + +All tools are prefixed with \`agentmemory_\`. Tool results are JSON. Always check what was returned before presenting to the user. +`; + function extractFilePaths(args: Record): string[] { const files: string[] = []; for (const key of FILE_KEYS) { @@ -170,6 +210,8 @@ export const AgentmemoryCapturePlugin: Plugin = async (ctx) => { const sid = props.info?.id || props.sessionID || activeSessionId; if (sid) { await post("/session/end", { sessionId: sid }); + post("/crystals/auto", { olderThanDays: 0 }); + post("/consolidate-pipeline", { tier: "all", force: true }); if (sid === activeSessionId) activeSessionId = null; stashedFiles.clear(); seenSubtaskIds.clear(); @@ -490,6 +532,7 @@ export const AgentmemoryCapturePlugin: Plugin = async (ctx) => { if (!contextInjectedSessions.has(sid)) { contextInjectedSessions.add(sid); + output.system.push(AGENTMEMORY_INSTRUCTIONS); const result = await postJson("/context", { sessionId: sid, project: projectPath, From 0aeab27777f63333f40c96cc4335d438bb757ec6 Mon Sep 17 00:00:00 2001 From: Trip <5579540+cl0ckt0wer@users.noreply.github.com> Date: Fri, 8 May 2026 08:00:43 -0400 Subject: [PATCH 4/4] =?UTF-8?q?fix(plugin):=20address=20CodeRabbit=20revie?= =?UTF-8?q?w=20=E2=80=94=20session-scoped=20state,=20await=20res.json(),?= =?UTF-8?q?=20markdown=20lint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugin/opencode/README.md | 8 +-- plugin/opencode/agentmemory-capture.ts | 73 ++++++++++++++++---------- plugin/opencode/commands/recall.md | 2 +- plugin/opencode/commands/remember.md | 2 +- 4 files changed, 52 insertions(+), 33 deletions(-) diff --git a/plugin/opencode/README.md b/plugin/opencode/README.md index 19518bdd1..53c445339 100644 --- a/plugin/opencode/README.md +++ b/plugin/opencode/README.md @@ -150,7 +150,7 @@ Restart OpenCode or open a new session. The plugin auto-captures everything. 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. -``` +```text System prompt = [OpenCode instructions] + [memory context] + [file enrichment] + [user message] ^ ^ first turn only every file-touching turn @@ -172,7 +172,7 @@ Claude Code and OpenCode take fundamentally different approaches to injecting me ### Claude Code: file-backed bridge (two-hop) -``` +```text agentmemory ──write──▶ MEMORY.md ──read──▶ Claude system prompt ``` @@ -183,7 +183,7 @@ agentmemory ──write──▶ MEMORY.md ──read──▶ Claude system ### OpenCode: direct injection (one-hop) -``` +```text agentmemory ──push──▶ OpenCode system prompt ``` @@ -213,7 +213,7 @@ agentmemory already persists everything in SQLite (`data/state_store.db`). Addin ## 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. +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_*` tools to use and when, without needing separate skill invocations. ## What's not covered (vs Claude Code plugin) diff --git a/plugin/opencode/agentmemory-capture.ts b/plugin/opencode/agentmemory-capture.ts index aa8fd92a0..453314ac9 100644 --- a/plugin/opencode/agentmemory-capture.ts +++ b/plugin/opencode/agentmemory-capture.ts @@ -24,7 +24,7 @@ async function postJson(path: string, body: Record): Promise(); -const seenSubtaskIds = new Set(); +const sessionStash = new Map>(); +const sessionSubtasks = new Map>(); +const sessionToolCalls = new Map>(); const contextInjectedSessions = new Set(); -const seenToolCallIds = new Set(); + +function stashFor(sid: string): Set { + let s = sessionStash.get(sid); + if (!s) { s = new Set(); sessionStash.set(sid, s); } + return s; +} +function subtasksFor(sid: string): Set { + let s = sessionSubtasks.get(sid); + if (!s) { s = new Set(); sessionSubtasks.set(sid, s); } + return s; +} +function toolCallsFor(sid: string): Set { + let s = sessionToolCalls.get(sid); + if (!s) { s = new Set(); sessionToolCalls.set(sid, s); } + return s; +} const AGENTMEMORY_INSTRUCTIONS = ` You have access to agentmemory for persistent cross-session memory. Use these tools proactively. @@ -130,10 +146,10 @@ export const AgentmemoryCapturePlugin: Plugin = async (ctx) => { if (type === "session.created") { const info = props.info as Record | undefined; activeSessionId = (info?.id as string) || props.sessionID || null; - stashedFiles.clear(); - seenSubtaskIds.clear(); - seenToolCallIds.clear(); if (activeSessionId) { + stashFor(activeSessionId).clear(); + subtasksFor(activeSessionId).clear(); + toolCallsFor(activeSessionId).clear(); contextInjectedSessions.delete(activeSessionId); } await post("/session/start", { @@ -213,9 +229,9 @@ export const AgentmemoryCapturePlugin: Plugin = async (ctx) => { post("/crystals/auto", { olderThanDays: 0 }); post("/consolidate-pipeline", { tier: "all", force: true }); if (sid === activeSessionId) activeSessionId = null; - stashedFiles.clear(); - seenSubtaskIds.clear(); - seenToolCallIds.clear(); + sessionStash.delete(sid); + sessionSubtasks.delete(sid); + sessionToolCalls.delete(sid); contextInjectedSessions.delete(sid); } } @@ -296,8 +312,8 @@ export const AgentmemoryCapturePlugin: Plugin = async (ctx) => { if (!sid) return; if (part.type === "subtask") { - if (seenSubtaskIds.has(part.id as string)) return; - seenSubtaskIds.add(part.id as string); + if (subtasksFor(sid).has(part.id as string)) return; + subtasksFor(sid).add(part.id as string); await observe(sid, "subagent_start", { subtask_id: part.id, agent: part.agent, @@ -314,8 +330,8 @@ export const AgentmemoryCapturePlugin: Plugin = async (ctx) => { const toolName = part.tool as string; if (state.status === "completed") { - if (seenToolCallIds.has(callId)) return; - seenToolCallIds.add(callId); + if (toolCallsFor(sid).has(callId)) return; + toolCallsFor(sid).add(callId); const st = state as Record; const startTime = ((st.time as any)?.start as number) || 0; const endTime = ((st.time as any)?.end as number) || 0; @@ -332,8 +348,8 @@ export const AgentmemoryCapturePlugin: Plugin = async (ctx) => { : [], }); } else if (state.status === "error") { - if (seenToolCallIds.has(callId)) return; - seenToolCallIds.add(callId); + if (toolCallsFor(sid).has(callId)) return; + toolCallsFor(sid).add(callId); const st = state as Record; const startTime = ((st.time as any)?.start as number) || 0; const endTime = ((st.time as any)?.end as number) || 0; @@ -370,7 +386,7 @@ export const AgentmemoryCapturePlugin: Plugin = async (ctx) => { if (part.type === "file") { const filename = (part as any).filename || (part as any).url || null; - if (filename) stashedFiles.add(filename); + if (filename) stashFor(sid).add(filename); return; } @@ -413,7 +429,7 @@ export const AgentmemoryCapturePlugin: Plugin = async (ctx) => { if (type === "file.edited") { const sid = activeSessionId; if (sid) { - stashedFiles.add(props.file as string); + stashFor(sid).add(props.file as string); } } @@ -478,7 +494,7 @@ export const AgentmemoryCapturePlugin: Plugin = async (ctx) => { .filter((p: any) => p.type === "file") .map((p: any) => p.filename || p.url) .filter(Boolean); - for (const f of files) stashedFiles.add(f); + for (const f of files) stashFor(sid).add(f); const textParts = parts.filter((p: any) => p.type === "text" && !p.synthetic && !p.ignored); const userText = textParts.map((p: any) => p.text || "").join("\n"); @@ -515,13 +531,15 @@ export const AgentmemoryCapturePlugin: Plugin = async (ctx) => { if (!FILE_TOOLS.has(input.tool)) return; const args = output.args as Record | undefined; if (!args) return; + if (!activeSessionId) return; for (const fp of extractFilePaths(args)) { - stashedFiles.add(fp); + stashFor(activeSessionId).add(fp); } - if (stashedFiles.size > MAX_STASHED_FILES) { - const keep = [...stashedFiles].slice(-MAX_STASHED_FILES); - stashedFiles.clear(); - for (const f of keep) stashedFiles.add(f); + const stash = stashFor(activeSessionId); + if (stash.size > MAX_STASHED_FILES) { + const keep = [...stash].slice(-MAX_STASHED_FILES); + stash.clear(); + for (const f of keep) stash.add(f); } }, @@ -542,9 +560,10 @@ export const AgentmemoryCapturePlugin: Plugin = async (ctx) => { } } - if (stashedFiles.size === 0) return; - const files = [...stashedFiles].slice(0, 10); - stashedFiles.clear(); + const stash = stashFor(sid); + if (stash.size === 0) return; + const files = [...stash].slice(0, 10); + stash.clear(); const enrichResult = await postJson("/enrich", { sessionId: sid, diff --git a/plugin/opencode/commands/recall.md b/plugin/opencode/commands/recall.md index de01f0884..b7ad1785e 100644 --- a/plugin/opencode/commands/recall.md +++ b/plugin/opencode/commands/recall.md @@ -2,7 +2,7 @@ Search past session observations and lessons for relevant context. Wrap the `mem ## Usage -``` +```text /recall [query] ``` diff --git a/plugin/opencode/commands/remember.md b/plugin/opencode/commands/remember.md index 196fc0043..e792f5fb9 100644 --- a/plugin/opencode/commands/remember.md +++ b/plugin/opencode/commands/remember.md @@ -2,7 +2,7 @@ Explicitly save an insight, decision, or learning to agentmemory for future sess ## Usage -``` +```text /remember [what to remember] ```