From 851e338011f9309b6cbf4e834a6f63f4f4bd1721 Mon Sep 17 00:00:00 2001 From: Som Samantray Date: Fri, 31 Jul 2026 22:39:07 +0530 Subject: [PATCH 1/7] fix(memory): guard mem::forget delete/count on record existence Calling mem::forget with a lesson id (lsn_*) deleted a nonexistent key from the memories keyspace, counted it, and reported success. Guard the delete, index cleanup, and counter on the kv.get result, matching the mem::governance-delete pattern, so nonexistent ids return { success: true, deleted: 0 } with no audit row. Closes #1120. --- src/functions/remember.ts | 18 ++++++++++-------- test/remember-forget-audit.test.ts | 30 ++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/src/functions/remember.ts b/src/functions/remember.ts index 5735b4f23..e6db32f97 100644 --- a/src/functions/remember.ts +++ b/src/functions/remember.ts @@ -181,15 +181,17 @@ export function registerRememberFunction(sdk: ISdk, kv: StateKV): void { if (data.memoryId) { const mem = await kv.get(KV.memories, data.memoryId); - await kv.delete(KV.memories, data.memoryId); - if (mem?.imageRef) { - await decrementImageRef(kv, sdk, mem.imageRef); + if (mem) { + await kv.delete(KV.memories, data.memoryId); + if (mem.imageRef) { + await decrementImageRef(kv, sdk, mem.imageRef); + } + await deleteAccessLog(kv, data.memoryId); + getSearchIndex().remove(data.memoryId); + vectorIndexRemove(data.memoryId); + deletedMemoryIds.push(data.memoryId); + deleted++; } - await deleteAccessLog(kv, data.memoryId); - getSearchIndex().remove(data.memoryId); - vectorIndexRemove(data.memoryId); - deletedMemoryIds.push(data.memoryId); - deleted++; } if ( diff --git a/test/remember-forget-audit.test.ts b/test/remember-forget-audit.test.ts index 7d17b543c..09126858a 100644 --- a/test/remember-forget-audit.test.ts +++ b/test/remember-forget-audit.test.ts @@ -122,6 +122,36 @@ describe("mem::forget audit coverage (issue #125)", () => { const auditRows = await kv.list("mem:audit"); expect(auditRows).toHaveLength(0); }); + + // Regression coverage for issue #1120: mem::forget must not report a + // deletion for ids it never touches (e.g. lesson ids live in KV.lessons, + // not KV.memories). + it("returns deleted: 0 for a nonexistent memoryId (lesson id)", async () => { + const sdk = mockSdk(); + const kv = mockKV(); + registerRememberFunction(sdk as never, kv as never); + + const result = await sdk.trigger({ + function_id: "mem::forget", + payload: { memoryId: "lsn_4f9cb07017a7c8ac" }, + }); + + expect(result).toEqual({ success: true, deleted: 0 }); + }); + + it("emits no audit row when memoryId does not exist", async () => { + const sdk = mockSdk(); + const kv = mockKV(); + registerRememberFunction(sdk as never, kv as never); + + await sdk.trigger({ + function_id: "mem::forget", + payload: { memoryId: "lsn_4f9cb07017a7c8ac" }, + }); + + const auditRows = await kv.list("mem:audit"); + expect(auditRows).toHaveLength(0); + }); }); // Delete paths must tear down the BM25 index entry and synchronously From a775421b2b6b6045eb5f7e3e30ca16be505fe83f Mon Sep 17 00:00:00 2001 From: Som Samantray Date: Fri, 31 Jul 2026 22:43:08 +0530 Subject: [PATCH 2/7] feat(lessons): add mem::lesson-delete soft-delete function Register mem::lesson-delete to set deleted: true on a lesson, mirroring the lesson-strengthen existence guard and audit pattern. Read paths already filter !l.deleted, and re-saving deleted content creates a fresh lesson. Adds lesson_delete to the audit operation union. --- src/functions/lessons.ts | 26 ++++++++++ src/types.ts | 1 + test/lessons.test.ts | 108 ++++++++++++++++++++++++++++++++++----- 3 files changed, 123 insertions(+), 12 deletions(-) diff --git a/src/functions/lessons.ts b/src/functions/lessons.ts index 9e69f464f..0314298ce 100644 --- a/src/functions/lessons.ts +++ b/src/functions/lessons.ts @@ -203,6 +203,32 @@ export function registerLessonsFunctions(sdk: ISdk, kv: StateKV): void { }, ); + sdk.registerFunction("mem::lesson-delete", + async (data: { lessonId: string }) => { + if (!data.lessonId) { + return { success: false, error: "lessonId is required" }; + } + + const lesson = await kv.get(KV.lessons, data.lessonId); + if (!lesson || lesson.deleted) { + return { success: false, error: "lesson not found" }; + } + + lesson.deleted = true; + lesson.updatedAt = new Date().toISOString(); + + await kv.set(KV.lessons, lesson.id, lesson); + + try { + await recordAudit(kv, "lesson_delete", "mem::lesson-delete", [ + lesson.id, + ]); + } catch {} + + return { success: true, lesson }; + }, + ); + sdk.registerFunction("mem::lesson-decay-sweep", async () => { const lessons = await kv.list(KV.lessons); diff --git a/src/types.ts b/src/types.ts index 113daeae1..7cda80ffb 100644 --- a/src/types.ts +++ b/src/types.ts @@ -596,6 +596,7 @@ export interface AuditEntry { | "lesson_save" | "lesson_recall" | "lesson_strengthen" + | "lesson_delete" | "obsidian_export" | "reflect" | "insight_search" diff --git a/test/lessons.test.ts b/test/lessons.test.ts index 4a55003e1..bf615dca3 100644 --- a/test/lessons.test.ts +++ b/test/lessons.test.ts @@ -329,24 +329,108 @@ describe("Lessons", () => { const after = await kv.get("mem:lessons", saved.lesson.id); expect(after!.deleted).toBe(true); }); + }); - it("uses lastDecayedAt for incremental delta (not full age)", async () => { + describe("mem::lesson-delete", () => { + it("soft-deletes an existing lesson", async () => { const saved = (await sdk.trigger("mem::lesson-save", { - content: "Incremental decay", - confidence: 0.8, + content: "Delete me", + confidence: 0.7, })) as { lesson: Lesson }; - const lesson = await kv.get("mem:lessons", saved.lesson.id); - lesson!.createdAt = new Date(Date.now() - 28 * 24 * 60 * 60 * 1000).toISOString(); - lesson!.lastDecayedAt = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(); - lesson!.confidence = 0.6; - await kv.set("mem:lessons", lesson!.id, lesson!); + const result = (await sdk.trigger("mem::lesson-delete", { + lessonId: saved.lesson.id, + })) as { success: boolean; lesson: Lesson }; - await sdk.trigger("mem::lesson-decay-sweep", {}); + expect(result.success).toBe(true); + expect(result.lesson.deleted).toBe(true); - const after = await kv.get("mem:lessons", saved.lesson.id); - expect(after!.confidence).toBeCloseTo(0.55, 2); - expect(after!.confidence).toBeGreaterThan(0.4); + const stored = await kv.get("mem:lessons", saved.lesson.id); + expect(stored!.deleted).toBe(true); + }); + + it("excludes a soft-deleted lesson from recall and list", async () => { + const saved = (await sdk.trigger("mem::lesson-save", { + content: "Hide me from recall", + confidence: 0.9, + })) as { lesson: Lesson }; + + await sdk.trigger("mem::lesson-delete", { lessonId: saved.lesson.id }); + + const recall = (await sdk.trigger("mem::lesson-recall", { + query: "hide recall", + })) as { lessons: Lesson[] }; + expect(recall.lessons.some((l) => l.id === saved.lesson.id)).toBe(false); + + const list = (await sdk.trigger("mem::lesson-list", {})) as { + lessons: Lesson[]; + }; + expect(list.lessons.some((l) => l.id === saved.lesson.id)).toBe(false); + }); + + it("returns not found for an already-deleted lesson", async () => { + const saved = (await sdk.trigger("mem::lesson-save", { + content: "Double delete", + })) as { lesson: Lesson }; + + await sdk.trigger("mem::lesson-delete", { lessonId: saved.lesson.id }); + const second = (await sdk.trigger("mem::lesson-delete", { + lessonId: saved.lesson.id, + })) as { success: boolean; error?: string }; + + expect(second.success).toBe(false); + expect(second.error).toBe("lesson not found"); + }); + + it("returns not found for a nonexistent lessonId", async () => { + const result = (await sdk.trigger("mem::lesson-delete", { + lessonId: "lsn_nonexistent", + })) as { success: boolean; error?: string }; + + expect(result.success).toBe(false); + expect(result.error).toBe("lesson not found"); + }); + + it("rejects a missing lessonId", async () => { + const result = (await sdk.trigger("mem::lesson-delete", {})) as { + success: boolean; + error?: string; + }; + + expect(result.success).toBe(false); + expect(result.error).toBe("lessonId is required"); + }); + + it("creates a fresh lesson when deleted content is re-saved", async () => { + const saved = (await sdk.trigger("mem::lesson-save", { + content: "Resave after delete", + })) as { lesson: Lesson }; + + await sdk.trigger("mem::lesson-delete", { lessonId: saved.lesson.id }); + + const resaved = (await sdk.trigger("mem::lesson-save", { + content: "Resave after delete", + })) as { action: string; lesson: Lesson }; + + expect(resaved.action).toBe("created"); + expect(resaved.lesson.id).toBe(saved.lesson.id); + expect(resaved.lesson.deleted).toBeUndefined(); + }); + + it("records a lesson_delete audit row", async () => { + const saved = (await sdk.trigger("mem::lesson-save", { + content: "Audited delete", + })) as { lesson: Lesson }; + + await sdk.trigger("mem::lesson-delete", { lessonId: saved.lesson.id }); + + const auditRows = (await kv.list("mem:audit")) as Array<{ + operation: string; + targetIds: string[]; + }>; + const row = auditRows.find((r) => r.operation === "lesson_delete"); + expect(row).toBeDefined(); + expect(row!.targetIds).toEqual([saved.lesson.id]); }); }); }); From fa8c0811556e55de038c85fed1df1efb91a7c904 Mon Sep 17 00:00:00 2001 From: Som Samantray Date: Fri, 31 Jul 2026 22:44:40 +0530 Subject: [PATCH 3/7] feat(mcp): expose memory_lesson_delete tool and REST endpoint Wire mem::lesson-delete through the MCP tool registry and dispatch case (memory_lesson_delete) and a POST /agentmemory/lessons/delete REST route with 400 for a missing lessonId and 404 for a nonexistent lesson. --- src/mcp/server.ts | 10 ++++++++++ src/mcp/tools-registry.ts | 12 ++++++++++++ src/triggers/api.ts | 13 +++++++++++++ 3 files changed, 35 insertions(+) diff --git a/src/mcp/server.ts b/src/mcp/server.ts index dbca07d9b..661fa3e13 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -1122,6 +1122,16 @@ export function registerMcpEndpoints( return { status_code: 200, body: { content: [{ type: "text", text: JSON.stringify(lessonRecallResult, null, 2) }] } }; } + case "memory_lesson_delete": { + if (typeof args.lessonId !== "string" || !args.lessonId.trim()) { + return { status_code: 400, body: { error: "lessonId is required" } }; + } + const lessonDeleteResult = await sdk.trigger({ function_id: "mem::lesson-delete", payload: { + lessonId: args.lessonId, + } }); + return { status_code: 200, body: { content: [{ type: "text", text: JSON.stringify(lessonDeleteResult, null, 2) }] } }; + } + case "memory_reflect": { const reflectResult = await sdk.trigger({ function_id: "mem::reflect", payload: { project: args.project, diff --git a/src/mcp/tools-registry.ts b/src/mcp/tools-registry.ts index c4df3499c..ba734f5eb 100644 --- a/src/mcp/tools-registry.ts +++ b/src/mcp/tools-registry.ts @@ -799,6 +799,18 @@ export const V070_TOOLS: McpToolDef[] = [ required: ["query"], }, }, + { + name: "memory_lesson_delete", + description: + "Soft-delete a lesson by id. Deleted lessons are excluded from recall and list; re-saving the same content creates a fresh lesson.", + inputSchema: { + type: "object", + properties: { + lessonId: { type: "string", description: "The lesson id (lsn_...)" }, + }, + required: ["lessonId"], + }, + }, { name: "memory_obsidian_export", description: diff --git a/src/triggers/api.ts b/src/triggers/api.ts index 99fb99084..24e4f0a72 100644 --- a/src/triggers/api.ts +++ b/src/triggers/api.ts @@ -3150,6 +3150,19 @@ export function registerApiTriggers( }); sdk.registerTrigger({ type: "http", function_id: "api::lesson-strengthen", config: { api_path: "/agentmemory/lessons/strengthen", http_method: "POST" } }); + sdk.registerFunction("api::lesson-delete", async (req: ApiRequest) => { + const denied = checkAuth(req, secret); + if (denied) return denied; + const body = req.body as Record; + if (!body?.lessonId || typeof body.lessonId !== "string") return { status_code: 400, body: { error: "lessonId is required" } }; + const result = await sdk.trigger({ function_id: "mem::lesson-delete", payload: { lessonId: body.lessonId } }); + if (result && (result as { success?: boolean; error?: string }).success === false && (result as { error?: string }).error === "lesson not found") { + return { status_code: 404, body: result }; + } + return { status_code: 200, body: result }; + }); + sdk.registerTrigger({ type: "http", function_id: "api::lesson-delete", config: { api_path: "/agentmemory/lessons/delete", http_method: "POST" } }); + sdk.registerFunction("api::obsidian-export", async (req: ApiRequest) => { const denied = checkAuth(req, secret); if (denied) return denied; From b380d7f69b66ea6d87f70784c89faa6b1eb33807 Mon Sep 17 00:00:00 2001 From: Som Samantray Date: Fri, 31 Jul 2026 22:51:00 +0530 Subject: [PATCH 4/7] chore(consistency): bump tool/endpoint counts to 54/129 Adds memory_lesson_delete to the registry, so update every count surface: tool-count test, README badge and prose, AGENTS.md stats, INSTALL_FOR_AGENTS.md, plugin manifests and docs, and the two code comments this change makes stale. REST endpoint count goes 128 to 129 for the new /agentmemory/lessons/delete route. --- AGENTS.md | 4 ++-- INSTALL_FOR_AGENTS.md | 4 ++-- README.md | 22 +++++++++---------- assets/tags/light/stat-tools.svg | 4 ++-- assets/tags/stat-tools.svg | 4 ++-- plugin/.claude-plugin/plugin.json | 2 +- plugin/.codex-plugin/plugin.json | 2 +- plugin/opencode/README.md | 2 +- plugin/plugin.json | 2 +- .../skills/agentmemory-mcp-tools/REFERENCE.md | 2 +- src/index.ts | 2 +- src/mcp/standalone.ts | 2 +- src/mcp/tools-registry.ts | 4 ++-- test/tool-count-consistency.test.ts | 2 +- 14 files changed, 29 insertions(+), 29 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index ead439f1b..6f64946fc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -116,8 +116,8 @@ Hook scripts in `src/hooks/` are standalone Node.js scripts (no iii-sdk import). ## Current Stats (v0.9.28) -- 53 MCP tools (8 visible by default, `AGENTMEMORY_TOOLS=all` for all) -- 128 REST endpoints +- 54 MCP tools (8 visible by default, `AGENTMEMORY_TOOLS=all` for all) +- 129 REST endpoints - 6 MCP resources, 3 MCP prompts - 12 hooks, 15 skills - 260+ iii functions diff --git a/INSTALL_FOR_AGENTS.md b/INSTALL_FOR_AGENTS.md index f368220b3..18c78d3bf 100644 --- a/INSTALL_FOR_AGENTS.md +++ b/INSTALL_FOR_AGENTS.md @@ -80,7 +80,7 @@ agentmemory connect If you cannot tell which agent you are, default to `claude-code`. After wiring, restart the agent or run its MCP reload command (for example `/mcp` in Claude Code) so it picks up the server. -Expect: the agent now lists agentmemory's tools. With the server running you should see the full set of 53 tools (for example `memory_save`, `memory_smart_search`, `memory_sessions`). If you see only 7 tools, the MCP shim could not reach a server, see Troubleshooting. +Expect: the agent now lists agentmemory's tools. With the server running you should see the full set of 54 tools (for example `memory_save`, `memory_smart_search`, `memory_sessions`). If you see only 7 tools, the MCP shim could not reach a server, see Troubleshooting. ## 6. Install native skills @@ -128,7 +128,7 @@ These are off by default because they spend tokens. Enable them only if the user ## Tool surface -The MCP server exposes 53 tools by default (`--tools all`). Use `--tools core` (or `AGENTMEMORY_TOOLS=core`) for a lean 8-tool set on hosts with tight tool limits. The 8 core tools cover save, recall, consolidate, smart search, sessions, diagnose, lesson save, and reflect. +The MCP server exposes 54 tools by default (`--tools all`). Use `--tools core` (or `AGENTMEMORY_TOOLS=core`) for a lean 8-tool set on hosts with tight tool limits. The 8 core tools cover save, recall, consolidate, smart search, sessions, diagnose, lesson save, and reflect. ## Lifecycle commands diff --git a/README.md b/README.md index 48f4045cd..877b2e6ff 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@

95.2% retrieval R@5 92% fewer tokens - 53 MCP tools + 54 MCP tools 12 auto hooks 0 external DBs 1,428+ tests passing @@ -511,7 +511,7 @@ Implementation details live in `src/cli.ts` (see `runUpgrade` around the `src/cl ### Claude Code (one block, paste it) ```text -Install agentmemory: run `npx @agentmemory/agentmemory` in a separate terminal to start the memory server. Then run `/plugin marketplace add rohitg00/agentmemory` and `/plugin install agentmemory` — the plugin registers all 12 hooks, 15 skills, AND auto-wires the `@agentmemory/mcp` stdio server via its `.mcp.json`, so you get 53 MCP tools (memory_smart_search, memory_save, memory_sessions, memory_governance_delete, etc.) without any extra config step. Verify with `curl http://localhost:3111/agentmemory/health`. The real-time viewer is at http://localhost:3113. +Install agentmemory: run `npx @agentmemory/agentmemory` in a separate terminal to start the memory server. Then run `/plugin marketplace add rohitg00/agentmemory` and `/plugin install agentmemory` — the plugin registers all 12 hooks, 15 skills, AND auto-wires the `@agentmemory/mcp` stdio server via its `.mcp.json`, so you get 54 MCP tools (memory_smart_search, memory_save, memory_sessions, memory_governance_delete, etc.) without any extra config step. Verify with `curl http://localhost:3111/agentmemory/health`. The real-time viewer is at http://localhost:3113. ``` #### Claude Code without the plugin install (MCP-standalone path) @@ -540,7 +540,7 @@ codex plugin add agentmemory@agentmemory The Codex plugin ships from the same `plugin/` directory as the Claude Code plugin. It registers: -- `@agentmemory/mcp` as an MCP server (proxies all 53 tools when `AGENTMEMORY_URL` points at a running agentmemory server; falls back to 7 tools locally when no server is reachable) +- `@agentmemory/mcp` as an MCP server (proxies all 54 tools when `AGENTMEMORY_URL` points at a running agentmemory server; falls back to 7 tools locally when no server is reachable) - 6 lifecycle hooks: `SessionStart`, `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, `PreCompact`, `Stop` - 8 invocable skills: `/recall`, `/remember`, `/session-history`, `/forget`, `/recap`, `/handoff`, `/commit-context`, `/commit-history`, plus 7 reference skills the agent loads on demand (MCP tools, REST API, config, agents, hooks, architecture, and the skill-authoring guide) @@ -574,7 +574,7 @@ copilot plugin install rohitg00/agentmemory:plugin

OpenClaw (paste this prompt) ```text -Install agentmemory for OpenClaw. Run `npx @agentmemory/agentmemory` in a separate terminal to start the memory server on localhost:3111. Then add this to my OpenClaw MCP config so agentmemory is available with all 53 memory tools: +Install agentmemory for OpenClaw. Run `npx @agentmemory/agentmemory` in a separate terminal to start the memory server on localhost:3111. Then add this to my OpenClaw MCP config so agentmemory is available with all 54 memory tools: { "mcpServers": { @@ -599,7 +599,7 @@ Full guide: [`integrations/openclaw/`](integrations/openclaw/) Hermes Agent (paste this prompt) ```text -Install agentmemory for Hermes. Run `npx @agentmemory/agentmemory` in a separate terminal to start the memory server on localhost:3111. Then add this to ~/.hermes/config.yaml so Hermes can use agentmemory as an MCP server with all 53 memory tools: +Install agentmemory for Hermes. Run `npx @agentmemory/agentmemory` in a separate terminal to start the memory server on localhost:3111. Then add this to ~/.hermes/config.yaml so Hermes can use agentmemory as an MCP server with all 54 memory tools: mcp_servers: agentmemory: @@ -956,11 +956,11 @@ npm install @huggingface/transformers

MCP Server

-53 tools, 6 resources, 3 prompts, and 15 skills, the most comprehensive MCP memory toolkit for any agent. +54 tools, 6 resources, 3 prompts, and 15 skills, the most comprehensive MCP memory toolkit for any agent. -> **MCP shim vs full server:** the published `@agentmemory/mcp` package is a thin shim. It exposes the full 53-tool surface **only when it can reach a running agentmemory server** via `AGENTMEMORY_URL` (proxy mode). With no server reachable, the shim falls back to a 7-tool local set (`memory_save`, `memory_recall`, `memory_smart_search`, `memory_sessions`, `memory_export`, `memory_audit`, `memory_governance_delete`). The `AGENTMEMORY_TOOLS=core|all` env var is a *server-side* flag — setting it in the shim's `env` block has no effect. If you see only 7 tools in Cursor / OpenCode / Gemini CLI, start `npx @agentmemory/agentmemory` (or the Docker stack) and set `AGENTMEMORY_URL=http://localhost:3111`. +> **MCP shim vs full server:** the published `@agentmemory/mcp` package is a thin shim. It exposes the full 54-tool surface **only when it can reach a running agentmemory server** via `AGENTMEMORY_URL` (proxy mode). With no server reachable, the shim falls back to a 7-tool local set (`memory_save`, `memory_recall`, `memory_smart_search`, `memory_sessions`, `memory_export`, `memory_audit`, `memory_governance_delete`). The `AGENTMEMORY_TOOLS=core|all` env var is a *server-side* flag — setting it in the shim's `env` block has no effect. If you see only 7 tools in Cursor / OpenCode / Gemini CLI, start `npx @agentmemory/agentmemory` (or the Docker stack) and set `AGENTMEMORY_URL=http://localhost:3111`. -### 53 Tools +### 54 Tools
Core tools (always available) @@ -982,7 +982,7 @@ npm install @huggingface/transformers
-Extended tools (53 total — set AGENTMEMORY_TOOLS=all) +Extended tools (54 total — set AGENTMEMORY_TOOLS=all) | Tool | Description | |------|-------------| @@ -1490,7 +1490,7 @@ Create `~/.agentmemory/.env`: # USER_ID= # TEAM_MODE=private -# Tool visibility: "core" (8 tools, lean fallback) or "all" (53 tools) +# Tool visibility: "core" (8 tools, lean fallback) or "all" (54 tools) # AGENTMEMORY_TOOLS=core ``` @@ -1498,7 +1498,7 @@ Create `~/.agentmemory/.env`:

API

-128 endpoints on port `3111`. The REST API binds to `127.0.0.1` by default. Protected endpoints require `Authorization: Bearer ` when `AGENTMEMORY_SECRET` is set, and mesh sync endpoints require `AGENTMEMORY_SECRET` on both peers. +129 endpoints on port `3111`. The REST API binds to `127.0.0.1` by default. Protected endpoints require `Authorization: Bearer ` when `AGENTMEMORY_SECRET` is set, and mesh sync endpoints require `AGENTMEMORY_SECRET` on both peers.
Key endpoints diff --git a/assets/tags/light/stat-tools.svg b/assets/tags/light/stat-tools.svg index e6d598532..9ed07e9fa 100644 --- a/assets/tags/light/stat-tools.svg +++ b/assets/tags/light/stat-tools.svg @@ -1,5 +1,5 @@ - + - 53 + 54 MCP TOOLS diff --git a/assets/tags/stat-tools.svg b/assets/tags/stat-tools.svg index 2f38d7a10..57c3dc4ee 100644 --- a/assets/tags/stat-tools.svg +++ b/assets/tags/stat-tools.svg @@ -1,5 +1,5 @@ - + - 53 + 54 MCP TOOLS diff --git a/plugin/.claude-plugin/plugin.json b/plugin/.claude-plugin/plugin.json index 49d4bacb2..27bdc81eb 100644 --- a/plugin/.claude-plugin/plugin.json +++ b/plugin/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "agentmemory", "version": "0.9.28", - "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.", + "description": "Persistent memory for AI coding agents -- captures tool usage, compresses via LLM, injects context into future sessions. 12 hooks, 54 MCP tools, 8 skills, real-time viewer.", "author": { "name": "Rohit Ghumare", "url": "https://github.com/rohitg00" diff --git a/plugin/.codex-plugin/plugin.json b/plugin/.codex-plugin/plugin.json index 9d3502899..ad262621d 100644 --- a/plugin/.codex-plugin/plugin.json +++ b/plugin/.codex-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "agentmemory", "version": "0.9.28", - "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.", + "description": "Persistent memory for AI coding agents -- captures tool usage, compresses via LLM, injects context into future sessions. 6 hooks, 54 MCP tools, 8 skills, real-time viewer.", "author": { "name": "Rohit Ghumare", "url": "https://github.com/rohitg00" diff --git a/plugin/opencode/README.md b/plugin/opencode/README.md index 60f5e5bb9..6bcc4b321 100644 --- a/plugin/opencode/README.md +++ b/plugin/opencode/README.md @@ -9,7 +9,7 @@

- 53 MCP tools + 54 MCP tools 22 hooks 2 slash commands 95.2% R@5 diff --git a/plugin/plugin.json b/plugin/plugin.json index 7d9cfca11..90d248c58 100644 --- a/plugin/plugin.json +++ b/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "agentmemory", "version": "0.9.28", - "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.", + "description": "Persistent memory for AI coding agents -- captures tool usage, compresses via LLM, injects context into future sessions. 12 hooks, 54 MCP tools, 15 skills, real-time viewer.", "author": { "name": "Rohit Ghumare", "url": "https://github.com/rohitg00" diff --git a/plugin/skills/agentmemory-mcp-tools/REFERENCE.md b/plugin/skills/agentmemory-mcp-tools/REFERENCE.md index 0c556fc77..dec8f1c93 100644 --- a/plugin/skills/agentmemory-mcp-tools/REFERENCE.md +++ b/plugin/skills/agentmemory-mcp-tools/REFERENCE.md @@ -3,7 +3,7 @@ Generated from `src/mcp/tools-registry.ts`. Do not edit the block below by hand; run `npm run skills:gen` after changing the registry. -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). +agentmemory exposes 54 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 | | --- | --- | --- | --- | diff --git a/src/index.ts b/src/index.ts index 1e623eae8..e89ad0bb1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -518,7 +518,7 @@ async function main() { `Ready. ${embeddingProvider ? "Triple-stream (BM25+Vector+Graph)" : "BM25+Graph"} search active.`, ); bootLog( - `REST API: 128 endpoints at http://localhost:${config.restPort}/agentmemory/*`, + `REST API: 129 endpoints at http://localhost:${config.restPort}/agentmemory/*`, ); bootLog( `MCP surface (opt-in via \`npx @agentmemory/mcp\`): ${getAllTools().length} tools · 6 resources · 3 prompts`, diff --git a/src/mcp/standalone.ts b/src/mcp/standalone.ts index dd66ecb1d..1ace150b1 100644 --- a/src/mcp/standalone.ts +++ b/src/mcp/standalone.ts @@ -339,7 +339,7 @@ async function handleProxyGeneric( handle: ProxyHandle, ): Promise<{ content: Array<{ type: string; text: string }> }> { // Forward to the server's full MCP surface so non-Claude clients can - // reach all 53 tools (lessons, sentinels, slots, signals, graph, …) + // reach all 54 tools (lessons, sentinels, slots, signals, graph, …) // instead of being capped at the 7 IMPLEMENTED_TOOLS set baked into // this shim. The server validates arguments per tool. const result = (await handle.call("/agentmemory/mcp/call", { diff --git a/src/mcp/tools-registry.ts b/src/mcp/tools-registry.ts index ba734f5eb..464cb3b0c 100644 --- a/src/mcp/tools-registry.ts +++ b/src/mcp/tools-registry.ts @@ -962,8 +962,8 @@ export function getAllTools(): McpToolDef[] { } // default switched from "core" (8 essential tools) to "all" -// (full 53-tool surface). README and plugin manifests have always -// advertised 53 tools "in proxy mode"; the old default left OpenCode / +// (full 54-tool surface). README and plugin manifests have always +// advertised 54 tools "in proxy mode"; the old default left OpenCode / // Claude Code users seeing 8 with no indication the other tools existed. // Users who want the lean essentials can still set AGENTMEMORY_TOOLS=core. export function getVisibleTools(): McpToolDef[] { diff --git a/test/tool-count-consistency.test.ts b/test/tool-count-consistency.test.ts index 6e845df06..a5b7b5b50 100644 --- a/test/tool-count-consistency.test.ts +++ b/test/tool-count-consistency.test.ts @@ -9,7 +9,7 @@ vi.mock("../src/logger.js", () => ({ import { getAllTools, ESSENTIAL_TOOLS } from "../src/mcp/tools-registry.js"; const ROOT = join(import.meta.dirname, ".."); -const EXPECTED_TOOL_COUNT = 53; +const EXPECTED_TOOL_COUNT = 54; function readText(relativePath: string): string { return readFileSync(join(ROOT, relativePath), "utf-8"); From 2a95aa6c1ff8cc4f72c9cfc5ce2c6ecf0bb1a168 Mon Sep 17 00:00:00 2001 From: Som Samantray Date: Fri, 31 Jul 2026 23:10:02 +0530 Subject: [PATCH 5/7] refactor(lessons): simplify 404 mapping and restore decay-delta test Cast the lesson-delete trigger result once instead of twice inline, and restore the lastDecayedAt incremental-delta decay test that was dropped when the lesson-delete describe block was added. --- src/triggers/api.ts | 3 ++- test/lessons.test.ts | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/triggers/api.ts b/src/triggers/api.ts index 24e4f0a72..5f72a6283 100644 --- a/src/triggers/api.ts +++ b/src/triggers/api.ts @@ -3156,7 +3156,8 @@ export function registerApiTriggers( const body = req.body as Record; if (!body?.lessonId || typeof body.lessonId !== "string") return { status_code: 400, body: { error: "lessonId is required" } }; const result = await sdk.trigger({ function_id: "mem::lesson-delete", payload: { lessonId: body.lessonId } }); - if (result && (result as { success?: boolean; error?: string }).success === false && (result as { error?: string }).error === "lesson not found") { + const resp = result as { success?: boolean; error?: string }; + if (resp?.success === false && resp.error === "lesson not found") { return { status_code: 404, body: result }; } return { status_code: 200, body: result }; diff --git a/test/lessons.test.ts b/test/lessons.test.ts index bf615dca3..b55fb5976 100644 --- a/test/lessons.test.ts +++ b/test/lessons.test.ts @@ -329,6 +329,25 @@ describe("Lessons", () => { const after = await kv.get("mem:lessons", saved.lesson.id); expect(after!.deleted).toBe(true); }); + + it("uses lastDecayedAt for incremental delta (not full age)", async () => { + const saved = (await sdk.trigger("mem::lesson-save", { + content: "Incremental decay", + confidence: 0.8, + })) as { lesson: Lesson }; + + const lesson = await kv.get("mem:lessons", saved.lesson.id); + lesson!.createdAt = new Date(Date.now() - 28 * 24 * 60 * 60 * 1000).toISOString(); + lesson!.lastDecayedAt = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(); + lesson!.confidence = 0.6; + await kv.set("mem:lessons", lesson!.id, lesson!); + + await sdk.trigger("mem::lesson-decay-sweep", {}); + + const after = await kv.get("mem:lessons", saved.lesson.id); + expect(after!.confidence).toBeCloseTo(0.55, 2); + expect(after!.confidence).toBeGreaterThan(0.4); + }); }); describe("mem::lesson-delete", () => { From bc4f3c709a484e51c916d507df9c9d94fb1c82f1 Mon Sep 17 00:00:00 2001 From: Som Samantray Date: Fri, 31 Jul 2026 23:27:25 +0530 Subject: [PATCH 6/7] fix(review): align 404 error shape and regenerate skill references Review fixes: the lesson-delete REST route now returns the repo-standard { error: 'lesson not found' } body on 404 instead of the function-shaped { success: false } payload, matching api::memory-by-id. Regenerated the autogen MCP and REST skill references so memory_lesson_delete and the lessons/delete route appear in the tables with accurate counts. --- plugin/skills/agentmemory-mcp-tools/REFERENCE.md | 1 + plugin/skills/agentmemory-rest-api/REFERENCE.md | 3 ++- src/triggers/api.ts | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/plugin/skills/agentmemory-mcp-tools/REFERENCE.md b/plugin/skills/agentmemory-mcp-tools/REFERENCE.md index dec8f1c93..b6b185835 100644 --- a/plugin/skills/agentmemory-mcp-tools/REFERENCE.md +++ b/plugin/skills/agentmemory-mcp-tools/REFERENCE.md @@ -28,6 +28,7 @@ agentmemory exposes 54 MCP tools. 8 are in the lean core set (`--tools core` or | `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_delete` | | `lessonId`*: string | Soft-delete a lesson by id. Deleted lessons are excluded from recall and list; re-saving the same content creates a fresh lesson. | | `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. | diff --git a/plugin/skills/agentmemory-rest-api/REFERENCE.md b/plugin/skills/agentmemory-rest-api/REFERENCE.md index d36171def..42259d06d 100644 --- a/plugin/skills/agentmemory-rest-api/REFERENCE.md +++ b/plugin/skills/agentmemory-rest-api/REFERENCE.md @@ -5,7 +5,7 @@ Generated from `src/triggers/api.ts`. Do not edit the block below by hand; run ` 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: +118 registered endpoints: | Method | Path | | --- | --- | @@ -64,6 +64,7 @@ The REST API is the primary surface. All paths are under `http://localhost:3111` | POST | `/agentmemory/leases/release` | | POST | `/agentmemory/leases/renew` | | POST | `/agentmemory/lessons` | +| POST | `/agentmemory/lessons/delete` | | POST | `/agentmemory/lessons/search` | | POST | `/agentmemory/lessons/strengthen` | | GET | `/agentmemory/livez` | diff --git a/src/triggers/api.ts b/src/triggers/api.ts index 5f72a6283..ee383defe 100644 --- a/src/triggers/api.ts +++ b/src/triggers/api.ts @@ -3158,7 +3158,7 @@ export function registerApiTriggers( const result = await sdk.trigger({ function_id: "mem::lesson-delete", payload: { lessonId: body.lessonId } }); const resp = result as { success?: boolean; error?: string }; if (resp?.success === false && resp.error === "lesson not found") { - return { status_code: 404, body: result }; + return { status_code: 404, body: { error: "lesson not found" } }; } return { status_code: 200, body: result }; }); From 4d95321d11eaf689f6569cf5274999115446d049 Mon Sep 17 00:00:00 2001 From: Som Samantray Date: Fri, 31 Jul 2026 23:41:42 +0530 Subject: [PATCH 7/7] fix(lessons): normalize lessonId at entry points and harden no-op test Address CodeRabbit review: trim lessonId once at both the MCP dispatch and REST route before triggering mem::lesson-delete (whitespace-padded ids previously 404'd or looked up raw), and extend the nonexistent- memoryId regression test to assert the no-op path performs no kv.delete and no search-index cleanup. --- src/mcp/server.ts | 2 +- src/triggers/api.ts | 5 +++-- test/remember-forget-audit.test.ts | 4 ++++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/mcp/server.ts b/src/mcp/server.ts index 661fa3e13..13240003b 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -1127,7 +1127,7 @@ export function registerMcpEndpoints( return { status_code: 400, body: { error: "lessonId is required" } }; } const lessonDeleteResult = await sdk.trigger({ function_id: "mem::lesson-delete", payload: { - lessonId: args.lessonId, + lessonId: args.lessonId.trim(), } }); return { status_code: 200, body: { content: [{ type: "text", text: JSON.stringify(lessonDeleteResult, null, 2) }] } }; } diff --git a/src/triggers/api.ts b/src/triggers/api.ts index ee383defe..6699ca869 100644 --- a/src/triggers/api.ts +++ b/src/triggers/api.ts @@ -3154,8 +3154,9 @@ export function registerApiTriggers( const denied = checkAuth(req, secret); if (denied) return denied; const body = req.body as Record; - if (!body?.lessonId || typeof body.lessonId !== "string") return { status_code: 400, body: { error: "lessonId is required" } }; - const result = await sdk.trigger({ function_id: "mem::lesson-delete", payload: { lessonId: body.lessonId } }); + const lessonId = typeof body?.lessonId === "string" ? body.lessonId.trim() : ""; + if (!lessonId) return { status_code: 400, body: { error: "lessonId is required" } }; + const result = await sdk.trigger({ function_id: "mem::lesson-delete", payload: { lessonId } }); const resp = result as { success?: boolean; error?: string }; if (resp?.success === false && resp.error === "lesson not found") { return { status_code: 404, body: { error: "lesson not found" } }; diff --git a/test/remember-forget-audit.test.ts b/test/remember-forget-audit.test.ts index 09126858a..f875b7c07 100644 --- a/test/remember-forget-audit.test.ts +++ b/test/remember-forget-audit.test.ts @@ -131,12 +131,16 @@ describe("mem::forget audit coverage (issue #125)", () => { const kv = mockKV(); registerRememberFunction(sdk as never, kv as never); + const deleteSpy = vi.spyOn(kv, "delete"); const result = await sdk.trigger({ function_id: "mem::forget", payload: { memoryId: "lsn_4f9cb07017a7c8ac" }, }); expect(result).toEqual({ success: true, deleted: 0 }); + // No-op path must not touch the memories keyspace or search index. + expect(deleteSpy).not.toHaveBeenCalled(); + expect(getSearchIndex().has("lsn_4f9cb07017a7c8ac")).toBe(false); }); it("emits no audit row when memoryId does not exist", async () => {