This document describes the memory object used by this project and the retrieval model built around it. The aim is to keep the schema lean while still supporting safe updates, deterministic retrieval, and future ranking strategies.
Elements
Practical Implementation Advice
{
"id": 1,
"content": "User prefers concise answers with examples.",
"tags": ["preference", "writing-style", "concise"],
"created_at": "2026-04-06T14:12:00.000000Z",
"updated_at": "2026-04-06T14:12:00.000000Z",
"last_accessed_at": "2026-04-10T09:21:00.000000Z",
"memory_type": "preference",
"status": "active",
"version": 1
}A stable unique identifier for the memory record.
- Database-generated integer ID
Without an id, you cannot reliably update, delete, merge, supersede, or reference a memory later.
Use a database-generated integer ID if the service owns persistence end to end. It is simple, compact, and works well with deterministic tie-breaking in retrieval.
The main text of the memory.
- A direct fact:
User lives in Albany. - A preference:
User prefers concise responses. - A temporary state:
User is traveling this week. - A task context note:
Project Falcon launch moved to May.
- Keep it atomic when possible
- Prefer one memory per distinct fact or idea
- Write it in normalized language rather than raw chat fragments
This is the actual memory payload the system will retrieve and use.
- Shorter content: easier to rank, compare, and inject into prompts
- Longer content: preserves nuance, but may be harder to maintain and deduplicate
Keep content focused and singular. If a memory contains multiple independent facts, split it into separate records.
A list of keywords or labels associated with the memory.
profileproject-falcondeadlinetravelwriting-style
- Freeform strings
- Controlled vocabulary
- Hybrid approach with a few controlled categories and some freeform tags
Tags make filtering and retrieval easier, especially before the system has richer structured fields or graph relationships.
- Freeform tags: fast to implement, flexible, but can become messy
- Controlled vocabulary: cleaner analytics and retrieval, but slower to evolve
- Hybrid: best balance for most systems
Use a hybrid approach. Keep a small set of standard tags for core concepts, and allow a limited set of extra freeform tags.
The timestamp when the memory was first created.
- ISO 8601 UTC timestamp such as
2026-04-06T14:12:00Z
It helps with auditability, ordering, debugging, analytics, and understanding how old a memory is.
- Standard across systems
- Easy to serialize and parse
- Avoids time zone ambiguity
Always store in ISO 8601 UTC.
The timestamp when the memory was last edited or changed.
- ISO 8601 UTC timestamp
A memory may be corrected, refined, or reclassified after creation. updated_at helps the system know which version is current.
A record can be old overall but recently corrected/patched. Keeping both fields lets you distinguish original age from latest revision.
Always update this field whenever any meaningful part of the record changes.
In this project, updated_at changes only when PATCH actually modifies at least one editable field.
The timestamp when the memory was last retrieved or used by the system.
- ISO 8601 UTC timestamp
nullif never accessed after creation
This is useful for ranking, retention, pruning, and decay strategies.
- Helps identify stale but unused memories
- Helps preserve records that are actively useful
- Supports future cleanup policies
nullinitially: clearly means never retrieved- Set to
created_atinitially: simpler if you treat creation as first use
Use null initially unless your system explicitly treats creation as a read event.
In this project, last_accessed_at is refreshed when a client reads one memory directly or when retrieval returns a memory in a paginated result set.
A categorical label describing what kind of memory this is.
preferencefactidentityevent
Different memory types should be ranked and retained differently.
preference: use for stable likes, dislikes, or style choicesfact: use for concrete statements that may be true independently of the current taskidentity: use for durable user or agent profile detailsevent: use for time-linked things that happened or will happen
The current lifecycle state of the memory.
activeinvaliddeleted
You need a way to retire or disable memories without losing record history.
active: memory is available for normal retrievalinvalid: determined to be wrong or unsafe to usedeleted: soft-deleted by the delete operation, hidden from single-record reads, and excluded from retrieval unlessstatus=deletedis requested
At minimum support active, invalid, and deleted.
A number that increments each time the record is updated.
- Integer starting at
1
Versioning supports safe updates, concurrency control, and auditability.
- Easy to compare
- Easy to increment
- Familiar for optimistic locking patterns
Start at 1 and increment on every meaningful update.
In this project, version increments only when PATCH changes the record.
This project uses one retrieval contract across HTTP and MCP.
- HTTP:
GET /memories - MCP bootstrap:
prime_memory_context - MCP retrieval:
search_memories
Both surfaces accept the same query fields:
statusmemory_typetagqsortlimitoffset
Both surfaces return the same envelope:
{
"items": [],
"total": 0,
"limit": 10,
"offset": 0,
"has_more": false
}statusandmemory_typeare exact structured filters.- Retrieval excludes
status=deletedby default unless the caller explicitly requestsstatus=deleted. tagis exact matching against the stored tag list.qis case-insensitive free-text matching overcontentand stored tags.%,_, and\are matched literally.- Filters compose with
AND.
This distinction matters because exact filters are predictable and contract-friendly, while q provides a lightweight lexical narrowing mechanism without introducing opaque ranking behavior.
- Allowed sort keys are
id,created_at,updated_at, andlast_accessed_at. idsorts ascending.- Other sort keys sort descending with
id DESCas a stable tie-breaker. limitdefaults to10and is capped at100.offsetdefaults to0.totalcounts matches before pagination.has_moretells the caller whether another page exists.
These rules keep retrieval deterministic, which makes pagination reliable and prevents page boundaries from drifting unpredictably when multiple rows share the same timestamp.
last_accessed_at is not just an audit field. It is part of the retrieval model.
- A memory returned to the caller has been used and should be marked as accessed.
- Only returned rows are refreshed.
- Filtered-out rows and rows outside the current page are not refreshed.
That design keeps access history meaningful for future retention, decay, and ranking logic. It avoids the misleading outcome where a broad query updates timestamps for many records the agent never actually saw.
This project intentionally keeps today's retrieval logic deterministic.
- Structured filters narrow the candidate set.
qprovides case-insensitive lexical matching overcontentand tags.- Explicit sort keys define presentation order.
- Pagination is applied after deterministic ordering.
That is different from semantic search. If you later add vector-based retrieval, treat it as a separate ranking mode or dedicated search tool instead of changing the meaning of this contract in place.
If you want to implement this quickly, define strict types for:
memory_typestatus
And keep these flexible:
contenttags
{
"memory_type": "fact",
"last_accessed_at": null,
"status": "active",
"version": 1
}When retrieving memories in this implementation, start with:
- exact lifecycle filters such as
status - exact structural filters such as
memory_typeandtag - lightweight
qmatching overcontentand tags - explicit deterministic sort keys
If you later add semantic search, combine that ranking with the existing lifecycle and access signals rather than replacing them blindly.