diff --git a/docs/changelog/sdk.mdx b/docs/changelog/sdk.mdx index feebc74ca..566ce2415 100644 --- a/docs/changelog/sdk.mdx +++ b/docs/changelog/sdk.mdx @@ -7,6 +7,13 @@ mode: "wide" + + +**New Features:** +- **Client:** `delete()` and async `delete()` accept `delete_linked` (default `False`). When `True`, deleting a memory also removes the older memories it superseded (the v3 `linked_memory_ids` chain), transitively — the delete-side counterpart of `latest_only`, so a superseded memory does not resurface after the current one is deleted ([#5270](https://github.com/mem0ai/mem0/pull/5270)) + + + **Bug Fixes:** @@ -932,6 +939,13 @@ See the [OSS v1 to v2 migration guide](https://docs.mem0.ai/migration/oss-v1-to- + + +**New Features:** +- **Client:** `delete()` accepts an options object with `deleteLinked` (serialized as `delete_linked`, default `false`). When `true`, deleting a memory also removes the older memories it superseded (the v3 linked chain), transitively — the delete-side counterpart of `latestOnly`, so a superseded memory does not resurface after the current one is deleted ([#5270](https://github.com/mem0ai/mem0/pull/5270)) + + + **Bug Fixes:** diff --git a/mem0-ts/package.json b/mem0-ts/package.json index ebec11c32..cc24d72ca 100644 --- a/mem0-ts/package.json +++ b/mem0-ts/package.json @@ -1,6 +1,6 @@ { "name": "mem0ai", - "version": "3.0.4", + "version": "3.0.5", "description": "The Memory Layer For Your AI Apps", "main": "./dist/index.js", "module": "./dist/index.mjs", diff --git a/mem0-ts/src/client/mem0.ts b/mem0-ts/src/client/mem0.ts index cac2fe36c..89fe45cef 100644 --- a/mem0-ts/src/client/mem0.ts +++ b/mem0-ts/src/client/mem0.ts @@ -9,6 +9,7 @@ import { SearchMemoryOptions, GetAllMemoryOptions, DeleteAllMemoryOptions, + DeleteMemoryOptions, MemoryUpdateBody, ProjectResponse, PromptUpdatePayload, @@ -377,11 +378,17 @@ export default class MemoryClient { return response; } - async delete(memoryId: string): Promise<{ message: string }> { + async delete( + memoryId: string, + options: DeleteMemoryOptions = {}, + ): Promise<{ message: string }> { if (this.telemetryId === "") await this.ping(); - this._captureEvent("delete", []); + this._captureEvent("delete", [Object.keys(options || {})]); + const snakeOptions = camelToSnakeKeys(this._prepareParams(options)); + // @ts-ignore + const query = new URLSearchParams(snakeOptions).toString(); return this._fetchWithErrorHandling( - `${this.host}/v1/memories/${memoryId}/`, + `${this.host}/v1/memories/${memoryId}/${query ? `?${query}` : ""}`, { method: "DELETE", headers: this.headers, diff --git a/mem0-ts/src/client/mem0.types.ts b/mem0-ts/src/client/mem0.types.ts index 0c5c1c93f..185b1dbea 100644 --- a/mem0-ts/src/client/mem0.types.ts +++ b/mem0-ts/src/client/mem0.types.ts @@ -39,6 +39,15 @@ export interface GetAllMemoryOptions { export interface DeleteAllMemoryOptions extends EntityOptions {} +export interface DeleteMemoryOptions { + /** + * When `true`, also delete the older memories this one superseded (the v3 + * linked chain), transitively — the delete-side counterpart of `latestOnly`. + * Off by default. Serialized as `delete_linked`. + */ + deleteLinked?: boolean; +} + // ─── Project Options ──────────────────────────────────────── export interface ProjectOptions { fields?: string[]; diff --git a/mem0-ts/src/client/tests/memoryClient.crud.test.ts b/mem0-ts/src/client/tests/memoryClient.crud.test.ts index 80bdcfa01..39e3f1733 100644 --- a/mem0-ts/src/client/tests/memoryClient.crud.test.ts +++ b/mem0-ts/src/client/tests/memoryClient.crud.test.ts @@ -200,9 +200,26 @@ describe("MemoryClient - delete()", () => { const client = new MemoryClient({ apiKey: TEST_API_KEY }); await client.delete("mem_123"); - expect( - findFetchCall(mock, "/v1/memories/mem_123/", "DELETE"), - ).toBeDefined(); + const call = findFetchCall(mock, "/v1/memories/mem_123/", "DELETE"); + expect(call).toBeDefined(); + // Default: no cascade query param, URL byte-identical to before. + expect(call![0]).not.toContain("delete_linked"); + }); + + test("serializes deleteLinked as delete_linked query param", async () => { + const extra = new Map(); + extra.set("/v1/memories/mem_123/", { + status: 200, + body: { message: "Memory deleted successfully", cascade_count: 1 }, + }); + const mock = setupMockFetch(extra); + + const client = new MemoryClient({ apiKey: TEST_API_KEY }); + await client.delete("mem_123", { deleteLinked: true }); + + const call = findFetchCall(mock, "/v1/memories/mem_123/", "DELETE"); + expect(call).toBeDefined(); + expect(call![0]).toContain("delete_linked=true"); }); }); diff --git a/mem0/client/main.py b/mem0/client/main.py index 69ff77bd8..d4633709f 100644 --- a/mem0/client/main.py +++ b/mem0/client/main.py @@ -360,11 +360,16 @@ class MemoryClient: return response.json() @api_error_handler - def delete(self, memory_id: str) -> Dict[str, Any]: + def delete(self, memory_id: str, delete_linked: bool = False) -> Dict[str, Any]: """Delete a specific memory by ID. Args: memory_id: The ID of the memory to delete. + delete_linked: When True, also delete the older memories this one + superseded (the v3 ``linked_memory_ids`` chain), transitively. + This is the delete-side counterpart of ``latest_only`` — it + stops a superseded memory from resurfacing after you delete the + current one. Defaults to False (only the given memory is deleted). Returns: A dictionary containing the API response. @@ -377,10 +382,12 @@ class MemoryClient: NetworkError: If network connectivity issues occur. MemoryNotFoundError: If the memory doesn't exist (for updates/deletes). """ - params = self._prepare_params() + params = self._prepare_params({"delete_linked": delete_linked or None}) response = self.client.delete(f"/v1/memories/{memory_id}/", params=params) response.raise_for_status() - capture_client_event("client.delete", self, {"memory_id": memory_id, "sync_type": "sync"}) + capture_client_event( + "client.delete", self, {"memory_id": memory_id, "delete_linked": delete_linked, "sync_type": "sync"} + ) return response.json() @api_error_handler @@ -1268,11 +1275,16 @@ class AsyncMemoryClient: return response.json() @api_error_handler - async def delete(self, memory_id: str) -> Dict[str, Any]: + async def delete(self, memory_id: str, delete_linked: bool = False) -> Dict[str, Any]: """Delete a specific memory by ID. Args: memory_id: The ID of the memory to delete. + delete_linked: When True, also delete the older memories this one + superseded (the v3 ``linked_memory_ids`` chain), transitively. + This is the delete-side counterpart of ``latest_only`` — it + stops a superseded memory from resurfacing after you delete the + current one. Defaults to False (only the given memory is deleted). Returns: A dictionary containing the API response. @@ -1285,10 +1297,12 @@ class AsyncMemoryClient: NetworkError: If network connectivity issues occur. MemoryNotFoundError: If the memory doesn't exist (for updates/deletes). """ - params = self._prepare_params() + params = self._prepare_params({"delete_linked": delete_linked or None}) response = await self.async_client.delete(f"/v1/memories/{memory_id}/", params=params) response.raise_for_status() - capture_client_event("client.delete", self, {"memory_id": memory_id, "sync_type": "async"}) + capture_client_event( + "client.delete", self, {"memory_id": memory_id, "delete_linked": delete_linked, "sync_type": "async"} + ) return response.json() @api_error_handler diff --git a/pyproject.toml b/pyproject.toml index 300d88a6a..13eb2fac6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "mem0ai" -version = "2.0.3" +version = "2.0.4" description = "Long-term memory for AI Agents" authors = [ { name = "Mem0", email = "support@mem0.ai" } diff --git a/tests/test_client.py b/tests/test_client.py index fb685c02a..66d01a4c0 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -147,3 +147,41 @@ class TestFilterOperatorPassthrough: call_args = mock_memory_client.client.post.call_args payload = call_args.kwargs.get("json", call_args.args[1] if len(call_args.args) > 1 else {}) assert payload["filters"] == complex_filter + + +class TestDeleteLinked: + """delete() should forward the opt-in delete_linked flag as a query param.""" + + def _setup_delete(self, client): + client.client.delete.return_value = MagicMock( + json=lambda: {"message": "Memory deleted successfully!"}, + raise_for_status=lambda: None, + ) + + def test_delete_default_omits_delete_linked(self, mock_memory_client): + """Default delete sends no delete_linked param — byte-identical to before.""" + self._setup_delete(mock_memory_client) + + mock_memory_client.delete("mem_123") + + call_args = mock_memory_client.client.delete.call_args + assert call_args.args[0] == "/v1/memories/mem_123/" + assert "delete_linked" not in call_args.kwargs.get("params", {}) + + def test_delete_linked_true_sets_param(self, mock_memory_client): + """delete_linked=True forwards delete_linked into the request params.""" + self._setup_delete(mock_memory_client) + + mock_memory_client.delete("mem_123", delete_linked=True) + + call_args = mock_memory_client.client.delete.call_args + assert call_args.kwargs.get("params", {}).get("delete_linked") is True + + def test_delete_linked_false_omits_param(self, mock_memory_client): + """delete_linked=False is stripped, so the default path is untouched.""" + self._setup_delete(mock_memory_client) + + mock_memory_client.delete("mem_123", delete_linked=False) + + call_args = mock_memory_client.client.delete.call_args + assert "delete_linked" not in call_args.kwargs.get("params", {})