This is a 4-part series on how agents remember: building short-, medium- and long-term memory that scales across users, agents, and kubernetes clusters.
Alice and bob are talking to the same agents. Alice interacts with various agents for infra management. Bob interacts with various agents for app development. Both are able to build on learnings from the last month. But how far should this go? Should Alice be able to recall memories from Bob’s interactions? Should a user allow a single agent to recall memories from across their agents?
This captures the design choices required in multi-tenancy for agentic memory management
Recently I spent some time extending the Kubernetes Agent Orchestration System (KAOS) to support multi-tiered memory persistence (aka short-, medium- and long-term memory). Along the way I hit most of the same issues that anyone would whilst building or integrating multi-tiered memory into a multi-tenant system, so I thought it would be useful to compile the learnings, design choices and examples into this series.
This is Part 2 of the series, and here I go through some of the design choices made for 3-tier multi-tenant memory. This follows Part 1, where we surveyed ~30 memory engines, built a working taxonomy, and landed on adopting Mem0 as a library behind our own interface, together with the list of gaps (observability, tenant isolation, kubernetes packaging, framework bridging) that become our integration work.
The objective throughout the series is:
Let’s make the memory layer BORING, so that the agents can continue to be the fun part.
This part consists of two sections:
session > agent > user, defined by a verified identity and a maxReadScope ceilingFinally we wrap up with five hard lessons we learned about tier and scope design that carry beyond KAOS.
As with my previous posts on observability for agentic systems and autonomous always-on agentic patterns, I use KAOS as the concrete implementation example, but the goal is to provide practical intuition for the primitives (tiers, scopes, folding, degradation), so that it applies whether you use KAOS, Mem0 directly, LangGraph, CrewAI, or a memory layer you wrote yourself.
Here’s a refresher on this 4-part series on Multi-Tiered / Multi-Tenant Agent Memory:
MemoryStore resource, its deployment topology, and how to integrate it in your own agent.Let’s get started.
As a reminder, the taxonomy defined in Part 1 consisted of five memory types: short-term (working), episodic, semantic, procedural and temporal.
However when aligning with our requirements, I settled with a simplified three tier model: a short-term window, a medium-term summary, and long-term “facts”. These are intuitively defined as follows:
|
Tier |
What it holds |
When it updates |
Backing |
|---|---|---|---|
|
Short-term |
The context window of the live session, bounded by a token budget |
Every turn (cheap append) |
Relational rows |
|
Medium-term |
Rolling summary per session, versioned so past summaries stay accessible |
On compaction, when the window hits its token budget |
Relational rows, append-only |
|
Long-term |
Atomic facts extracted from context window, keyed by scope, recalled semantically |
In the background, after compaction |
Mem0 into the vector store |
Now that these tiers are defined, it was possible to also formalise the following design decisions:
These definitions also allow us to design the single coherent service that offers the short-, medium- and long-term memory tiers; the “MemoryStore Service”.
Article content
I will cover more on the MemoryStore service in Part 3 where we actually design the infrastructure components for Kubernetes. Before we get there however, we need to talk about another important (+ tricky) topic:
Access Scopes: or who should be able to remember what?
Every memory operation in a multi-tenant system needs an answer to “whose memory is it?”. And the answer has to come from the design of the system components.
We first have to start on the write path before we can define a solution for the read path access.
For the write, the constraint introduced was the following:
A single conversation is authored through an agent, on behalf of a user (or autonomous agent), inside of a session, on one memory store.
This means the service records all of these as metadata provenance for every memory input stored. Writes are therefore compound and invariant, while a read resolves to a single scope level and is a matter of policy.
That separation is what lets a write be recalled at several levels later without being duplicated. This is possible because the same memory data that an agent stored for “Alice” carries: 1) her user_id, 2) the agent identity, and 3) the session identifier.
This allows recalls at different levels to each find it through a different owner key.
Article content
For reads, I decided to use a hierarchical relationship across these three levels, where each wider level contains the previous one:
Article content
The access scope is what restricts read-level access, as it can be understood intuitively in the graph above.
This gives each level one documented meaning under each security posture:
|
Level |
Meaning |
With user auth on |
With user auth off |
|---|---|---|---|
|
|
the current conversation |
current agent x user x session |
current agent x session |
|
|
this agent’s memory of the verified context |
current agent x user |
this agent’s whole pool |
|
|
the verified user across all agents on the store |
current user, across agents |
rejected at deploy time |
|
|
the entire memory store for the agents using it |
all users x agents x sessions in that store |
all agents x sessions |
The identity always comes from the gateway-verified request headers, never from the request body or the model, which means that when auth is enabled, memory access is enforced by design at the control and data plane level.
The only way to access the store level (or the agent-wide pool across users) is through cluster-admin permissions: the memory service refuses these levels on any request arriving through an agent, so no grant, tool, or prompt can reach them, and the only remaining path is kubectl port-forward, which Kubernetes RBAC gates. If auth is disabled, the level meanings still hold logically, however there is no verified identity to enforce them against.
Article content
Because the read levels are totally ordered as a hierarchy, we are also able to define them with a maxReadScope that allows for a threshold definition (for example “max agent level and below”).
This also allows the MemoryStore to carry its own maxReadScope ceiling (default is agent), and an agent may not claim above its store’s, so cross-agent user reads exist only where the store owner deliberately raised the ceiling.
Article content
As covered in my previous post on autonomous always-on agents, KAOS supports agents that run without user input. I was able to also cover these instances by ensuring that a self-initiated iteration runs with the agent’s own identity as its user identifier. This means that all requirements for memory scopes are still satisfied, and an autonomous loop’s memory stays private to the loop, even if a user also queries that same autonomous agent.
This scope model is probably the obvious choice; the trickier question is how do we enforce the sharing restrictions at a project-like scope.
There were a few design options for this:
org:team:agent) and agents share memory up to the point where their paths diverge. Every version of this I drafted ended up re-creating an authorization system that the two simpler options already covered.Interestingly enough, when looking at how the managed platforms handle this, they expose a two-level version of the same tradeoff. Each one has a hard container that their control plane creates and manages, and lighter logical partitions inside it. A few examples:
Based on these tradeoffs, I went for option (2), one group per MemoryStore as it enforces this at the control plane. The store itself is the sharing boundary, which is exactly why the whole-store read scope is named store. This meant that I don’t have to build a full intra-store group management layer, and the data layer simply records the store’s group key as internal metadata on each record.
The way it’s designed to is set up to support finer grouping at the MemoryStore level by design, as we basically are storing everything under one global group per store.
Now finally once I adopted these design choices, I realised that there were a few caveats that came up, which I had to accept / address:
WHERE clause silently returns another tenant’s rows. To mitigate this, the filtering is centralised in a single storage module so there is one place to audit, and on Postgres the relational tiers can be hardened further with row-level security, where a FORCE ROW LEVEL SECURITY policy pins every query to the scope set on the transaction, so a missing predicate returns nothing instead of everything. Worth noting this RLS cannot be enforced at the vector level - however isolation between stores relies on none of this, since each store is its own deployment with its own database connection, which we cover in Part 3.forget as a single operation that fans out across all three tiers in one pass, deleting the short-term rows, the summaries, and the scope-filtered long-term facts. Note this is destruction, which is different from supersession, where facts are merely marked invalid but kept for history.Now that we have sorted the tiers and the access scopes, let’s distil the lessons from this part before we make it all run as infrastructure in part 3.
Here are the patterns from this part that I would carry into any agentic memory system.
Same-session verbatim windows and cross-session distilled facts are different memory tiers with different stores, lifecycles, and failure modes. Conflating them for any reason would add more complexity than simplification.
Summaries, facts, and embeddings are lossy, but recomputable. Keep the verbatim record durable and you can survive both a lost extraction and a change of mind about your extraction strategy.
We use long-term memory stores for atomic facts, whereas the medium-term memory is built with a rolling summary that provides continuity. Summaries and windows can be stored relationally and deterministically.
Derive scope server-side from authenticated identity. When the model is allowed to search, bound the levels it can reach with a maxReadScope ceiling. Treat what comes back as untrusted data with provenance, since memory poisoning and cross-session injection are demonstrated attacks with published success rates.
Sharing topology can be a deployment choice instead of an authorization system, with scope filtering within a store and physical isolation by deploying a store per tenant. This may seem restrictive, but it’s the model that most production platforms (+ cloud providers) follow.
We opened Part 2 with Alice and Bob talking to the same agents, and with the question of how should their memories be available and accessable. After this initial design, we can now answer it precisely.
Should Alice recall memories from Bob’s interactions? Never through an agent, because every recall is bound to the verified identity on the request.
Should a single agent reach across a user’s other agents? Only when the user level sits within its maxReadScope ceiling, which both the agent and the store owner have to allow.
Everything wider than that belongs to the cluster admin, behind Kubernetes RBAC.
That is the conceptual core of the series: three tiers that decide what an agent remembers, and a scope model that decides who it remembers it for.
In Part 3 we turn this design into running infrastructure with the MemoryStore Kubernetes resource, the topology decision behind it, and the degradation contract that keeps a memory outage from taking an agent down, together with how you can integrate the same pattern in your own agent from scratch.
Stay tuned for next week!
The series:
MemoryStore resource, its deployment topology, and how to integrate it in your own agent (coming soon…).