> For the complete documentation index, see [llms.txt](https://docs.telys.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.telys.ai/recipes/chat-memory.md).

# Chat memory

A long conversation overflows a model's context window. Instead of resending the whole transcript every turn, store each turn in Telys and recall only the turns relevant to the current message. Store, recall, and prune to keep a session bounded:

1. **Store** every turn with `add_texts`, partitioned by session.
2. **Recall** the most relevant prior turns with `search_text` before answering.
3. **Prune** turns past a budget with `delete`, then reclaim space with `compact`.

{% hint style="info" %}
Install with `pipx install telys`, sign in with `telys login`, then verify with `telys runtime verify`. The lexical embedder needs the native kernel from the verified runtime. See [Install Telys](/start-here/install.md).
{% endhint %}

## Partition by session

Each conversation is one partition. Build the session scope with `scope_key(...)` and store it under a single `scope` column, so recall for one conversation never sees another. Telys has no temporal query API, so track recency client-side with a monotonic turn counter you assign as you go — that counter lets you prune the oldest turns deterministically.

## Full example

{% code title="chat\_memory.py" lineNumbers="true" %}

```python
from telys import Telys, Eq, scope_key
from telys.embedding import AlgentaMultigramEmbedder


class ChatMemory:
    """Per-session chat history with relevance recall and recency pruning."""

    def __init__(self, path: str = "./chat-memory", name: str = "turns", max_turns: int = 200):
        self.max_turns = max_turns
        self.embedder = AlgentaMultigramEmbedder()
        self.telys = Telys(path)
        try:
            self.col = self.telys.open_collection(name, embedder=self.embedder)
        except FileNotFoundError:
            self.col = self.telys.create_collection(
                name,
                dim=self.embedder.profile.dimension,
                partition_by="scope",
                embedder=self.embedder,
                filter_columns=("role", "turn", "text"),
            )
            self.col.save()
        self._counter: dict[str, int] = {}

    def _scope(self, session_id: str) -> str:
        return scope_key("session", session_id)

    def add_turn(self, session_id: str, role: str, text: str) -> str:
        """Append one chat turn (role = 'user' or 'assistant')."""
        scope = self._scope(session_id)
        n = self._counter.get(scope, 0)
        self._counter[scope] = n + 1
        turn_id = f"{scope}:{n:08d}"
        self.col.add_texts(
            texts=[text],
            ids=[turn_id],
            metadata=[{"scope": scope, "role": role, "turn": n, "text": text}],
        )
        return turn_id

    def recall(self, session_id: str, query: str, top_k: int = 6) -> list[dict]:
        res = self.col.search_text(
            query,
            top_k=top_k,
            where=Eq("scope", self._scope(session_id)),
            with_metadata=True,
        )
        hits = [
            {"turn": m["turn"], "role": m["role"], "text": m["text"], "score": s}
            for s, m in zip(res["scores"], res["metadata"])
        ]
        # Present recalled turns in conversational order.
        return sorted(hits, key=lambda h: h["turn"])

    def prune(self, session_id: str) -> int:
        """Delete turns older than the newest `max_turns`, then compact."""
        scope = self._scope(session_id)
        ids = self.col.ids(where=Eq("scope", scope))
        if len(ids) <= self.max_turns:
            return 0
        # ids sort lexicographically by the zero-padded turn number.
        stale = sorted(ids)[: len(ids) - self.max_turns]
        self.col.delete(stale)
        self.col.compact()
        return len(stale)

    def save(self) -> None:
        self.col.save()


if __name__ == "__main__":
    chat = ChatMemory(max_turns=200)
    sid = "conv-42"

    chat.add_turn(sid, "user", "I want to plan a trip to Lisbon in spring.")
    chat.add_turn(sid, "assistant", "Great — spring in Lisbon is mild. Any budget?")
    chat.add_turn(sid, "user", "Around 1500 euros, and I love seafood.")

    for h in chat.recall(sid, "what food does the user like?"):
        print(f"[{h['role']} #{h['turn']}] {h['text']}  ({h['score']:.3f})")

    removed = chat.prune(sid)
    print(f"pruned {removed} old turns")
    chat.save()
```

{% endcode %}

## Recall then answer

Before calling your model, recall the relevant slice of history and prepend it. This is cheaper than resending the full transcript and keeps older-but-relevant turns available past the context window.

```python
def respond(chat: ChatMemory, session_id: str, user_message: str) -> str:
    chat.add_turn(session_id, "user", user_message)
    history = chat.recall(session_id, user_message, top_k=6)
    context = "\n".join(f"{h['role']}: {h['text']}" for h in history)

    reply = generate_reply(f"Relevant history:\n{context}\n\nuser: {user_message}")

    chat.add_turn(session_id, "assistant", reply)
    chat.prune(session_id)   # keep the session bounded
    chat.save()
    return reply
```

## Expected result

The script recalls the seafood turn as the top hit for a food query, prints recalled turns in conversational order, and reports how many were pruned:

```
[user #2] Around 1500 euros, and I love seafood.  (0.641)
[user #0] I want to plan a trip to Lisbon in spring.  (0.122)
pruned 0 old turns
```

Below `max_turns`, `prune` deletes nothing. Once a session exceeds the budget, `delete` drops the oldest turns and `compact` reclaims their storage.

{% hint style="info" %}
`delete` removes only ids that are present; `compact` physically reclaims deleted rows — run it after a batch of deletes, not per turn. See [Delete & compact](/working-with-data/delete-and-compact.md).
{% endhint %}

## Next steps

<table data-view="cards"><thead><tr><th></th><th></th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td><strong>Agent long-term memory</strong></td><td>Durable per-user memory that outlives a single conversation.</td><td><a href="/pages/QYbY40mZFZxkolWKKL7V">/pages/QYbY40mZFZxkolWKKL7V</a></td></tr><tr><td><strong>Delete &#x26; compact</strong></td><td>How pruning and space reclamation work.</td><td><a href="/pages/RllSb4oFIVsEKxs8s4a3">/pages/RllSb4oFIVsEKxs8s4a3</a></td></tr><tr><td><strong>Search by text</strong></td><td>The recall primitive in depth.</td><td><a href="/pages/ljGcBbwv7A5FMTi0vZWR">/pages/ljGcBbwv7A5FMTi0vZWR</a></td></tr></tbody></table>


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.telys.ai/recipes/chat-memory.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
