Skip to main content
You need PostgreSQL 14+ with pgvector 0.8+, an OpenAI key, and LiveKit Agents 1.6 or 1.7.
1

Install

The agent below also needs an STT, a TTS and an LLM, and those are separate LiveKit plugins. Install whichever you use; this guide uses Deepgram and OpenAI:
2

Start a database

Any PostgreSQL with pgvector works. For local development:
docker-compose.yml
3

Create the schema

Migrations are a deliberate step, not something that runs at startup. Twenty workers booting at once and racing DDL is a real failure: CREATE TABLE IF NOT EXISTS is not race-safe in PostgreSQL.
Migration 0002 creates an unprivileged voicemem_app role. Give it a password and point your agent at that, not at the admin role:
Row-level security only protects a role that can be constrained. Superusers and BYPASSRLS roles walk straight through the policies no matter what they say. voicemem-db status reports whether isolation is actually in force for the role you connected as.
4

Wire it into your agent

agent.py
Four lines do the work:
preemptive_generation is enabled by default. Injecting memory invalidates the speculative generation LiveKit already started, so leaving it on means paying for a discarded LLM call every single turn. Disable it as shown above.
Give the caller room to finish. LiveKit commits a turn after min_delay of silence (0.5 s by default) as soon as its turn detector rates what it has heard as a finished sentence. A caller who pauses mid-thought is split into two turns, and the fragment is what memory has to retrieve on:
A pause past min_delay splits one thought into two turns
Nothing was injected into that first turn. ā€œI’m at a cafe right now.ā€ carries no signal to retrieve on, so the agent answered a half-formed thought with an empty memory block. The question that memory could have helped with landed in the next turn.The margin is narrow. Measured on real calls, a 461 ms pause stayed one turn and a 1.2 s pause split, which is the 0.5 s threshold doing exactly what it says.
You pay that delay on every turn, so treat 0.8 s as a starting point and tune it against real calls. Retrieval itself is not the constraint here: it prefetches on interim transcripts and finishes while the caller is still speaking.
5

Confirm it remembers

Run the agent, say something about yourself, hang up, then reconnect and ask about it.
First call
A later call, new session
Nothing was passed between the two sessions. The second one retrieved from Postgres.

What it costs

Measured, not estimated. 8 stored turns, 12 queries, text-embedding-3-small and gpt-4o-mini, against a same-host pgvector/pgvector:pg17 container. The single embedding call is almost all of it. Everything this package does costs about 19 ms. That is why retrieval prefetches on interim transcripts: on a hit, the round trip already happened while the caller was still speaking. Writing runs in the background at 3.8 s p50 and 2 LLM calls per ingested turn, dropping to 1 when the store is empty and conflict resolution is skipped.