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
-MCP server
+22 hooks + MCP + plugin
@@ -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/
+```
+
---
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 @@
+
+
+ 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
+
+```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]
```