diff --git a/docs/api-reference/memory/add-memories.mdx b/docs/api-reference/memory/add-memories.mdx
index 672bb8747..ab17cb376 100644
--- a/docs/api-reference/memory/add-memories.mdx
+++ b/docs/api-reference/memory/add-memories.mdx
@@ -50,6 +50,7 @@ Provide conversation messages for Mem0 to extract memories from. At least one en
| `app_id` | string | No* | Associates the memory with an app. |
| `metadata` | object | Optional | Custom key/value metadata (e.g., `{"topic": "preferences"}`). |
| `infer` | boolean (default `true`) | Optional | Set to `false` to skip inference and store the provided text as-is. |
+| `expiration_date` | string | Optional | Date in `YYYY-MM-DD` format. The memory is visible through this date and hidden by default after it passes. |
> \* At least one entity ID (`user_id`, `agent_id`, `app_id`, or `run_id`) is required.
@@ -83,3 +84,11 @@ The request is queued for background processing. The response contains an `event
Poll the event status via `GET /v1/event/{event_id}/`. Status will be `SUCCEEDED` or `FAILED` once processing completes.
+
+
+Memories with `expiration_date` remain stored after they expire. Search and get-all hide them by default; pass `show_expired: true` to include them.
+
+
+
+Python uses `expiration_date`; TypeScript uses `expirationDate`.
+
diff --git a/docs/api-reference/memory/get-memories.mdx b/docs/api-reference/memory/get-memories.mdx
index d89d82fc3..ed81d352b 100644
--- a/docs/api-reference/memory/get-memories.mdx
+++ b/docs/api-reference/memory/get-memories.mdx
@@ -6,6 +6,10 @@ openapi: post /v3/memories/
List memories scoped by filters with paginated results. Entity IDs (`user_id`, `agent_id`, `app_id`, `run_id`) **must** be passed inside the `filters` object — top-level entity IDs are rejected with 400.
+Expired memories are hidden by default. Pass `show_expired: true` to include memories whose `expiration_date` has passed.
+
+Python uses `show_expired`; TypeScript uses `showExpired`.
+
The `filters` object supports complex logical operations (AND, OR, NOT) and comparison operators:
- `in`: Matches any of the values specified
@@ -32,6 +36,7 @@ memories = client.get_all(
}
]
},
+ show_expired=False,
page=1,
page_size=50
)
@@ -46,12 +51,14 @@ memories = client.get_all(
{
"id": "f4cbdb08-7062-4f3e-8eb2-9f5c80dfe64c",
"memory": "Alex is planning a trip to San Francisco from July 1st to July 10th",
+ "expiration_date": null,
"created_at": "2024-07-01T12:00:00Z",
"updated_at": "2024-07-01T12:00:00Z"
},
{
"id": "a2b8c3d4-5e6f-7g8h-9i0j-1k2l3m4n5o6p",
"memory": "Alex prefers vegetarian restaurants",
+ "expiration_date": null,
"created_at": "2024-07-05T15:30:00Z",
"updated_at": "2024-07-05T15:30:00Z"
}
diff --git a/docs/api-reference/memory/search-memories.mdx b/docs/api-reference/memory/search-memories.mdx
index c91c1a0f4..9ec4dd76c 100644
--- a/docs/api-reference/memory/search-memories.mdx
+++ b/docs/api-reference/memory/search-memories.mdx
@@ -8,6 +8,10 @@ Relevance-ranked hybrid search across stored memories. V3 uses multi-signal retr
Entity IDs (`user_id`, `agent_id`, `app_id`, `run_id`) **must** be passed inside the `filters` object — top-level entity IDs are rejected with 400. At least one entity ID is required.
+Expired memories are hidden by default. Pass `show_expired: true` to include memories whose `expiration_date` has passed.
+
+Python uses `show_expired`; TypeScript uses `showExpired`.
+
The `filters` object supports complex logical operations (AND, OR, NOT) and comparison operators:
- `in`: Matches any of the values specified
- `gte`: Greater than or equal to
@@ -30,6 +34,7 @@ The `filters` object supports complex logical operations (AND, OR, NOT) and comp
```python Platform API Example
related_memories = client.search(
query="What are Alice's hobbies?",
+ show_expired=False,
filters={
"OR": [
{
@@ -54,6 +59,7 @@ related_memories = client.search(
"category": "hobbies"
},
"score": 0.82,
+ "expiration_date": null,
"created_at": "2024-07-26T10:29:36.630547-07:00",
"updated_at": null,
"categories": ["hobbies"]
diff --git a/docs/api-reference/memory/update-memory.mdx b/docs/api-reference/memory/update-memory.mdx
index 23f3138fd..af4081a93 100644
--- a/docs/api-reference/memory/update-memory.mdx
+++ b/docs/api-reference/memory/update-memory.mdx
@@ -1,5 +1,14 @@
---
title: 'Update Memory'
-description: "Update the content or metadata of a single memory by its unique ID using the PUT endpoint."
+description: "Update the content, metadata, timestamp, or expiration date of a single memory by its unique ID using the PUT endpoint."
openapi: put /v1/memories/{memory_id}/
----
\ No newline at end of file
+---
+
+Use this endpoint to update mutable memory fields. To make a memory expire, set `expiration_date` to a `YYYY-MM-DD` date. To make it permanent again, send `expiration_date: null`.
+
+```python
+client.update("mem_123", expiration_date="2030-01-31")
+client.update("mem_123", expiration_date=None)
+```
+
+TypeScript uses `expirationDate`.
diff --git a/docs/migration/oss-v2-to-v3.mdx b/docs/migration/oss-v2-to-v3.mdx
index 9d40fd937..6086c466d 100644
--- a/docs/migration/oss-v2-to-v3.mdx
+++ b/docs/migration/oss-v2-to-v3.mdx
@@ -62,7 +62,7 @@ These changes produce a **+20 point improvement on LoCoMo** (71.4 → 91.6) and
|---|---|---|---|
| Constructor | `MemoryClient(api_key, org_id, project_id)` | `MemoryClient(api_key)` | Remove `org_id`, `project_id` from constructor |
| Method options | `client.add(messages, **kwargs)` | `client.add(messages, options=AddMemoryOptions(...))` | Use typed option classes (or `**kwargs` still works) |
-| Removed params | `api_version`, `output_format`, `async_mode`, `filter_memories`, `expiration_date`, `keyword_search`, `force_add_only`, `batch_size`, `immutable`, `includes`, `excludes`, `enable_graph`, `org_name`, `project_name` | — | Remove from all calls |
+| Removed params | `api_version`, `output_format`, `async_mode`, `filter_memories`, `keyword_search`, `force_add_only`, `batch_size`, `immutable`, `includes`, `excludes`, `enable_graph`, `org_name`, `project_name` | — | Remove from all calls |
### TypeScript Client SDK
@@ -70,7 +70,7 @@ These changes produce a **+20 point improvement on LoCoMo** (71.4 → 91.6) and
|---|---|---|---|
| Constructor | `new MemoryClient({ apiKey, organizationId, projectId })` | `new MemoryClient({ apiKey })` | Remove `organizationId`, `projectId`, `organizationName`, `projectName` |
| All params | snake_case: `user_id`, `agent_id`, `top_k` | camelCase: `userId`, `agentId`, `topK` | Rename all params to camelCase |
-| Removed params | `api_version`, `output_format`, `async_mode`, `enable_graph`, `org_id`, `project_id`, `org_name`, `project_name`, `filter_memories`, `batch_size`, `force_add_only`, `immutable`, `expiration_date`, `includes`, `excludes`, `keyword_search` | — | Remove from all calls |
+| Removed params | `api_version`, `output_format`, `async_mode`, `enable_graph`, `org_id`, `project_id`, `org_name`, `project_name`, `filter_memories`, `batch_size`, `force_add_only`, `immutable`, `includes`, `excludes`, `keyword_search` | — | Remove from all calls |
| Output format enum | `OutputFormat.V1`, `OutputFormat.V1_1` | Removed | v1.1 is now always used |
| API version enum | `API_VERSION.V1`, `API_VERSION.V2` | Removed | Handled internally |
@@ -423,7 +423,7 @@ These parameters have been removed across all SDKs. Remove them from your code:
**All methods:** `api_version`, `output_format`, `async_mode`, `org_name`, `project_name`, `org_id`, `project_id`
-**add():** `enable_graph`, `immutable`, `expiration_date`, `filter_memories`, `batch_size`, `force_add_only`, `includes`, `excludes`, `keyword_search`
+**add():** `enable_graph`, `immutable`, `filter_memories`, `batch_size`, `force_add_only`, `includes`, `excludes`, `keyword_search`
**search():** `enable_graph`
@@ -437,7 +437,7 @@ These parameters have been removed across all SDKs. Remove them from your code:
**All methods:** `OutputFormat` enum, `API_VERSION` enum
-**add():** `enable_graph` / `enableGraph`, `async_mode` / `asyncMode`, `output_format` / `outputFormat`, `immutable`, `expiration_date` / `expirationDate`, `filter_memories` / `filterMemories`, `batch_size` / `batchSize`, `force_add_only` / `forceAddOnly`, `includes`, `excludes`, `keyword_search` / `keywordSearch`
+**add():** `enable_graph` / `enableGraph`, `async_mode` / `asyncMode`, `output_format` / `outputFormat`, `immutable`, `filter_memories` / `filterMemories`, `batch_size` / `batchSize`, `force_add_only` / `forceAddOnly`, `includes`, `excludes`, `keyword_search` / `keywordSearch`
**search():** `enable_graph` / `enableGraph`
diff --git a/docs/migration/platform-v2-to-v3.mdx b/docs/migration/platform-v2-to-v3.mdx
index 36261bc2f..d58ed5af1 100644
--- a/docs/migration/platform-v2-to-v3.mdx
+++ b/docs/migration/platform-v2-to-v3.mdx
@@ -215,7 +215,7 @@ client.add(messages, user_id="alice")
# async_mode and output_format removed (async by default, v1.1 always)
```
-**Removed parameters:** `org_id`, `project_id`, `api_version`, `output_format`, `async_mode`, `enable_graph`, `immutable`, `expiration_date`, `filter_memories`, `batch_size`, `force_add_only`, `includes`, `excludes`, `keyword_search`, `org_name`, `project_name`
+**Removed parameters:** `org_id`, `project_id`, `api_version`, `output_format`, `async_mode`, `enable_graph`, `immutable`, `filter_memories`, `batch_size`, `force_add_only`, `includes`, `excludes`, `keyword_search`, `org_name`, `project_name`
### TypeScript Client SDK
@@ -242,7 +242,7 @@ await client.search("query", {
});
```
-**Removed:** `OutputFormat` enum, `API_VERSION` enum, `organizationId`, `projectId`, `organizationName`, `projectName`, `enableGraph`, `asyncMode`, `outputFormat`, `immutable`, `expirationDate`, `filterMemories`, `batchSize`, `forceAddOnly`, `includes`, `excludes`, `keywordSearch`
+**Removed:** `OutputFormat` enum, `API_VERSION` enum, `organizationId`, `projectId`, `organizationName`, `projectName`, `enableGraph`, `asyncMode`, `outputFormat`, `immutable`, `filterMemories`, `batchSize`, `forceAddOnly`, `includes`, `excludes`, `keywordSearch`
For the full list of parameter changes across all SDKs, see the [OSS migration guide](/migration/oss-v2-to-v3#removed-parameters-reference).
diff --git a/docs/openapi.json b/docs/openapi.json
index 8609e2d58..3904ce363 100644
--- a/docs/openapi.json
+++ b/docs/openapi.json
@@ -1779,6 +1779,11 @@
"type": "object",
"description": "Entity and metadata filters. Must include at least one entity ID (`user_id`, `agent_id`, `app_id`, or `run_id`).",
"additionalProperties": true
+ },
+ "show_expired": {
+ "type": "boolean",
+ "default": false,
+ "description": "When true, include memories whose `expiration_date` has passed. Expired memories are hidden by default."
}
}
},
@@ -1977,6 +1982,12 @@
"additionalProperties": true,
"description": "User-supplied metadata to attach to each extracted memory."
},
+ "expiration_date": {
+ "type": "string",
+ "format": "date",
+ "nullable": true,
+ "description": "Optional expiration date in YYYY-MM-DD format. After this date, memories are hidden from search and get-all unless `show_expired` is true."
+ },
"custom_instructions": {
"type": "string",
"description": "Project-level instructions that guide extraction for this call."
@@ -2094,6 +2105,11 @@
"description": "Entity and metadata filters. Must include at least one entity ID (`user_id`, `agent_id`, `app_id`, or `run_id`). Supports `AND`, `OR`, `NOT`, and comparison operators (`in`, `gte`, `lte`, `gt`, `lt`, `contains`, `icontains`, `ne`).",
"additionalProperties": true
},
+ "show_expired": {
+ "type": "boolean",
+ "default": false,
+ "description": "When true, include memories whose `expiration_date` has passed. Expired memories are hidden by default."
+ },
"top_k": {
"type": "integer",
"minimum": 1,
@@ -2432,6 +2448,12 @@
"metadata": {
"type": "object",
"description": "Additional metadata associated with the memory"
+ },
+ "expiration_date": {
+ "type": "string",
+ "format": "date",
+ "nullable": true,
+ "description": "Expiration date in YYYY-MM-DD format, or null to clear the expiration date."
}
}
}
@@ -6256,4 +6278,4 @@
}
},
"x-original-swagger-version": "2.0"
-}
\ No newline at end of file
+}
diff --git a/mem0-ts/src/client/mem0.ts b/mem0-ts/src/client/mem0.ts
index 713650d34..6d5f4c7e1 100644
--- a/mem0-ts/src/client/mem0.ts
+++ b/mem0-ts/src/client/mem0.ts
@@ -281,19 +281,22 @@ export default class MemoryClient {
text,
metadata,
timestamp,
+ expirationDate,
}: {
text?: string;
metadata?: Record;
timestamp?: number | string;
+ expirationDate?: string | null;
},
): Promise> {
if (
text === undefined &&
metadata === undefined &&
- timestamp === undefined
+ timestamp === undefined &&
+ expirationDate === undefined
) {
throw new Error(
- "At least one of text, metadata, or timestamp must be provided for update.",
+ "At least one of text, metadata, timestamp, or expirationDate must be provided for update.",
);
}
@@ -302,6 +305,7 @@ export default class MemoryClient {
if (text !== undefined) payload.text = text;
if (metadata !== undefined) payload.metadata = metadata;
if (timestamp !== undefined) payload.timestamp = timestamp;
+ if (expirationDate !== undefined) payload.expiration_date = expirationDate;
const payloadKeys = Object.keys(payload);
this._captureEvent("update", [payloadKeys]);
diff --git a/mem0-ts/src/client/mem0.types.ts b/mem0-ts/src/client/mem0.types.ts
index ba778415d..8cf862205 100644
--- a/mem0-ts/src/client/mem0.types.ts
+++ b/mem0-ts/src/client/mem0.types.ts
@@ -13,6 +13,7 @@ export interface AddMemoryOptions extends EntityOptions {
customCategories?: custom_categories[];
customInstructions?: string;
timestamp?: number;
+ expirationDate?: string;
structuredDataSchema?: Record;
}
@@ -25,6 +26,7 @@ export interface SearchMemoryOptions {
latestOnly?: boolean;
fields?: string[];
categories?: string[];
+ showExpired?: boolean;
}
export interface GetAllMemoryOptions {
@@ -35,6 +37,7 @@ export interface GetAllMemoryOptions {
endDate?: string;
latestOnly?: boolean;
categories?: string[];
+ showExpired?: boolean;
}
export interface DeleteAllMemoryOptions extends EntityOptions {}
@@ -119,6 +122,7 @@ export interface Memory {
memoryType?: string;
score?: number;
metadata?: any | null;
+ expirationDate?: string | null;
owner?: string | null;
agentId?: string | null;
appId?: string | null;
diff --git a/mem0-ts/src/client/tests/memoryClient.crud.test.ts b/mem0-ts/src/client/tests/memoryClient.crud.test.ts
index 0da9be2a9..db2b20f65 100644
--- a/mem0-ts/src/client/tests/memoryClient.crud.test.ts
+++ b/mem0-ts/src/client/tests/memoryClient.crud.test.ts
@@ -59,6 +59,21 @@ describe("MemoryClient - add()", () => {
expect(getFetchBody(call!).user_id).toBe("user_1");
});
+ test("serializes expirationDate as expiration_date", async () => {
+ const extra = new Map();
+ extra.set("/v3/memories/add/", { status: 200, body: [createMockMemory()] });
+ const mock = setupMockFetch(extra);
+
+ const client = new MemoryClient({ apiKey: TEST_API_KEY });
+ await client.add([{ role: "user", content: "test" }], {
+ userId: "u1",
+ expirationDate: "2030-01-31",
+ });
+
+ const call = findFetchCall(mock, "/v3/memories/add/", "POST");
+ expect(getFetchBody(call!).expiration_date).toBe("2030-01-31");
+ });
+
test("throws an error when given an empty messages array", async () => {
setupMockFetch();
@@ -176,11 +191,26 @@ describe("MemoryClient - update()", () => {
expect(body.timestamp).toBe(1710600000);
});
+ test("sends expirationDate as expiration_date, including null", async () => {
+ const extra = new Map();
+ extra.set("/v1/memories/mem_123/", {
+ status: 200,
+ body: createMockMemory(),
+ });
+ const mock = setupMockFetch(extra);
+
+ const client = new MemoryClient({ apiKey: TEST_API_KEY });
+ await client.update("mem_123", { expirationDate: null });
+
+ const call = findFetchCall(mock, "/v1/memories/mem_123/", "PUT");
+ expect(getFetchBody(call!).expiration_date).toBeNull();
+ });
+
test("throws when no fields provided", async () => {
setupMockFetch();
const client = new MemoryClient({ apiKey: TEST_API_KEY });
await expect(client.update("mem_123", {})).rejects.toThrow(
- "At least one of text, metadata, or timestamp must be provided",
+ "At least one of text, metadata, timestamp, or expirationDate must be provided",
);
});
});
diff --git a/mem0-ts/src/client/tests/memoryClient.search.test.ts b/mem0-ts/src/client/tests/memoryClient.search.test.ts
index f2f1ef32e..9b36babe3 100644
--- a/mem0-ts/src/client/tests/memoryClient.search.test.ts
+++ b/mem0-ts/src/client/tests/memoryClient.search.test.ts
@@ -81,6 +81,24 @@ describe("MemoryClient - search()", () => {
expect(getFetchBody(call!).latest_only).toBe(true);
});
+ test("serializes showExpired as show_expired", async () => {
+ const extra = new Map();
+ extra.set("/v3/memories/search/", {
+ status: 200,
+ body: { results: [] },
+ });
+ const mock = setupMockFetch(extra);
+
+ const client = new MemoryClient({ apiKey: TEST_API_KEY });
+ await client.search("test", {
+ filters: { user_id: "u1" },
+ showExpired: true,
+ });
+
+ const call = findFetchCall(mock, "/v3/memories/search/", "POST");
+ expect(getFetchBody(call!).show_expired).toBe(true);
+ });
+
test("passes complex OR filters through to the API body", async () => {
const extra = new Map();
extra.set("/v3/memories/search/", {
@@ -300,4 +318,22 @@ describe("MemoryClient - getAll() entity param rejection", () => {
const call = findFetchCall(mock, "/v3/memories/", "POST");
expect(getFetchBody(call!).latest_only).toBe(true);
});
+
+ test("serializes showExpired as show_expired", async () => {
+ const extra = new Map();
+ extra.set("/v3/memories/", {
+ status: 200,
+ body: { results: [] },
+ });
+ const mock = setupMockFetch(extra);
+
+ const client = new MemoryClient({ apiKey: TEST_API_KEY });
+ await client.getAll({
+ filters: { user_id: "u1" },
+ showExpired: true,
+ });
+
+ const call = findFetchCall(mock, "/v3/memories/", "POST");
+ expect(getFetchBody(call!).show_expired).toBe(true);
+ });
});
diff --git a/mem0/client/main.py b/mem0/client/main.py
index 6f07508d3..c26aa7231 100644
--- a/mem0/client/main.py
+++ b/mem0/client/main.py
@@ -344,24 +344,26 @@ class MemoryClient:
Args:
memory_id: The ID of the memory to update.
options: Typed options (UpdateMemoryOptions) with text, metadata,
- and/or timestamp fields.
- **kwargs: Alternatively pass text, metadata, timestamp as keyword args.
+ timestamp, and/or expiration_date fields.
+ **kwargs: Alternatively pass text, metadata, timestamp, or
+ expiration_date as keyword args.
Returns:
Dict[str, Any]: The response from the server.
Raises:
- ValueError: If none of text, metadata, or timestamp are provided.
+ ValueError: If none of text, metadata, timestamp, or expiration_date are provided.
Example:
>>> client.update("mem_123", UpdateMemoryOptions(text="Updated text"))
>>> client.update("mem_123", text="Updated text")
+ >>> client.update("mem_123", expiration_date=None)
"""
payload = {**(options.model_dump(exclude_unset=True) if options else {}), **kwargs}
- payload = {k: v for k, v in payload.items() if v is not None}
+ payload = {k: v for k, v in payload.items() if v is not None or k == "expiration_date"}
if not payload:
- raise ValueError("At least one of text, metadata, or timestamp must be provided for update.")
+ raise ValueError("At least one of text, metadata, timestamp, or expiration_date must be provided for update.")
capture_client_event("client.update", self, {"memory_id": memory_id, "sync_type": "sync"})
params = self._prepare_params()
@@ -1260,24 +1262,26 @@ class AsyncMemoryClient:
Args:
memory_id: The ID of the memory to update.
options: Typed options (UpdateMemoryOptions) with text, metadata,
- and/or timestamp fields.
- **kwargs: Alternatively pass text, metadata, timestamp as keyword args.
+ timestamp, and/or expiration_date fields.
+ **kwargs: Alternatively pass text, metadata, timestamp, or
+ expiration_date as keyword args.
Returns:
Dict[str, Any]: The response from the server.
Raises:
- ValueError: If none of text, metadata, or timestamp are provided.
+ ValueError: If none of text, metadata, timestamp, or expiration_date are provided.
Example:
>>> await client.update("mem_123", UpdateMemoryOptions(text="Updated text"))
>>> await client.update("mem_123", text="Updated text")
+ >>> await client.update("mem_123", expiration_date=None)
"""
payload = {**(options.model_dump(exclude_unset=True) if options else {}), **kwargs}
- payload = {k: v for k, v in payload.items() if v is not None}
+ payload = {k: v for k, v in payload.items() if v is not None or k == "expiration_date"}
if not payload:
- raise ValueError("At least one of text, metadata, or timestamp must be provided for update.")
+ raise ValueError("At least one of text, metadata, timestamp, or expiration_date must be provided for update.")
capture_client_event("client.update", self, {"memory_id": memory_id, "sync_type": "async"})
params = self._prepare_params()
diff --git a/mem0/client/types.py b/mem0/client/types.py
index 04d72c01d..492904a8d 100644
--- a/mem0/client/types.py
+++ b/mem0/client/types.py
@@ -29,6 +29,7 @@ class AddMemoryOptions(BaseModel):
)
custom_instructions: Optional[str] = Field(default=None, description="Custom instructions for fact extraction")
timestamp: Optional[int] = Field(default=None, description="Unix timestamp for the memory")
+ expiration_date: Optional[str] = Field(default=None, description="Expiration date in YYYY-MM-DD format")
structured_data_schema: Optional[Dict[str, Any]] = Field(
default=None, description="Schema for structured data extraction"
)
@@ -50,6 +51,7 @@ class SearchMemoryOptions(BaseModel):
threshold: Optional[float] = Field(default=None, description="Minimum similarity score threshold")
fields: Optional[List[str]] = Field(default=None, description="Fields to include in the response")
categories: Optional[List[str]] = Field(default=None, description="Categories to filter by")
+ show_expired: Optional[bool] = Field(default=None, description="Whether to include expired memories")
class GetAllMemoryOptions(BaseModel):
@@ -71,6 +73,7 @@ class GetAllMemoryOptions(BaseModel):
default=None, description="Filter memories created on or before this date (ISO 8601)"
)
categories: Optional[List[str]] = Field(default=None, description="Categories to filter by")
+ show_expired: Optional[bool] = Field(default=None, description="Whether to include expired memories")
class DeleteAllMemoryOptions(BaseModel):
@@ -91,6 +94,7 @@ class UpdateMemoryOptions(BaseModel):
text: Optional[str] = Field(default=None, description="New text content for the memory")
metadata: Optional[Dict[str, Any]] = Field(default=None, description="Updated metadata")
timestamp: Optional[Union[int, float, str]] = Field(default=None, description="Updated timestamp")
+ expiration_date: Optional[str] = Field(default=None, description="Expiration date in YYYY-MM-DD format, or None to clear")
class ProjectUpdateOptions(BaseModel):
diff --git a/mem0/memory/main.py b/mem0/memory/main.py
index 5adc2c5e1..1d81c0a28 100644
--- a/mem0/memory/main.py
+++ b/mem0/memory/main.py
@@ -9,7 +9,7 @@ import time
import uuid
import warnings
from copy import deepcopy
-from datetime import datetime, timezone
+from datetime import date, datetime, timezone
from typing import Any, Dict, Optional
from pydantic import ValidationError
@@ -378,9 +378,37 @@ def _entity_collection_name(provider: str, collection_name: str) -> str:
return f"{collection_name}{separator}entities"
+def _normalize_expiration_date(value: Any) -> Optional[str]:
+ if value is None:
+ return None
+ if isinstance(value, datetime):
+ return value.date().isoformat()
+ if isinstance(value, date):
+ return value.isoformat()
+ if isinstance(value, str):
+ try:
+ return date.fromisoformat(value).isoformat()
+ except ValueError as exc:
+ raise ValueError("expiration_date must be a valid date in YYYY-MM-DD format.") from exc
+ raise ValueError("expiration_date must be a date string in YYYY-MM-DD format.")
+
+
+def _payload_is_expired(payload: Optional[Dict[str, Any]]) -> bool:
+ if not payload:
+ return False
+ expiration_date = payload.get("expiration_date")
+ if not expiration_date:
+ return False
+ try:
+ return date.fromisoformat(str(expiration_date)) < datetime.now(timezone.utc).date()
+ except ValueError:
+ return False
+
+
setup_config()
logger = logging.getLogger(__name__)
+_UNSET = object()
_PROJECT_UPDATE_UNSUPPORTED_ERROR = "Project updates are not supported by the OSS Memory SDK."
@@ -694,6 +722,7 @@ class Memory(MemoryBase):
run_id: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
timestamp: Optional[Any] = None,
+ expiration_date: Optional[Any] = None,
infer: bool = True,
memory_type: Optional[str] = None,
prompt: Optional[str] = None,
@@ -712,6 +741,8 @@ class Memory(MemoryBase):
run_id (str, optional): ID of the run creating the memory. Defaults to None.
metadata (dict, optional): Metadata to store with the memory. Defaults to None.
timestamp (Any, optional): Platform-only temporal parameter. Not supported in OSS.
+ expiration_date (Any, optional): Date in YYYY-MM-DD format. Expired memories are hidden
+ from search and get_all unless show_expired is True.
infer (bool, optional): If True (default), an LLM is used to extract key facts from
'messages' and decide whether to add, update, or delete related memories.
If False, 'messages' are added as raw memories directly.
@@ -737,6 +768,7 @@ class Memory(MemoryBase):
if timestamp is not None:
raise ValueError(get_temporal_feature_error_message("sync", "add", "timestamp"))
+ normalized_expiration_date = _normalize_expiration_date(expiration_date)
temporal_usage_notice = detect_temporal_usage_from_metadata(metadata)
processed_metadata, effective_filters = _build_filters_and_metadata(
user_id=user_id,
@@ -744,6 +776,8 @@ class Memory(MemoryBase):
run_id=run_id,
input_metadata=metadata,
)
+ if normalized_expiration_date is not None:
+ processed_metadata["expiration_date"] = normalized_expiration_date
if memory_type is not None and memory_type != MemoryType.PROCEDURAL.value:
raise Mem0ValidationError(
@@ -1141,6 +1175,7 @@ class Memory(MemoryBase):
"actor_id",
"role",
"attributed_to",
+ "expiration_date",
]
core_and_promoted_keys = {"data", "hash", "created_at", "updated_at", "id", "text_lemmatized", "attributed_to", *promoted_payload_keys}
@@ -1169,6 +1204,7 @@ class Memory(MemoryBase):
*,
filters: Optional[Dict[str, Any]] = None,
top_k: int = 20,
+ show_expired: bool = False,
**kwargs,
):
"""
@@ -1179,6 +1215,7 @@ class Memory(MemoryBase):
Must contain at least one of: user_id, agent_id, run_id.
Example: filters={"user_id": "u1", "agent_id": "a1"}
top_k (int, optional): The maximum number of memories to return. Defaults to 20.
+ show_expired (bool, optional): Include expired memories. Defaults to False.
Returns:
dict: A dictionary containing a list of memories under the "results" key.
@@ -1217,6 +1254,7 @@ class Memory(MemoryBase):
)
limit = top_k
+ fetch_limit = limit if show_expired else max(limit * 4, 60)
scale_threshold_notice = detect_scale_threshold_from_top_k(top_k)
keys, encoded_ids = process_telemetry_filters(effective_filters)
@@ -1224,7 +1262,7 @@ class Memory(MemoryBase):
"mem0.get_all", self, {"limit": limit, "keys": keys, "encoded_ids": encoded_ids, "sync_type": "sync"}
)
- all_memories_result = self._get_all_from_vector_store(effective_filters, limit)
+ all_memories_result = self._get_all_from_vector_store(effective_filters, fetch_limit, show_expired, limit)
if scale_threshold_notice:
display_scale_threshold_notice(self, "sync", "get_all", *scale_threshold_notice)
@@ -1232,7 +1270,7 @@ class Memory(MemoryBase):
display_first_run_notice(self, "sync", "get_all")
return {"results": all_memories_result}
- def _get_all_from_vector_store(self, filters, limit):
+ def _get_all_from_vector_store(self, filters, limit, show_expired=False, output_limit=None):
memories_result = self.vector_store.list(filters=filters, top_k=limit)
# Handle different vector store return formats by inspecting first element
@@ -1255,11 +1293,14 @@ class Memory(MemoryBase):
"actor_id",
"role",
"attributed_to",
+ "expiration_date",
]
core_and_promoted_keys = {"data", "hash", "created_at", "updated_at", "id", "text_lemmatized", "attributed_to", *promoted_payload_keys}
formatted_memories = []
for mem in actual_memories:
+ if not show_expired and _payload_is_expired(mem.payload):
+ continue
memory_item_dict = MemoryItem(
id=mem.id,
memory=mem.payload.get("data", ""),
@@ -1277,6 +1318,8 @@ class Memory(MemoryBase):
memory_item_dict["metadata"] = additional_metadata
formatted_memories.append(memory_item_dict)
+ if output_limit is not None and len(formatted_memories) >= output_limit:
+ break
return formatted_memories
@@ -1290,6 +1333,7 @@ class Memory(MemoryBase):
rerank: bool = False,
explain: bool = False,
reference_date: Optional[Any] = None,
+ show_expired: bool = False,
**kwargs,
):
"""
@@ -1322,6 +1366,7 @@ class Memory(MemoryBase):
rerank (bool, optional): Whether to rerank results. Defaults to False.
explain (bool, optional): Whether to include score_details for each result. Defaults to False.
reference_date (Any, optional): Platform-only temporal parameter. Not supported in OSS.
+ show_expired (bool, optional): Include expired memories. Defaults to False.
Returns:
dict: A dictionary containing the search results under a "results" key.
@@ -1393,7 +1438,9 @@ class Memory(MemoryBase):
)
search_start = time.perf_counter()
- original_memories = self._search_vector_store(query, effective_filters, limit, threshold, explain=explain)
+ original_memories = self._search_vector_store(
+ query, effective_filters, limit, threshold, explain=explain, show_expired=show_expired
+ )
search_elapsed_seconds = time.perf_counter() - search_start
# Apply reranking if enabled and reranker is available
@@ -1525,7 +1572,7 @@ class Memory(MemoryBase):
return True
return False
- def _search_vector_store(self, query, filters, limit, threshold=0.1, explain=False):
+ def _search_vector_store(self, query, filters, limit, threshold=0.1, explain=False, show_expired=False):
# Guard against None threshold (backward compat)
if threshold is None:
threshold = 0.1
@@ -1566,11 +1613,14 @@ class Memory(MemoryBase):
# Step 7: Build candidate set from semantic results
candidates = []
for mem in semantic_results:
+ payload = mem.payload if hasattr(mem, 'payload') else {}
+ if not show_expired and _payload_is_expired(payload):
+ continue
mem_id = str(mem.id)
candidates.append({
"id": mem_id,
"score": mem.score,
- "payload": mem.payload if hasattr(mem, 'payload') else {},
+ "payload": payload,
})
# Step 8: Score and rank
@@ -1591,6 +1641,7 @@ class Memory(MemoryBase):
"actor_id",
"role",
"attributed_to",
+ "expiration_date",
]
core_and_promoted_keys = {"data", "hash", "created_at", "updated_at", "id", "text_lemmatized", "attributed_to", *promoted_payload_keys}
@@ -1708,14 +1759,21 @@ class Memory(MemoryBase):
return memory_boosts
- def update(self, memory_id, data, metadata: Optional[Dict[str, Any]] = None):
+ def update(
+ self,
+ memory_id,
+ data: Optional[str] = None,
+ metadata: Optional[Dict[str, Any]] = None,
+ expiration_date: Any = _UNSET,
+ ):
"""
Update a memory by ID.
Args:
memory_id (str): ID of the memory to update.
- data (str): New content to update the memory with.
+ data (str, optional): New content to update the memory with.
metadata (dict, optional): Metadata to update with the memory. Defaults to None.
+ expiration_date (Any, optional): Date in YYYY-MM-DD format, or None to clear it.
Returns:
dict: Success message indicating the memory was updated.
@@ -1726,9 +1784,19 @@ class Memory(MemoryBase):
"""
capture_event("mem0.update", self, {"memory_id": memory_id, "sync_type": "sync"})
- existing_embeddings = {data: self.embedding_model.embed(data, "update")}
+ if data is None and metadata is None and expiration_date is _UNSET:
+ raise ValueError("At least one of data, metadata, or expiration_date must be provided.")
- self._update_memory(memory_id, data, existing_embeddings, metadata)
+ update_metadata = deepcopy(metadata) if metadata is not None else None
+ if expiration_date is not _UNSET:
+ update_metadata = update_metadata or {}
+ update_metadata["expiration_date"] = _normalize_expiration_date(expiration_date)
+
+ existing_embeddings = {}
+ if data is not None:
+ existing_embeddings[data] = self.embedding_model.embed(data, "update")
+
+ self._update_memory(memory_id, data, existing_embeddings, update_metadata)
display_first_run_notice(self, "sync", "update")
return {"message": "Memory updated successfully!"}
@@ -1895,6 +1963,11 @@ class Memory(MemoryBase):
raise ValueError(f"Memory with id {memory_id} not found. Please provide a valid 'memory_id'")
prev_value = existing_memory.payload.get("data")
+ if data is None:
+ data = prev_value
+ if not isinstance(data, str):
+ raise ValueError(f"Memory with id {memory_id} does not have text content to update")
+ text_changed = data != prev_value
new_metadata = deepcopy(existing_memory.payload)
if metadata is not None:
@@ -1936,8 +2009,9 @@ class Memory(MemoryBase):
# Entity-store cleanup: strip this memory's id from old-text entities,
# then re-extract entities from the new text and link them back.
session_filters = {k: new_metadata[k] for k in ("user_id", "agent_id", "run_id") if new_metadata.get(k)}
- self._remove_memory_from_entity_store(memory_id, session_filters)
- self._link_entities_for_memory(memory_id, data, session_filters)
+ if text_changed:
+ self._remove_memory_from_entity_store(memory_id, session_filters)
+ self._link_entities_for_memory(memory_id, data, session_filters)
return memory_id
@@ -2284,6 +2358,7 @@ class AsyncMemory(MemoryBase):
run_id: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
timestamp: Optional[Any] = None,
+ expiration_date: Optional[Any] = None,
infer: bool = True,
memory_type: Optional[str] = None,
prompt: Optional[str] = None,
@@ -2299,6 +2374,8 @@ class AsyncMemory(MemoryBase):
run_id (str, optional): ID of the run creating the memory. Defaults to None.
metadata (dict, optional): Metadata to store with the memory. Defaults to None.
timestamp (Any, optional): Platform-only temporal parameter. Not supported in OSS.
+ expiration_date (Any, optional): Date in YYYY-MM-DD format. Expired memories are hidden
+ from search and get_all unless show_expired is True.
infer (bool, optional): Whether to infer the memories. Defaults to True.
memory_type (str, optional): Type of memory to create. Defaults to None.
Pass "procedural_memory" to create procedural memories.
@@ -2310,10 +2387,13 @@ class AsyncMemory(MemoryBase):
if timestamp is not None:
raise ValueError(await get_temporal_feature_error_message_async("async", "add", "timestamp"))
+ normalized_expiration_date = _normalize_expiration_date(expiration_date)
temporal_usage_notice = detect_temporal_usage_from_metadata(metadata)
processed_metadata, effective_filters = _build_filters_and_metadata(
user_id=user_id, agent_id=agent_id, run_id=run_id, input_metadata=metadata
)
+ if normalized_expiration_date is not None:
+ processed_metadata["expiration_date"] = normalized_expiration_date
if memory_type is not None and memory_type != MemoryType.PROCEDURAL.value:
raise ValueError(
@@ -2716,6 +2796,7 @@ class AsyncMemory(MemoryBase):
"actor_id",
"role",
"attributed_to",
+ "expiration_date",
]
core_and_promoted_keys = {"data", "hash", "created_at", "updated_at", "id", "text_lemmatized", "attributed_to", *promoted_payload_keys}
@@ -2744,6 +2825,7 @@ class AsyncMemory(MemoryBase):
*,
filters: Optional[Dict[str, Any]] = None,
top_k: int = 20,
+ show_expired: bool = False,
**kwargs,
):
"""
@@ -2754,6 +2836,7 @@ class AsyncMemory(MemoryBase):
Must contain at least one of: user_id, agent_id, run_id.
Example: filters={"user_id": "u1", "agent_id": "a1"}
top_k (int, optional): The maximum number of memories to return. Defaults to 20.
+ show_expired (bool, optional): Include expired memories. Defaults to False.
Returns:
dict: A dictionary containing a list of memories under the "results" key.
@@ -2792,6 +2875,7 @@ class AsyncMemory(MemoryBase):
)
limit = top_k
+ fetch_limit = limit if show_expired else max(limit * 4, 60)
scale_threshold_notice = detect_scale_threshold_from_top_k(top_k)
keys, encoded_ids = process_telemetry_filters(effective_filters)
@@ -2799,7 +2883,7 @@ class AsyncMemory(MemoryBase):
"mem0.get_all", self, {"limit": limit, "keys": keys, "encoded_ids": encoded_ids, "sync_type": "async"}
)
- all_memories_result = await self._get_all_from_vector_store(effective_filters, limit)
+ all_memories_result = await self._get_all_from_vector_store(effective_filters, fetch_limit, show_expired, limit)
if scale_threshold_notice:
await display_scale_threshold_notice_async(self, "async", "get_all", *scale_threshold_notice)
@@ -2807,7 +2891,7 @@ class AsyncMemory(MemoryBase):
await display_first_run_notice_async(self, "async", "get_all")
return {"results": all_memories_result}
- async def _get_all_from_vector_store(self, filters, limit):
+ async def _get_all_from_vector_store(self, filters, limit, show_expired=False, output_limit=None):
memories_result = await asyncio.to_thread(self.vector_store.list, filters=filters, top_k=limit)
# Handle different vector store return formats by inspecting first element
@@ -2830,11 +2914,14 @@ class AsyncMemory(MemoryBase):
"actor_id",
"role",
"attributed_to",
+ "expiration_date",
]
core_and_promoted_keys = {"data", "hash", "created_at", "updated_at", "id", "text_lemmatized", "attributed_to", *promoted_payload_keys}
formatted_memories = []
for mem in actual_memories:
+ if not show_expired and _payload_is_expired(mem.payload):
+ continue
memory_item_dict = MemoryItem(
id=mem.id,
memory=mem.payload.get("data", ""),
@@ -2852,6 +2939,8 @@ class AsyncMemory(MemoryBase):
memory_item_dict["metadata"] = additional_metadata
formatted_memories.append(memory_item_dict)
+ if output_limit is not None and len(formatted_memories) >= output_limit:
+ break
return formatted_memories
@@ -2865,6 +2954,7 @@ class AsyncMemory(MemoryBase):
rerank: bool = False,
explain: bool = False,
reference_date: Optional[Any] = None,
+ show_expired: bool = False,
**kwargs,
):
"""
@@ -2897,6 +2987,7 @@ class AsyncMemory(MemoryBase):
rerank (bool, optional): Whether to rerank results. Defaults to False.
explain (bool, optional): Whether to include score_details for each result. Defaults to False.
reference_date (Any, optional): Platform-only temporal parameter. Not supported in OSS.
+ show_expired (bool, optional): Include expired memories. Defaults to False.
Returns:
dict: A dictionary containing the search results under a "results" key.
@@ -2972,7 +3063,9 @@ class AsyncMemory(MemoryBase):
)
search_start = time.perf_counter()
- original_memories = await self._search_vector_store(query, effective_filters, limit, threshold, explain=explain)
+ original_memories = await self._search_vector_store(
+ query, effective_filters, limit, threshold, explain=explain, show_expired=show_expired
+ )
search_elapsed_seconds = time.perf_counter() - search_start
# Apply reranking if enabled and reranker is available
@@ -3107,7 +3200,7 @@ class AsyncMemory(MemoryBase):
return True
return False
- async def _search_vector_store(self, query, filters, limit, threshold=0.1, explain=False):
+ async def _search_vector_store(self, query, filters, limit, threshold=0.1, explain=False, show_expired=False):
if threshold is None:
threshold = 0.1
@@ -3147,11 +3240,14 @@ class AsyncMemory(MemoryBase):
# Step 7: Build candidate set from semantic results
candidates = []
for mem in semantic_results:
+ payload = mem.payload if hasattr(mem, 'payload') else {}
+ if not show_expired and _payload_is_expired(payload):
+ continue
mem_id = str(mem.id)
candidates.append({
"id": mem_id,
"score": mem.score,
- "payload": mem.payload if hasattr(mem, 'payload') else {},
+ "payload": payload,
})
# Step 8: Score and rank
@@ -3172,6 +3268,7 @@ class AsyncMemory(MemoryBase):
"actor_id",
"role",
"attributed_to",
+ "expiration_date",
]
core_and_promoted_keys = {"data", "hash", "created_at", "updated_at", "id", "text_lemmatized", "attributed_to", *promoted_payload_keys}
@@ -3280,14 +3377,21 @@ class AsyncMemory(MemoryBase):
return memory_boosts
- async def update(self, memory_id, data, metadata: Optional[Dict[str, Any]] = None):
+ async def update(
+ self,
+ memory_id,
+ data: Optional[str] = None,
+ metadata: Optional[Dict[str, Any]] = None,
+ expiration_date: Any = _UNSET,
+ ):
"""
Update a memory by ID asynchronously.
Args:
memory_id (str): ID of the memory to update.
- data (str): New content to update the memory with.
+ data (str, optional): New content to update the memory with.
metadata (dict, optional): Metadata to update with the memory. Defaults to None.
+ expiration_date (Any, optional): Date in YYYY-MM-DD format, or None to clear it.
Returns:
dict: Success message indicating the memory was updated.
@@ -3298,10 +3402,20 @@ class AsyncMemory(MemoryBase):
"""
capture_event("mem0.update", self, {"memory_id": memory_id, "sync_type": "async"})
- embeddings = await asyncio.to_thread(self.embedding_model.embed, data, "update")
- existing_embeddings = {data: embeddings}
+ if data is None and metadata is None and expiration_date is _UNSET:
+ raise ValueError("At least one of data, metadata, or expiration_date must be provided.")
- await self._update_memory(memory_id, data, existing_embeddings, metadata)
+ update_metadata = deepcopy(metadata) if metadata is not None else None
+ if expiration_date is not _UNSET:
+ update_metadata = update_metadata or {}
+ update_metadata["expiration_date"] = _normalize_expiration_date(expiration_date)
+
+ existing_embeddings = {}
+ if data is not None:
+ embeddings = await asyncio.to_thread(self.embedding_model.embed, data, "update")
+ existing_embeddings[data] = embeddings
+
+ await self._update_memory(memory_id, data, existing_embeddings, update_metadata)
await display_first_run_notice_async(self, "async", "update")
return {"message": "Memory updated successfully!"}
@@ -3499,6 +3613,11 @@ class AsyncMemory(MemoryBase):
raise ValueError(f"Memory with id {memory_id} not found. Please provide a valid 'memory_id'")
prev_value = existing_memory.payload.get("data")
+ if data is None:
+ data = prev_value
+ if not isinstance(data, str):
+ raise ValueError(f"Memory with id {memory_id} does not have text content to update")
+ text_changed = data != prev_value
new_metadata = deepcopy(existing_memory.payload)
if metadata is not None:
@@ -3542,8 +3661,9 @@ class AsyncMemory(MemoryBase):
# Entity-store cleanup: strip this memory's id from old-text entities,
# then re-extract entities from the new text and link them back.
session_filters = {k: new_metadata[k] for k in ("user_id", "agent_id", "run_id") if new_metadata.get(k)}
- await self._remove_memory_from_entity_store(memory_id, session_filters)
- await self._link_entities_for_memory(memory_id, data, session_filters)
+ if text_changed:
+ await self._remove_memory_from_entity_store(memory_id, session_filters)
+ await self._link_entities_for_memory(memory_id, data, session_filters)
return memory_id
diff --git a/server/main.py b/server/main.py
index 54eabea88..5819d1192 100644
--- a/server/main.py
+++ b/server/main.py
@@ -182,14 +182,16 @@ class MemoryCreate(BaseModel):
agent_id: Optional[str] = None
run_id: Optional[str] = None
metadata: Optional[Dict[str, Any]] = None
+ expiration_date: Optional[str] = Field(None, description="Expiration date in YYYY-MM-DD format.")
infer: Optional[bool] = Field(None, description="Whether to extract facts from messages. Defaults to True.")
memory_type: Optional[str] = Field(None, description="Type of memory to store (e.g. 'core').")
prompt: Optional[str] = Field(None, description="Custom prompt to use for fact extraction.")
class MemoryUpdate(BaseModel):
- text: str = Field(..., description="New content to update the memory with.")
+ text: Optional[str] = Field(None, description="New content to update the memory with.")
metadata: Optional[Dict[str, Any]] = Field(None, description="Metadata to update.")
+ expiration_date: Optional[str] = Field(None, description="Expiration date in YYYY-MM-DD format, or null to clear.")
class SearchRequest(BaseModel):
@@ -201,6 +203,7 @@ class SearchRequest(BaseModel):
top_k: Optional[int] = Field(None, description="Maximum number of results to return.")
threshold: Optional[float] = Field(None, description="Minimum similarity score for results.")
explain: Optional[bool] = Field(None, description="Include score details for each search result.")
+ show_expired: Optional[bool] = Field(None, description="Include expired memories.")
class GenerateInstructionsRequest(BaseModel):
@@ -379,7 +382,7 @@ def add_memory(memory_create: MemoryCreate, _auth=Depends(verify_auth)):
ALL_MEMORIES_LIMIT = 1000
-_RESERVED_PAYLOAD_KEYS = {"data", "user_id", "agent_id", "run_id", "hash", "created_at", "updated_at"}
+_RESERVED_PAYLOAD_KEYS = {"data", "user_id", "agent_id", "run_id", "hash", "created_at", "updated_at", "expiration_date"}
def _serialize_memory(row: Any) -> Dict[str, Any]:
@@ -391,6 +394,7 @@ def _serialize_memory(row: Any) -> Dict[str, Any]:
"agent_id": payload.get("agent_id"),
"run_id": payload.get("run_id"),
"hash": payload.get("hash"),
+ "expiration_date": payload.get("expiration_date"),
"metadata": {k: v for k, v in payload.items() if k not in _RESERVED_PAYLOAD_KEYS},
"created_at": payload.get("created_at"),
"updated_at": payload.get("updated_at"),
@@ -410,6 +414,7 @@ def get_all_memories(
run_id: Optional[str] = None,
agent_id: Optional[str] = None,
top_k: Optional[int] = Query(None, ge=0, le=ALL_MEMORIES_LIMIT),
+ show_expired: bool = Query(False),
_auth=Depends(verify_auth),
):
"""Retrieve stored memories. Lists all memories when no identifier is provided (admin only)."""
@@ -418,6 +423,7 @@ def get_all_memories(
auth_type = getattr(request.state, "auth_type", "none")
if _auth is not None and _auth.role != "admin" and auth_type not in {"admin_api_key", "disabled"}:
raise HTTPException(status_code=403, detail="Admin role required to list all memories.")
+ # Admin all-memory listing is intentionally raw; scoped get_all below applies expiry visibility.
return _list_all_memories(limit=top_k if top_k is not None else ALL_MEMORIES_LIMIT)
filters = {
k: v for k, v in {"user_id": user_id, "run_id": run_id, "agent_id": agent_id}.items() if v is not None
@@ -425,6 +431,7 @@ def get_all_memories(
params = {"filters": filters}
if top_k is not None:
params["top_k"] = top_k
+ params["show_expired"] = show_expired
return get_memory_instance().get_all(**params)
except HTTPException:
raise
@@ -465,6 +472,8 @@ def search_memories(search_req: SearchRequest, _auth=Depends(verify_auth)):
params["threshold"] = search_req.threshold
if search_req.explain is not None:
params["explain"] = search_req.explain
+ if search_req.show_expired is not None:
+ params["show_expired"] = search_req.show_expired
return get_memory_instance().search(query=search_req.query, filters=filters, **params)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@@ -478,9 +487,15 @@ def search_memories(search_req: SearchRequest, _auth=Depends(verify_auth)):
def update_memory(memory_id: str, updated_memory: MemoryUpdate, _auth=Depends(verify_auth)):
"""Update an existing memory."""
try:
- return get_memory_instance().update(
- memory_id=memory_id, data=updated_memory.text, metadata=updated_memory.metadata
- )
+ fields_set = getattr(updated_memory, "model_fields_set", getattr(updated_memory, "__fields_set__", set()))
+ params = {"memory_id": memory_id}
+ if "text" in fields_set:
+ params["data"] = updated_memory.text
+ if "metadata" in fields_set:
+ params["metadata"] = updated_memory.metadata
+ if "expiration_date" in fields_set:
+ params["expiration_date"] = updated_memory.expiration_date
+ return get_memory_instance().update(**params)
except (ValueError, Mem0ValidationError) as e:
raise _client_error(e)
except Exception:
diff --git a/tests/memory/test_main.py b/tests/memory/test_main.py
index 5fae70a67..e8a490885 100644
--- a/tests/memory/test_main.py
+++ b/tests/memory/test_main.py
@@ -167,6 +167,33 @@ class TestAsyncUpdate:
"test_id", "Updated memory", {"Updated memory": [0.1, 0.2, 0.3]}, {}
)
+ @pytest.mark.asyncio
+ async def test_async_update_can_change_expiration_date_without_changing_text(self, mock_async_memory, mocker):
+ mock_async_memory.embedding_model.embed = Mock(return_value=[0.1, 0.2, 0.3])
+ mock_async_memory.vector_store.get = Mock(
+ return_value=Mock(
+ payload={
+ "data": "Existing memory",
+ "user_id": "test_user",
+ "created_at": "2026-01-01T00:00:00+00:00",
+ "expiration_date": "2026-12-31",
+ }
+ )
+ )
+ mock_async_memory.vector_store.update = Mock()
+ mock_async_memory.db.add_history = Mock()
+ mock_async_memory._remove_memory_from_entity_store = mocker.AsyncMock()
+ mock_async_memory._link_entities_for_memory = mocker.AsyncMock()
+
+ result = await mock_async_memory.update("test_id", expiration_date="2999-01-01")
+
+ assert result["message"] == "Memory updated successfully!"
+ payload = mock_async_memory.vector_store.update.call_args.kwargs["payload"]
+ assert payload["data"] == "Existing memory"
+ assert payload["expiration_date"] == "2999-01-01"
+ mock_async_memory._remove_memory_from_entity_store.assert_not_called()
+ mock_async_memory._link_entities_for_memory.assert_not_called()
+
@pytest.mark.asyncio
class TestAsyncAddToVectorStoreErrors:
diff --git a/tests/test_client.py b/tests/test_client.py
index 671ed973d..2c5934575 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -51,6 +51,20 @@ class TestSearchEntityParamRejection:
json={"query": "test query", "filters": {"user_id": "u1"}},
)
+ def test_search_passes_show_expired(self, mock_memory_client):
+ """search() should pass show_expired to the API."""
+ mock_response = MagicMock()
+ mock_response.json.return_value = {"results": []}
+ mock_response.raise_for_status.return_value = None
+ mock_memory_client.client.post.return_value = mock_response
+
+ mock_memory_client.search("test query", filters={"user_id": "u1"}, show_expired=True)
+
+ mock_memory_client.client.post.assert_called_once_with(
+ "/v3/memories/search/",
+ json={"query": "test query", "filters": {"user_id": "u1"}, "show_expired": True},
+ )
+
def test_search_rejects_user_id_kwarg(self, mock_memory_client):
"""search() should reject user_id as top-level kwarg."""
with pytest.raises(ValueError, match=r"user_id"):
@@ -100,6 +114,39 @@ class TestGetAllEntityParamRejection:
with pytest.raises(ValueError, match=r"run_id"):
mock_memory_client.get_all(run_id="r1")
+ def test_get_all_passes_show_expired(self, mock_memory_client):
+ """get_all() should pass show_expired to the API."""
+ mock_response = MagicMock()
+ mock_response.json.return_value = {"results": []}
+ mock_response.raise_for_status.return_value = None
+ mock_memory_client.client.post.return_value = mock_response
+
+ mock_memory_client.get_all(filters={"user_id": "u1"}, show_expired=True)
+
+ mock_memory_client.client.post.assert_called_once_with(
+ "/v3/memories/",
+ json={"filters": {"user_id": "u1"}, "show_expired": True},
+ )
+
+
+class TestUpdateExpirationDate:
+ """Tests for update expiration_date payload handling."""
+
+ def test_update_preserves_null_expiration_date(self, mock_memory_client):
+ """update() should send expiration_date=None so the API can clear it."""
+ mock_response = MagicMock()
+ mock_response.json.return_value = {"id": "mem_1", "expiration_date": None}
+ mock_response.raise_for_status.return_value = None
+ mock_memory_client.client.put.return_value = mock_response
+
+ mock_memory_client.update("mem_1", expiration_date=None)
+
+ mock_memory_client.client.put.assert_called_once_with(
+ "/v1/memories/mem_1/",
+ json={"expiration_date": None},
+ params={},
+ )
+
class TestFilterOperatorPassthrough:
"""Tests that AND/OR/NOT filter operators are passed through to the API."""
diff --git a/tests/test_main.py b/tests/test_main.py
index 8c8fc516a..4146c471d 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -65,6 +65,24 @@ def test_add(memory_instance):
)
+def test_add_stores_expiration_date(memory_instance):
+ memory_instance._add_to_vector_store = Mock(return_value=[{"memory": "Test memory", "event": "ADD"}])
+
+ memory_instance.add(
+ messages=[{"role": "user", "content": "Test message"}],
+ user_id="test_user",
+ expiration_date="2999-01-01",
+ )
+
+ memory_instance._add_to_vector_store.assert_called_once_with(
+ [{"role": "user", "content": "Test message"}],
+ {"user_id": "test_user", "expiration_date": "2999-01-01"},
+ {"user_id": "test_user"},
+ True,
+ prompt=None,
+ )
+
+
def test_get(memory_instance):
mock_memory = Mock(
id="test_id",
@@ -117,6 +135,39 @@ def test_search(memory_instance):
)
+def test_search_hides_expired_memories_by_default(memory_instance):
+ mock_memories = [
+ Mock(id="1", payload={"data": "Expired memory", "user_id": "test_user", "expiration_date": "2000-01-01"}, score=0.9),
+ Mock(id="2", payload={"data": "Active memory", "user_id": "test_user", "expiration_date": "2999-01-01"}, score=0.8),
+ ]
+ memory_instance.vector_store.search = Mock(return_value=mock_memories)
+ memory_instance.vector_store.keyword_search = Mock(return_value=None)
+ memory_instance.embedding_model.embed = Mock(return_value=[0.1, 0.2, 0.3])
+
+ with patch("mem0.memory.main.lemmatize_for_bm25", return_value="test query"), \
+ patch("mem0.memory.main.extract_entities", return_value=[]):
+ result = memory_instance.search("test query", filters={"user_id": "test_user"})
+
+ assert [memory["memory"] for memory in result["results"]] == ["Active memory"]
+ assert result["results"][0]["expiration_date"] == "2999-01-01"
+
+
+def test_search_can_show_expired_memories(memory_instance):
+ mock_memories = [
+ Mock(id="1", payload={"data": "Expired memory", "user_id": "test_user", "expiration_date": "2000-01-01"}, score=0.9),
+ Mock(id="2", payload={"data": "Active memory", "user_id": "test_user"}, score=0.8),
+ ]
+ memory_instance.vector_store.search = Mock(return_value=mock_memories)
+ memory_instance.vector_store.keyword_search = Mock(return_value=None)
+ memory_instance.embedding_model.embed = Mock(return_value=[0.1, 0.2, 0.3])
+
+ with patch("mem0.memory.main.lemmatize_for_bm25", return_value="test query"), \
+ patch("mem0.memory.main.extract_entities", return_value=[]):
+ result = memory_instance.search("test query", filters={"user_id": "test_user"}, show_expired=True)
+
+ assert [memory["memory"] for memory in result["results"]] == ["Expired memory", "Active memory"]
+
+
def test_update(memory_instance):
memory_instance.embedding_model = Mock()
memory_instance.embedding_model.embed = Mock(return_value=[0.1, 0.2, 0.3])
@@ -161,6 +212,42 @@ def test_update_with_empty_metadata(memory_instance):
)
+@pytest.mark.parametrize(
+ ("expiration_date", "expected_expiration_date"),
+ [
+ ("2999-01-01", "2999-01-01"),
+ (None, None),
+ ],
+)
+def test_update_can_change_expiration_date_without_changing_text(
+ memory_instance, expiration_date, expected_expiration_date
+):
+ memory_instance.embedding_model.embed = Mock(return_value=[0.1, 0.2, 0.3])
+ memory_instance.vector_store.get = Mock(
+ return_value=Mock(
+ payload={
+ "data": "Existing memory",
+ "user_id": "test_user",
+ "created_at": "2026-01-01T00:00:00+00:00",
+ "expiration_date": "2026-12-31",
+ }
+ )
+ )
+ memory_instance.vector_store.update = Mock()
+ memory_instance.db.add_history = Mock()
+ memory_instance._remove_memory_from_entity_store = Mock()
+ memory_instance._link_entities_for_memory = Mock()
+
+ result = memory_instance.update("test_id", expiration_date=expiration_date)
+
+ assert result["message"] == "Memory updated successfully!"
+ payload = memory_instance.vector_store.update.call_args.kwargs["payload"]
+ assert payload["data"] == "Existing memory"
+ assert payload["expiration_date"] == expected_expiration_date
+ memory_instance._remove_memory_from_entity_store.assert_not_called()
+ memory_instance._link_entities_for_memory.assert_not_called()
+
+
def test_delete(memory_instance):
memory_instance._delete_memory = Mock()
@@ -200,7 +287,30 @@ def test_get_all(memory_instance):
assert result["results"][0]["memory"] == "Memory 1"
assert result["results"][0]["user_id"] == "test_user"
- memory_instance.vector_store.list.assert_called_once_with(filters={"user_id": "test_user"}, top_k=20)
+
+def test_get_all_hides_expired_memories_by_default(memory_instance):
+ mock_memories = [
+ Mock(id="1", payload={"data": "Expired memory", "user_id": "test_user", "expiration_date": "2000-01-01"}),
+ Mock(id="2", payload={"data": "Active memory", "user_id": "test_user", "expiration_date": "2999-01-01"}),
+ ]
+ memory_instance.vector_store.list = Mock(return_value=(mock_memories, None))
+
+ result = memory_instance.get_all(filters={"user_id": "test_user"})
+
+ assert [memory["memory"] for memory in result["results"]] == ["Active memory"]
+ assert result["results"][0]["expiration_date"] == "2999-01-01"
+
+
+def test_get_all_can_show_expired_memories(memory_instance):
+ mock_memories = [
+ Mock(id="1", payload={"data": "Expired memory", "user_id": "test_user", "expiration_date": "2000-01-01"}),
+ Mock(id="2", payload={"data": "Active memory", "user_id": "test_user"}),
+ ]
+ memory_instance.vector_store.list = Mock(return_value=(mock_memories, None))
+
+ result = memory_instance.get_all(filters={"user_id": "test_user"}, show_expired=True)
+
+ assert [memory["memory"] for memory in result["results"]] == ["Expired memory", "Active memory"]
def test_no_telemetry_vector_store_when_disabled():
diff --git a/tests/test_server_params.py b/tests/test_server_params.py
index 7c55db12b..a5d7a2b82 100644
--- a/tests/test_server_params.py
+++ b/tests/test_server_params.py
@@ -564,12 +564,20 @@ class TestUpdateMemory:
resp = client.put("/memories/mem-1", json={"text": "Likes tennis"})
assert resp.status_code == 200
_, kwargs = mock_memory.update.call_args
- assert kwargs["metadata"] is None
+ assert "metadata" not in kwargs
- def test_missing_text_returns_422(self, client):
- """text is required — omitting it should fail validation."""
- resp = client.put("/memories/mem-1", json={"metadata": {"k": "v"}})
- assert resp.status_code == 422
+ def test_expiration_date_forwarded_without_text(self, client, mock_memory):
+ resp = client.put("/memories/mem-1", json={"expiration_date": "2999-01-01"})
+ assert resp.status_code == 200
+ _, kwargs = mock_memory.update.call_args
+ assert kwargs["expiration_date"] == "2999-01-01"
+ assert "data" not in kwargs
+
+ def test_null_expiration_date_forwarded_for_clear(self, client, mock_memory):
+ resp = client.put("/memories/mem-1", json={"expiration_date": None})
+ assert resp.status_code == 200
+ _, kwargs = mock_memory.update.call_args
+ assert kwargs["expiration_date"] is None
def test_dict_not_passed_as_data(self, client, mock_memory):
"""Regression test for #3933: the entire dict must NOT be passed as data."""