feat(sdk): add delete_linked option to MemoryClient.delete (Python + TS)
Plumbs the platform v3 `delete_linked` flag through the hosted MemoryClient in
both SDKs. When true, DELETE /v1/memories/{id}/ also removes the older memories
the target superseded (the linked_memory_ids chain) — the delete-side counterpart
of `latest_only`, so a superseded memory doesn't resurface after the current one
is deleted.
- Python: MemoryClient.delete / AsyncMemoryClient.delete gain `delete_linked: bool = False`
forwarded as a query param (omitted when False — default byte-identical).
- TypeScript: client.delete accepts `{ deleteLinked?: boolean }`, serialized to
`delete_linked` via the existing camelToSnakeKeys path (matches latestOnly).
- Unit tests for both: default omits the param; opt-in forwards it.
Client-side passthrough only; no changes to the OSS memory engine.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
88934304c6
commit
46e4b91a06
@@ -9,6 +9,7 @@ import {
|
|||||||
SearchMemoryOptions,
|
SearchMemoryOptions,
|
||||||
GetAllMemoryOptions,
|
GetAllMemoryOptions,
|
||||||
DeleteAllMemoryOptions,
|
DeleteAllMemoryOptions,
|
||||||
|
DeleteMemoryOptions,
|
||||||
MemoryUpdateBody,
|
MemoryUpdateBody,
|
||||||
ProjectResponse,
|
ProjectResponse,
|
||||||
PromptUpdatePayload,
|
PromptUpdatePayload,
|
||||||
@@ -377,11 +378,17 @@ export default class MemoryClient {
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(memoryId: string): Promise<{ message: string }> {
|
async delete(
|
||||||
|
memoryId: string,
|
||||||
|
options: DeleteMemoryOptions = {},
|
||||||
|
): Promise<{ message: string }> {
|
||||||
if (this.telemetryId === "") await this.ping();
|
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(
|
return this._fetchWithErrorHandling(
|
||||||
`${this.host}/v1/memories/${memoryId}/`,
|
`${this.host}/v1/memories/${memoryId}/${query ? `?${query}` : ""}`,
|
||||||
{
|
{
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
headers: this.headers,
|
headers: this.headers,
|
||||||
|
|||||||
@@ -39,6 +39,14 @@ export interface GetAllMemoryOptions {
|
|||||||
|
|
||||||
export interface DeleteAllMemoryOptions extends EntityOptions {}
|
export interface DeleteAllMemoryOptions extends EntityOptions {}
|
||||||
|
|
||||||
|
export interface DeleteMemoryOptions {
|
||||||
|
// When true, also delete the older memories this one superseded
|
||||||
|
// (the v3 linked_memory_ids chain), transitively. The delete-side
|
||||||
|
// counterpart of latestOnly — stops a superseded memory from resurfacing
|
||||||
|
// after the current one is deleted. Serialized as `delete_linked`.
|
||||||
|
deleteLinked?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Project Options ────────────────────────────────────────
|
// ─── Project Options ────────────────────────────────────────
|
||||||
export interface ProjectOptions {
|
export interface ProjectOptions {
|
||||||
fields?: string[];
|
fields?: string[];
|
||||||
|
|||||||
@@ -200,9 +200,26 @@ describe("MemoryClient - delete()", () => {
|
|||||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||||
await client.delete("mem_123");
|
await client.delete("mem_123");
|
||||||
|
|
||||||
expect(
|
const call = findFetchCall(mock, "/v1/memories/mem_123/", "DELETE");
|
||||||
findFetchCall(mock, "/v1/memories/mem_123/", "DELETE"),
|
expect(call).toBeDefined();
|
||||||
).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<string, { status: number; body: unknown }>();
|
||||||
|
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");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+20
-6
@@ -360,11 +360,16 @@ class MemoryClient:
|
|||||||
return response.json()
|
return response.json()
|
||||||
|
|
||||||
@api_error_handler
|
@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.
|
"""Delete a specific memory by ID.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
memory_id: The ID of the memory to delete.
|
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:
|
Returns:
|
||||||
A dictionary containing the API response.
|
A dictionary containing the API response.
|
||||||
@@ -377,10 +382,12 @@ class MemoryClient:
|
|||||||
NetworkError: If network connectivity issues occur.
|
NetworkError: If network connectivity issues occur.
|
||||||
MemoryNotFoundError: If the memory doesn't exist (for updates/deletes).
|
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 = self.client.delete(f"/v1/memories/{memory_id}/", params=params)
|
||||||
response.raise_for_status()
|
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()
|
return response.json()
|
||||||
|
|
||||||
@api_error_handler
|
@api_error_handler
|
||||||
@@ -1268,11 +1275,16 @@ class AsyncMemoryClient:
|
|||||||
return response.json()
|
return response.json()
|
||||||
|
|
||||||
@api_error_handler
|
@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.
|
"""Delete a specific memory by ID.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
memory_id: The ID of the memory to delete.
|
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:
|
Returns:
|
||||||
A dictionary containing the API response.
|
A dictionary containing the API response.
|
||||||
@@ -1285,10 +1297,12 @@ class AsyncMemoryClient:
|
|||||||
NetworkError: If network connectivity issues occur.
|
NetworkError: If network connectivity issues occur.
|
||||||
MemoryNotFoundError: If the memory doesn't exist (for updates/deletes).
|
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 = await self.async_client.delete(f"/v1/memories/{memory_id}/", params=params)
|
||||||
response.raise_for_status()
|
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()
|
return response.json()
|
||||||
|
|
||||||
@api_error_handler
|
@api_error_handler
|
||||||
|
|||||||
@@ -147,3 +147,41 @@ class TestFilterOperatorPassthrough:
|
|||||||
call_args = mock_memory_client.client.post.call_args
|
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 {})
|
payload = call_args.kwargs.get("json", call_args.args[1] if len(call_args.args) > 1 else {})
|
||||||
assert payload["filters"] == complex_filter
|
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", {})
|
||||||
|
|||||||
Reference in New Issue
Block a user