SokkoSokko
← Back to blog

How to Give Your AI Agent Long-Term Memory That Survives Restarts

Sokko Team10 min read

An agent that forgets everything the moment its process restarts is not much of an assistant. This tutorial walks through how to give an AI agent long-term memory that survives restarts, using a persistent volume for files, a relational database for structured state, and a vector store for semantic recall.

Most demos skip this part because a laptop session keeps everything in RAM for as long as the notebook stays open. Real agents crash, redeploy, and get rescheduled onto new nodes. The difference between a toy and a coworker is whether the memory outlives the container.

Why the Context Window Is Not Memory

The large language model at the center of your agent has no memory of its own. Every request you send it is stateless: the model reads the tokens you provide, produces a completion, and forgets the exchange. What looks like memory in a chat product is just the client resending the whole transcript on every turn.

That transcript lives in the context window, which is finite. Even a large window fills up after a few long documents or a busy afternoon of tool calls. When it fills, you either truncate the oldest turns or the request fails. Worse, the window lives in the memory of a single process. Restart the process, roll out a new version, or let a container get evicted, and the window is gone. This is one concrete reason why running an AI agent on your laptop fails: the machine sleeps, the process dies, and the accumulated state evaporates with it.

So the working definition is simple. The context window is short-term, working memory. Long-term memory is anything you deliberately write to durable storage and read back later. Building that second layer is the whole job.

The Four Kinds of Memory an Agent Needs

Before writing code, it helps to name the different things people lump under "memory." They have different shapes and belong in different stores.

Memory typeWhat it holdsWhere it livesExample
Short-term / workingThe current task and recent turnsContext window (RAM)The message you are answering right now
Long-term semanticFacts and knowledge, retrieved by meaningVector store"The user prefers metric units"
EpisodicPast conversations and events, retrieved by time or topicDatabase plus vector store"Last Tuesday we debugged the payment webhook"
Structured stateTyped records the agent reads and updatesRelational databaseA row tracking an open support ticket

Working memory is handled for you by the context window. The other three are what you persist. Semantic memory answers "what do I know that is relevant to this query," episodic memory answers "what happened before," and structured state answers "what is the current, authoritative value of this field." A capable personal AI assistant agent usually needs all three: it recalls your preferences (semantic), remembers past requests (episodic), and tracks live to-dos (structured).

Keeping these separate matters because you retrieve them differently. You do not run a similarity search to find a user's email address; you look it up by primary key. You do not store a fuzzy recollection of a conversation in a strict schema; you embed it and search by meaning.

The Persistence Stack: Volume, Database, Vector Store

A practical long-term memory stack has three tiers, from simplest to richest.

  1. A persistent volume for raw files: uploaded documents, generated artifacts, cached model outputs, and a SQLite file if you want the whole thing in one place to start.

  2. A relational database (Postgres or SQLite) for structured state and episodic logs: one row per conversation, per fact, per task, with timestamps and foreign keys you can query precisely.

  3. A vector store (pgvector, Chroma, or Qdrant) for semantic recall: text chunks stored alongside their embeddings so you can find relevant memories by meaning rather than exact match.

The persistent volume is the foundation everyone forgets. Your agent writes files, your SQLite database is a file, and your local vector index may be a file too. If that file sits on ephemeral container storage, it disappears on the next restart. Here is a minimal Kubernetes PersistentVolumeClaim that requests durable storage for a /workspace mount:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: agent-workspace
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 20Gi
  storageClassName: standard
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: memory-agent
spec:
  replicas: 1
  selector:
    matchLabels:
      app: memory-agent
  template:
    metadata:
      labels:
        app: memory-agent
    spec:
      containers:
        - name: agent
          image: your-registry/memory-agent:latest
          volumeMounts:
            - name: workspace
              mountPath: /workspace
      volumes:
        - name: workspace
          persistentVolumeClaim:
            claimName: agent-workspace

The claim outlives the pod. When the container restarts, the new pod mounts the same volume at /workspace, and the SQLite file, cached artifacts, and any local index are exactly where you left them. Wiring this up by hand is the fiddly part of deploy your agent on Kubernetes, which is why a managed platform where the agent's files simply survive restarts saves a lot of yaml.

Choosing a vector store

For structured state, use Postgres. For semantic memory, the honest advice is to start with the store that adds the least new infrastructure. If you already run Postgres, add the pgvector extension and keep everything in one database. If you want a purpose-built engine with rich filtering and horizontal scale, run Qdrant. For quick local prototypes, Chroma is the fastest thing to stand up. All three store vectors and run similarity search; they differ mostly in operational weight.

Storing and Retrieving Semantic Memory in Python

The mechanics of semantic memory are: turn text into an embedding vector, store that vector next to the text, and later find the closest vectors to a query embedding. Here is a compact example using Postgres with pgvector and an OpenAI-style embeddings client.

import os
import psycopg
from openai import OpenAI

client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
DB_URL = os.environ["DATABASE_URL"]  # points at durable Postgres

def embed(text: str) -> list[float]:
    resp = client.embeddings.create(
        model="text-embedding-3-small",
        input=text,
    )
    return resp.data[0].embedding

def init_schema() -> None:
    with psycopg.connect(DB_URL) as conn:
        conn.execute("CREATE EXTENSION IF NOT EXISTS vector")
        conn.execute(
            """
            CREATE TABLE IF NOT EXISTS memories (
                id BIGSERIAL PRIMARY KEY,
                agent_id TEXT NOT NULL,
                content TEXT NOT NULL,
                embedding vector(1536),
                created_at TIMESTAMPTZ DEFAULT now()
            )
            """
        )

def remember(agent_id: str, content: str) -> None:
    vec = embed(content)
    with psycopg.connect(DB_URL) as conn:
        conn.execute(
            "INSERT INTO memories (agent_id, content, embedding) VALUES (%s, %s, %s)",
            (agent_id, content, vec),
        )

Two details make this durable rather than disposable. First, DATABASE_URL points at a Postgres instance that runs as its own service, not an in-process store that dies with the agent. Second, every memory carries an agent_id and a timestamp, so you can scope recall to one agent and reason about recency later.

Retrieval is a similarity search. The <=> operator in pgvector computes cosine distance, so ordering by it ascending returns the closest memories first:

def recall(agent_id: str, query: str, k: int = 5) -> list[str]:
    query_vec = embed(query)
    with psycopg.connect(DB_URL) as conn:
        rows = conn.execute(
            """
            SELECT content
            FROM memories
            WHERE agent_id = %s
            ORDER BY embedding <=> %s
            LIMIT %s
            """,
            (agent_id, query_vec, k),
        ).fetchall()
    return [r[0] for r in rows]

That is the whole semantic layer in two functions. remember writes durable memories, and recall fetches the top-k most relevant ones for a given query.

Wiring Memory Into the Retrieval Loop

Storage is only useful if the agent actually consults it before answering. The retrieval loop runs on every turn and looks like this:

  1. Take the incoming user message.

  2. Embed it and run a similarity search to pull the top-k relevant long-term memories.

  3. Optionally fetch structured state (open tasks, user profile) by direct query.

  4. Assemble a prompt: system instructions, the retrieved memories, recent turns from the context window, and the new message.

  5. Call the model, return the answer, and write anything worth keeping back to storage.

In code, the assembly step is straightforward string composition:

def build_prompt(agent_id: str, user_message: str) -> list[dict]:
    memories = recall(agent_id, user_message, k=5)
    memory_block = "\n".join(f"- {m}" for m in memories)
    system = (
        "You are a helpful assistant with long-term memory. "
        "Use these recalled facts if relevant:\n" + memory_block
    )
    return [
        {"role": "system", "content": system},
        {"role": "user", "content": user_message},
    ]

Notice that retrieval happens before the model call, and persistence happens after it. The model never touches the database directly; your code decides what to inject and what to save. That separation keeps the loop debuggable. If recall returns junk, you can inspect the query and the returned rows without re-running the model.

A common refinement is to write memories selectively. Not every turn deserves to be remembered forever. A cheap heuristic is to store user statements that assert a preference or a fact ("I use Postgres 16," "my timezone is UTC+1") and skip small talk. You can even ask the model itself to extract durable facts from a conversation and call remember only on those.

Keeping Memory From Growing Without Bound

If you save every message forever, three things degrade. Retrieval gets slower, similarity search starts surfacing near-duplicate memories, and your storage bill climbs. Long-term memory needs pruning and compaction, the same way a log needs rotation.

The main technique is summarization. Periodically, take a batch of older episodic memories and ask the model to compress them into a shorter summary, then store the summary and archive or delete the raw rows. A conversation that took forty turns might collapse into three durable facts plus a one-paragraph recap.

def compact(agent_id: str, older_than_days: int = 30) -> None:
    with psycopg.connect(DB_URL) as conn:
        rows = conn.execute(
            """
            SELECT id, content FROM memories
            WHERE agent_id = %s
              AND created_at < now() - make_interval(days => %s)
            ORDER BY created_at
            """,
            (agent_id, older_than_days),
        ).fetchall()
        if not rows:
            return
        joined = "\n".join(r[1] for r in rows)
        summary = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[
                {"role": "system", "content": "Summarize these memories into durable facts."},
                {"role": "user", "content": joined},
            ],
        ).choices[0].message.content
        remember(agent_id, f"[summary] {summary}")
        ids = tuple(r[0] for r in rows)
        conn.execute("DELETE FROM memories WHERE id = ANY(%s)", (list(ids),))

Other useful controls: deduplicate on write by checking whether a very similar memory already exists (a similarity search with a distance threshold), cap the number of memories per agent, and add an approximate index such as HNSW so search stays fast as the table grows. The point is that memory is a managed resource, not an append-only pile.

Why AI Agent Long-Term Memory Needs Durable Storage

Every layer above rests on one assumption: the storage survives the process. A vector store is worthless if its data lives on a disk that gets wiped on redeploy. Postgres is only durable if it runs as a managed service with real volumes and backups, not as a container that shares the agent's lifecycle. Even the humble SQLite file needs to sit on a volume that reattaches after a restart.

This is the honest catch with the laptop approach and with naive container deployments. You can write flawless retrieval code, and it will still lose everything the first time the machine sleeps or the pod reschedules onto a new node with fresh local disk. Long-term memory is defined by what happens after the crash, not before it.

That is the gap Sokko is built to close. It runs your agent 24/7 on secure infrastructure we manage and gives each agent a private workspace that survives restarts and redeploys, plus secure secrets, team roles, and live logs, without asking you to run any servers yourself. Point DATABASE_URL at your own database and write files into your agent's private workspace, or turn on the managed Memory add-on: long-term memory you seed from MEMORY.md, USER.md, and a memory/ folder, so the memory you save today is still there next week.

To recap the path from ephemeral to durable: treat the context window as working memory only, split long-term memory into semantic, episodic, and structured tiers, back them with a persistent volume plus Postgres plus a vector store, run an explicit retrieve-then-persist loop on every turn, and compact old memories so the store stays healthy. Do that on storage that outlives the container, and you have given your AI agent long-term memory that genuinely survives restarts.