-
+
@@ -504,7 +504,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 59 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 60 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)
@@ -955,9 +955,9 @@ npm install @xenova/transformers
-59 tools, 6 resources, 3 prompts, and 15 skills, the most comprehensive MCP memory toolkit for any agent.
+60 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 58-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 60-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
@@ -1503,7 +1503,7 @@ Create `~/.agentmemory/.env`:
-135 endpoints on port `3111`. The REST API binds to `127.0.0.1` by default.
+136 endpoints on port `3111`. The REST API binds to `127.0.0.1` by default.
Only `/agentmemory/livez` is public. Protected endpoints, including detailed
health telemetry, require `Authorization: Bearer `. Explicit
cross-project `scope: "global"` requires the separate
diff --git a/assets/tags/light/stat-tools.svg b/assets/tags/light/stat-tools.svg
index e125bec96..acf22dbd7 100644
--- a/assets/tags/light/stat-tools.svg
+++ b/assets/tags/light/stat-tools.svg
@@ -1,5 +1,5 @@
diff --git a/assets/tags/stat-tools.svg b/assets/tags/stat-tools.svg
index dd6863cdb..c78bc760a 100644
--- a/assets/tags/stat-tools.svg
+++ b/assets/tags/stat-tools.svg
@@ -1,5 +1,5 @@
diff --git a/ci/r13-test-manifest.json b/ci/r13-test-manifest.json
index 53e665c6b..853e6e20f 100644
--- a/ci/r13-test-manifest.json
+++ b/ci/r13-test-manifest.json
@@ -1,5 +1,5 @@
{
"count": 159,
"sha256": "987f48c8beda0e67f96c4b16a60c5637cca072d2bb054493561a74fbf4e984b7",
- "content_sha256": "75371c531f6b4a71fe2c6ea72e483bbb3005d570b8e680e88371ba928b3c1843"
+ "content_sha256": "5b4e61aba030ee6139c86f29cf7c2eccda3923cb959578b1aada8e67efee6184"
}
diff --git a/plugin/.claude-plugin/plugin.json b/plugin/.claude-plugin/plugin.json
index ac2759062..02d0fbe16 100644
--- a/plugin/.claude-plugin/plugin.json
+++ b/plugin/.claude-plugin/plugin.json
@@ -1,7 +1,7 @@
{
"name": "agentmemory",
"version": "0.9.28-chronode.12",
- "description": "Persistent memory for AI coding agents -- captures tool usage, compresses via LLM, injects context into future sessions. 12 hooks, 59 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, 60 MCP tools, 15 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 ec850edb3..bfecb109b 100644
--- a/plugin/.codex-plugin/plugin.json
+++ b/plugin/.codex-plugin/plugin.json
@@ -1,7 +1,7 @@
{
"name": "agentmemory",
"version": "0.9.28-chronode.12",
- "description": "Persistent memory for AI coding agents -- captures tool usage, compresses via LLM, injects context into future sessions. 11 hooks, 59 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. 11 hooks, 60 MCP tools, 15 skills, real-time viewer.",
"author": {
"name": "Rohit Ghumare",
"url": "https://github.com/rohitg00"
diff --git a/plugin/plugin.json b/plugin/plugin.json
index 465d354ce..98849f79e 100644
--- a/plugin/plugin.json
+++ b/plugin/plugin.json
@@ -1,7 +1,7 @@
{
"name": "agentmemory",
"version": "0.9.28-chronode.12",
- "description": "Persistent memory for AI coding agents -- captures tool usage, compresses via LLM, injects context into future sessions. 12 hooks, 59 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, 60 MCP tools, 15 skills, real-time viewer.",
"author": {
"name": "Rohit Ghumare",
"url": "https://github.com/rohitg00"
diff --git a/plugin/scripts/standalone.mjs b/plugin/scripts/standalone.mjs
index b3cb50a92..f0472e630 100755
--- a/plugin/scripts/standalone.mjs
+++ b/plugin/scripts/standalone.mjs
@@ -1306,6 +1306,18 @@ const V070_TOOLS = [
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: "Export memories, lessons, and crystals as Obsidian-compatible Markdown files with YAML frontmatter and wikilinks for graph view.",
@@ -2032,10 +2044,16 @@ const READ_ONLY_LOCAL_FALLBACK_TOOLS = new Set([
"memory_export",
"memory_audit"
]);
+const SUPPORTED_PROTOCOL_VERSIONS = [
+ "2025-11-25",
+ "2025-06-18",
+ "2025-03-26",
+ "2024-11-05"
+];
const SERVER_INFO = {
name: "agentmemory",
version: VERSION,
- protocolVersion: "2024-11-05"
+ protocolVersion: SUPPORTED_PROTOCOL_VERSIONS[0]
};
function getStandalonePersistPath() {
return process.env["STANDALONE_PERSIST_PATH"]?.trim() || join(homedir(), ".agentmemory", "standalone.json");
@@ -2388,14 +2406,17 @@ async function finishShutdown() {
}
const transport = createStdioTransport(async (method, params) => {
switch (method) {
- case "initialize": return {
- protocolVersion: SERVER_INFO.protocolVersion,
- capabilities: { tools: { listChanged: false } },
- serverInfo: {
- name: SERVER_INFO.name,
- version: SERVER_INFO.version
- }
- };
+ case "initialize": {
+ const requested = params?.protocolVersion;
+ return {
+ protocolVersion: typeof requested === "string" && SUPPORTED_PROTOCOL_VERSIONS.includes(requested) ? requested : SERVER_INFO.protocolVersion,
+ capabilities: { tools: { listChanged: false } },
+ serverInfo: {
+ name: SERVER_INFO.name,
+ version: SERVER_INFO.version
+ }
+ };
+ }
case "notifications/initialized": return {};
case "tools/list": return handleToolsList();
case "tools/call": {
diff --git a/plugin/skills/agentmemory-mcp-tools/REFERENCE.md b/plugin/skills/agentmemory-mcp-tools/REFERENCE.md
index acb12f8d6..55a5751a5 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 59 MCP tools. 8 are in the lean core set (`--tools core` or `AGENTMEMORY_TOOLS=core`); the rest load with `--tools all` (default).
+agentmemory exposes 60 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 |
| --- | --- | --- | --- |
@@ -31,6 +31,7 @@ agentmemory exposes 59 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, `scope`: 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, `scope`: 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, `scope`: 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 9ec0b0d72..e9d5dc29d 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.
-124 registered endpoints:
+125 registered endpoints:
| Method | Path |
| --- | --- |
@@ -68,6 +68,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/scripts/evidence/generate-interface-inventory.test.mjs b/scripts/evidence/generate-interface-inventory.test.mjs
index 905962545..0a4f4559d 100644
--- a/scripts/evidence/generate-interface-inventory.test.mjs
+++ b/scripts/evidence/generate-interface-inventory.test.mjs
@@ -33,10 +33,10 @@ test("generates the complete governed interface denominator", () => {
{ cwd: root, stdio: "pipe" },
);
const inventory = JSON.parse(readFileSync(output, "utf8"));
- assert.equal(inventory.counts.http_routes, 135);
+ assert.equal(inventory.counts.http_routes, 136);
assert.equal(inventory.counts.missing_auth_routes, 0);
assert.equal(inventory.counts.mcp_transport_routes, 6);
- assert.equal(inventory.counts.mcp_tools, 59);
+ assert.equal(inventory.counts.mcp_tools, 60);
assert.equal(inventory.counts.mcp_resources, 5);
assert.equal(inventory.counts.mcp_prompts, 3);
assert.equal(inventory.counts.mcp_standalone_fallback_tools, 7);
diff --git a/src/config.ts b/src/config.ts
index a0d923d67..49079a263 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -292,19 +292,20 @@ export function loadClaudeBridgeConfig(): ClaudeBridgeConfig {
const lineBudget = safeParseInt(env["CLAUDE_MEMORY_LINE_BUDGET"], 200);
let memoryFilePath = "";
if (enabled && projectPath) {
- // Claude Code stores MEMORY.md at
- // ~/.claude/projects//MEMORY.md
+ // Claude Code stores project memory at
+ // ~/.claude/projects//memory/MEMORY.md
// where is the project path with `/` and `\` swapped for `-`.
// The leading `-` from an absolute POSIX path is preserved (Claude
// Code keeps it; stripping it produced a slug Claude never reads).
- // There's also no `memory/` subdirectory — the file sits directly
- // under the slug dir.
+ // The `memory/` subdirectory holds MEMORY.md (the index) plus one
+ // per-topic `.md` file per memory (verified against Claude Code 2.x).
const safePath = projectPath.replace(/[/\\]/g, "-");
memoryFilePath = join(
homedir(),
".claude",
"projects",
safePath,
+ "memory",
"MEMORY.md",
);
}
diff --git a/src/functions/lessons.ts b/src/functions/lessons.ts
index 540471601..9a06ff2f5 100644
--- a/src/functions/lessons.ts
+++ b/src/functions/lessons.ts
@@ -277,6 +277,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/functions/remember.ts b/src/functions/remember.ts
index 1ede955c9..03e8007f5 100644
--- a/src/functions/remember.ts
+++ b/src/functions/remember.ts
@@ -190,15 +190,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/src/index.ts b/src/index.ts
index 954329317..94b1457bf 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -653,7 +653,7 @@ async function main() {
: `Operational with ${startupSearchStatus.status} search index; health remains degraded until repair completes.`,
);
bootLog(
- `REST API: 135 endpoints at http://localhost:${config.restPort}/agentmemory/*`,
+ `REST API: 136 endpoints at http://localhost:${config.restPort}/agentmemory/*`,
);
bootLog(
`MCP surface (opt-in via \`npx @agentmemory/mcp\`): ${getAllTools().length} tools · 5 resources · 3 prompts`,
diff --git a/src/mcp/server.ts b/src/mcp/server.ts
index 5351b7310..13033b663 100644
--- a/src/mcp/server.ts
+++ b/src/mcp/server.ts
@@ -1310,6 +1310,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.trim(),
+ } });
+ return { status_code: 200, body: { content: [{ type: "text", text: JSON.stringify(lessonDeleteResult, null, 2) }] } };
+ }
+
case "memory_reflect": {
const projectScope = parseProjectScope(args);
if (!projectScope) {
diff --git a/src/mcp/standalone.ts b/src/mcp/standalone.ts
index 5850fbfb4..4b7ccbf1f 100644
--- a/src/mcp/standalone.ts
+++ b/src/mcp/standalone.ts
@@ -37,10 +37,21 @@ const READ_ONLY_LOCAL_FALLBACK_TOOLS = new Set([
"memory_audit",
]);
+// Revisions this stdio server can speak. Hosts that require a newer
+// revision than a hardcoded default disconnect with -32000 on initialize,
+// so initialize echoes the client's requested revision when supported and
+// otherwise answers with the newest one we support.
+const SUPPORTED_PROTOCOL_VERSIONS = [
+ "2025-11-25",
+ "2025-06-18",
+ "2025-03-26",
+ "2024-11-05",
+];
+
const SERVER_INFO = {
name: "agentmemory",
version: VERSION,
- protocolVersion: "2024-11-05",
+ protocolVersion: SUPPORTED_PROTOCOL_VERSIONS[0],
};
function getStandalonePersistPath(): string {
@@ -493,7 +504,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 58 tools (lessons, sentinels, slots, signals, graph, ...)
+ // reach all 60 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", {
@@ -621,15 +632,23 @@ async function finishShutdown(): Promise {
const transport = createStdioTransport(async (method, params) => {
switch (method) {
- case "initialize":
+ case "initialize": {
+ const requested = (params as { protocolVersion?: unknown } | undefined)
+ ?.protocolVersion;
+ const protocolVersion =
+ typeof requested === "string" &&
+ SUPPORTED_PROTOCOL_VERSIONS.includes(requested)
+ ? requested
+ : SERVER_INFO.protocolVersion;
return {
- protocolVersion: SERVER_INFO.protocolVersion,
+ protocolVersion,
capabilities: { tools: { listChanged: false } },
serverInfo: {
name: SERVER_INFO.name,
version: SERVER_INFO.version,
},
};
+ }
case "notifications/initialized":
return {};
diff --git a/src/mcp/tools-registry.ts b/src/mcp/tools-registry.ts
index de63a5bda..1a7c3a96f 100644
--- a/src/mcp/tools-registry.ts
+++ b/src/mcp/tools-registry.ts
@@ -954,6 +954,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:
@@ -1298,7 +1310,7 @@ export function getAllTools(): McpToolDef[] {
}
// default switched from "core" (8 essential tools) to "all"
-// (full 58-tool surface). README and plugin manifests have always
+// (full 60-tool surface). README and plugin manifests have always
// advertised the full tool surface; 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.
diff --git a/src/triggers/api.ts b/src/triggers/api.ts
index 296b1d498..de9017d4d 100644
--- a/src/triggers/api.ts
+++ b/src/triggers/api.ts
@@ -4566,6 +4566,21 @@ export function registerApiTriggers(
});
registerApiTrigger({ 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;
+ 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" } };
+ }
+ return { status_code: 200, body: result };
+ });
+ registerApiTrigger({ 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;
diff --git a/src/types.ts b/src/types.ts
index 8d62cf777..51042d277 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -781,6 +781,7 @@ export interface AuditEntry {
| "lesson_save"
| "lesson_recall"
| "lesson_strengthen"
+ | "lesson_delete"
| "obsidian_export"
| "reflect"
| "insight_search"
diff --git a/test/claude-bridge-path.test.ts b/test/claude-bridge-path.test.ts
index fa3573d3c..f9c8f1853 100644
--- a/test/claude-bridge-path.test.ts
+++ b/test/claude-bridge-path.test.ts
@@ -4,10 +4,10 @@ import { join } from "node:path";
import { loadClaudeBridgeConfig } from "../src/config.js";
// bridge path must match Claude Code's slug convention exactly:
-// ~/.claude/projects//MEMORY.md
+// ~/.claude/projects//memory/MEMORY.md
// where replaces every / and \ with - and KEEPS any leading -.
-// The previous code stripped the leading - and added a /memory/
-// subdirectory; the bridge then wrote a file Claude Code never read.
+// The memory/ subdirectory holds MEMORY.md (the index) plus per-topic
+// .md files — this is where Claude Code 2.x actually reads/writes.
describe("loadClaudeBridgeConfig path (#625)", () => {
const ORIG_ENV = { ...process.env };
beforeEach(() => {
@@ -24,16 +24,15 @@ describe("loadClaudeBridgeConfig path (#625)", () => {
process.env["CLAUDE_PROJECT_PATH"] = "/home/user/repos/my-project";
const cfg = loadClaudeBridgeConfig();
expect(cfg.memoryFilePath).toBe(
- join(homedir(), ".claude", "projects", "-home-user-repos-my-project", "MEMORY.md"),
+ join(homedir(), ".claude", "projects", "-home-user-repos-my-project", "memory", "MEMORY.md"),
);
});
- it("writes MEMORY.md directly under the slug dir, no memory/ subdir", () => {
+ it("writes MEMORY.md inside the memory/ subdir under the slug dir", () => {
process.env["CLAUDE_MEMORY_BRIDGE"] = "true";
process.env["CLAUDE_PROJECT_PATH"] = "/Users/x/agentmemory";
const cfg = loadClaudeBridgeConfig();
- expect(cfg.memoryFilePath).not.toMatch(/[/\\]memory[/\\]MEMORY\.md$/);
- expect(cfg.memoryFilePath).toMatch(/-Users-x-agentmemory[/\\]MEMORY\.md$/);
+ expect(cfg.memoryFilePath).toMatch(/-Users-x-agentmemory[/\\]memory[/\\]MEMORY\.md$/);
});
it("returns empty memoryFilePath when bridge disabled", () => {
@@ -53,6 +52,6 @@ describe("loadClaudeBridgeConfig path (#625)", () => {
process.env["CLAUDE_MEMORY_BRIDGE"] = "true";
process.env["CLAUDE_PROJECT_PATH"] = "C:\\Users\\x\\project";
const cfg = loadClaudeBridgeConfig();
- expect(cfg.memoryFilePath).toMatch(/C:-Users-x-project[/\\]MEMORY\.md$/);
+ expect(cfg.memoryFilePath).toMatch(/C:-Users-x-project[/\\]memory[/\\]MEMORY\.md$/);
});
});
diff --git a/test/lessons.test.ts b/test/lessons.test.ts
index 82dbceea5..0a2e37ab9 100644
--- a/test/lessons.test.ts
+++ b/test/lessons.test.ts
@@ -358,4 +358,107 @@ describe("Lessons", () => {
expect(after!.confidence).toBeGreaterThan(0.4);
});
});
+
+ describe("mem::lesson-delete", () => {
+ it("soft-deletes an existing lesson", async () => {
+ const saved = (await sdk.trigger("mem::lesson-save", {
+ content: "Delete me",
+ confidence: 0.7,
+ })) as { lesson: Lesson };
+
+ const result = (await sdk.trigger("mem::lesson-delete", {
+ lessonId: saved.lesson.id,
+ })) as { success: boolean; lesson: Lesson };
+
+ expect(result.success).toBe(true);
+ expect(result.lesson.deleted).toBe(true);
+
+ 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]);
+ });
+ });
});
diff --git a/test/mcp-standalone.test.ts b/test/mcp-standalone.test.ts
index 256465110..a4f0a2ebc 100644
--- a/test/mcp-standalone.test.ts
+++ b/test/mcp-standalone.test.ts
@@ -26,6 +26,7 @@ import {
} from "../src/mcp/tools-registry.js";
import { InMemoryKV } from "../src/mcp/in-memory-kv.js";
import { handleToolCall as rawHandleToolCall } from "../src/mcp/standalone.js";
+import { createStdioTransport } from "../src/mcp/transport.js";
import { KV } from "../src/state/schema.js";
import {
resetHandleForTests,
@@ -604,3 +605,32 @@ describe("handleToolCall", () => {
expect(parsed.requested).toBe(2);
});
});
+
+describe("initialize protocol version negotiation", () => {
+ type InitResult = { protocolVersion: string };
+ const handler = () =>
+ vi.mocked(createStdioTransport).mock.calls[0][0] as (
+ method: string,
+ params?: unknown,
+ ) => Promise;
+
+ it("echoes a supported requested version", async () => {
+ const res = await handler()("initialize", { protocolVersion: "2025-06-18" });
+ expect(res.protocolVersion).toBe("2025-06-18");
+ });
+
+ it("echoes the oldest supported version", async () => {
+ const res = await handler()("initialize", { protocolVersion: "2024-11-05" });
+ expect(res.protocolVersion).toBe("2024-11-05");
+ });
+
+ it("answers an unsupported requested version with the latest supported", async () => {
+ const res = await handler()("initialize", { protocolVersion: "1900-01-01" });
+ expect(res.protocolVersion).toBe("2025-11-25");
+ });
+
+ it("answers a missing requested version with the latest supported", async () => {
+ const res = await handler()("initialize", {});
+ expect(res.protocolVersion).toBe("2025-11-25");
+ });
+});
diff --git a/test/remember-forget-audit.test.ts b/test/remember-forget-audit.test.ts
index 7d17b543c..f875b7c07 100644
--- a/test/remember-forget-audit.test.ts
+++ b/test/remember-forget-audit.test.ts
@@ -122,6 +122,40 @@ 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 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 () => {
+ 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
diff --git a/test/tool-count-consistency.test.ts b/test/tool-count-consistency.test.ts
index 0ca862a6b..8812303ee 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 = 59;
+const EXPECTED_TOOL_COUNT = 60;
function readText(relativePath: string): string {
return readFileSync(join(ROOT, relativePath), "utf-8");