chore: import upstream snapshot with attribution
CI / test (20, macos-latest) (push) Waiting to run
CI / test (20, ubuntu-latest) (push) Waiting to run
CI / test (22, macos-latest) (push) Waiting to run
CI / test (22, ubuntu-latest) (push) Waiting to run

This commit is contained in:
wehub-resource-sync
2026-07-13 13:01:18 +08:00
commit 979fb22d7c
613 changed files with 113494 additions and 0 deletions
+13
View File
@@ -0,0 +1,13 @@
{
"name": "agentmemory",
"version": "0.9.27",
"description": "Persistent memory for AI coding agents -- captures tool usage, compresses via LLM, injects context into future sessions. 12 hooks, 53 MCP tools, 8 skills, real-time viewer.",
"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",
"skills": ["./skills/"]
}
+15
View File
@@ -0,0 +1,15 @@
{
"name": "agentmemory",
"version": "0.9.27",
"description": "Persistent memory for AI coding agents -- captures tool usage, compresses via LLM, injects context into future sessions. 6 hooks, 53 MCP tools, 8 skills, real-time viewer.",
"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",
"skills": "./skills/",
"mcpServers": "./.mcp.json",
"hooks": "./hooks/hooks.codex.json"
}
+15
View File
@@ -0,0 +1,15 @@
{
"mcpServers": {
"agentmemory": {
"type": "local",
"command": "npx",
"args": ["-y", "@agentmemory/mcp"],
"env": {
"AGENTMEMORY_URL": "${AGENTMEMORY_URL:-http://localhost:3111}",
"AGENTMEMORY_SECRET": "${AGENTMEMORY_SECRET:-}",
"AGENTMEMORY_TOOLS": "${AGENTMEMORY_TOOLS:-all}"
},
"tools": ["*"]
}
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"mcpServers": {
"agentmemory": {
"command": "npx",
"args": ["-y", "@agentmemory/mcp"],
"env": {
"AGENTMEMORY_URL": "${AGENTMEMORY_URL:-http://localhost:3111}",
"AGENTMEMORY_SECRET": "${AGENTMEMORY_SECRET:-}",
"AGENTMEMORY_TOOLS": "${AGENTMEMORY_TOOLS:-all}"
}
}
}
}
+67
View File
@@ -0,0 +1,67 @@
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/session-start.mjs\"",
"statusMessage": "agentmemory: loading session context"
}
]
}
],
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/prompt-submit.mjs\"",
"statusMessage": "agentmemory: recalling relevant memories"
}
]
}
],
"PreToolUse": [
{
"matcher": "Edit|Write|Read|Glob|Grep",
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/pre-tool-use.mjs\""
}
]
}
],
"PostToolUse": [
{
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/post-tool-use.mjs\""
}
]
}
],
"PreCompact": [
{
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/pre-compact.mjs\""
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/stop.mjs\""
}
]
}
]
}
}
+72
View File
@@ -0,0 +1,72 @@
{
"version": 1,
"hooks": {
"sessionStart": [
{
"type": "command",
"command": "node ${COPILOT_PLUGIN_ROOT}/scripts/session-start.mjs"
}
],
"userPromptSubmitted": [
{
"type": "command",
"command": "node ${COPILOT_PLUGIN_ROOT}/scripts/prompt-submit.mjs"
}
],
"preToolUse": [
{
"type": "command",
"matcher": "edit|write|create|read|view|glob|grep",
"command": "node ${COPILOT_PLUGIN_ROOT}/scripts/pre-tool-use.mjs"
}
],
"postToolUse": [
{
"type": "command",
"command": "node ${COPILOT_PLUGIN_ROOT}/scripts/post-tool-use.mjs"
}
],
"postToolUseFailure": [
{
"type": "command",
"command": "node ${COPILOT_PLUGIN_ROOT}/scripts/post-tool-failure.mjs"
}
],
"preCompact": [
{
"type": "command",
"command": "node ${COPILOT_PLUGIN_ROOT}/scripts/pre-compact.mjs"
}
],
"agentStop": [
{
"type": "command",
"command": "node ${COPILOT_PLUGIN_ROOT}/scripts/stop.mjs"
}
],
"sessionEnd": [
{
"type": "command",
"command": "node ${COPILOT_PLUGIN_ROOT}/scripts/session-end.mjs"
}
],
"subagentStart": [
{
"type": "command",
"command": "node ${COPILOT_PLUGIN_ROOT}/scripts/subagent-start.mjs"
}
],
"subagentStop": [
{
"type": "command",
"command": "node ${COPILOT_PLUGIN_ROOT}/scripts/subagent-stop.mjs"
}
],
"notification": [
{
"type": "command",
"command": "node ${COPILOT_PLUGIN_ROOT}/scripts/notification.mjs"
}
]
}
}
+125
View File
@@ -0,0 +1,125 @@
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/session-start.mjs\""
}
]
}
],
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/prompt-submit.mjs\""
}
]
}
],
"PreToolUse": [
{
"matcher": "Edit|Write|Read|Glob|Grep",
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/pre-tool-use.mjs\""
}
]
}
],
"PostToolUse": [
{
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/post-tool-use.mjs\""
}
]
}
],
"PostToolUseFailure": [
{
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/post-tool-failure.mjs\""
}
]
}
],
"PreCompact": [
{
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/pre-compact.mjs\""
}
]
}
],
"SubagentStart": [
{
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/subagent-start.mjs\""
}
]
}
],
"SubagentStop": [
{
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/subagent-stop.mjs\""
}
]
}
],
"Notification": [
{
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/notification.mjs\""
}
]
}
],
"TaskCompleted": [
{
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/task-completed.mjs\""
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/stop.mjs\""
}
]
}
],
"SessionEnd": [
{
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/session-end.mjs\""
}
]
}
]
}
}
+229
View File
@@ -0,0 +1,229 @@
<h1 align="center">
<img src="https://github.com/opencode-ai.png?size=80" alt="OpenCode" width="28" height="28" align="center" />
&nbsp;agentmemory for OpenCode
</h1>
<p align="center">
<strong>Your OpenCode agents remember everything. No more re-explaining.</strong><br/>
<sub>Persistent cross-session memory via <a href="https://github.com/rohitg00/agentmemory">agentmemory</a> — 95.2% retrieval accuracy on <a href="https://arxiv.org/abs/2410.10813">LongMemEval-S</a>.</sub>
</p>
<p align="center">
<img src="https://img.shields.io/badge/MCP-53_tools-1f6feb?style=flat-square" alt="53 MCP tools" />
<img src="https://img.shields.io/badge/Plugin-22_hooks-1f6feb?style=flat-square" alt="22 hooks" />
<img src="https://img.shields.io/badge/Commands-2_slash-1f6feb?style=flat-square" alt="2 slash commands" />
<img src="https://img.shields.io/badge/R@5-95.2%25-00875f?style=flat-square" alt="95.2% R@5" />
</p>
---
## 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.
```text
System prompt = [OpenCode instructions] + [memory context] + [file enrichment] + [user message]
^ ^
first turn only every file-touching turn
```
**Differences from Claude's PreToolUse:**
| Dimension | Claude (PreToolUse) | OpenCode (two-hop pipeline) |
|---|---|---|
| Injection mechanism | stdout → context window | `output.system[]` → system prompt |
| Timing | Same turn (parallel with tool) | Next turn (before next LLM call) |
| File set | Per-tool (immediate) | Batched (all files since last enrichment) |
| Coverage | Edit/Write/Read/Glob/Grep only | Edit/Write/Read/Glob/Grep only |
| What gets injected | `<agentmemory-file-context>` + bug memories | Identical `/enrich` response |
## MEMORY.md vs AGENTS.md: how context flows
Claude Code and OpenCode take fundamentally different approaches to injecting memory context into the agent's system prompt.
### Claude Code: file-backed bridge (two-hop)
```
agentmemory ──write──▶ MEMORY.md ──read──▶ Claude system prompt
```
- The `claude-bridge/sync` endpoint serializes agentmemory observations into a `MEMORY.md` file in the project root
- Claude Code reads `MEMORY.md` on session start and prepends it to the system prompt
- **Sync is periodic** — sessions only get fresh context when the bridge last ran (session end, pre-compact)
- **Coupling**: memory data lives in a git-trackable file, visible to CI, team members, and other tools
### OpenCode: direct injection (one-hop)
```
agentmemory ──push──▶ OpenCode system prompt
```
- `experimental.chat.system.transform` calls `/context` at runtime and pushes the response directly into `output.system[]`
- **Always current** — context is fetched at session start (once) and before file-touching turns (per-batch)
- **No file intermediary** — no stale copies, no merge conflicts, no disk I/O
- `AGENTS.md` is a static instruction file for project conventions, coding standards, and tool guidance — agentmemory does not read or write it
### Tradeoffs
| Dimension | Claude (MEMORY.md bridge) | OpenCode (direct injection) |
|---|---|---|
| Freshness | Stale between syncs | Always current (fetched at call time) |
| Visibility | Human-readable file in repo | In-memory injection only |
| Simplicity | Two moving parts (bridge + file) | One step (API → system prompt) |
| Team sharing | File is git-trackable, CI-friendly | Memory shared via agentmemory server API |
| Integration | Any tool can read MEMORY.md | Requires OpenCode plugin SDK |
### Why OpenCode goes direct
agentmemory already persists everything in SQLite (`data/state_store.db`). Adding an intermediate MEMORY.md file would duplicate data, introduce sync lag, and require the model to re-parse structured context from markdown. Direct injection delivers the same data with lower latency and zero staleness — the agent always sees what agentmemory knows right now.
## Slash commands
- `/recall <query>` — Search past observations and lessons
- `/remember <text>` — Save an insight to long-term memory
## Session instruction injection
Agentmemory usage instructions are injected into the system prompt on the first turn of every session via `experimental.chat.system.transform` (alongside memory context from `/context`). This is functionally equivalent to Claude Code's skills mechanism — the agent learns which `agentmemory_memory_*` tools to use and when, without needing separate skill invocations.
## What's not covered (vs Claude Code plugin)
| Claude feature | Reason |
|---|---|
| SubagentStop | OpenCode's `SubtaskPart` type has no completion/result fields; subtask lifecycle ends are not exposed as distinct events in the OpenCode SDK |
| TaskCompleted | No team/teammate concept in OpenCode; `todo.updated` captures task state changes as a partial equivalent |
| Stop | `session.compacted` event handler exists; `experimental.session.compacting` injection hook defined in SDK but Go binary (v1.14.41) doesn't wire it — will auto-activate when upstream implements it |
| Skills (remember/recall/forget/session-history) | Covered by injected system instructions via `experimental.chat.system.transform` — agent receives usage guidance on first turn |
| Consolidation pipeline (crystals/auto + consolidate-pipeline) | Now called on `session.deleted` — mirrors Claude's `CONSOLIDATION_ENABLED=true` behavior |
| Claude MEMORY.md bridge | OpenCode-specific; OpenCode uses its own AGENTS.md mechanism, not Claude's MEMORY.md |
All other Claude Code hooks have direct or pipeline equivalents in this plugin. 12 of 12 Claude hook types covered.
+687
View File
@@ -0,0 +1,687 @@
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;
const DEBUG = process.env.OPENCODE_AGENTMEMORY_DEBUG === "1";
const SECRET = process.env.AGENTMEMORY_SECRET || "";
function authHeaders(): Record<string, string> {
const headers: Record<string, string> = { "Content-Type": "application/json" };
if (SECRET) headers["Authorization"] = `Bearer ${SECRET}`;
return headers;
}
async function post(path: string, body: Record<string, unknown>, timeoutMs = 5000): Promise<void> {
try {
await fetch(`${API}/agentmemory${path}`, {
method: "POST",
headers: authHeaders(),
body: JSON.stringify(body),
signal: AbortSignal.timeout(timeoutMs),
});
} catch (e) {
if (DEBUG) console.error(`[agentmemory] POST ${path} failed:`, (e as Error).message);
}
}
async function postJson(path: string, body: Record<string, unknown>): Promise<unknown | null> {
try {
const res = await fetch(`${API}/agentmemory${path}`, {
method: "POST",
headers: authHeaders(),
body: JSON.stringify(body),
signal: AbortSignal.timeout(5000),
});
return res.ok ? await res.json() : null;
} catch (e) {
if (DEBUG) console.error(`[agentmemory] POST ${path} failed:`, (e as Error).message);
return null;
}
}
async function observe(
sessionId: string,
hookType: string,
data: Record<string, unknown>,
): Promise<void> {
await post("/observe", {
hookType,
sessionId,
project: projectPath,
cwd: projectPath,
timestamp: new Date().toISOString(),
data,
});
}
let activeSessionId: string | null = null;
let pendingConfig: Record<string, unknown> | null = null;
let projectPath: string | null = null;
const stashedFiles = new Map<string, Set<string>>();
const seenSubtaskIds = new Map<string, Set<string>>();
const seenToolCallIds = new Map<string, Set<string>>();
const contextInjectedSessions = new Set<string>();
// cache the context returned by POST /session/start so the chat
// system-transform hook can inject it without a second /context fetch.
// Auto-injection now happens at session.created (immediately) AND at
// the first prompt_submit (fallback for older OpenCode builds that
// don't implement experimental.chat.system.transform).
const startContextCache = new Map<string, string>();
function stashFor(sid: string): Set<string> {
let s = stashedFiles.get(sid);
if (!s) { s = new Set<string>(); stashedFiles.set(sid, s); }
return s;
}
function subtaskSetFor(sid: string): Set<string> {
let s = seenSubtaskIds.get(sid);
if (!s) { s = new Set<string>(); seenSubtaskIds.set(sid, s); }
return s;
}
function toolCallSetFor(sid: string): Set<string> {
let s = seenToolCallIds.get(sid);
if (!s) { s = new Set<string>(); seenToolCallIds.set(sid, s); }
return s;
}
function pruneSessionMaps(sid: string): void {
stashedFiles.delete(sid);
seenSubtaskIds.delete(sid);
seenToolCallIds.delete(sid);
}
function safeSlice(v: unknown, max: number): string {
if (typeof v === "string") return v.slice(0, max);
if (v == null) return "";
try { return JSON.stringify(v).slice(0, max); } catch { return ""; }
}
const AGENTMEMORY_INSTRUCTIONS = `<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 memory tools start with \`agentmemory_memory_\`. Use the exact names as they appear in your tool list. Tool results are JSON. Always check what was returned before presenting to the user.
</agentmemory-instructions>`;
function extractFilePaths(args: Record<string, unknown>): 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<string, unknown>;
if (typeof e.message === "string") return e.message;
if (e.data && typeof e.data === "object") {
const d = e.data as Record<string, unknown>;
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.worktree || ctx.project?.id || process.cwd();
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<string, unknown> | undefined;
activeSessionId = (info?.id as string) || props.sessionID || null;
if (!activeSessionId) return;
stashedFiles.set(activeSessionId, new Set());
seenSubtaskIds.delete(activeSessionId);
seenToolCallIds.delete(activeSessionId);
contextInjectedSessions.delete(activeSessionId);
// Snapshot the session id locally — `activeSessionId` is mutable
// and another `session.created` event during the await could
// rebind it, causing context to be cached against the wrong key.
const sessionId = activeSessionId;
const startResult = await postJson("/session/start", {
sessionId,
title: info?.title ?? null,
parentID: info?.parentID ?? null,
version: info?.version ?? null,
project: projectPath,
cwd: projectPath,
});
// cache the context returned at session/start so the
// chat.system.transform hook injects it without a second fetch.
const startCtx = (startResult as any)?.context;
if (typeof startCtx === "string" && startCtx.length > 0) {
startContextCache.set(sessionId, startCtx);
}
if (pendingConfig) {
await observe(sessionId, "config_loaded", pendingConfig);
pendingConfig = null;
}
}
// ── session.idle ── (summarize handled in session.status idle branch)
// ── session.status ──
if (type === "session.status") {
const status = props.status as Record<string, unknown> | 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: safeSlice(status.message, 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<string, unknown> | 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<Record<string, unknown>>;
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) {
if (DEBUG) console.error("[agentmemory] session.deleted with no session ID");
return;
}
await post("/session/end", { sessionId: sid });
post("/crystals/auto", { olderThanDays: 7 }, 30000);
post("/consolidate-pipeline", { tier: "all", force: true }, 30000);
if (sid === activeSessionId) activeSessionId = null;
stashedFiles.delete(sid);
startContextCache.delete(sid);
seenSubtaskIds.delete(sid);
seenToolCallIds.delete(sid);
contextInjectedSessions.delete(sid);
}
// ── session.error ──
if (type === "session.error") {
const sid = props.sessionID || activeSessionId;
if (sid) {
await observe(sid, "post_tool_failure", {
tool_name: "session.error",
tool_input: "",
tool_output: safeSlice(props.error, 8000),
});
}
}
// ── message.updated ──
if (type === "message.updated") {
const info = props.info as Record<string, unknown> | undefined;
if (!info) return;
if (info.role === "assistant") {
const sid = props.sessionID || (info.sessionID as string) || activeSessionId;
if (!sid) return;
const tokens = info.tokens as Record<string, unknown> | 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 && typeof (info.time as any).completed === "number")
? (info.time as any).completed - ((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<string, unknown> | undefined;
if (!part) return;
const sid = (part.sessionID as string) || props.sessionID || activeSessionId;
if (!sid) return;
if (part.type === "subtask") {
const subtaskId = part.id as string;
if (!subtaskId) return;
const subtaskSet = subtaskSetFor(sid);
if (subtaskSet.has(subtaskId)) return;
subtaskSet.add(subtaskId);
await observe(sid, "subagent_start", {
subtask_id: part.id,
agent: part.agent,
prompt: safeSlice(part.prompt, 4000),
description: safeSlice(part.description, 2000),
});
return;
}
if (part.type === "tool") {
const state = part.state as Record<string, unknown> | undefined;
if (!state) return;
const callId = part.callID as string;
if (!callId) return;
const toolName = part.tool as string;
if (state.status === "completed") {
const callSet = toolCallSetFor(sid);
if (callSet.has(callId)) return;
callSet.add(callId);
const st = state as Record<string, unknown>;
const rawTime = (st.time as any) || {};
const startTime = typeof rawTime.start === "number" ? rawTime.start : null;
const endTime = typeof rawTime.end === "number" ? rawTime.end : null;
await observe(sid, "post_tool_use", {
tool_name: toolName,
call_id: callId,
tool_input: safeSlice(st.input, 4000),
tool_output: safeSlice(st.output, 8000),
title: st.title ?? null,
metadata: st.metadata || {},
duration_ms: (startTime != null && endTime != null) ? endTime - startTime : null,
attachments: Array.isArray(st.attachments)
? (st.attachments as Array<Record<string, unknown>>).map(a => a.filename || a.url)
: [],
});
} else if (state.status === "error") {
const callSet = toolCallSetFor(sid);
if (callSet.has(callId)) return;
callSet.add(callId);
const st = state as Record<string, unknown>;
const rawTime = (st.time as any) || {};
const startTime = typeof rawTime.start === "number" ? rawTime.start : null;
const endTime = typeof rawTime.end === "number" ? rawTime.end : null;
await observe(sid, "post_tool_failure", {
tool_name: toolName,
call_id: callId,
tool_input: safeSlice(st.input, 4000),
tool_output: safeSlice(st.error, 8000),
duration_ms: (startTime != null && endTime != null) ? 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: safeSlice((part as any).text, 4000),
});
return;
}
if (part.type === "file") {
const filename = (part as any).filename || (part as any).url || null;
if (filename) stashFor(sid).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: safeSlice((part as any).error, 2000),
});
return;
}
}
// ── file.edited ──
if (type === "file.edited") {
const sid = props.sessionID || activeSessionId;
if (sid && typeof props.file === "string" && props.file.length > 0) {
const stash = stashFor(sid);
stash.add(props.file);
if (stash.size > MAX_STASHED_FILES) {
const keep = [...stash].slice(-MAX_STASHED_FILES);
stash.clear();
for (const f of keep) stash.add(f);
}
}
}
// ── 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.slice(0, 100) : [];
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) {
const stash = stashFor(sid);
stash.add(f);
if (stash.size > MAX_STASHED_FILES) {
const keep = [...stash].slice(-MAX_STASHED_FILES);
stash.clear();
for (const k of keep) stash.add(k);
}
}
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, "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) => {
if (!input.model || !output) return;
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 sid = input.sessionID || activeSessionId;
if (!sid) return;
const args = output.args as Record<string, unknown> | undefined;
if (!args) return;
const stash = stashFor(sid);
for (const fp of extractFilePaths(args)) {
stash.add(fp);
}
if (stash.size > MAX_STASHED_FILES) {
const keep = [...stash].slice(-MAX_STASHED_FILES);
stash.clear();
for (const f of keep) stash.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)) {
if (!Array.isArray(output.system)) return;
output.system.push(AGENTMEMORY_INSTRUCTIONS);
// prefer the context already fetched at session.created;
// fall back to a fresh /context call if the cache missed (e.g.
// session resumed across plugin reloads).
let ctx = startContextCache.get(sid);
if (typeof ctx !== "string" || ctx.length === 0) {
const result = await postJson("/context", {
sessionId: sid,
project: projectPath,
});
ctx = (result as any)?.context;
} else {
startContextCache.delete(sid);
}
if (typeof ctx === "string" && ctx.length > 0) {
output.system.push(ctx);
}
contextInjectedSessions.add(sid);
}
const stash = stashFor(sid);
if (stash.size === 0) return;
const files = [...stash].slice(0, 10);
const enrichResult = await postJson("/enrich", {
sessionId: sid,
files,
toolName: "enrich_inject",
});
const enrichCtx = (enrichResult as any)?.context;
if (typeof enrichCtx === "string" && enrichCtx.length > 0) {
if (Array.isArray(output.system)) {
output.system.push(enrichCtx);
}
for (const f of files) stash.delete(f);
}
},
// ── 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,
});
const ctx = (result as any)?.context;
if (typeof ctx === "string" && ctx.length > 0) {
if (Array.isArray(output.context)) {
output.context.push(ctx);
}
}
},
// ── config ──
config: async (input) => {
const payload: Record<string, unknown> = {
theme: input.theme ?? null,
model: input.model ?? null,
autoupdate: input.autoupdate ?? null,
agents: typeof input.agent === "object" && input.agent !== null && !Array.isArray(input.agent)
? Object.keys(input.agent as Record<string, unknown>)
: Array.isArray(input.agent) ? input.agent : [],
mcp_servers: typeof input.mcp === "object" && input.mcp !== null && !Array.isArray(input.mcp)
? Object.keys(input.mcp as Record<string, unknown>)
: Array.isArray(input.mcp) ? input.mcp : [],
providers: typeof input.provider === "object" && input.provider !== null && !Array.isArray(input.provider)
? Object.keys(input.provider as Record<string, unknown>)
: Array.isArray(input.provider) ? input.provider : [],
permission: input.permission ?? null,
};
if (activeSessionId) {
await observe(activeSessionId, "config_loaded", payload);
} else {
pendingConfig = payload;
}
},
};
};
+19
View File
@@ -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.
+19
View File
@@ -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.
+12
View File
@@ -0,0 +1,12 @@
{
"name": "agentmemory-capture",
"version": "0.9.20",
"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"
}
+15
View File
@@ -0,0 +1,15 @@
{
"name": "agentmemory",
"version": "0.9.27",
"description": "Persistent memory for AI coding agents -- captures tool usage, compresses via LLM, injects context into future sessions. 12 hooks, 53 MCP tools, 15 skills, real-time viewer.",
"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",
"skills": "skills/",
"mcpServers": ".mcp.copilot.json",
"hooks": "hooks/hooks.copilot.json"
}
+551
View File
@@ -0,0 +1,551 @@
//#region src/state/schema.ts
const KV = {
sessions: "mem:sessions",
observations: (sessionId) => `mem:obs:${sessionId}`,
memories: "mem:memories",
summaries: "mem:summaries",
config: "mem:config",
metrics: "mem:metrics",
health: "mem:health",
embeddings: (obsId) => `mem:emb:${obsId}`,
bm25Index: "mem:index:bm25",
relations: "mem:relations",
profiles: "mem:profiles",
claudeBridge: "mem:claude-bridge",
graphNodes: "mem:graph:nodes",
graphEdges: "mem:graph:edges",
semantic: "mem:semantic",
procedural: "mem:procedural",
teamShared: (teamId) => `mem:team:${teamId}:shared`,
teamUsers: (teamId, userId) => `mem:team:${teamId}:users:${userId}`,
teamProfile: (teamId) => `mem:team:${teamId}:profile`,
audit: "mem:audit",
actions: "mem:actions",
actionEdges: "mem:action-edges",
leases: "mem:leases",
routines: "mem:routines",
routineRuns: "mem:routine-runs",
signals: "mem:signals",
checkpoints: "mem:checkpoints",
mesh: "mem:mesh",
sketches: "mem:sketches",
facets: "mem:facets",
sentinels: "mem:sentinels",
crystals: "mem:crystals"
};
//#endregion
//#region src/state/keyed-mutex.ts
const locks = /* @__PURE__ */ new Map();
function withKeyedLock(key, fn) {
const next = (locks.get(key) ?? Promise.resolve()).then(fn, fn);
const cleanup = next.then(() => {}, () => {});
locks.set(key, cleanup);
cleanup.then(() => {
if (locks.get(key) === cleanup) locks.delete(key);
});
return next;
}
//#endregion
//#region src/functions/diagnostics.ts
const ALL_CATEGORIES = [
"actions",
"leases",
"sentinels",
"sketches",
"signals",
"sessions",
"memories",
"mesh"
];
const TWENTY_FOUR_HOURS_MS = 1440 * 60 * 1e3;
const ONE_HOUR_MS = 3600 * 1e3;
function registerDiagnosticsFunction(sdk, kv) {
sdk.registerFunction("mem::diagnose", async (data) => {
const categories = data.categories && data.categories.length > 0 ? data.categories.filter((c) => ALL_CATEGORIES.includes(c)) : ALL_CATEGORIES;
const checks = [];
const now = Date.now();
if (categories.includes("actions")) {
const actions = await kv.list(KV.actions);
const allEdges = await kv.list(KV.actionEdges);
const leases = await kv.list(KV.leases);
const actionMap = new Map(actions.map((a) => [a.id, a]));
for (const action of actions) {
if (action.status === "active") {
if (!leases.some((l) => l.actionId === action.id && l.status === "active" && new Date(l.expiresAt).getTime() > now)) checks.push({
name: `active-no-lease:${action.id}`,
category: "actions",
status: "warn",
message: `Action "${action.title}" is active but has no active lease`,
fixable: false
});
}
if (action.status === "blocked") {
const deps = allEdges.filter((e) => e.sourceActionId === action.id && e.type === "requires");
if (deps.length > 0) {
if (deps.every((d) => {
const target = actionMap.get(d.targetActionId);
return target && target.status === "done";
})) checks.push({
name: `blocked-deps-done:${action.id}`,
category: "actions",
status: "fail",
message: `Action "${action.title}" is blocked but all dependencies are done`,
fixable: true
});
}
}
if (action.status === "pending") {
const deps = allEdges.filter((e) => e.sourceActionId === action.id && e.type === "requires");
if (deps.length > 0) {
if (deps.some((d) => {
const target = actionMap.get(d.targetActionId);
return !target || target.status !== "done";
})) checks.push({
name: `pending-unsatisfied-deps:${action.id}`,
category: "actions",
status: "fail",
message: `Action "${action.title}" is pending but has unsatisfied dependencies`,
fixable: true
});
}
}
}
if (!checks.some((c) => c.category === "actions" && c.status !== "pass")) checks.push({
name: "actions-ok",
category: "actions",
status: "pass",
message: `All ${actions.length} actions are consistent`,
fixable: false
});
}
if (categories.includes("leases")) {
const leases = await kv.list(KV.leases);
const actions = await kv.list(KV.actions);
const actionIds = new Set(actions.map((a) => a.id));
let leaseIssues = 0;
for (const lease of leases) {
if (lease.status === "active" && new Date(lease.expiresAt).getTime() <= now) {
checks.push({
name: `expired-lease:${lease.id}`,
category: "leases",
status: "fail",
message: `Lease ${lease.id} for action ${lease.actionId} expired at ${lease.expiresAt}`,
fixable: true
});
leaseIssues++;
}
if (!actionIds.has(lease.actionId)) {
checks.push({
name: `orphaned-lease:${lease.id}`,
category: "leases",
status: "fail",
message: `Lease ${lease.id} references non-existent action ${lease.actionId}`,
fixable: true
});
leaseIssues++;
}
}
if (leaseIssues === 0) checks.push({
name: "leases-ok",
category: "leases",
status: "pass",
message: `All ${leases.length} leases are healthy`,
fixable: false
});
}
if (categories.includes("sentinels")) {
const sentinels = await kv.list(KV.sentinels);
const actions = await kv.list(KV.actions);
const actionIds = new Set(actions.map((a) => a.id));
let sentinelIssues = 0;
for (const sentinel of sentinels) {
if (sentinel.status === "watching" && sentinel.expiresAt && new Date(sentinel.expiresAt).getTime() <= now) {
checks.push({
name: `expired-sentinel:${sentinel.id}`,
category: "sentinels",
status: "fail",
message: `Sentinel "${sentinel.name}" expired at ${sentinel.expiresAt}`,
fixable: true
});
sentinelIssues++;
}
for (const actionId of sentinel.linkedActionIds) if (!actionIds.has(actionId)) {
checks.push({
name: `sentinel-missing-action:${sentinel.id}:${actionId}`,
category: "sentinels",
status: "warn",
message: `Sentinel "${sentinel.name}" references non-existent action ${actionId}`,
fixable: false
});
sentinelIssues++;
}
}
if (sentinelIssues === 0) checks.push({
name: "sentinels-ok",
category: "sentinels",
status: "pass",
message: `All ${sentinels.length} sentinels are healthy`,
fixable: false
});
}
if (categories.includes("sketches")) {
const sketches = await kv.list(KV.sketches);
let sketchIssues = 0;
for (const sketch of sketches) if (sketch.status === "active" && new Date(sketch.expiresAt).getTime() <= now) {
checks.push({
name: `expired-sketch:${sketch.id}`,
category: "sketches",
status: "fail",
message: `Sketch "${sketch.title}" expired at ${sketch.expiresAt}`,
fixable: true
});
sketchIssues++;
}
if (sketchIssues === 0) checks.push({
name: "sketches-ok",
category: "sketches",
status: "pass",
message: `All ${sketches.length} sketches are healthy`,
fixable: false
});
}
if (categories.includes("signals")) {
const signals = await kv.list(KV.signals);
let signalIssues = 0;
for (const signal of signals) if (signal.expiresAt && new Date(signal.expiresAt).getTime() <= now) {
checks.push({
name: `expired-signal:${signal.id}`,
category: "signals",
status: "fail",
message: `Signal from "${signal.from}" expired at ${signal.expiresAt}`,
fixable: true
});
signalIssues++;
}
if (signalIssues === 0) checks.push({
name: "signals-ok",
category: "signals",
status: "pass",
message: `All ${signals.length} signals are healthy`,
fixable: false
});
}
if (categories.includes("sessions")) {
const sessions = await kv.list(KV.sessions);
let sessionIssues = 0;
for (const session of sessions) if (session.status === "active" && now - new Date(session.startedAt).getTime() > TWENTY_FOUR_HOURS_MS) {
checks.push({
name: `abandoned-session:${session.id}`,
category: "sessions",
status: "warn",
message: `Session ${session.id} has been active for over 24 hours`,
fixable: false
});
sessionIssues++;
}
if (sessionIssues === 0) checks.push({
name: "sessions-ok",
category: "sessions",
status: "pass",
message: `All ${sessions.length} sessions are healthy`,
fixable: false
});
}
if (categories.includes("memories")) {
const memories = await kv.list(KV.memories);
const memoryIds = new Set(memories.map((m) => m.id));
const supersededBy = /* @__PURE__ */ new Map();
let memoryIssues = 0;
for (const memory of memories) if (memory.supersedes && memory.supersedes.length > 0) for (const sid of memory.supersedes) {
if (!memoryIds.has(sid)) {
checks.push({
name: `memory-missing-supersedes:${memory.id}:${sid}`,
category: "memories",
status: "warn",
message: `Memory "${memory.title}" supersedes non-existent memory ${sid}`,
fixable: false
});
memoryIssues++;
}
supersededBy.set(sid, memory.id);
}
for (const memory of memories) if (memory.isLatest && supersededBy.has(memory.id)) {
checks.push({
name: `memory-stale-latest:${memory.id}`,
category: "memories",
status: "fail",
message: `Memory "${memory.title}" has isLatest=true but is superseded by ${supersededBy.get(memory.id)}`,
fixable: true
});
memoryIssues++;
}
if (memoryIssues === 0) checks.push({
name: "memories-ok",
category: "memories",
status: "pass",
message: `All ${memories.length} memories are consistent`,
fixable: false
});
}
if (categories.includes("mesh")) {
const peers = await kv.list(KV.mesh);
let meshIssues = 0;
for (const peer of peers) {
if (peer.lastSyncAt && now - new Date(peer.lastSyncAt).getTime() > ONE_HOUR_MS) {
checks.push({
name: `stale-peer:${peer.id}`,
category: "mesh",
status: "warn",
message: `Peer "${peer.name}" last synced over 1 hour ago`,
fixable: false
});
meshIssues++;
}
if (peer.status === "error") {
checks.push({
name: `error-peer:${peer.id}`,
category: "mesh",
status: "warn",
message: `Peer "${peer.name}" is in error state`,
fixable: false
});
meshIssues++;
}
}
if (meshIssues === 0) checks.push({
name: "mesh-ok",
category: "mesh",
status: "pass",
message: `All ${peers.length} mesh peers are healthy`,
fixable: false
});
}
return {
success: true,
checks,
summary: {
pass: checks.filter((c) => c.status === "pass").length,
warn: checks.filter((c) => c.status === "warn").length,
fail: checks.filter((c) => c.status === "fail").length,
fixable: checks.filter((c) => c.fixable).length
}
};
});
sdk.registerFunction("mem::heal", async (data) => {
const dryRun = data.dryRun ?? false;
const categories = data.categories && data.categories.length > 0 ? data.categories.filter((c) => ALL_CATEGORIES.includes(c)) : ALL_CATEGORIES;
let fixed = 0;
let skipped = 0;
const details = [];
const now = Date.now();
if (categories.includes("actions")) {
const actions = await kv.list(KV.actions);
const allEdges = await kv.list(KV.actionEdges);
const actionMap = new Map(actions.map((a) => [a.id, a]));
for (const action of actions) {
if (action.status === "blocked") {
const deps = allEdges.filter((e) => e.sourceActionId === action.id && e.type === "requires");
if (deps.length > 0) {
if (deps.every((d) => {
const target = actionMap.get(d.targetActionId);
return target && target.status === "done";
})) {
if (dryRun) {
details.push(`[dry-run] Would unblock action "${action.title}" (${action.id})`);
fixed++;
continue;
}
if (await withKeyedLock(`mem:action:${action.id}`, async () => {
const fresh = await kv.get(KV.actions, action.id);
if (!fresh || fresh.status !== "blocked") return false;
const freshDeps = (await kv.list(KV.actionEdges)).filter((e) => e.sourceActionId === fresh.id && e.type === "requires");
const freshActions = await kv.list(KV.actions);
const freshMap = new Map(freshActions.map((a) => [a.id, a]));
if (!freshDeps.every((d) => {
const target = freshMap.get(d.targetActionId);
return target && target.status === "done";
})) return false;
fresh.status = "pending";
fresh.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
await kv.set(KV.actions, fresh.id, fresh);
return true;
})) {
details.push(`Unblocked action "${action.title}" (${action.id})`);
fixed++;
} else skipped++;
}
}
}
if (action.status === "pending") {
const deps = allEdges.filter((e) => e.sourceActionId === action.id && e.type === "requires");
if (deps.length > 0) {
if (deps.some((d) => {
const target = actionMap.get(d.targetActionId);
return !target || target.status !== "done";
})) {
if (dryRun) {
details.push(`[dry-run] Would block action "${action.title}" (${action.id})`);
fixed++;
continue;
}
if (await withKeyedLock(`mem:action:${action.id}`, async () => {
const fresh = await kv.get(KV.actions, action.id);
if (!fresh || fresh.status !== "pending") return false;
const freshDeps = (await kv.list(KV.actionEdges)).filter((e) => e.sourceActionId === fresh.id && e.type === "requires");
const freshActions = await kv.list(KV.actions);
const freshMap = new Map(freshActions.map((a) => [a.id, a]));
if (!freshDeps.some((d) => {
const target = freshMap.get(d.targetActionId);
return !target || target.status !== "done";
})) return false;
fresh.status = "blocked";
fresh.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
await kv.set(KV.actions, fresh.id, fresh);
return true;
})) {
details.push(`Blocked action "${action.title}" (${action.id})`);
fixed++;
} else skipped++;
}
}
}
}
}
if (categories.includes("leases")) {
const leases = await kv.list(KV.leases);
const actions = await kv.list(KV.actions);
const actionIds = new Set(actions.map((a) => a.id));
for (const lease of leases) {
if (lease.status === "active" && new Date(lease.expiresAt).getTime() <= now) {
if (dryRun) {
details.push(`[dry-run] Would expire lease ${lease.id} for action ${lease.actionId}`);
fixed++;
continue;
}
if (await withKeyedLock(`mem:action:${lease.actionId}`, async () => {
const fresh = await kv.get(KV.leases, lease.id);
if (!fresh || fresh.status !== "active" || new Date(fresh.expiresAt).getTime() > Date.now()) return false;
fresh.status = "expired";
await kv.set(KV.leases, fresh.id, fresh);
const action = await kv.get(KV.actions, fresh.actionId);
if (action && action.status === "active" && action.assignedTo === fresh.agentId) {
action.status = "pending";
action.assignedTo = void 0;
action.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
await kv.set(KV.actions, action.id, action);
}
return true;
})) {
details.push(`Expired lease ${lease.id} for action ${lease.actionId}`);
fixed++;
} else skipped++;
continue;
}
if (!actionIds.has(lease.actionId)) {
if (dryRun) {
details.push(`[dry-run] Would delete orphaned lease ${lease.id}`);
fixed++;
continue;
}
await kv.delete(KV.leases, lease.id);
details.push(`Deleted orphaned lease ${lease.id}`);
fixed++;
}
}
}
if (categories.includes("sentinels")) {
const sentinels = await kv.list(KV.sentinels);
for (const sentinel of sentinels) if (sentinel.status === "watching" && sentinel.expiresAt && new Date(sentinel.expiresAt).getTime() <= now) {
if (dryRun) {
details.push(`[dry-run] Would expire sentinel "${sentinel.name}" (${sentinel.id})`);
fixed++;
continue;
}
if (await withKeyedLock(`mem:sentinel:${sentinel.id}`, async () => {
const fresh = await kv.get(KV.sentinels, sentinel.id);
if (!fresh || fresh.status !== "watching") return false;
if (!fresh.expiresAt || new Date(fresh.expiresAt).getTime() > Date.now()) return false;
fresh.status = "expired";
await kv.set(KV.sentinels, fresh.id, fresh);
return true;
})) {
details.push(`Expired sentinel "${sentinel.name}" (${sentinel.id})`);
fixed++;
} else skipped++;
}
}
if (categories.includes("sketches")) {
const sketches = await kv.list(KV.sketches);
for (const sketch of sketches) if (sketch.status === "active" && new Date(sketch.expiresAt).getTime() <= now) {
if (dryRun) {
details.push(`[dry-run] Would discard expired sketch "${sketch.title}" (${sketch.id})`);
fixed++;
continue;
}
if (await withKeyedLock(`mem:sketch:${sketch.id}`, async () => {
const fresh = await kv.get(KV.sketches, sketch.id);
if (!fresh || fresh.status !== "active" || new Date(fresh.expiresAt).getTime() > Date.now()) return false;
const allEdges = await kv.list(KV.actionEdges);
const actionIdSet = new Set(fresh.actionIds);
for (const edge of allEdges) if (actionIdSet.has(edge.sourceActionId) || actionIdSet.has(edge.targetActionId)) await kv.delete(KV.actionEdges, edge.id);
for (const actionId of fresh.actionIds) await kv.delete(KV.actions, actionId);
fresh.status = "discarded";
fresh.discardedAt = (/* @__PURE__ */ new Date()).toISOString();
await kv.set(KV.sketches, fresh.id, fresh);
return true;
})) {
details.push(`Discarded expired sketch "${sketch.title}" (${sketch.id})`);
fixed++;
} else skipped++;
}
}
if (categories.includes("signals")) {
const signals = await kv.list(KV.signals);
for (const signal of signals) if (signal.expiresAt && new Date(signal.expiresAt).getTime() <= now) {
if (dryRun) {
details.push(`[dry-run] Would delete expired signal ${signal.id}`);
fixed++;
continue;
}
await kv.delete(KV.signals, signal.id);
details.push(`Deleted expired signal ${signal.id}`);
fixed++;
}
}
if (categories.includes("memories")) {
const memories = await kv.list(KV.memories);
const supersededBy = /* @__PURE__ */ new Map();
for (const memory of memories) if (memory.supersedes && memory.supersedes.length > 0) for (const sid of memory.supersedes) supersededBy.set(sid, memory.id);
for (const memory of memories) if (memory.isLatest && supersededBy.has(memory.id)) {
if (dryRun) {
details.push(`[dry-run] Would set isLatest=false on memory "${memory.title}" (${memory.id})`);
fixed++;
continue;
}
if (await withKeyedLock(`mem:memory:${memory.id}`, async () => {
const fresh = await kv.get(KV.memories, memory.id);
if (!fresh || !fresh.isLatest) return false;
fresh.isLatest = false;
fresh.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
await kv.set(KV.memories, fresh.id, fresh);
return true;
})) {
details.push(`Set isLatest=false on memory "${memory.title}" (${memory.id})`);
fixed++;
} else skipped++;
}
}
return {
success: true,
fixed,
skipped,
details
};
});
}
//#endregion
export { registerDiagnosticsFunction };
//# sourceMappingURL=diagnostics.mjs.map
+76
View File
@@ -0,0 +1,76 @@
#!/usr/bin/env node
import { execSync } from "node:child_process";
import { basename } from "node:path";
//#region src/hooks/_project.ts
function resolveProject(cwd) {
const explicit = process.env["AGENTMEMORY_PROJECT_NAME"];
if (explicit && explicit.trim()) return explicit.trim();
const dir = cwd && cwd.trim() ? cwd : process.cwd();
try {
const top = execSync("git rev-parse --show-toplevel", {
cwd: dir,
stdio: [
"ignore",
"pipe",
"ignore"
],
timeout: 500
}).toString().trim();
if (top) return basename(top);
} catch {}
return basename(dir);
}
//#endregion
//#region src/hooks/notification.ts
function isSdkChildContext(payload) {
if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true;
if (!payload || typeof payload !== "object") return false;
return payload.entrypoint === "sdk-ts";
}
const REST_URL = process.env["AGENTMEMORY_URL"] || "http://localhost:3111";
const SECRET = process.env["AGENTMEMORY_SECRET"] || "";
function authHeaders() {
const h = { "Content-Type": "application/json" };
if (SECRET) h["Authorization"] = `Bearer ${SECRET}`;
return h;
}
async function main() {
let input = "";
for await (const chunk of process.stdin) input += chunk;
let data;
try {
data = JSON.parse(input);
} catch {
return;
}
if (isSdkChildContext(data)) return;
const notificationType = data.notification_type ?? data.notificationType;
if (notificationType !== "permission_prompt") return;
const rawSessionId = data.session_id ?? data.sessionId;
const sessionId = typeof rawSessionId === "string" && rawSessionId.length > 0 ? rawSessionId : "unknown";
fetch(`${REST_URL}/agentmemory/observe`, {
method: "POST",
headers: authHeaders(),
body: JSON.stringify({
hookType: "notification",
sessionId,
project: resolveProject(data.cwd),
cwd: data.cwd || process.cwd(),
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
data: {
notification_type: notificationType,
title: data.title,
message: data.message
}
}),
signal: AbortSignal.timeout(2e3)
}).catch(() => {});
setTimeout(() => process.exit(0), 500).unref();
}
main();
//#endregion
export { };
//# sourceMappingURL=notification.mjs.map
+102
View File
@@ -0,0 +1,102 @@
#!/usr/bin/env node
import { execFile } from "node:child_process";
import { promisify } from "node:util";
//#region src/hooks/post-commit.ts
const exec = promisify(execFile);
function isSdkChildContext(payload) {
if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true;
if (!payload || typeof payload !== "object") return false;
return payload.entrypoint === "sdk-ts";
}
const REST_URL = process.env["AGENTMEMORY_URL"] || "http://localhost:3111";
const SECRET = process.env["AGENTMEMORY_SECRET"] || "";
const TIMEOUT_MS = 1500;
function authHeaders() {
const h = { "Content-Type": "application/json" };
if (SECRET) h["Authorization"] = `Bearer ${SECRET}`;
return h;
}
async function git(args, cwd) {
try {
const { stdout } = await exec("git", args, {
cwd,
timeout: 1500
});
return stdout.trim();
} catch {
return null;
}
}
async function main() {
let input = "";
for await (const chunk of process.stdin) input += chunk;
let data = {};
if (input.trim()) try {
data = JSON.parse(input);
} catch {}
if (isSdkChildContext(data)) return;
const cwd = data.cwd || process.env["AGENTMEMORY_CWD"] || process.cwd();
const sessionId = data.session_id || process.env["AGENTMEMORY_SESSION_ID"] || void 0;
const sha = process.env["AGENTMEMORY_COMMIT_SHA"] || await git(["rev-parse", "HEAD"], cwd);
if (!sha) return;
const branch = await git([
"rev-parse",
"--abbrev-ref",
"HEAD"
], cwd);
const repo = await git([
"config",
"--get",
"remote.origin.url"
], cwd);
const message = await git([
"log",
"-1",
"--pretty=%B",
sha
], cwd);
const author = await git([
"log",
"-1",
"--pretty=%an <%ae>",
sha
], cwd);
const authoredAt = await git([
"log",
"-1",
"--pretty=%aI",
sha
], cwd);
const filesRaw = await git([
"diff-tree",
"--no-commit-id",
"--name-only",
"-r",
sha
], cwd);
const files = filesRaw ? filesRaw.split("\n").filter(Boolean) : void 0;
const body = {
sessionId,
sha,
branch: branch || void 0,
repo: repo || void 0,
message: message || void 0,
author: author || void 0,
authoredAt: authoredAt || void 0,
files
};
try {
await fetch(`${REST_URL}/agentmemory/session/commit`, {
method: "POST",
headers: authHeaders(),
body: JSON.stringify(body),
signal: AbortSignal.timeout(TIMEOUT_MS)
});
} catch {}
}
main();
//#endregion
export { };
//# sourceMappingURL=post-commit.mjs.map
+77
View File
@@ -0,0 +1,77 @@
#!/usr/bin/env node
import { execSync } from "node:child_process";
import { basename } from "node:path";
//#region src/hooks/_project.ts
function resolveProject(cwd) {
const explicit = process.env["AGENTMEMORY_PROJECT_NAME"];
if (explicit && explicit.trim()) return explicit.trim();
const dir = cwd && cwd.trim() ? cwd : process.cwd();
try {
const top = execSync("git rev-parse --show-toplevel", {
cwd: dir,
stdio: [
"ignore",
"pipe",
"ignore"
],
timeout: 500
}).toString().trim();
if (top) return basename(top);
} catch {}
return basename(dir);
}
//#endregion
//#region src/hooks/post-tool-failure.ts
function isSdkChildContext(payload) {
if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true;
if (!payload || typeof payload !== "object") return false;
return payload.entrypoint === "sdk-ts";
}
const REST_URL = process.env["AGENTMEMORY_URL"] || "http://localhost:3111";
const SECRET = process.env["AGENTMEMORY_SECRET"] || "";
function authHeaders() {
const h = { "Content-Type": "application/json" };
if (SECRET) h["Authorization"] = `Bearer ${SECRET}`;
return h;
}
async function main() {
let input = "";
for await (const chunk of process.stdin) input += chunk;
let data;
try {
data = JSON.parse(input);
} catch {
return;
}
if (isSdkChildContext(data)) return;
if (data.is_interrupt || data.isInterrupt) return;
const sessionId = data.session_id || data.sessionId || "unknown";
const toolName = data.tool_name ?? data.toolName;
const toolInput = data.tool_input ?? data.toolArgs;
const error = data.error ?? data.errorMessage;
fetch(`${REST_URL}/agentmemory/observe`, {
method: "POST",
headers: authHeaders(),
body: JSON.stringify({
hookType: "post_tool_failure",
sessionId,
project: resolveProject(data.cwd),
cwd: data.cwd || process.cwd(),
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
data: {
tool_name: toolName,
tool_input: typeof toolInput === "string" ? toolInput.slice(0, 4e3) : JSON.stringify(toolInput ?? "").slice(0, 4e3),
error: typeof error === "string" ? error.slice(0, 4e3) : JSON.stringify(error ?? "").slice(0, 4e3)
}
}),
signal: AbortSignal.timeout(3e3)
}).catch(() => {});
setTimeout(() => process.exit(0), 500).unref();
}
main();
//#endregion
export { };
//# sourceMappingURL=post-tool-failure.mjs.map
+122
View File
@@ -0,0 +1,122 @@
#!/usr/bin/env node
import { execSync } from "node:child_process";
import { basename } from "node:path";
//#region src/hooks/_project.ts
function resolveProject(cwd) {
const explicit = process.env["AGENTMEMORY_PROJECT_NAME"];
if (explicit && explicit.trim()) return explicit.trim();
const dir = cwd && cwd.trim() ? cwd : process.cwd();
try {
const top = execSync("git rev-parse --show-toplevel", {
cwd: dir,
stdio: [
"ignore",
"pipe",
"ignore"
],
timeout: 500
}).toString().trim();
if (top) return basename(top);
} catch {}
return basename(dir);
}
//#endregion
//#region src/hooks/post-tool-use.ts
function isSdkChildContext(payload) {
if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true;
if (!payload || typeof payload !== "object") return false;
return payload.entrypoint === "sdk-ts";
}
const REST_URL = process.env["AGENTMEMORY_URL"] || "http://localhost:3111";
const SECRET = process.env["AGENTMEMORY_SECRET"] || "";
function authHeaders() {
const h = { "Content-Type": "application/json" };
if (SECRET) h["Authorization"] = `Bearer ${SECRET}`;
return h;
}
async function main() {
let input = "";
for await (const chunk of process.stdin) input += chunk;
let data;
try {
data = JSON.parse(input);
} catch {
return;
}
if (isSdkChildContext(data)) return;
const sessionId = data.session_id || data.sessionId || "unknown";
const toolName = data.tool_name ?? data.toolName;
const toolInput = data.tool_input ?? data.toolArgs;
const { imageData, cleanOutput } = extractImageData(toolOutput(data));
fetch(`${REST_URL}/agentmemory/observe`, {
method: "POST",
headers: authHeaders(),
body: JSON.stringify({
hookType: "post_tool_use",
sessionId,
project: resolveProject(data.cwd),
cwd: data.cwd || process.cwd(),
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
data: {
tool_name: toolName,
tool_input: toolInput,
tool_output: truncate(cleanOutput, 8e3),
...imageData ? { image_data: imageData } : {}
}
}),
signal: AbortSignal.timeout(3e3)
}).catch(() => {});
setTimeout(() => process.exit(0), 500).unref();
}
function toolOutput(data) {
if (data.tool_response !== void 0) return data.tool_response;
if (data.tool_output !== void 0) return data.tool_output;
const result = data.tool_result ?? data.toolResult;
if (typeof result === "object" && result !== null) {
const obj = result;
return obj.text_result_for_llm ?? obj.textResultForLlm ?? result;
}
return result;
}
function isBase64Image(val) {
return typeof val === "string" && (val.startsWith("data:image/") || val.startsWith("iVBORw0KGgo") || val.startsWith("/9j/"));
}
function extractImageData(output) {
if (isBase64Image(output)) return {
imageData: output,
cleanOutput: "[image data extracted]"
};
if (typeof output === "object" && output !== null && !Array.isArray(output)) {
const obj = output;
let imageData;
const clean = {};
for (const [key, val] of Object.entries(obj)) if (!imageData && isBase64Image(val)) {
imageData = val;
clean[key] = "[image data extracted]";
} else clean[key] = val;
return {
imageData,
cleanOutput: clean
};
}
return {
imageData: void 0,
cleanOutput: output
};
}
function truncate(value, max) {
if (typeof value === "string" && value.length > max) return value.slice(0, max) + "\n[...truncated]";
if (typeof value === "object" && value !== null) {
const str = JSON.stringify(value);
if (str.length > max) return str.slice(0, max) + "...[truncated]";
return value;
}
return value;
}
main();
//#endregion
export { };
//# sourceMappingURL=post-tool-use.mjs.map
+80
View File
@@ -0,0 +1,80 @@
#!/usr/bin/env node
import { execSync } from "node:child_process";
import { basename } from "node:path";
//#region src/hooks/_project.ts
function resolveProject(cwd) {
const explicit = process.env["AGENTMEMORY_PROJECT_NAME"];
if (explicit && explicit.trim()) return explicit.trim();
const dir = cwd && cwd.trim() ? cwd : process.cwd();
try {
const top = execSync("git rev-parse --show-toplevel", {
cwd: dir,
stdio: [
"ignore",
"pipe",
"ignore"
],
timeout: 500
}).toString().trim();
if (top) return basename(top);
} catch {}
return basename(dir);
}
//#endregion
//#region src/hooks/pre-compact.ts
function isSdkChildContext(payload) {
if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true;
if (!payload || typeof payload !== "object") return false;
return payload.entrypoint === "sdk-ts";
}
const REST_URL = process.env["AGENTMEMORY_URL"] || "http://localhost:3111";
const SECRET = process.env["AGENTMEMORY_SECRET"] || "";
function authHeaders() {
const h = { "Content-Type": "application/json" };
if (SECRET) h["Authorization"] = `Bearer ${SECRET}`;
return h;
}
async function main() {
let input = "";
for await (const chunk of process.stdin) input += chunk;
let data;
try {
data = JSON.parse(input);
} catch {
return;
}
if (isSdkChildContext(data)) return;
const sessionId = data.session_id || data.sessionId || "unknown";
const project = resolveProject(data.cwd);
if (process.env["CLAUDE_MEMORY_BRIDGE"] === "true") try {
await fetch(`${REST_URL}/agentmemory/claude-bridge/sync`, {
method: "POST",
headers: authHeaders(),
body: JSON.stringify({}),
signal: AbortSignal.timeout(5e3)
});
} catch {}
try {
const res = await fetch(`${REST_URL}/agentmemory/context`, {
method: "POST",
headers: authHeaders(),
body: JSON.stringify({
sessionId,
project,
budget: 1500
}),
signal: AbortSignal.timeout(5e3)
});
if (res.ok) {
const result = await res.json();
if (result.context) process.stdout.write(result.context);
}
} catch {}
}
main();
//#endregion
export { };
//# sourceMappingURL=pre-compact.mjs.map
+84
View File
@@ -0,0 +1,84 @@
#!/usr/bin/env node
//#region src/hooks/pre-tool-use.ts
function isSdkChildContext(payload) {
if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true;
if (!payload || typeof payload !== "object") return false;
return payload.entrypoint === "sdk-ts";
}
const INJECT_CONTEXT = process.env["AGENTMEMORY_INJECT_CONTEXT"] === "true";
const REST_URL = process.env["AGENTMEMORY_URL"] || "http://localhost:3111";
const SECRET = process.env["AGENTMEMORY_SECRET"] || "";
function authHeaders() {
const h = { "Content-Type": "application/json" };
if (SECRET) h["Authorization"] = `Bearer ${SECRET}`;
return h;
}
async function main() {
if (!INJECT_CONTEXT) return;
let input = "";
for await (const chunk of process.stdin) input += chunk;
let data;
try {
data = JSON.parse(input);
} catch {
return;
}
if (isSdkChildContext(data)) return;
const toolName = typeof data.tool_name === "string" ? data.tool_name : typeof data.toolName === "string" ? data.toolName : void 0;
if (!toolName) return;
const normalizedToolName = toolName.toLowerCase();
if (![
"edit",
"write",
"create",
"read",
"view",
"glob",
"grep"
].includes(normalizedToolName)) return;
const rawToolInput = data.tool_input ?? data.toolArgs;
const toolInput = typeof rawToolInput === "object" && rawToolInput !== null && !Array.isArray(rawToolInput) ? rawToolInput : {};
const files = [];
const fileKeys = normalizedToolName === "grep" ? ["path", "file"] : [
"file_path",
"path",
"file",
"pattern"
];
for (const key of fileKeys) {
const val = toolInput[key];
if (typeof val === "string" && val.length > 0) files.push(val);
}
if (files.length === 0) return;
const terms = [];
if (normalizedToolName === "grep" || normalizedToolName === "glob") {
const pattern = toolInput["pattern"];
if (typeof pattern === "string" && pattern.length > 0) terms.push(pattern);
}
const rawSessionId = data.session_id || data.sessionId;
const sessionId = typeof rawSessionId === "string" && rawSessionId.length > 0 ? rawSessionId : "unknown";
const project = typeof data.project === "string" && data.project.trim().length > 0 ? data.project.trim() : void 0;
try {
const res = await fetch(`${REST_URL}/agentmemory/enrich`, {
method: "POST",
headers: authHeaders(),
body: JSON.stringify({
sessionId,
files,
terms,
toolName,
...project !== void 0 && { project }
}),
signal: AbortSignal.timeout(2e3)
});
if (res.ok) {
const result = await res.json();
if (result.context) process.stdout.write(result.context);
}
} catch {}
}
main();
//#endregion
export { };
//# sourceMappingURL=pre-tool-use.mjs.map
+69
View File
@@ -0,0 +1,69 @@
#!/usr/bin/env node
import { execSync } from "node:child_process";
import { basename } from "node:path";
//#region src/hooks/_project.ts
function resolveProject(cwd) {
const explicit = process.env["AGENTMEMORY_PROJECT_NAME"];
if (explicit && explicit.trim()) return explicit.trim();
const dir = cwd && cwd.trim() ? cwd : process.cwd();
try {
const top = execSync("git rev-parse --show-toplevel", {
cwd: dir,
stdio: [
"ignore",
"pipe",
"ignore"
],
timeout: 500
}).toString().trim();
if (top) return basename(top);
} catch {}
return basename(dir);
}
//#endregion
//#region src/hooks/prompt-submit.ts
function isSdkChildContext(payload) {
if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true;
if (!payload || typeof payload !== "object") return false;
return payload.entrypoint === "sdk-ts";
}
const REST_URL = process.env["AGENTMEMORY_URL"] || "http://localhost:3111";
const SECRET = process.env["AGENTMEMORY_SECRET"] || "";
function authHeaders() {
const h = { "Content-Type": "application/json" };
if (SECRET) h["Authorization"] = `Bearer ${SECRET}`;
return h;
}
async function main() {
let input = "";
for await (const chunk of process.stdin) input += chunk;
let data;
try {
data = JSON.parse(input);
} catch {
return;
}
if (isSdkChildContext(data)) return;
const sessionId = data.session_id || data.sessionId || "unknown";
fetch(`${REST_URL}/agentmemory/observe`, {
method: "POST",
headers: authHeaders(),
body: JSON.stringify({
hookType: "prompt_submit",
sessionId,
project: resolveProject(data.cwd),
cwd: data.cwd || process.cwd(),
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
data: { prompt: data.prompt ?? data.userPrompt }
}),
signal: AbortSignal.timeout(3e3)
}).catch(() => {});
setTimeout(() => process.exit(0), 500).unref();
}
main();
//#endregion
export { };
//# sourceMappingURL=prompt-submit.mjs.map
+60
View File
@@ -0,0 +1,60 @@
#!/usr/bin/env node
//#region src/hooks/session-end.ts
function isSdkChildContext(payload) {
if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true;
if (!payload || typeof payload !== "object") return false;
return payload.entrypoint === "sdk-ts";
}
const REST_URL = process.env["AGENTMEMORY_URL"] || "http://localhost:3111";
const SECRET = process.env["AGENTMEMORY_SECRET"] || "";
function authHeaders() {
const h = { "Content-Type": "application/json" };
if (SECRET) h["Authorization"] = `Bearer ${SECRET}`;
return h;
}
async function main() {
let input = "";
for await (const chunk of process.stdin) input += chunk;
let data;
try {
data = JSON.parse(input);
} catch {
return;
}
if (isSdkChildContext(data)) return;
const sessionId = data.session_id || data.sessionId || "unknown";
fetch(`${REST_URL}/agentmemory/session/end`, {
method: "POST",
headers: authHeaders(),
body: JSON.stringify({ sessionId }),
signal: AbortSignal.timeout(3e4)
}).catch(() => {});
if (process.env["CONSOLIDATION_ENABLED"] === "true") {
fetch(`${REST_URL}/agentmemory/crystals/auto`, {
method: "POST",
headers: authHeaders(),
body: JSON.stringify({ olderThanDays: 0 }),
signal: AbortSignal.timeout(6e4)
}).catch(() => {});
fetch(`${REST_URL}/agentmemory/consolidate-pipeline`, {
method: "POST",
headers: authHeaders(),
body: JSON.stringify({
tier: "all",
force: true
}),
signal: AbortSignal.timeout(12e4)
}).catch(() => {});
}
if (process.env["CLAUDE_MEMORY_BRIDGE"] === "true") fetch(`${REST_URL}/agentmemory/claude-bridge/sync`, {
method: "POST",
headers: authHeaders(),
signal: AbortSignal.timeout(3e4)
}).catch(() => {});
setTimeout(() => process.exit(0), 1500).unref();
}
main();
//#endregion
export { };
//# sourceMappingURL=session-end.mjs.map
+87
View File
@@ -0,0 +1,87 @@
#!/usr/bin/env node
import { execSync } from "node:child_process";
import { basename } from "node:path";
//#region src/hooks/_project.ts
function resolveProject(cwd) {
const explicit = process.env["AGENTMEMORY_PROJECT_NAME"];
if (explicit && explicit.trim()) return explicit.trim();
const dir = cwd && cwd.trim() ? cwd : process.cwd();
try {
const top = execSync("git rev-parse --show-toplevel", {
cwd: dir,
stdio: [
"ignore",
"pipe",
"ignore"
],
timeout: 500
}).toString().trim();
if (top) return basename(top);
} catch {}
return basename(dir);
}
//#endregion
//#region src/hooks/session-start.ts
function isSdkChildContext(payload) {
if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true;
if (!payload || typeof payload !== "object") return false;
return payload.entrypoint === "sdk-ts";
}
const INJECT_CONTEXT = process.env["AGENTMEMORY_INJECT_CONTEXT"] === "true";
const REST_URL = process.env["AGENTMEMORY_URL"] || "http://localhost:3111";
const SECRET = process.env["AGENTMEMORY_SECRET"] || "";
const INJECT_TIMEOUT_MS = 1500;
const REGISTER_TIMEOUT_MS = 800;
function authHeaders() {
const h = { "Content-Type": "application/json" };
if (SECRET) h["Authorization"] = `Bearer ${SECRET}`;
return h;
}
async function main() {
let input = "";
for await (const chunk of process.stdin) input += chunk;
let data;
try {
data = JSON.parse(input);
} catch {
return;
}
if (isSdkChildContext(data)) return;
const sessionId = data.session_id || data.sessionId || `ses_${Date.now().toString(36)}`;
const cwd = data.cwd || process.cwd();
const project = resolveProject(data.cwd);
const url = `${REST_URL}/agentmemory/session/start`;
const init = {
method: "POST",
headers: authHeaders(),
body: JSON.stringify({
sessionId,
project,
cwd
})
};
if (!INJECT_CONTEXT) {
fetch(url, {
...init,
signal: AbortSignal.timeout(REGISTER_TIMEOUT_MS)
}).catch(() => {});
return;
}
try {
const res = await fetch(url, {
...init,
signal: AbortSignal.timeout(INJECT_TIMEOUT_MS)
});
if (res.ok) {
const result = await res.json();
if (result.context) process.stdout.write(result.context);
}
} catch {}
}
main();
//#endregion
export { };
//# sourceMappingURL=session-start.mjs.map
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env node
//#region src/hooks/stop.ts
function isSdkChildContext(payload) {
if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true;
if (!payload || typeof payload !== "object") return false;
return payload.entrypoint === "sdk-ts";
}
const REST_URL = process.env["AGENTMEMORY_URL"] || "http://localhost:3111";
const SECRET = process.env["AGENTMEMORY_SECRET"] || "";
function authHeaders() {
const h = { "Content-Type": "application/json" };
if (SECRET) h["Authorization"] = `Bearer ${SECRET}`;
return h;
}
async function main() {
let input = "";
for await (const chunk of process.stdin) input += chunk;
let data;
try {
data = JSON.parse(input);
} catch {
return;
}
if (isSdkChildContext(data)) return;
const sessionId = data.session_id || data.sessionId || "unknown";
fetch(`${REST_URL}/agentmemory/summarize`, {
method: "POST",
headers: authHeaders(),
body: JSON.stringify({ sessionId }),
signal: AbortSignal.timeout(12e4)
}).catch(() => {});
fetch(`${REST_URL}/agentmemory/session/end`, {
method: "POST",
headers: authHeaders(),
body: JSON.stringify({ sessionId }),
signal: AbortSignal.timeout(5e3)
}).catch(() => {});
setTimeout(() => process.exit(0), 1500).unref();
}
main();
//#endregion
export { };
//# sourceMappingURL=stop.mjs.map
+75
View File
@@ -0,0 +1,75 @@
#!/usr/bin/env node
import { execSync } from "node:child_process";
import { basename } from "node:path";
//#region src/hooks/_project.ts
function resolveProject(cwd) {
const explicit = process.env["AGENTMEMORY_PROJECT_NAME"];
if (explicit && explicit.trim()) return explicit.trim();
const dir = cwd && cwd.trim() ? cwd : process.cwd();
try {
const top = execSync("git rev-parse --show-toplevel", {
cwd: dir,
stdio: [
"ignore",
"pipe",
"ignore"
],
timeout: 500
}).toString().trim();
if (top) return basename(top);
} catch {}
return basename(dir);
}
//#endregion
//#region src/hooks/subagent-start.ts
function isSdkChildContext(payload) {
if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true;
if (!payload || typeof payload !== "object") return false;
return payload.entrypoint === "sdk-ts";
}
const REST_URL = process.env["AGENTMEMORY_URL"] || "http://localhost:3111";
const SECRET = process.env["AGENTMEMORY_SECRET"] || "";
const TIMEOUT_MS = 800;
function authHeaders() {
const h = { "Content-Type": "application/json" };
if (SECRET) h["Authorization"] = `Bearer ${SECRET}`;
return h;
}
async function main() {
let input = "";
for await (const chunk of process.stdin) input += chunk;
let data;
try {
data = JSON.parse(input);
} catch {
return;
}
if (isSdkChildContext(data)) return;
const sessionId = data.session_id || data.sessionId || "unknown";
const agentId = data.agent_id || data.agentName;
const agentType = data.agent_type || data.agentDisplayName || data.agentName;
fetch(`${REST_URL}/agentmemory/observe`, {
method: "POST",
headers: authHeaders(),
body: JSON.stringify({
hookType: "subagent_start",
sessionId,
project: resolveProject(data.cwd),
cwd: data.cwd || process.cwd(),
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
data: {
agent_id: agentId,
agent_type: agentType
}
}),
signal: AbortSignal.timeout(TIMEOUT_MS)
}).catch(() => {});
setTimeout(() => process.exit(0), 500).unref();
}
main();
//#endregion
export { };
//# sourceMappingURL=subagent-start.mjs.map
+76
View File
@@ -0,0 +1,76 @@
#!/usr/bin/env node
import { execSync } from "node:child_process";
import { basename } from "node:path";
//#region src/hooks/_project.ts
function resolveProject(cwd) {
const explicit = process.env["AGENTMEMORY_PROJECT_NAME"];
if (explicit && explicit.trim()) return explicit.trim();
const dir = cwd && cwd.trim() ? cwd : process.cwd();
try {
const top = execSync("git rev-parse --show-toplevel", {
cwd: dir,
stdio: [
"ignore",
"pipe",
"ignore"
],
timeout: 500
}).toString().trim();
if (top) return basename(top);
} catch {}
return basename(dir);
}
//#endregion
//#region src/hooks/subagent-stop.ts
function isSdkChildContext(payload) {
if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true;
if (!payload || typeof payload !== "object") return false;
return payload.entrypoint === "sdk-ts";
}
const REST_URL = process.env["AGENTMEMORY_URL"] || "http://localhost:3111";
const SECRET = process.env["AGENTMEMORY_SECRET"] || "";
function authHeaders() {
const h = { "Content-Type": "application/json" };
if (SECRET) h["Authorization"] = `Bearer ${SECRET}`;
return h;
}
async function main() {
let input = "";
for await (const chunk of process.stdin) input += chunk;
let data;
try {
data = JSON.parse(input);
} catch {
return;
}
if (isSdkChildContext(data)) return;
const sessionId = data.session_id || data.sessionId || "unknown";
const agentId = data.agent_id || data.agentName;
const agentType = data.agent_type || data.agentDisplayName || data.agentName;
const lastMsg = typeof data.last_assistant_message === "string" ? data.last_assistant_message.slice(0, 4e3) : "";
fetch(`${REST_URL}/agentmemory/observe`, {
method: "POST",
headers: authHeaders(),
body: JSON.stringify({
hookType: "subagent_stop",
sessionId,
project: resolveProject(data.cwd),
cwd: data.cwd || process.cwd(),
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
data: {
agent_id: agentId,
agent_type: agentType,
last_message: lastMsg
}
}),
signal: AbortSignal.timeout(2e3)
}).catch(() => {});
setTimeout(() => process.exit(0), 500).unref();
}
main();
//#endregion
export { };
//# sourceMappingURL=subagent-stop.mjs.map
+75
View File
@@ -0,0 +1,75 @@
#!/usr/bin/env node
import { execSync } from "node:child_process";
import { basename } from "node:path";
//#region src/hooks/_project.ts
function resolveProject(cwd) {
const explicit = process.env["AGENTMEMORY_PROJECT_NAME"];
if (explicit && explicit.trim()) return explicit.trim();
const dir = cwd && cwd.trim() ? cwd : process.cwd();
try {
const top = execSync("git rev-parse --show-toplevel", {
cwd: dir,
stdio: [
"ignore",
"pipe",
"ignore"
],
timeout: 500
}).toString().trim();
if (top) return basename(top);
} catch {}
return basename(dir);
}
//#endregion
//#region src/hooks/task-completed.ts
function isSdkChildContext(payload) {
if (process.env["AGENTMEMORY_SDK_CHILD"] === "1") return true;
if (!payload || typeof payload !== "object") return false;
return payload.entrypoint === "sdk-ts";
}
const REST_URL = process.env["AGENTMEMORY_URL"] || "http://localhost:3111";
const SECRET = process.env["AGENTMEMORY_SECRET"] || "";
function authHeaders() {
const h = { "Content-Type": "application/json" };
if (SECRET) h["Authorization"] = `Bearer ${SECRET}`;
return h;
}
async function main() {
let input = "";
for await (const chunk of process.stdin) input += chunk;
let data;
try {
data = JSON.parse(input);
} catch {
return;
}
if (isSdkChildContext(data)) return;
const sessionId = data.session_id || "unknown";
fetch(`${REST_URL}/agentmemory/observe`, {
method: "POST",
headers: authHeaders(),
body: JSON.stringify({
hookType: "task_completed",
sessionId,
project: resolveProject(data.cwd),
cwd: data.cwd || process.cwd(),
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
data: {
task_id: data.task_id,
task_subject: data.task_subject,
task_description: typeof data.task_description === "string" ? data.task_description.slice(0, 2e3) : "",
teammate_name: data.teammate_name,
team_name: data.team_name
}
}),
signal: AbortSignal.timeout(2e3)
}).catch(() => {});
setTimeout(() => process.exit(0), 500).unref();
}
main();
//#endregion
export { };
//# sourceMappingURL=task-completed.mjs.map
+38
View File
@@ -0,0 +1,38 @@
# Troubleshooting agentmemory skills
Shared recovery steps for all user-invocable agentmemory skills. Each skill's
Troubleshooting section points here instead of duplicating the block.
## "MCP tool not available"
If a `memory_*` MCP tool does not appear, the stdio MCP shim never started.
Walk these in order:
1. Run `/plugin list` in the host and confirm `agentmemory` shows as enabled.
2. Restart the host. The plugin's `.mcp.json` is only read on startup, so a
freshly installed or re-enabled plugin will not register tools mid-session.
3. Check `/mcp` and confirm the `agentmemory` server shows a live connection.
## REST fallback
When the MCP tools stay unavailable but the daemon is running, call the REST
API directly:
1. Set `AGENTMEMORY_URL` to the daemon base URL (default `http://localhost:3111`).
2. Add `Authorization: Bearer $AGENTMEMORY_SECRET` ONLY when `AGENTMEMORY_SECRET`
is set. The default localhost daemon is open and rejects a stray header.
Endpoint map by skill:
| Skill | REST call |
| --------------- | --------------------------------------------------------------- |
| remember | `POST /agentmemory/remember` |
| recall | `POST /agentmemory/smart-search` |
| recap | `GET /agentmemory/sessions` + `POST /agentmemory/smart-search` |
| handoff | `GET /agentmemory/sessions` + `POST /agentmemory/smart-search` |
| session-history | `GET /agentmemory/sessions` |
| commit-context | `GET /agentmemory/session/by-commit?sha=<sha>` |
| commit-history | `GET /agentmemory/commits` (URL-encode every query param) |
The daemon reads `.mcp.json` on startup only, so any port or auth change needs a
restart before either transport sees it.
@@ -0,0 +1,28 @@
# agentmemory connect adapters reference
Generated from `src/cli/connect/index.ts`. Do not edit the block below by hand; run `npm run skills:gen` after adding or removing an adapter.
<!-- AUTOGEN:agents START - generated by scripts/skills/generate.ts, do not edit by hand -->
`agentmemory connect <agent>` wires the memory server into a host agent. 18 adapters:
| Agent | Name | Protocol |
| --- | --- | --- |
| Antigravity | `antigravity` | Using MCP via mcp_config.json. Antigravity replaces Gemini CLI (sunset 2026-06-18). |
| Claude Code | `claude-code` | Using MCP. Hooks are also available, see https://github.com/rohitg00/agentmemory#claude-code-one-block-paste-it. |
| Cline | `cline` | Using MCP via ~/.cline/mcp.json (CLI). VS Code users: add the same block via Cline Settings → MCP Servers → Edit JSON. |
| Codex CLI | `codex` | Using MCP. Hooks ship via the Codex plugin; on Codex Desktop, also pass --with-hooks to install the global hooks.json workaround for openai/codex#16430. |
| Continue | `continue` | Using MCP via ~/.continue/config.yaml (preferred) or config.json (legacy, only when no yaml). |
| GitHub Copilot CLI | `copilot-cli` | Using MCP. Install the plugin too for full hooks/skills coverage. |
| Cursor | `cursor` | Using MCP (the only protocol Cursor speaks). Memory bridge runs at :3111 underneath. |
| Droid (Factory.ai) | `droid` | Using MCP via ~/.factory/mcp.json. The `/mcp` slash command inside droid lists configured servers. |
| Gemini CLI | `gemini-cli` | Using MCP (the only protocol Gemini CLI speaks). Memory bridge runs at :3111 underneath. |
| Hermes Agent | `hermes` | Using MCP. Hooks are also available, see https://github.com/rohitg00/agentmemory/tree/main/integrations/hermes. |
| Kiro | `kiro` | Using MCP via ~/.kiro/settings/mcp.json (user-level). Workspace overrides live in .kiro/settings/mcp.json. |
| OpenClaw | `openclaw` | Using MCP. Hooks are also available, see https://github.com/rohitg00/agentmemory/tree/main/integrations/openclaw. |
| OpenCode | `opencode` | Using MCP via ~/.config/opencode/opencode.json (top-level `mcp` key). For full auto-capture, also install the bundled plugin in plugin/opencode/. |
| OpenHuman | `openhuman` | Using native hooks (REST API at :3111). MCP not required. |
| pi | `pi` | Using native hooks (REST API at :3111). MCP not required. |
| Qwen Code | `qwen` | Using MCP via ~/.qwen/settings.json. Qwen Code's hook system can also be wired separately, see docs. |
| Warp | `warp` | Using MCP via ~/.warp/.mcp.json. Skills auto-discover from .claude/skills/ if the Claude Code plugin is also installed. |
| Zed | `zed` | Using MCP via ~/.config/zed/settings.json (key: context_servers). |
<!-- AUTOGEN:agents END -->
+34
View File
@@ -0,0 +1,34 @@
---
name: agentmemory-agents
description: How agentmemory wires into host coding agents via the connect command. Use when installing agentmemory into a specific agent, when asked which agents are supported, or when a connect adapter writes the wrong config path.
user-invocable: false
---
`agentmemory connect <agent>` merges the memory server into a host agent's config and preserves any existing servers. REST is the underlying protocol; for MCP-only hosts the adapter wires the stdio MCP bridge.
## Quick start
```bash
agentmemory connect claude-code # or cursor, codex, gemini-cli, ...
```
After wiring, restart the host or run its MCP reload (for example `/mcp` in Claude Code) so it picks up the server. Then confirm the agent lists agentmemory's tools.
## Workflow
1. Detect the calling agent. If unknown, default to `claude-code`.
2. Run `agentmemory connect <name>` using a name from the table in REFERENCE.md.
3. Verify: the host should show the full tool set with a server running. Only 7 tools means the MCP shim could not reach a server (see ../_shared/TROUBLESHOOTING.md).
## Notes
- The action skills (remember, recall, and the rest) are installed separately with `npx skills add rohitg00/agentmemory`. `connect` makes tools available; skills teach the agent when to use them.
- Windows: use WSL2. Native Windows runs the server but `connect` is not supported there.
## See also
- agentmemory-mcp-tools, agentmemory-rest-api, agentmemory-hooks.
## Reference
The full adapter list with display names and protocol notes lives in REFERENCE.md, generated from `src/cli/connect/`.
@@ -0,0 +1,33 @@
---
name: agentmemory-architecture
description: How agentmemory is built, the iii engine primitives it runs on, its storage model, ports, and the viewer. Use when reasoning about how memory is stored or retrieved end to end, when extending the system, or when answering how agentmemory works under the hood.
user-invocable: false
---
agentmemory is a memory server for coding agents. It runs locally, captures observations, indexes them for hybrid retrieval, and serves them back over REST and MCP. It is built on the iii engine.
## iii primitives
Everything is a function, a trigger, or worker state on the iii engine. There is no separate plugin system; the worker registers functions (`mem::*`) and HTTP triggers (`api::*`) and the engine routes calls. agentmemory does not bypass iii; new capability is a new function plus a trigger.
## Retrieval model
Recall is hybrid: BM25 keyword search plus vector similarity plus graph expansion over linked concepts. The default install needs no API key because embeddings run on-device and BM25 needs none. An LLM provider only adds richer summaries and auto-injection, both opt-in.
## Storage and lifecycle
Memories carry content, concepts, files, importance, and timestamps, grouped into sessions and optionally linked to commits. A lifecycle of capture, compress, consolidate, and forget keeps the store useful over time rather than letting it grow unbounded.
## Ports
REST is the anchor at 3111. Streams = N+1 (3112), viewer = N+2 (3113), engine = N+46023 (49134). `--instance N` shifts the whole block by N*100.
## Viewer
A real-time web viewer at `http://localhost:3113` shows memory building as sessions run. Useful for demos and for confirming capture is working.
## See also
- agentmemory-mcp-tools and agentmemory-rest-api for the surfaces.
- agentmemory-hooks for automatic capture.
- agentmemory-config for ports and feature flags.
@@ -0,0 +1,42 @@
# agentmemory configuration reference
Generated by scanning `src/` for `AGENTMEMORY_*` usage. Do not edit the block below by hand; run `npm run skills:gen` after adding or removing a variable. Internal markers ending in two underscores are excluded.
<!-- AUTOGEN:env START - generated by scripts/skills/generate.ts, do not edit by hand -->
Configuration is read from the environment and from `~/.agentmemory/.env` (no `export` prefix). 34 recognized variables:
- `AGENTMEMORY_AGENT_SCOPE`
- `AGENTMEMORY_ALLOW_AGENT_SDK`
- `AGENTMEMORY_AUTO_COMPRESS`
- `AGENTMEMORY_COMMIT_SHA`
- `AGENTMEMORY_COPILOT_MCP_BLOCK`
- `AGENTMEMORY_CWD`
- `AGENTMEMORY_DEBUG`
- `AGENTMEMORY_DROP_STALE_INDEX`
- `AGENTMEMORY_EXPORT_ROOT`
- `AGENTMEMORY_FOLLOWUP_WINDOW_SECONDS`
- `AGENTMEMORY_FORCE_PROXY`
- `AGENTMEMORY_GRAPH_WEIGHT`
- `AGENTMEMORY_III_CONFIG`
- `AGENTMEMORY_III_VERSION`
- `AGENTMEMORY_IMAGE_EMBEDDINGS`
- `AGENTMEMORY_IMAGE_STORE_MAX_BYTES`
- `AGENTMEMORY_INJECT_CONTEXT`
- `AGENTMEMORY_LLM_TIMEOUT_MS`
- `AGENTMEMORY_MCP_BLOCK`
- `AGENTMEMORY_PROBE_TIMEOUT_MS`
- `AGENTMEMORY_PROJECT_NAME`
- `AGENTMEMORY_PROVIDER`
- `AGENTMEMORY_REFLECT`
- `AGENTMEMORY_SDK_CHILD`
- `AGENTMEMORY_SECRET`
- `AGENTMEMORY_SESSION_ID`
- `AGENTMEMORY_SLOTS`
- `AGENTMEMORY_SUPPRESS_COST_WARNING`
- `AGENTMEMORY_TOOLS`
- `AGENTMEMORY_URL`
- `AGENTMEMORY_USE_DOCKER`
- `AGENTMEMORY_VERBOSE`
- `AGENTMEMORY_VIEWER_HOST`
- `AGENTMEMORY_VIEWER_URL`
<!-- AUTOGEN:env END -->
+37
View File
@@ -0,0 +1,37 @@
---
name: agentmemory-config
description: agentmemory configuration, environment variables, ports, and feature flags. Use when enabling a feature, changing ports, setting an API key, configuring auth, or explaining why a feature is off by default.
user-invocable: false
---
agentmemory reads configuration from the environment and from `~/.agentmemory/.env` (one `KEY=value` per line, no `export` prefix). Restart the server after changing it.
## Quick start
Enable richer memory and set a provider key in `~/.agentmemory/.env`:
```env
ANTHROPIC_API_KEY=sk-ant-...
AGENTMEMORY_AUTO_COMPRESS=true
AGENTMEMORY_INJECT_CONTEXT=true
```
## Defaults worth knowing
- No API key is required. Without one, agentmemory runs zero-LLM with BM25 plus local embeddings.
- Token-spending features ship OFF on purpose: `AGENTMEMORY_AUTO_COMPRESS` (LLM summaries) and `AGENTMEMORY_INJECT_CONTEXT` (auto context injection) both cost tokens proportional to tool-use frequency.
- Tool visibility: `AGENTMEMORY_TOOLS=all` (default) or `core` for the lean set.
- Auth: set `AGENTMEMORY_SECRET` to require `Authorization: Bearer` on the REST API.
## Ports
REST is the anchor at 3111. Streams = N+1 (3112), viewer = N+2 (3113), engine = N+46023 (49134). Relocate the whole block with `--port <N>` or `--instance <N>`.
## See also
- agentmemory-rest-api for how the secret is used.
- agentmemory-architecture for the port quartet rationale.
## Reference
The full recognized-variable list lives in REFERENCE.md, generated by scanning `src/`.
@@ -0,0 +1,20 @@
# agentmemory hooks reference
Generated from `plugin/hooks/hooks.json`. Do not edit the block below by hand; run `npm run skills:gen` after changing the hook registration.
<!-- AUTOGEN:hooks START - generated by scripts/skills/generate.ts, do not edit by hand -->
The Claude Code plugin registers hooks on 12 lifecycle events to capture observations automatically:
- `Notification`
- `PostToolUse`
- `PostToolUseFailure`
- `PreCompact`
- `PreToolUse`
- `SessionEnd`
- `SessionStart`
- `Stop`
- `SubagentStart`
- `SubagentStop`
- `TaskCompleted`
- `UserPromptSubmit`
<!-- AUTOGEN:hooks END -->
+39
View File
@@ -0,0 +1,39 @@
---
name: agentmemory-hooks
description: The agentmemory plugin hooks that capture observations automatically across the agent session lifecycle. Use when explaining how memory gets captured without manual saves, when debugging missing observations, or when tuning what gets recorded.
user-invocable: false
---
The Claude Code plugin registers lifecycle hooks so memory is captured automatically. You do not have to call `memory_save` for routine work; the hooks observe tool use, prompts, and session boundaries and write observations for you.
## Quick start
Install the plugin and the hooks register themselves:
```bash
/plugin marketplace add rohitg00/agentmemory
/plugin install agentmemory
```
Watch observations land live at `http://localhost:3113`.
## What the hooks do
- Session start and end frame each unit of work and let `handoff` resume it.
- Tool-use hooks capture what changed and why, the raw material for `recall` and `recap`.
- Prompt-submit captures intent. Pre-compact preserves context before the host trims it.
- A post-commit hook links commits to sessions, which powers `commit-context` and `commit-history`.
## Important
- Capture is on by default and is zero-LLM. Turning observations into LLM summaries (`AGENTMEMORY_AUTO_COMPRESS`) and injecting them back into context (`AGENTMEMORY_INJECT_CONTEXT`) are separate opt-ins because they spend tokens.
- If observations are missing, confirm the plugin is enabled and the server is running. See ../_shared/TROUBLESHOOTING.md.
## See also
- agentmemory-config for the capture and injection flags.
- The handoff, recap, and session-history skills consume what these hooks record.
## Reference
The exact registered hook events live in REFERENCE.md, generated from `plugin/hooks/hooks.json`.
@@ -0,0 +1,65 @@
# agentmemory MCP tools reference
Generated from `src/mcp/tools-registry.ts`. Do not edit the block below by hand; run `npm run skills:gen` after changing the registry.
<!-- AUTOGEN:tools START - generated by scripts/skills/generate.ts, do not edit by hand -->
agentmemory exposes 53 MCP tools. 8 are in the lean core set (`--tools core` or `AGENTMEMORY_TOOLS=core`); the rest load with `--tools all` (default).
| Tool | Core | Parameters | Purpose |
| --- | --- | --- | --- |
| `memory_action_create` | | `title`*: string, `description`: string, `priority`: number, `project`: string, `tags`: string, `parentId`: string, `requires`: string | Create an actionable work item with typed dependencies. Actions track what agents need to do and how work items relate to each other. |
| `memory_action_update` | | `actionId`*: string, `status`: string, `result`: string, `priority`: number | Update an action's status, priority, or details. Set status to 'done' to complete it and unblock dependent actions. |
| `memory_audit` | | `operation`: string, `limit`: number | View the audit trail of memory operations. |
| `memory_checkpoint` | | `operation`*: string, `name`: string, `checkpointId`: string, `status`: string, `type`: string, `linkedActionIds`: string | Create or resolve an external checkpoint (CI result, approval, deploy status) that gates action progress. |
| `memory_claude_bridge_sync` | | `direction`*: string | Sync memory state to/from Claude Code's native MEMORY.md file. |
| `memory_commit_lookup` | | `sha`*: string | Look up the agent session(s) that produced a specific git commit, given its SHA. Returns the commit metadata and linked sessions. |
| `memory_commits` | | `branch`: string, `repo`: string, `limit`: number | List recent commits linked to agent sessions, optionally filtered by branch or repo. |
| `memory_compress_file` | | `filePath`*: string | Compress a markdown file to reduce token usage while preserving headings, URLs, and code blocks. Creates a .original.md backup before writing. |
| `memory_consolidate` | yes | `tier`: string | Run the 4-tier memory consolidation pipeline (working -> episodic -> semantic -> procedural). |
| `memory_crystallize` | | `actionIds`*: string, `project`: string, `sessionId`: string | Compress completed action chains into compact crystal digests using LLM summarization. Extracts narrative, key outcomes, files affected, and lessons. |
| `memory_diagnose` | yes | `categories`: string | Run health checks across all subsystems (actions, leases, sentinels, sketches, signals, sessions, memories, mesh). Identifies stuck, orphaned, and inconsistent state. |
| `memory_export` | | none | Export all memory data as JSON. |
| `memory_facet_query` | | `matchAll`: string, `matchAny`: string, `targetType`: string | Query targets by facet tags with AND/OR logic. Find all actions tagged priority:urgent AND team:backend. |
| `memory_facet_tag` | | `targetId`*: string, `targetType`*: string, `dimension`*: string, `value`*: string | Attach a structured tag (dimension:value) to an action, memory, or observation for multi-dimensional categorization. |
| `memory_file_history` | | `files`*: string, `sessionId`: string | Get past observations about specific files. |
| `memory_frontier` | | `project`: string, `agentId`: string, `limit`: number | Get all unblocked actions ranked by priority and urgency. Returns the frontier of actionable work with no unsatisfied dependencies. |
| `memory_governance_delete` | | `memoryIds`*: string, `reason`: string | Delete specific memories with audit trail. |
| `memory_graph_query` | | `startNodeId`: string, `nodeType`: string, `maxDepth`: number, `query`: string | Query the knowledge graph for entities and relationships. |
| `memory_heal` | | `categories`: string, `dryRun`: string | Auto-fix all fixable issues found by diagnostics. Unblocks stuck actions, expires stale leases, cleans up orphaned data. |
| `memory_insight_list` | | `project`: string, `minConfidence`: number, `limit`: number | List synthesized insights, higher-order observations derived from patterns across memories, lessons, and crystals. |
| `memory_lease` | | `actionId`*: string, `agentId`*: string, `operation`*: string, `result`: string, `ttlMs`: number | Acquire, release, or renew an exclusive lease on an action. Prevents multiple agents from working on the same thing. |
| `memory_lesson_recall` | | `query`*: string, `project`: string, `minConfidence`: number, `limit`: number | Search lessons by query. Returns lessons sorted by confidence and recency. Use to check what the agent has learned before making decisions. |
| `memory_lesson_save` | yes | `content`*: string, `context`: string, `confidence`: number, `project`: string, `tags`: string | Save a lesson learned from this session. Lessons have confidence scores that strengthen when reinforced and decay when not used. Duplicate content auto-strengthens the existing lesson. |
| `memory_mesh_sync` | | `peerId`: string, `direction`: string | Sync memories and actions with peer agentmemory instances for multi-agent collaboration. |
| `memory_next` | | `project`: string, `agentId`: string | Get the single most important next action to work on. Combines dependency resolution, priority, and recency into a score. |
| `memory_obsidian_export` | | `vaultDir`: string, `types`: string | Export memories, lessons, and crystals as Obsidian-compatible Markdown files with YAML frontmatter and wikilinks for graph view. |
| `memory_patterns` | | `project`: string | Detect recurring patterns across sessions. |
| `memory_profile` | | `project`*: string, `refresh`: string | User/project profile with top concepts and file patterns. |
| `memory_recall` | yes | `query`*: string, `limit`: number, `format`: string, `token_budget`: number | Search past session observations for relevant context. Use when you need to recall what happened in previous sessions, find past decisions, or look up how a file was modified before. |
| `memory_reflect` | yes | `project`: string, `maxClusters`: number | Traverse the knowledge graph, group related memories by concept clusters, and synthesize higher-order insights via LLM. Returns new and reinforced insights. |
| `memory_relations` | | `memoryId`*: string, `maxHops`: number, `minConfidence`: number | Query the memory relationship graph. |
| `memory_routine_run` | | `routineId`*: string, `project`: string, `initiatedBy`: string | Instantiate a frozen workflow routine, creating actions for each step with proper dependencies. |
| `memory_save` | yes | `content`*: string, `type`: string, `concepts`: string, `files`: string, `project`: string | Explicitly save an important insight, decision, or pattern to long-term memory. |
| `memory_sentinel_create` | | `name`*: string, `type`*: string, `config`: string, `linkedActionIds`: string, `expiresInMs`: number | Create an event-driven sentinel that watches for conditions (webhook, timer, threshold, pattern, approval) and auto-unblocks gated actions when triggered. |
| `memory_sentinel_trigger` | | `sentinelId`*: string, `result`: string | Externally fire a sentinel, providing an optional result payload. Unblocks any gated actions. |
| `memory_sessions` | yes | none | List recent sessions with their status and observation counts. |
| `memory_signal_read` | | `agentId`*: string, `unreadOnly`: string, `threadId`: string, `limit`: number | Read messages for an agent. Marks delivered messages as read. |
| `memory_signal_send` | | `from`*: string, `to`: string, `content`*: string, `type`: string, `replyTo`: string | Send a message to another agent or broadcast. Supports threading, typed messages, and TTL expiration. |
| `memory_sketch_create` | | `title`*: string, `description`: string, `expiresInMs`: number, `project`: string | Create an ephemeral action graph for exploratory work. Auto-expires after TTL. Can be promoted to permanent actions or discarded. |
| `memory_sketch_promote` | | `sketchId`*: string, `project`: string | Promote a sketch's ephemeral actions to permanent actions. Makes the exploratory work official. |
| `memory_slot_append` | | `label`*: string, `text`*: string | Append text to an existing slot. Fails with 413 if the append would exceed the slot's sizeLimit, agent must compact via memory_slot_replace first. |
| `memory_slot_create` | | `label`*: string, `content`: string, `sizeLimit`: number, `description`: string, `pinned`: string, `scope`: string | Create a new slot. Reject if a slot with the same label already exists. |
| `memory_slot_delete` | | `label`*: string | Delete a slot. Seeded default slots can be deleted unless marked readOnly. |
| `memory_slot_get` | | `label`*: string | Read a single slot by label. |
| `memory_slot_list` | | none | List all memory slots (pinned + project + global). Slots are editable, size-limited memory units the agent can read and modify across sessions. |
| `memory_slot_replace` | | `label`*: string, `content`*: string | Replace slot content in place. Fails if content exceeds sizeLimit. |
| `memory_smart_search` | yes | `query`*: string, `expandIds`: string, `limit`: number | Hybrid semantic+keyword search with progressive disclosure. |
| `memory_snapshot_create` | | `message`: string | Create a git-versioned snapshot of current memory state. |
| `memory_team_feed` | | `limit`: number | Get recent shared items from all team members. |
| `memory_team_share` | | `itemId`*: string, `itemType`*: string | Share a memory or observation with team members. |
| `memory_timeline` | | `anchor`*: string, `project`: string, `before`: number, `after`: number | Chronological observations around an anchor point. |
| `memory_verify` | | `id`*: string | Verify a memory or observation by tracing its citation chain back to source observations and session context. Returns provenance information including confidence scores. |
| `memory_vision_search` | | `queryText`: string, `queryImageRef`: string, `queryImageBase64`: string, `topK`: number, `sessionId`: string | Cross-modal image search via CLIP embeddings. Pass queryText to find screenshots matching a description, or queryImageBase64/queryImageRef to find similar images. Requires AGENTMEMORY_IMAGE_EMBEDDINGS=true. |
`*` marks required parameters.
<!-- AUTOGEN:tools END -->
@@ -0,0 +1,39 @@
---
name: agentmemory-mcp-tools
description: Map of every agentmemory MCP tool, what each does, and its parameters. Use when choosing which memory tool to call, when a tool name or argument is unclear, or when answering what agentmemory can do via MCP.
user-invocable: false
---
agentmemory exposes its full capability set as MCP tools. This skill is the index: it tells you which tool to reach for and where to find exact parameters.
## Quick start
Save then recall:
1. `memory_save` with `content` (the insight), `concepts` (comma-separated keywords), `files` (comma-separated paths).
2. `memory_smart_search` with `query` and `limit` to retrieve it later. This runs hybrid BM25 plus vector plus graph-expanded search.
## Tool families
- Capture: `memory_save`, `memory_observe` flows, `memory_compress_file`.
- Retrieve: `memory_smart_search`, `memory_recall`, `memory_file_history`, `memory_timeline`, `memory_vision_search`.
- Sessions and commits: `memory_sessions`, `memory_commits`, `memory_commit_lookup`.
- Knowledge and graph: `memory_lesson_save`, `memory_lesson_recall`, `memory_graph_query`, `memory_relations`, `memory_patterns`, `memory_crystallize`.
- Structured slots: `memory_slot_create`, `memory_slot_append`, `memory_slot_get`, `memory_slot_list`, `memory_slot_replace`, `memory_slot_delete`.
- Governance and health: `memory_governance_delete`, `memory_audit`, `memory_verify`, `memory_heal`, `memory_diagnose`.
## Workflow
1. Pick the narrowest tool for the task. Prefer `memory_smart_search` for open recall, `memory_recall` when you already have a focused query, `memory_sessions` for session listings.
2. Look up exact parameter names and which are required in REFERENCE.md before calling.
3. Pass only documented fields. REST handlers whitelist fields and drop unknown ones.
## See also
- agentmemory-rest-api for the HTTP equivalents.
- agentmemory-config for tool-visibility and feature flags.
- The user-invocable action skills (remember, recall, recap, handoff, forget) wrap the most common tools.
## Reference
Full tool table with parameters and the core-set marking lives in REFERENCE.md, generated from source so it never drifts.
@@ -0,0 +1,129 @@
# agentmemory REST API reference
Generated from `src/triggers/api.ts`. Do not edit the block below by hand; run `npm run skills:gen` after changing the registered endpoints.
<!-- AUTOGEN:rest START - generated by scripts/skills/generate.ts, do not edit by hand -->
The REST API is the primary surface. All paths are under `http://localhost:3111` (override with `--port`). When `AGENTMEMORY_SECRET` is set, send `Authorization: Bearer $AGENTMEMORY_SECRET`; localhost is otherwise open.
117 registered endpoints:
| Method | Path |
| --- | --- |
| POST | `/agentmemory/actions` |
| POST | `/agentmemory/actions/edges` |
| GET | `/agentmemory/actions/get` |
| POST | `/agentmemory/actions/update` |
| GET | `/agentmemory/audit` |
| POST | `/agentmemory/auto-forget` |
| GET | `/agentmemory/branch/detect` |
| GET | `/agentmemory/branch/sessions` |
| GET | `/agentmemory/branch/worktrees` |
| POST | `/agentmemory/cascade-update` |
| POST | `/agentmemory/checkpoints` |
| POST | `/agentmemory/checkpoints/resolve` |
| GET | `/agentmemory/claude-bridge/read` |
| POST | `/agentmemory/claude-bridge/sync` |
| GET | `/agentmemory/commits` |
| POST | `/agentmemory/compress-file` |
| GET | `/agentmemory/config/flags` |
| POST | `/agentmemory/consolidate` |
| POST | `/agentmemory/consolidate-pipeline` |
| POST | `/agentmemory/context` |
| GET | `/agentmemory/crystals` |
| POST | `/agentmemory/crystals/auto` |
| POST | `/agentmemory/crystals/create` |
| POST | `/agentmemory/diagnostics` |
| GET | `/agentmemory/diagnostics/followup` |
| POST | `/agentmemory/diagnostics/heal` |
| POST | `/agentmemory/enrich` |
| POST | `/agentmemory/evict` |
| POST | `/agentmemory/evolve` |
| GET | `/agentmemory/export` |
| POST | `/agentmemory/facets` |
| POST | `/agentmemory/facets/query` |
| POST | `/agentmemory/facets/remove` |
| GET | `/agentmemory/facets/stats` |
| POST | `/agentmemory/file-context` |
| POST | `/agentmemory/flow/compress` |
| POST | `/agentmemory/forget` |
| GET | `/agentmemory/frontier` |
| POST | `/agentmemory/generate-rules` |
| POST | `/agentmemory/governance/bulk-delete` |
| DELETE | `/agentmemory/governance/memories` |
| POST | `/agentmemory/graph/build` |
| POST | `/agentmemory/graph/extract` |
| POST | `/agentmemory/graph/query` |
| POST | `/agentmemory/graph/reset` |
| POST | `/agentmemory/graph/snapshot-rebuild` |
| GET | `/agentmemory/graph/stats` |
| GET | `/agentmemory/health` |
| POST | `/agentmemory/import` |
| GET | `/agentmemory/insights` |
| POST | `/agentmemory/insights/search` |
| POST | `/agentmemory/leases/acquire` |
| POST | `/agentmemory/leases/release` |
| POST | `/agentmemory/leases/renew` |
| POST | `/agentmemory/lessons` |
| POST | `/agentmemory/lessons/search` |
| POST | `/agentmemory/lessons/strengthen` |
| GET | `/agentmemory/livez` |
| GET | `/agentmemory/memories` |
| GET | `/agentmemory/memories/:id` |
| GET | `/agentmemory/mesh/export` |
| POST | `/agentmemory/mesh/peers` |
| POST | `/agentmemory/mesh/receive` |
| POST | `/agentmemory/mesh/sync` |
| POST | `/agentmemory/migrate` |
| GET | `/agentmemory/next` |
| GET | `/agentmemory/observations` |
| POST | `/agentmemory/observe` |
| POST | `/agentmemory/obsidian/export` |
| POST | `/agentmemory/patterns` |
| GET | `/agentmemory/procedural` |
| GET | `/agentmemory/profile` |
| POST | `/agentmemory/reflect` |
| POST | `/agentmemory/relations` |
| POST | `/agentmemory/remember` |
| POST | `/agentmemory/replay/import-jsonl` |
| GET | `/agentmemory/replay/load` |
| GET | `/agentmemory/replay/sessions` |
| POST | `/agentmemory/routines` |
| POST | `/agentmemory/routines/run` |
| GET | `/agentmemory/routines/status` |
| POST | `/agentmemory/search` |
| GET | `/agentmemory/semantic` |
| POST | `/agentmemory/sentinels` |
| POST | `/agentmemory/sentinels/cancel` |
| POST | `/agentmemory/sentinels/check` |
| POST | `/agentmemory/sentinels/trigger` |
| GET | `/agentmemory/session/by-commit` |
| POST | `/agentmemory/session/commit` |
| POST | `/agentmemory/session/end` |
| POST | `/agentmemory/session/start` |
| GET | `/agentmemory/sessions` |
| GET | `/agentmemory/signals` |
| POST | `/agentmemory/signals/send` |
| POST | `/agentmemory/sketches` |
| POST | `/agentmemory/sketches/add` |
| POST | `/agentmemory/sketches/discard` |
| POST | `/agentmemory/sketches/gc` |
| POST | `/agentmemory/sketches/promote` |
| GET | `/agentmemory/slot` |
| POST | `/agentmemory/slot/append` |
| POST | `/agentmemory/slot/reflect` |
| POST | `/agentmemory/slot/replace` |
| GET | `/agentmemory/slots` |
| POST | `/agentmemory/smart-search` |
| POST | `/agentmemory/snapshot/create` |
| POST | `/agentmemory/snapshot/restore` |
| GET | `/agentmemory/snapshots` |
| POST | `/agentmemory/summarize` |
| GET | `/agentmemory/team/feed` |
| GET | `/agentmemory/team/profile` |
| POST | `/agentmemory/team/share` |
| POST | `/agentmemory/timeline` |
| POST | `/agentmemory/verify` |
| GET | `/agentmemory/viewer` |
| POST | `/agentmemory/vision-embed` |
| POST | `/agentmemory/vision-search` |
<!-- AUTOGEN:rest END -->
@@ -0,0 +1,43 @@
---
name: agentmemory-rest-api
description: The agentmemory HTTP REST API surface, the primary protocol for talking to the memory server. Use when calling agentmemory over HTTP, when MCP is unavailable and you need a fallback, or when integrating a host that does not speak MCP.
user-invocable: false
---
REST is agentmemory's primary surface. MCP is a bridge on top of it. Every memory operation has an HTTP endpoint under `http://localhost:3111/agentmemory/*`.
## Quick start
```bash
# liveness
curl -fsS http://localhost:3111/agentmemory/livez
# save
curl -X POST http://localhost:3111/agentmemory/remember \
-H "Content-Type: application/json" \
-d '{"content":"chose JWT refresh rotation","concepts":["jwt-refresh-rotation"]}'
# recall
curl -X POST http://localhost:3111/agentmemory/smart-search \
-H "Content-Type: application/json" \
-d '{"query":"auth token strategy","limit":5}'
```
## Auth
By default localhost is open and no auth is needed. When `AGENTMEMORY_SECRET` is set, every request needs `Authorization: Bearer $AGENTMEMORY_SECRET`. See agentmemory-config.
## Conventions
- Save returns `201`, reads return `200`, validation errors return `400`.
- Handlers whitelist body fields and drop unknown ones, so passing extra keys is safe but ignored.
- The port is configurable with `--port` or `--instance`; streams, viewer, and engine derive from it.
## See also
- agentmemory-mcp-tools for the MCP equivalents.
- agentmemory-config for the port quartet and the secret.
## Reference
The full endpoint list with methods lives in REFERENCE.md, generated from `src/triggers/api.ts`.
+71
View File
@@ -0,0 +1,71 @@
# commit-context worked examples
## 1. Line range with a linked session
User: "Why is this retry loop here?" (pointing at lines 40-52 of refresh.ts)
Find the SHA:
```bash
git blame -L 40,52 src/auth/refresh.ts
# 9a1b2c3d (dev 2026-06-07) ... retry on revoked token
```
Look it up:
```json
memory_commit_lookup { "sha": "9a1b2c3d4e5f60718293a4b5c6d7e8f901234567" }
```
Response:
```json
{
"commit": { "sha": "9a1b2c3d...", "short": "9a1b2c3", "branch": "main",
"author": "dev", "message": "rotate refresh tokens" },
"sessions": [ { "id": "7f3a9c21", "project": "app", "observationCount": 14,
"summary": "Reworked refresh rotation" } ]
}
```
Present:
> `9a1b2c3` on `main` by dev: "rotate refresh tokens". Linked to session
> `7f3a9c2` "Auth refresh rework" (14 obs). The retry loop handles a token
> revoked mid-flight, per the session's high-importance observations.
## 2. Function lookup, commit predates linking
User: "What was the agent doing when it wrote validateScope?"
```bash
git log -L :validateScope:src/auth/scope.ts -n 1
# 1122aabb ...
```
```json
memory_commit_lookup { "sha": "1122aabbccddeeff00112233445566778899aabb" }
```
Response:
```json
{ "commit": null }
```
Present:
> `1122aab` predates agent session linking, so there is no recorded session.
> From `git show`: it added `validateScope` to enforce per-token scopes. I can
> show the full diff if useful.
## 3. Bare path
User: "Give me context on src/middleware/limit.ts."
```bash
git log -n 1 -- src/middleware/limit.ts
```
Take the SHA, run `memory_commit_lookup`, present commit plus linked session in
the same shape as example 1.
+64
View File
@@ -0,0 +1,64 @@
---
name: commit-context
description: Trace a file, function, or line back to the agent session that produced its current commit. Use when the user asks "why is this code here", "what was the agent doing when this changed", "who wrote this", or wants context on a specific location in the codebase.
argument-hint: "[file, function, or line]"
user-invocable: true
---
The user wants commit context for: $ARGUMENTS
## Quick start
```bash
git blame -L 40,52 src/auth/refresh.ts # -> SHA 9a1b2c3d
```
```json
memory_commit_lookup { "sha": "9a1b2c3d4e5f60718293a4b5c6d7e8f901234567" }
```
Expected output:
```text
9a1b2c3 on main by dev: "rotate refresh tokens"
Linked session 7f3a9c2 "Auth refresh rework", 14 obs.
```
## Why
Report only what git and the lookup return. When the lookup gives `commit: null`,
the commit predates session linking; do not invent intent.
## Workflow
1. Find the SHA: `git blame -L <start>,<end> <file>` for a line range;
`git log -L :<function>:<file>` for a function; `git log -n 1 -- <file>` for a
bare path.
2. Look it up: `memory_commit_lookup { "sha": "<full-sha>" }`.
3. Present the commit (sha, short sha, branch, author, message), the linked
session(s) (id, project, started/ended, observation count, summary), and the
importance >= 7 observations via `memory_recall` when available.
## Anti-patterns
WRONG: lookup returns `{ "commit": null }`, you narrate "the agent was
refactoring auth" from the diff alone.
RIGHT: "This commit predates session linking, so there is no recorded agent
session. From `git show`: it changed token rotation in refresh.ts."
## Checklist
- SHA came from git blame/log, not a guess.
- `commit: null` reported as "predates linking", no fabricated session.
- Session details quote the lookup response verbatim.
- No intent claimed beyond what observations state.
## See also
- `commit-history`: list many agent-linked commits at once.
- `recall`: dig deeper into the linked session's observations.
## Troubleshooting
See ../_shared/TROUBLESHOOTING.md if `memory_commit_lookup` is not available.
+71
View File
@@ -0,0 +1,71 @@
# commit-history worked examples
## 1. Branch filter
User: "Show agent commits on main."
Invocation:
```json
memory_commits { "branch": "main", "limit": 100 }
```
Response:
```json
{
"commits": [
{ "short": "9a1b2c3", "branch": "main", "authoredAt": "2026-06-07T09:12:00Z",
"message": "rotate refresh tokens", "sessionIds": ["7f3a9c21"],
"observationCount": 14, "files": 3 },
{ "short": "b21d004", "branch": "main", "authoredAt": "2026-06-05T14:40:00Z",
"message": "rate limiter audit", "sessionIds": ["b21d004e"],
"observationCount": 9, "files": 1 }
]
}
```
Present:
> - `9a1b2c3` main 2026-06-07 "rotate refresh tokens", session `7f3a9c2` (14 obs, 3 files)
> - `b21d004` main 2026-06-05 "rate limiter audit", session `b21d004` (9 obs, 1 file)
## 2. Bare number as limit
User: "commit-history 5"
Treat `5` as the limit:
```json
memory_commits { "limit": 5 }
```
Render the five newest linked commits in the same format.
## 3. Empty result
User: "Show agent commits on release-2.0."
```json
memory_commits { "branch": "release-2.0", "limit": 100 }
```
Response:
```json
{ "commits": [] }
```
Present:
> No agent-linked commits on `release-2.0`. Drop the branch filter to see all
> linked commits, or try a different branch.
REST fallback for this same call, with encoding:
```http
GET /agentmemory/commits?branch=release-2.0&limit=100
```
Build it with `URLSearchParams` so a branch like `feat/a&b` becomes
`feat%2Fa%26b` rather than breaking the query.
+62
View File
@@ -0,0 +1,62 @@
---
name: commit-history
description: List recent git commits linked to agent sessions, optionally filtered by branch or repo. Use when the user asks "show agent commits", "what has the agent shipped", "list linked commits", or wants commits with their session context.
argument-hint: "[branch=... repo=... limit=...]"
user-invocable: true
---
The user wants a list of agent-linked commits. Filter args: $ARGUMENTS
## Quick start
```json
memory_commits { "branch": "main", "limit": 20 }
```
Expected output:
```text
9a1b2c3 main 2026-06-07 "rotate refresh tokens" · session 7f3a9c2 (14 obs)
b21d004 main 2026-06-05 "rate limiter audit" · session b21d004 (9 obs)
```
## Why
Render only the commits the tool returned, newest first. An empty result means
the filter matched nothing, not that work is missing.
## Workflow
1. Parse `$ARGUMENTS` for `branch=<name>`, `repo=<url-or-fragment>`,
`limit=<n>`. A bare numeric token is the limit. Defaults: no branch, no repo,
limit 100, max 500.
2. Call `memory_commits` with the parsed filters.
3. Render reverse-chronologically: short sha, branch, authored timestamp, first
line of the message, linked session id(s) (first 8) with observation counts,
and file count when `files` is present.
4. Empty result: tell the user the filter matched nothing and suggest dropping
the branch or repo filter.
## Anti-patterns
WRONG (REST fallback): concatenate `?branch=` + raw branch name, so a name with
`?`, `&`, or `#` corrupts the query string.
RIGHT: URL-encode every value with `URLSearchParams`/`encodeURIComponent` before
appending to `GET /agentmemory/commits`.
## Checklist
- Filters parsed; bare number treated as limit; limit capped at 500.
- Output is reverse-chronological.
- Session ids and observation counts come straight from the response.
- REST fallback URL-encodes branch, repo, and limit.
## See also
- `commit-context`: drill into one commit's session.
- `recall`: search the observations behind a linked session.
## Troubleshooting
See ../_shared/TROUBLESHOOTING.md if `memory_commits` is not available.
+75
View File
@@ -0,0 +1,75 @@
# forget worked examples
## 1. Delete a leaked secret
User: "Forget that note where I pasted the API key."
Search:
```json
memory_smart_search { "query": "api key", "limit": 20 }
```
Response:
```json
{
"results": [
{ "id": "abc12345", "sessionId": "7f3a9c21", "title": "Pasted staging API key",
"narrative": "STAGING_KEY=sk-live-..." }
]
}
```
Present and gate:
> Found 1 match:
> - `abc12345` (session `7f3a9c2`) "Pasted staging API key"
>
> Delete this memory? (yes/no)
After the user says yes:
```json
memory_governance_delete { "memoryIds": ["abc12345"], "reason": "user privacy request" }
```
Response:
```json
{ "deleted": 1 }
```
Present:
> Deleted 1 memory (`abc12345`).
## 2. Drop a whole session's observations
User: "Delete everything from the throwaway spike session."
Search, identify the session, then collect every memory id in it:
```json
memory_smart_search { "query": "spike prototype throwaway", "limit": 20 }
```
Response has 4 results all with `sessionId: c98f1100`. Show all four, get a yes,
then pass every id:
```json
memory_governance_delete {
"memoryIds": ["m1", "m2", "m3", "m4"],
"reason": "drop throwaway spike session"
}
```
Never send `{ "sessionId": "c98f1100" }`; the MCP deletes by memory id only.
## 3. User declines
User: "Actually, on second thought, keep them."
Do not call delete. Confirm:
> Understood, nothing deleted. The 4 memories from session `c98f110` are intact.
+66
View File
@@ -0,0 +1,66 @@
---
name: forget
description: Delete specific observations from agentmemory after showing them and getting explicit confirmation. Use when the user says "forget this", "delete memory", "remove that note", or wants to scrub specific data for privacy.
argument-hint: "[what to forget - session ID, file path, or search term]"
user-invocable: true
---
The user wants to remove data from agentmemory: $ARGUMENTS
## Quick start
```json
memory_smart_search { "query": "old api key in config", "limit": 20 }
```
Show the matches, get a yes, then:
```json
memory_governance_delete { "memoryIds": ["abc12345", "def67890"], "reason": "user privacy request" }
```
Expected output:
```text
Found 2 matching memories. Confirmed. Deleted 2 memories.
```
## Why
This is destructive and irreversible. Show exactly what will be deleted and get
an explicit yes before calling delete. Delete by memory ID, never a bare session.
## Workflow
1. Search with `memory_smart_search`, the user's text as `query`, `limit: 20`.
2. Show what matched: session ids, memory ids, titles. Ask for explicit
confirmation. Do not proceed on silence or a vague "sure, whatever".
3. On confirmation, call `memory_governance_delete` with `memoryIds` (array or
comma-separated string) and optional `reason` (default `plugin skill request`).
4. To drop a whole session, collect every memory id in that session from the
search results and pass them all. The MCP does not accept a bare `sessionId`.
5. Report the deletion count back.
## Anti-patterns
WRONG: search returns matches, you immediately call `memory_governance_delete`
without showing them or waiting for a yes.
RIGHT: list the matches, ask "Delete these 2? (yes/no)", and only delete after
an explicit yes.
## Checklist
- Matches were shown to the user before any delete.
- An explicit yes was received, not assumed.
- `memoryIds` holds real ids from the search, never a bare `sessionId`.
- Final message states the actual count deleted.
## See also
- `remember`: the write side; forget is its undo.
- `recall`: find the exact memory id before deleting.
## Troubleshooting
See ../_shared/TROUBLESHOOTING.md if `memory_smart_search` or `memory_governance_delete` is not available.
+56
View File
@@ -0,0 +1,56 @@
# handoff worked examples
## 1. Resume with an open question
User: "Where were we?"
Project cwd: `/Users/dev/app`. Invocation:
```json
memory_sessions { "limit": 20 }
```
Response:
```json
{
"sessions": [
{ "id": "7f3a9c21", "cwd": "/Users/dev/app", "title": "Auth refresh rework",
"status": "completed", "startedAt": "2026-06-07T09:00:00Z",
"summary": "Reworked refresh rotation. Open: should logout revoke all device tokens?",
"concepts": ["jwt-refresh-rotation", "logout"] }
]
}
```
Pull supporting context:
```json
memory_recall { "query": "jwt-refresh-rotation logout", "limit": 10 }
```
Present:
> Resuming `7f3a9c2` "Auth refresh rework".
> **Open question:** should logout revoke all device tokens or just the current one?
> Touched `auth/refresh.ts`, `auth/logout.ts`. Refresh rotation is done.
> **Next step:** decide the revoke scope, then finish `auth/logout.ts`.
## 2. Sibling-repo guard
Project cwd: `/Users/dev/repo-a`. Sessions include one with `cwd:
/Users/dev/repo-a-staging`.
A raw prefix would match `repo-a-staging`. The boundary check rejects it because
`/Users/dev/repo-a-staging` does not equal `/Users/dev/repo-a` and does not start
with `/Users/dev/repo-a/`. Pick the real `repo-a` session instead, or fall back
to the most recent session overall if none matches.
## 3. Empty session
User: "Resume."
If the matched session has zero observations:
> The most recent session `c98f110` for this project has no recorded
> observations. Nothing to hand off. Want to start fresh from the current state?
+68
View File
@@ -0,0 +1,68 @@
---
name: handoff
description: Resume the most recent agent session for the current working directory, leading with any unanswered question. Use when the user says "where were we", "resume", "handoff", "pick up where I left off", or starts a session with no fresh context.
argument-hint: "[optional cwd override]"
user-invocable: true
---
The user wants to resume work. Optional cwd override: $ARGUMENTS
## Quick start
```json
memory_sessions { "limit": 20 }
```
Pick the most recent session whose `cwd` matches this project, then:
`memory_recall { "query": "<session top concepts>", "limit": 10 }`.
Expected output:
```text
Resuming 7f3a9c2 "Auth refresh rework".
Open question: should logout revoke all device tokens or just the current one?
Next step: decide revoke scope, then update auth/logout.ts.
```
## Why
Match the session by directory boundary, not raw prefix, so a sibling repo never
gets mistaken for this one. Never invent observations for an empty session.
## Workflow
1. Resolve the project path: if `$ARGUMENTS` is given, normalize it to absolute
(`path.resolve(process.cwd(), $ARGUMENTS)`); else use the cwd.
2. Call `memory_sessions`. Pick the most recent session whose normalized `cwd`
matches by directory boundary: equality, OR `cwd.startsWith(projectPath + sep)`,
OR `projectPath.startsWith(cwd + sep)`. Prefer `completed` over `abandoned`.
No match: fall back to the single most recent session overall.
3. If the session ended on an unanswered user-facing question, surface it FIRST.
Look in `summary` or recent `conversation` observations whose `narrative`
ends in `?`.
4. Summarize: title/summary, key files, key decisions or errors, using
`memory_recall` on the top concepts, limit 10.
5. End with one concrete "next step?" pointer.
## Anti-patterns
WRONG: `session.cwd.startsWith(projectPath)` matches `/repo-a-staging` when the
project is `/repo-a`, resuming the wrong repo's session.
RIGHT: `session.cwd === projectPath || session.cwd.startsWith(projectPath + sep)`,
a directory-boundary check that cannot cross sibling repos.
## Checklist
- cwd override resolved to an absolute, normalized path.
- Match used a directory-boundary check, not a raw prefix.
- Unanswered question (if any) leads the response.
- Empty session is reported plainly, with an offer to start fresh.
## See also
- `recap`, `session-history`, `recall`: same session data, broader views.
## Troubleshooting
See ../_shared/TROUBLESHOOTING.md if `memory_sessions` or `memory_recall` is not available.
+74
View File
@@ -0,0 +1,74 @@
# recall worked examples
## 1. Hit on a past decision
User: "Did we ever decide how to cache sessions?"
Invocation:
```json
memory_smart_search { "query": "session caching decision", "limit": 10 }
```
Response:
```json
{
"results": [
{ "sessionId": "7f3a9c21", "type": "decision", "title": "Drop Redis for iii state store",
"narrative": "We decided to drop Redis and use the iii-engine state store for session caching.",
"importance": 8 }
]
}
```
Present:
> Yes. In session `7f3a9c21` you decided to drop Redis and use the iii-engine
> state store for session caching (importance 8).
## 2. Multiple results across sessions
User: "Recall anything about the rate limiter."
Invocation:
```json
memory_smart_search { "query": "rate limiter", "limit": 10 }
```
Response:
```json
{
"results": [
{ "sessionId": "b21d004e", "type": "code", "title": "limit.ts counts per-IP",
"narrative": "Rate limiter counts per-IP, not per-user.", "importance": 6 },
{ "sessionId": "c98f1100", "type": "conversation", "title": "Discussed sliding window",
"narrative": "Considered a sliding-window limiter, deferred.", "importance": 4 }
]
}
```
Present them grouped by session, leading with the per-IP code note.
## 3. No results
User: "Recall the GraphQL schema migration."
Invocation:
```json
memory_smart_search { "query": "graphql schema migration", "limit": 10 }
```
Response:
```json
{ "results": [] }
```
Present:
> No memories matched that. Try `schema change`, `graphql`, or `migration` as
> narrower terms, or `recap this week` to scan recent sessions.
+60
View File
@@ -0,0 +1,60 @@
---
name: recall
description: Search agentmemory for past observations, sessions, and learnings about a topic using hybrid BM25 plus vector plus graph search. Use when the user says "recall", "what did we do about", "did we ever", "have we seen", or needs context from past sessions.
argument-hint: "[search query]"
user-invocable: true
---
The user wants to recall past context about: $ARGUMENTS
## Quick start
```json
memory_smart_search { "query": "jwt refresh token rotation", "limit": 10 }
```
Expected output:
```text
2 results across 2 sessions.
[importance 8] decision · "Rotate refresh tokens on every use" (session 7f3a9c21)
[importance 5] code · "limit.ts counts per-IP" (session b21d004e)
```
## Why
Only surface what the tool returned. Never fabricate an observation, a session
id, or an importance score. If nothing comes back, say so.
## Workflow
1. Call `memory_smart_search` with the user's text as `query` and `limit: 10`.
Pass `project` when the user scopes to a specific repo.
2. Group results by session.
3. For each observation show its type, title, and narrative.
4. Lead with the high-signal observations (importance >= 7).
5. If zero results, suggest 2-3 alternative search terms and stop. Do not guess.
## Anti-patterns
WRONG: results are empty, so you write "We probably discussed token expiry last
week" from assumption.
RIGHT: "No memories matched that query. Try `refresh token`, `session expiry`,
or `auth rotation`."
## Checklist
- Every observation shown came from the tool response.
- Results grouped by session, high-importance first.
- Empty results trigger alternative-term suggestions, not invention.
- No session id or score was paraphrased or rounded.
## See also
- `remember`: the write side; recall retrieves what it stores.
- `recap`, `handoff`, `session-history`: session-scoped views of the same data.
## Troubleshooting
See ../_shared/TROUBLESHOOTING.md if `memory_smart_search` is not available.
+60
View File
@@ -0,0 +1,60 @@
# recap worked examples
## 1. This week
User: "Recap this week."
Window: last 7 days. Invocation:
```json
memory_sessions { "limit": 30 }
```
Response (after filtering to cwd and the 7-day window):
```json
{
"sessions": [
{ "id": "7f3a9c21", "cwd": "/Users/dev/app", "title": "Auth refresh rework",
"startedAt": "2026-06-07T09:00:00Z", "observationCount": 14, "status": "completed",
"concepts": ["jwt-refresh-rotation", "auth-flow"] },
{ "id": "b21d004e", "cwd": "/Users/dev/app", "title": "Rate limiter audit",
"startedAt": "2026-06-05T14:00:00Z", "observationCount": 9, "status": "completed",
"concepts": ["rate-limiter", "per-ip-bug"] }
]
}
```
Per session, pull highlights:
```json
memory_recall { "query": "jwt-refresh-rotation auth-flow", "limit": 3 }
```
Present:
> **2026-06-07**
> - `7f3a9c2` Auth refresh rework, 14 obs, completed
> - [8] Rotate refresh tokens on every use
>
> **2026-06-05**
> - `b21d004e` Rate limiter audit, 9 obs, completed
> - [7] limit.ts counts per-IP, not per-user
>
> 2 sessions across 2 days, 23 observations.
## 2. Bare number
User: "recap 3"
Treat as `last 3`. Call `memory_sessions { "limit": 3 }`, group by date, same format.
## 3. Empty window
User: "Recap today."
If `memory_sessions` returns no session whose `startedAt` is today and whose
`cwd` matches:
> No sessions today for this project. The most recent was yesterday, `7f3a9c2`
> Auth refresh rework. Want a recap of that instead?
+63
View File
@@ -0,0 +1,63 @@
---
name: recap
description: Summarize the last N agent sessions for the current project, grouped by date, with highlight observations per session. Use when the user asks "recap", "what have we been doing", "today", "this week", or wants a rollup of recent work.
argument-hint: "[last N | today | this week]"
user-invocable: true
---
The user wants a recap. Time window args: $ARGUMENTS
## Quick start
```json
memory_sessions { "limit": 30 }
```
Then per surviving session: `memory_recall { "query": "<top concepts>", "limit": 3 }`.
Expected output:
```text
2026-06-07
7f3a9c2 · "Auth refresh rework" · 14 obs · completed
- [8] Rotate refresh tokens on every use
3 sessions across 2 days, 41 observations.
```
## Why
Only summarize sessions and observations the tools returned. An empty window is
a real answer, not a prompt to invent activity.
## Workflow
1. Parse `$ARGUMENTS`: `today` = current local date; `this week` = last 7 days;
`last <n>` or bare numeric = most recent N; empty = `last 10`.
2. Call `memory_sessions`, filter to the current project (match `cwd` against the
working directory), apply the window, sort by `startedAt` descending.
3. Group survivors by local calendar date (YYYY-MM-DD).
4. Per session list id (first 8), title or first prompt, observation count,
status. Indent 2-3 highlights (importance >= 7) from `memory_recall`.
5. End with "N sessions across M days, K observations."
## Anti-patterns
WRONG: window is empty, so you summarize "a productive week of auth work" from
memory of the conversation.
RIGHT: "No sessions in the last 7 days for this project."
## Checklist
- Window parsed correctly from the argument.
- Sessions filtered to the current project's cwd.
- Highlights come from `memory_recall`, not paraphrase.
- Totals line reflects the actual counts shown.
## See also
- `handoff`, `session-history`, `recall`: same session data, different lens.
## Troubleshooting
See ../_shared/TROUBLESHOOTING.md if `memory_sessions` or `memory_recall` is not available.
+67
View File
@@ -0,0 +1,67 @@
# remember worked examples
## 1. Saving a decision
User: "Remember that we decided to drop Redis and use the iii-engine state store for session caching."
Invocation:
```json
memory_save {
"content": "We decided to drop Redis and use the iii-engine state store for session caching to remove a moving part.",
"concepts": "session-caching, iii-state-store, dropped-redis",
"files": ""
}
```
Response:
```json
{ "id": "7f3a9c21", "concepts": ["session-caching", "iii-state-store", "dropped-redis"], "saved": true }
```
Present:
> Saved. Tagged it `session-caching`, `iii-state-store`, `dropped-redis`, so a
> later recall on caching or Redis will surface this decision.
## 2. Saving a code-specific gotcha
User: "Save this: the rate limiter in middleware/limit.ts counts per-IP, not per-user, so logged-in users on shared IPs collide."
Invocation:
```json
memory_save {
"content": "The rate limiter in middleware/limit.ts counts per-IP, not per-user, so logged-in users on shared IPs collide.",
"concepts": "rate-limiter, per-ip-bug, shared-ip-collision",
"files": "src/middleware/limit.ts"
}
```
Response:
```json
{ "id": "b21d004e", "concepts": ["rate-limiter", "per-ip-bug", "shared-ip-collision"], "saved": true }
```
Present:
> Saved memory `b21d004e` referencing `src/middleware/limit.ts`. Concepts:
> `rate-limiter`, `per-ip-bug`, `shared-ip-collision`.
## 3. Vague request, no files
User: "Remember this for later."
Ask once for the substance, then save:
```json
memory_save {
"content": "Staging deploys must run the migration job before the app rollout, never after.",
"concepts": "staging-deploy, migration-ordering, rollout-sequence",
"files": ""
}
```
Present the confirmation with the concepts echoed back.
+61
View File
@@ -0,0 +1,61 @@
---
name: remember
description: Save an insight, decision, or learning to agentmemory's long-term storage with searchable concept tags. Use when the user says "remember this", "save this", "note that", "don't forget", or wants to preserve knowledge for future sessions.
argument-hint: "[what to remember]"
user-invocable: true
---
The user wants to save this to long-term memory: $ARGUMENTS
## Quick start
```json
memory_save {
"content": "We rotate JWT refresh tokens on every use; the old token is revoked server-side in auth/refresh.ts.",
"concepts": "jwt-refresh-rotation, token-revocation, auth-flow",
"files": "src/auth/refresh.ts"
}
```
Expected output:
```text
Saved memory abc12345 with 3 concepts: jwt-refresh-rotation, token-revocation, auth-flow.
```
## Why
A memory is only as useful as the terms that retrieve it. Tag with specific
concepts so a future `recall` finds it, and preserve the user's own phrasing.
## Workflow
1. Pull the core insight, decision, or fact out of `$ARGUMENTS`.
2. Extract 2-5 lowercased concept phrases. Prefer specific over generic
(`jwt-refresh-rotation` beats `auth`).
3. Extract referenced file paths (absolute or repo-relative). Empty if none.
4. Call `memory_save` with `content`, `concepts` (comma-separated string), and
`files` (comma-separated string).
5. Confirm the save and echo the concepts so the user knows the retrieval terms.
## Anti-patterns
WRONG: `concepts: "stuff, code, notes"` (generic tags nothing can find later).
RIGHT: `concepts: "jwt-refresh-rotation, token-revocation"` (specific, retrievable).
## Checklist
- Content preserves the user's phrasing, not a paraphrase.
- Concepts are specific, lowercased, 2-5 items.
- File paths are real references, not guesses.
- Confirmation echoes the exact concepts tagged.
## See also
- `recall`: retrieve what you save here (the pair to this skill).
- `forget`: remove a memory you saved by mistake.
## Troubleshooting
See ../_shared/TROUBLESHOOTING.md if `memory_save` is not available.
+59
View File
@@ -0,0 +1,59 @@
# session-history worked examples
## 1. Standard timeline
User: "Show me the session history."
Invocation:
```json
memory_sessions { "limit": 20 }
```
Response:
```json
{
"sessions": [
{ "id": "7f3a9c21", "project": "app", "startedAt": "2026-06-07T09:00:00Z",
"status": "completed", "observationCount": 14,
"summary": "Reworked refresh rotation",
"highlights": [ { "type": "decision", "title": "Rotate refresh tokens on every use" } ] },
{ "id": "b21d004e", "project": "app", "startedAt": "2026-06-05T14:00:00Z",
"status": "completed", "observationCount": 9,
"highlights": [ { "type": "code", "title": "limit.ts counts per-IP" } ] }
]
}
```
Present:
> - `7f3a9c2` app, 2026-06-07 09:00, completed, 14 obs
> - decision: Rotate refresh tokens on every use (summary: Reworked refresh rotation)
> - `b21d004` app, 2026-06-05 14:00, completed, 9 obs
> - code: limit.ts counts per-IP
## 2. Session with no observations
If a returned session has `observationCount: 0`, list it but say so:
> - `c98f110` app, 2026-06-04 11:00, abandoned, 0 obs (no recorded work)
## 3. Empty history
User: "What did we do last time?"
```json
memory_sessions { "limit": 20 }
```
Response:
```json
{ "sessions": [] }
```
Present:
> No recorded sessions yet for this project. Once you work with memory capture
> on, they will show here. Use `remember` to save a note now.
+61
View File
@@ -0,0 +1,61 @@
---
name: session-history
description: Show what happened in recent past sessions on this project as a clean timeline. Use when the user asks "what did we do last time", "session history", "past sessions", or wants an overview of previous work.
user-invocable: true
---
The user wants an overview of recent sessions on this project.
## Quick start
```json
memory_sessions { "limit": 20 }
```
Expected output:
```text
7f3a9c2 · app · 2026-06-07 09:00 · completed · 14 obs
- decision: Rotate refresh tokens on every use
b21d004 · app · 2026-06-05 14:00 · completed · 9 obs
- code: limit.ts counts per-IP
```
## Why
Only show sessions and observations the tool returned. An empty history is a
real answer, never a cue to invent past work.
## Workflow
1. Call `memory_sessions` with `limit: 20` for a meaningful window.
2. Present in reverse chronological order: session id (first 8), project, start
time, status.
3. For sessions with observations, show the key highlights (type plus title).
4. Note the total observation count per session.
5. When a session summary exists, surface its title and the key decisions.
## Anti-patterns
WRONG: the tool returns two sessions, you describe "several sessions of steady
progress" and add ones you remember from the conversation.
RIGHT: show exactly the two sessions returned, each with its real id, status, and
observation count.
## Checklist
- Every session shown came from the tool response.
- Order is reverse-chronological.
- Per-session observation counts match the response.
- No session or highlight was invented or merged.
## See also
- `recap`: same data grouped by date with highlights.
- `handoff`: jump straight into the most recent session.
- `recall`: search across all sessions by topic.
## Troubleshooting
See ../_shared/TROUBLESHOOTING.md if `memory_sessions` is not available.
@@ -0,0 +1,40 @@
---
name: write-agentmemory-skill
description: The house format and rules for writing or updating an agentmemory skill. Use when adding a new skill, restructuring an existing one, or reviewing a skill contribution for consistency.
user-invocable: false
---
agentmemory skills follow one tiered format so they stay skimmable, accurate, and current. Match it exactly.
## Directory layout
```text
plugin/skills/<name>/
SKILL.md (required, under 100 lines)
REFERENCE.md (optional, dense facts; auto-generate data tables)
EXAMPLES.md (optional, worked transcripts)
```
## SKILL.md rules
- Frontmatter: `name`, `description`, optional `argument-hint`, and `user-invocable`. Set `user-invocable: true` only for skills the user runs as a slash command; reference and knowledge skills are `false`.
- Description is two sentences and the only thing the agent sees when deciding to load the skill. Sentence one states the capability. Sentence two starts "Use when" and lists concrete triggers. Keep it distinct from sibling skills, under 1024 chars, third person.
- Body order: Quick start (one concrete example), Why (the governing principle), Workflow (numbered steps with decision gates), Anti-patterns (a WRONG vs RIGHT callout for the top mistake), Checklist, See also (cross-link siblings), Reference or Troubleshooting pointer.
- Stay under 100 lines. Move dense facts to REFERENCE.md and examples to EXAMPLES.md.
- Cross-references link one level deep only. Shared recovery steps live in `../_shared/TROUBLESHOOTING.md`, never inlined.
## Keep it current
Facts that exist in source (tool names and parameters, REST endpoints, env vars, connect adapters, hook events) are generated, never hand-typed. Edit the source, then run `npm run skills:gen`. CI runs `npm run skills:check` and fails on drift, so generated tables cannot fall behind the code.
## Style
No external or competitor product names. No emojis. No em-dashes. No filler. State the thing and stop.
## Checklist
- Description has a "Use when" sentence with real triggers.
- SKILL.md is under 100 lines.
- No time-sensitive claims and no duplicated troubleshooting block.
- Concrete example present; generated facts come from the generator.
- Cross-links resolve and go one level deep.