Skip to content

feat(mcp): expose memory_forget tool so observations and sessions can actually be deleted - #842

Closed
serhiizghama wants to merge 3 commits into
rohitg00:mainfrom
serhiizghama:feat/memory-forget-tool-833
Closed

feat(mcp): expose memory_forget tool so observations and sessions can actually be deleted#842
serhiizghama wants to merge 3 commits into
rohitg00:mainfrom
serhiizghama:feat/memory-forget-tool-833

Conversation

@serhiizghama

@serhiizghama serhiizghama commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

Fixes #833

Problem

The forget skill (and any MCP-only client) has no way to delete observations or sessions:

  • memory_governance_delete only reads/writes the KV.memories store. Observation IDs (obs_*) never exist there, so the skill's documented happy path is a guaranteed no-op.
  • Worse, the tool returned { "deleted": 0, "success": true } for that no-op, so users were told their data was removed while the .bin files under mem:obs:<sessionId> stayed fully intact.
  • The engine already has the correct function — mem::forget handles memories, per-session observation lists, and whole-session deletion (record + summary) while keeping the BM25/vector indexes and audit log consistent — but it was only reachable over REST (POST /agentmemory/forget), not via MCP.

Solution

  1. New memory_forget MCP tool mapping to mem::forget (registry + mcp::tools::call case). Accepts memoryId, or sessionId with optional comma-separated/array observationIds; bare sessionId deletes all of the session's observations plus the session record and summary, matching the REST behavior. Validation mirrors the REST route (sessionId or memoryId is required) plus a specific error for observationIds without sessionId.
  2. mem::forget now returns its breakdown (memoriesDeleted, observationsDeleted, sessionDeleted) — it already tracked these for the audit row; returning them lets callers confirm to the user what was actually removed.
  3. mem::governance-delete stops reporting no-ops as success: IDs that don't exist in the memories store are returned in a notFound array and success is only true when every requested ID was deleted. The audit row records notFound too.
  4. forget skill + OpenCode instructions rewritten to route observation/session deletion through memory_forget and keep memory_governance_delete for saved memories (mem_*), removing the incorrect "deletes by memory ID only" workaround that never worked.

Tool count badges/docs/manifests bumped 53 → 54 to keep test/consistency.test.ts green.

Testing

  • New test/mcp-forget-tool.test.ts: registry listing, validation errors, payload forwarding for all three deletion shapes (CSV + array observationIds), and the error path.
  • test/governance.test.ts: non-existent IDs now expect success: false + notFound; added partial-delete and all-deleted cases.
  • Full suite: 127 files, 1410 tests pass; npm run build clean.

Summary by CodeRabbit

  • New Features

    • Added memory forget functionality for deleting observations, sessions, or individual saved memories with explicit user confirmation.
  • Documentation

    • Updated MCP tools count from 53 to 54 across all documentation and plugin manifests.
    • Clarified memory deletion workflows distinguishing between deleting saved memories versus observations/sessions.

@vercel

vercel Bot commented Jun 6, 2026

Copy link
Copy Markdown

@serhiizghama is attempting to deploy a commit to the rohitg00's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

Complex PR? Review this PR in Change Stack to move by importance, not file order.

Review Change Stack

📝 Walkthrough

Walkthrough

This PR implements a new memory_forget MCP tool and fixes delete semantics for observations and sessions. It adds the tool to the MCP registry, implements a handler that routes calls to the underlying mem::forget function, updates deletion functions to report missing IDs and partial-failure states, and rewrites skill documentation to direct users to the correct deletion paths.

Changes

Memory Forget MCP Tool and Deletion Semantics

Layer / File(s) Summary
Documentation and manifest version sync (53 → 54 tools)
AGENTS.md, README.md, plugin/.claude-plugin/plugin.json, plugin/.codex-plugin/plugin.json, plugin/opencode/README.md, plugin/plugin.json
All documentation, plugin manifests, and badges are updated to reflect the new memory_forget tool, incrementing the documented MCP tool count from 53 to 54 across integration guides and configuration examples.
Core deletion functions: governance and remember
src/functions/governance.ts, src/functions/remember.ts
mem::governance-delete now tracks missing memory IDs in a notFound array and sets success: false when deletions fail. mem::forget enriches its return payload with separate counts for memoriesDeleted, observationsDeleted, and a sessionDeleted flag.
MCP tool registration and handler
src/mcp/tools-registry.ts, src/mcp/server.ts, test/mcp-standalone.test.ts
Adds memory_forget tool definition to V040_TOOLS with schema for sessionId, observationIds, and memoryId; implements the MCP handler that validates inputs and routes to mem::forget; updates tool registry test expectation from 8 to 9 tools.
Forget skill and plugin instructions
plugin/skills/forget/SKILL.md, plugin/opencode/agentmemory-capture.ts
Rewrites forget skill to document two deletion paths: memory_forget for observations/sessions (with sessionId ± observationIds), and memory_governance_delete only for saved memories (mem_*). Includes explicit warnings about which tool targets which store and clarified failure reporting via notFound.
New memory_forget MCP tool test suite
test/mcp-forget-tool.test.ts
Comprehensive test coverage with mocked SDK/KV verifying tool presence in registry, validation of required argument combinations, correct forwarding of delete payloads to mem::forget, normalization of comma-separated and array observationIds, and error handling when the function throws.
Governance-delete test updates for notFound semantics
test/governance.test.ts
Replaces prior single non-existent-ID test with three cases validating the updated result contract: all-missing IDs yield success: false with full notFound array; partial deletions yield success: false with only missing IDs listed; complete deletions yield success: true with notFound omitted.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • rohitg00/agentmemory#636: Both PRs modify src/functions/remember.ts (mem::forget) and src/functions/governance.ts (mem::governance-delete), with the main PR's MCP tool exposure and return-payload changes directly intertwined with the retrieved PR's index-cleanup work.
  • rohitg00/agentmemory#650: Both PRs affect src/mcp/tools-registry.ts—the main PR adds the new memory_forget tool to the registered set, while the retrieved PR changes getVisibleTools() to default to the full tool surface visibility.

Suggested reviewers

  • rohitg00

Poem

🐰 Forget now remembers what's truly gone,
Observations and sessions—the old way was wrong,
No more silent failures with success that fakes,
The notFound array shows which deletes don't take!
One tool for memories, one for the rest,
Clear paths and true answers—this forget's the best.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: introducing the memory_forget MCP tool to enable deletion of observations and sessions.
Linked Issues check ✅ Passed All objectives from #833 are met: memory_forget MCP tool added, governance delete now reports notFound for missing IDs, forget skill updated to use correct endpoint, and documentation updated.
Out of Scope Changes check ✅ Passed All changes directly support the primary objective of fixing issue #833. Documentation updates to tool counts (53→54) are housekeeping necessary to keep tests passing.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
README.md (1)

892-918: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Align MCP section labels with the new 54-tool total.

The top of the MCP section says 54 tools, but the subsection labels still say 53 Tools and 53 total. Update those labels to avoid contradictory docs.

As per coding guidelines, when adding/removing MCP tools, README MCP tool counts must be updated consistently (search for “MCP tools”).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 892 - 918, Update the MCP tool count labels to match
the new 54-tool total: change the "### 53 Tools" heading to "### 54 Tools" and
update the "<summary>Extended tools (53 total — set
AGENTMEMORY_TOOLS=all)</summary>" text to reflect "54 total" (and search for any
other occurrences of "53 Tools", "53 total", or "MCP tools" in the README and
replace them with "54"/"54 total" to keep counts consistent).

Sources: Coding guidelines, Learnings

src/functions/remember.ts (1)

182-214: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fix over-counting in mem::forget deletion results and audit details.

The new breakdown fields at Line 261-Line 267 can report deletions that never happened in KV, because counters are incremented unconditionally in Line 182-Line 193 and Line 200-Line 214 even when records are absent. That makes deleted, memoriesDeleted, observationsDeleted, and audit details inaccurate.

Suggested fix
       if (data.memoryId) {
         const mem = await kv.get<Memory>(KV.memories, data.memoryId);
-        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++;
+        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++;
+        }
       }
@@
         for (const obsId of data.observationIds) {
           const obs = await kv.get<{ imageData?: string; imageRef?: string }>(
             KV.observations(data.sessionId),
             obsId,
           );
-          await kv.delete(KV.observations(data.sessionId), obsId);
-          if (obs?.imageData) await decrementImageRef(kv, sdk, obs.imageData);
-          if (obs?.imageRef && obs.imageRef !== obs.imageData) {
-            await decrementImageRef(kv, sdk, obs.imageRef);
-          }
-          getSearchIndex().remove(obsId);
-          vectorIndexRemove(obsId);
-          deletedObservationIds.push(obsId);
-          deleted++;
+          if (obs) {
+            await kv.delete(KV.observations(data.sessionId), obsId);
+            if (obs.imageData) await decrementImageRef(kv, sdk, obs.imageData);
+            if (obs.imageRef && obs.imageRef !== obs.imageData) {
+              await decrementImageRef(kv, sdk, obs.imageRef);
+            }
+            getSearchIndex().remove(obsId);
+            vectorIndexRemove(obsId);
+            deletedObservationIds.push(obsId);
+            deleted++;
+          }
         }
       }

Also applies to: 261-267

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/functions/remember.ts` around lines 182 - 214, The code increments
deletion counters and appends IDs (deleted, deletedMemoryIds,
deletedObservationIds, memoriesDeleted, observationsDeleted) even when KV
entries don't exist; change the logic in the mem deletion block (where you call
kv.get<Memory>(KV.memories, data.memoryId), decrementImageRef, deleteAccessLog,
getSearchIndex().remove, vectorIndexRemove) to only perform delete, index
removal, ref-decrementing, push to deletedMemoryIds and increment deleted and
memoriesDeleted if mem is truthy (exists); likewise, in the observations loop
(where you call kv.get(..., obsId), kv.delete, decrementImageRef,
getSearchIndex().remove, vectorIndexRemove) only perform kv.delete side-effects,
push obsId into deletedObservationIds, and increment deleted and
observationsDeleted when obs was found, and adjust the imageRef vs imageData
decrement logic to run only when obs exists.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@plugin/skills/forget/SKILL.md`:
- Around line 20-23: The SKILL.md text for memory_governance_delete is
inaccurate: update the description for the memory_governance_delete call
(referenced by name) to state that the runtime expects a comma-separated string
for memoryIds (not an array) and that the backend default reason is "manual
deletion" (not "plugin skill request"); update any examples or parameter notes
in SKILL.md that reference memoryIds or reason to match this runtime contract.

In `@src/mcp/server.ts`:
- Around line 624-643: The handler currently normalizes malformed observationIds
to [] which gets omitted from the mem::forget payload and can cause
whole-session deletion; update the validation around
asNonEmptyString(args.sessionId), asNonEmptyString(args.memoryId) and
parseCsvList(args.observationIds) so that if args.observationIds is present you
validate its raw type (typeof args.observationIds === "string"), parse it with
parseCsvList, then ensure the result is a non-empty array of non-empty strings
(each entry typeof === "string" && entry.trim().length>0); if this validation
fails return a 400 with an explicit error like "malformed observationIds" and do
not call sdk.trigger({ function_id: "mem::forget", ...}) without observationIds
— only include observationIds in the trigger payload when they passed
validation.

---

Outside diff comments:
In `@README.md`:
- Around line 892-918: Update the MCP tool count labels to match the new 54-tool
total: change the "### 53 Tools" heading to "### 54 Tools" and update the
"<summary>Extended tools (53 total — set AGENTMEMORY_TOOLS=all)</summary>" text
to reflect "54 total" (and search for any other occurrences of "53 Tools", "53
total", or "MCP tools" in the README and replace them with "54"/"54 total" to
keep counts consistent).

In `@src/functions/remember.ts`:
- Around line 182-214: The code increments deletion counters and appends IDs
(deleted, deletedMemoryIds, deletedObservationIds, memoriesDeleted,
observationsDeleted) even when KV entries don't exist; change the logic in the
mem deletion block (where you call kv.get<Memory>(KV.memories, data.memoryId),
decrementImageRef, deleteAccessLog, getSearchIndex().remove, vectorIndexRemove)
to only perform delete, index removal, ref-decrementing, push to
deletedMemoryIds and increment deleted and memoriesDeleted if mem is truthy
(exists); likewise, in the observations loop (where you call kv.get(..., obsId),
kv.delete, decrementImageRef, getSearchIndex().remove, vectorIndexRemove) only
perform kv.delete side-effects, push obsId into deletedObservationIds, and
increment deleted and observationsDeleted when obs was found, and adjust the
imageRef vs imageData decrement logic to run only when obs exists.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 991419c0-cef4-4d0c-8250-f9a4b4c4f95c

📥 Commits

Reviewing files that changed from the base of the PR and between a323fb0 and 5c8a362.

⛔ Files ignored due to path filters (2)
  • assets/tags/light/stat-tools.svg is excluded by !**/*.svg
  • assets/tags/stat-tools.svg is excluded by !**/*.svg
📒 Files selected for processing (15)
  • AGENTS.md
  • README.md
  • plugin/.claude-plugin/plugin.json
  • plugin/.codex-plugin/plugin.json
  • plugin/opencode/README.md
  • plugin/opencode/agentmemory-capture.ts
  • plugin/plugin.json
  • plugin/skills/forget/SKILL.md
  • src/functions/governance.ts
  • src/functions/remember.ts
  • src/mcp/server.ts
  • src/mcp/tools-registry.ts
  • test/governance.test.ts
  • test/mcp-forget-tool.test.ts
  • test/mcp-standalone.test.ts

Comment on lines +20 to 23
- **Saved memories** (`mem_*` IDs) → call `memory_governance_delete` with:
- `memoryIds: [<id>, ...]` — an array (or comma-separated string) of memory IDs
- `reason: "<short reason>"` — optional, defaults to `"plugin skill request"`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Align governance-delete instructions with the actual runtime contract.

This text currently says memoryIds can be an array and that reason defaults to "plugin skill request". In current runtime behavior, MCP memory_governance_delete expects a comma-separated string in this path, and backend default reason is "manual deletion".

Proposed wording fix
-   - **Saved memories** (`mem_*` IDs) → call `memory_governance_delete` with:
-     - `memoryIds: [<id>, ...]` — an array (or comma-separated string) of memory IDs
-     - `reason: "<short reason>"` — optional, defaults to `"plugin skill request"`
+   - **Saved memories** (`mem_*` IDs) → call `memory_governance_delete` with:
+     - `memoryIds: "<id_1>,<id_2>"` — comma-separated memory IDs
+     - `reason: "<short reason>"` — optional, defaults to `"manual deletion"`
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugin/skills/forget/SKILL.md` around lines 20 - 23, The SKILL.md text for
memory_governance_delete is inaccurate: update the description for the
memory_governance_delete call (referenced by name) to state that the runtime
expects a comma-separated string for memoryIds (not an array) and that the
backend default reason is "manual deletion" (not "plugin skill request"); update
any examples or parameter notes in SKILL.md that reference memoryIds or reason
to match this runtime contract.

Comment thread src/mcp/server.ts
Comment on lines +624 to +643
const sessionId = asNonEmptyString(args.sessionId);
const memoryId = asNonEmptyString(args.memoryId);
const observationIds = parseCsvList(args.observationIds);
if (observationIds.length > 0 && !sessionId) {
return {
status_code: 400,
body: { error: "observationIds requires sessionId" },
};
}
if (!sessionId && !memoryId) {
return {
status_code: 400,
body: { error: "sessionId or memoryId is required" },
};
}
try {
const result = await sdk.trigger({ function_id: "mem::forget", payload: {
...(sessionId ? { sessionId } : {}),
...(observationIds.length > 0 ? { observationIds } : {}),
...(memoryId ? { memoryId } : {}),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject malformed observationIds to avoid accidental full-session deletion.

At Line 626, malformed observationIds (e.g., non-string array entries or empty CSV payload) are normalized to [], and then omitted from the trigger payload. With a sessionId, that can unintentionally invoke whole-session deletion semantics in mem::forget instead of targeted observation deletion.

Proposed fix
           case "memory_forget": {
             const sessionId = asNonEmptyString(args.sessionId);
             const memoryId = asNonEmptyString(args.memoryId);
-            const observationIds = parseCsvList(args.observationIds);
+            const hasObservationIds = args.observationIds !== undefined;
+            if (
+              hasObservationIds &&
+              typeof args.observationIds !== "string" &&
+              !Array.isArray(args.observationIds)
+            ) {
+              return {
+                status_code: 400,
+                body: { error: "observationIds must be a comma-separated string or string[]" },
+              };
+            }
+            if (
+              Array.isArray(args.observationIds) &&
+              args.observationIds.some((v) => typeof v !== "string" || !v.trim())
+            ) {
+              return {
+                status_code: 400,
+                body: { error: "observationIds array must contain non-empty strings" },
+              };
+            }
+            const observationIds = parseCsvList(args.observationIds);
+            if (hasObservationIds && observationIds.length === 0) {
+              return {
+                status_code: 400,
+                body: { error: "observationIds must contain at least one ID" },
+              };
+            }
             if (observationIds.length > 0 && !sessionId) {
               return {
                 status_code: 400,
                 body: { error: "observationIds requires sessionId" },
               };

As per coding guidelines: MCP tool handlers must validate arguments with typeof checks and parse CSV args in a controlled way before triggering functions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mcp/server.ts` around lines 624 - 643, The handler currently normalizes
malformed observationIds to [] which gets omitted from the mem::forget payload
and can cause whole-session deletion; update the validation around
asNonEmptyString(args.sessionId), asNonEmptyString(args.memoryId) and
parseCsvList(args.observationIds) so that if args.observationIds is present you
validate its raw type (typeof args.observationIds === "string"), parse it with
parseCsvList, then ensure the result is a non-empty array of non-empty strings
(each entry typeof === "string" && entry.trim().length>0); if this validation
fails return a 400 with an explicit error like "malformed observationIds" and do
not call sdk.trigger({ function_id: "mem::forget", ...}) without observationIds
— only include observationIds in the trigger payload when they passed
validation.

Source: Coding guidelines

@serhiizghama

Copy link
Copy Markdown
Contributor Author

Closing — it's gone stale and picked up conflicts in the meantime. Can reopen if it's still useful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

/agentmemory:forget deletes nothing — memory_governance_delete only targets the memories store, not observations (reports success: true)

1 participant