Cloud Engineer Lab
Cloud Engineer Lab
Cloud Engineer Lab
Cloud Engineer Lab
© 2026
AI Agent Memory: Short-Term, Long-Term, and Persistent Memory Explained

AI Agent Memory: Short-Term, Long-Term, and Persistent Memory Explained

Three genuinely different engineering problems hide behind one word. An agent that confuses them either forgets things mid-task or loses them on restart.

12 min read
Share

Two failures look identical from the outside and come from opposite causes. In the first, an agent is told early in a long conversation to always use PowerShell instead of bash, and forty turns later suggests a bash script anyway, not because it disagreed, but because that instruction scrolled out of what it could actually attend to. In the second, an agent correctly remembers a user's timezone across an entire session, gets redeployed for an unrelated bug fix, and asks for the timezone again the next morning, because the "memory" holding it was never anything more durable than a Python dictionary in a process that no longer exists.

Both are memory bugs. They require completely different fixes, because "memory" in an agent system isn't one thing, it's at least three architecturally distinct mechanisms with different lifetimes, different storage, and different failure modes, and conflating them is exactly how both bugs above get built by accident.

The one sentence to remember

Short-term memory is a scope question (does this survive past the current run?), long-term memory is a content question (what's actually worth carrying forward?), and persistent memory is an infrastructure question (does the storage holding it survive a restart, a crash, or a redeploy?). Treating any two of these as the same problem is where agent memory systems actually break.


Three Layers, Three Different Problems

LayerScopeTypical storageWhat breaks when it's wrong
Short-termOne agent run, discarded afterThe context window itselfInstructions or facts fall out of context and get silently ignored
Long-termAcross sessions, for one user or taskA structured store, keyed and searchableEverything gets re-explained every session, or irrelevant history piles up forever
PersistentSurvives process restarts, crashes, redeploysDurable storage: a database or disk-backed storeLong-term memory quietly resets to empty the moment the process holding it dies
Short-Term: the running context window for this one run
Consolidation: deciding what's actually worth keeping
Long-Term: structured, searchable facts scoped to a user or task
Persistent Storage: the durable layer underneath, a database, not a dict
Recall: retrieved back into a future short-term context when relevant

Short-Term Memory: The Context Window Itself

This is the least architecturally interesting layer and the one every agent has by default, because it's just the running list of messages sent to the model on every call. It requires no design decision to exist. It requires a real one to not silently fail as a conversation grows:

python
class ShortTermMemory:
    def __init__(self, max_messages: int = 40):
        self.messages: list[dict] = []
        self.max_messages = max_messages
 
    def add(self, role: str, content: str):
        self.messages.append({"role": role, "content": content})
        if len(self.messages) > self.max_messages:
            self._consolidate()
 
    def _consolidate(self):
        # Summarize the oldest half instead of silently dropping it.
        oldest = self.messages[: self.max_messages // 2]
        summary = summarize(oldest)
        remainder = self.messages[self.max_messages // 2 :]
        self.messages = [{"role": "system", "content": f"Earlier context: {summary}"}] + remainder

Naive truncation and 'lost in the middle' are two different failure modes, and both are real

The obvious fix, just drop the oldest messages once a limit is hit, silently deletes whatever instruction or constraint was established early in the conversation, which is exactly the bash-instead-of-PowerShell failure from the opening. The less obvious problem is that even within a context window that technically still contains everything, models attend less reliably to information buried in the middle of a very long context than to what's near the beginning or the most recent turns, a well-documented effect generally referred to as "lost in the middle." Summarizing aggressively as context grows, rather than trusting the model to reliably use everything still technically present, addresses both problems at once.

Short-term memory never survives past the run it belongs to, and that's correct, not a limitation to work around. It exists to give the current reasoning step everything it needs right now. What happens to anything worth keeping after the run ends is a different layer's job entirely.


Long-Term Memory: Deciding What Actually Survives

Long-term memory holds structured, retrievable facts that should carry into future sessions, a user's stated preference, a correction the agent got wrong once and shouldn't repeat, a project's current state. The two real design questions here are how something gets in, and how it gets found again.

python
import time
 
class LongTermMemory:
    def __init__(self, store: "PersistentStore"):
        self.store = store
 
    def remember(self, user_id: str, key: str, value: str):
        self.store.upsert(user_id, key, value, timestamp=time.time())
 
    def recall(self, user_id: str, query: str, top_k: int = 5) -> list[dict]:
        # Semantic retrieval over this user's own remembered facts,
        # the same cosine-similarity mechanism covered in
        # RAG From Scratch, applied to self-generated memory instead
        # of externally authored documents.
        return self.store.search(user_id, query, top_k=top_k)

Two genuinely different ways something ends up in long-term memory, and most real systems need both:

Explicit: the agent calls a remember tool on purpose

Given a tool like remember(key, value), the model can decide mid-conversation that something is worth keeping, "the user prefers metric units," and store it deliberately. This is precise but depends entirely on the model reliably recognizing what's worth remembering in the moment, which it won't always do.

Automatic: consolidation at the end of a session

A separate summarization pass reviews the full short-term transcript after a session ends and extracts what's durable from what was just working context, the same consolidation step shown in ShortTermMemory._consolidate() above, run once more at a coarser grain. This catches what the model didn't think to flag explicitly, at the cost of running an extra pass that itself might misjudge what mattered.


Persistent Memory: Making Long-Term Memory Actually Survive

Here's the distinction that the opening timezone bug depends on entirely: "long-term" describes how long something should conceptually matter. It says nothing about whether the thing holding it will still exist after a crash. LongTermMemory above just delegates to a PersistentStore, and that's deliberate, because durability is a separate concern with its own separate failure mode:

python
import sqlite3
import json
 
class PersistentStore:
    def __init__(self, db_path: str = "agent_memory.db"):
        self.conn = sqlite3.connect(db_path)
        self.conn.execute("""
            CREATE TABLE IF NOT EXISTS memories (
                user_id TEXT NOT NULL,
                key TEXT NOT NULL,
                value TEXT NOT NULL,
                embedding TEXT NOT NULL,
                timestamp REAL NOT NULL,
                PRIMARY KEY (user_id, key)
            )
        """)
        self.conn.commit()
 
    def upsert(self, user_id: str, key: str, value: str, timestamp: float):
        vector = embed_query(value)  # reuses the embedding step from RAG From Scratch
        self.conn.execute(
            "INSERT OR REPLACE INTO memories VALUES (?, ?, ?, ?, ?)",
            (user_id, key, value, json.dumps(vector), timestamp),
        )
        self.conn.commit()
 
    def search(self, user_id: str, query: str, top_k: int = 5) -> list[dict]:
        rows = self.conn.execute(
            "SELECT key, value, embedding FROM memories WHERE user_id = ?", (user_id,)
        ).fetchall()
        query_vector = embed_query(query)
        scored = [
            {"key": k, "value": v, "score": cosine_similarity(json.loads(e), query_vector)}
            for k, v, e in rows
        ]
        return sorted(scored, key=lambda r: r["score"], reverse=True)[:top_k]

An in-memory dict is not long-term memory, no matter how long the process happens to stay running

The timezone bug from the opening comes from exactly this substitution: a dict keyed by user ID, holding facts the agent genuinely intended to keep long-term, living entirely in one process's RAM. It works perfectly in every test, every demo, and every session until that process restarts for any reason, a deploy, a crash, an autoscaler cycling instances, at which point every "long-term" memory it held vanishes with no error, no warning, and no indication to the user that anything was lost. SQLite, or any real database, survives exactly the event that a dict doesn't, which is the entire reason this layer needs to be named and designed separately instead of assumed to come for free.


The Concurrency Problem Persistence Alone Doesn't Solve

Moving to SQLite fixes the restart problem. It doesn't automatically fix a second one: if the agent runs as multiple concurrent instances behind a load balancer, each instance's own local SQLite file diverges from the others the moment two instances handle the same user. A fact remembered by instance A is invisible to instance B until both are reading from the same shared database, not one file per instance. At real production scale, this typically means moving from local SQLite to a shared, networked store, Postgres with pgvector for the same similarity search shown above, or a managed vector database, but the underlying requirement doesn't change: every instance that might serve a given user needs to be reading and writing the exact same persistent store, not a copy of it.


How This Differs From RAG

RAG From Scratch builds nearly the same retrieval mechanism, chunk, embed, store, search by cosine similarity, and it's worth being precise about what's actually different here, because the code above deliberately reuses it. RAG retrieves from a corpus of documents someone else authored: runbooks, policies, product docs. Long-term memory retrieves from state the agent itself generated about its own interactions: what this specific user said, what the agent decided, what turned out to be true. The retrieval math is the same. What's being retrieved, and who authored it, is not, and that distinction matters most exactly where it seems least important: when deciding how much to trust what comes back. A document from an approved runbook and a memory the agent wrote about itself six weeks ago deserve different default levels of confidence, one is verified organizational knowledge, the other is the system's own possibly-outdated belief about the world.


Memory Isolation Is a Security Boundary

Cross-user memory leakage is a real, specific failure mode, not a hypothetical one

Every function above takes user_id as an explicit parameter for a reason that goes beyond data organization: a recall() call that isn't scoped correctly can surface one user's remembered facts, preferences, or conversation history to a different user entirely, especially in a system where memory is retrieved by semantic similarity rather than an exact, enforced key match. This is the same identity and boundary discipline covered in AI Agent Identity, applied specifically to memory: the retrieval query has to be constrained to the requesting user's own partition before similarity search ever runs, not filtered afterward, because a similarity search that scans across every user's memories and filters late has already put the wrong data within one bug of being returned.


Forgetting Is a Feature, Not a Bug

Long-term memory that only ever grows eventually becomes its own failure mode: retrieval gets slower, results get noisier as old, no-longer-true facts compete with current ones, and, for anything involving real personal data, indefinite retention becomes a genuine privacy liability rather than just an engineering inconvenience. Three practical answers, usually combined:

Time-to-live expiration

Attach a timestamp, as PersistentStore.upsert() does above, and treat memories past a defined age as expired for retrieval purposes even if they're not physically deleted yet, cheap to implement and effective for facts that naturally go stale, like "the current on-call engineer" or "today's incident status."

Explicit overwrite

A new remember() call with the same key should replace the old value outright, which the INSERT OR REPLACE in the schema above already does, rather than accumulating a growing list of possibly-contradictory facts about the same thing.

Deliberate deletion

A real forget(user_id, key) path, reachable by an explicit user request, isn't optional if any of what's stored is personal data. "The agent remembers things about me" is a feature. "The agent remembers things about me and there's no way to make it stop" is a liability, and the gap between those two is usually a single missing function.


One Full Trace, Across All Three Layers

An agent handling an incident, restarted mid-week for an unrelated deploy, illustrates every layer at once:

StageWhat happens
Session 1, short-termThe engineer says "I'm the on-call for payments this week, ping me directly, not the whole channel"
ConsolidationAt session end, that instruction is extracted as a durable fact, not just left to expire with the transcript
Long-term writeremember(user_id, "notification_preference", "direct ping, not channel")
Persistent writeThe fact is written to the shared database, not a local process dict
Process restartAn unrelated deploy restarts the agent process entirely
Session 2, days laterA new incident starts. Short-term memory is empty, this is a brand new run
Long-term recallrecall(user_id, "how should I notify this person") retrieves the stored preference from the persistent store, unaffected by the restart in between
ResultThe agent pings directly, correctly, with zero re-explanation needed, because the fact never depended on the process that first heard it still being alive

The Bottom Line

"Give the agent memory" sounds like one feature and is actually three separate engineering decisions: how much of the current run to keep in context without drowning the model in irrelevant history, what's actually worth carrying forward once the run ends, and whether the thing storing that is durable enough to survive the process restarting without anyone noticing it happened. Most agent memory bugs trace back to exactly one of those three being treated as if it were one of the other two, an instruction that should have been consolidated but wasn't, or a fact that was faithfully remembered by code that was never actually built to survive a restart.

The audit worth running on your own agent's memory

For anything your system currently "remembers," ask which of the three layers it actually lives in. If the honest answer is "a variable in the running process," that's long-term memory wearing persistent memory's job, and it will fail exactly the way the timezone example did, quietly, on the next restart, with nobody finding out until a user notices.

CChetan Yamger

Written by

Chetan Yamger

Cloud Engineer · AI Automation Architect · Modern Workplace Consultant

Cloud Engineer, AI Automation Architect, and Modern Workplace Consultant based in Amsterdam, Netherlands. Specializing in scalable, secure enterprise solutions with Microsoft Azure, Intune, PowerShell, and AI-driven automation using ChatGPT, Gemini, and modern LLM technologies.

Cloud & Modern WorkplaceMicrosoft Intune & MDMAzure & Microsoft 365AI AutomationPrompt EngineeringPowerShell & Graph APIWindows AutopilotConditional Access & Zero TrustSCCM / MECM & MSIXVDI / WVDPower BINode.js & Next.js
Newsletter

Stay in the loop.
New articles, straight to you.

Deep-dive technical articles on Intune, PowerShell, and AI — no noise, no spam.

New article notifications
No spam, ever
Free forever

Discussion

Share your thoughts — your email stays private

Leave a comment

0/2000

Your email is used to prevent spam and will never be displayed.