← Blog · Download & Install · Deployment guide

Give your local LLM a memory: sessions + long-term recall, built into the server

September 2026 · SearchAI Inference Server team

one request fieldserver-side history long-term memory across sessionshybrid vector + keyword recall all on your hardware

Chat APIs are stateless: every request re-sends the whole conversation, every app reinvents history storage, and nothing is remembered between sessions. Cloud vendors solve this with hosted conversation state — which means your users' conversations live in someone else's database.

As of v1.1.0, the SearchAI Inference Server owns this server-side, on your box: sessions (the server keeps the turns), background summarization (old turns fold into a rolling summary so context never explodes), and long-term memory (durable facts extracted in the background and recalled across sessions by hybrid vector + keyword search). It's all additive — requests without these fields behave exactly as before — and all of it, including the embeddings, runs on the models already on your server. Nothing leaves your network.

Sessions: one field, and the server keeps the conversation

Add a session id to a normal /v1/chat/completions call. Send only the new message each turn — the server prepends the stored history:

curl -s http://127.0.0.1:8081/v1/chat/completions \
  -H "Authorization: Bearer $SEARCHAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "q35-4b",
    "session": "support-ticket-4821",
    "messages": [{"role":"user","content":"The customer name is Dana Reyes and she prefers teal."}]
  }'

Next turn, just ask — no history resend:

-d '{
  "model": "q35-4b",
  "session": "support-ticket-4821",
  "messages": [{"role":"user","content":"What color should the mockup use?"}]
}'
# → "teal" — the server supplied the context

With the OpenAI Python SDK it's one extra parameter:

client.chat.completions.create(
    model="q35-4b",
    messages=[{"role": "user", "content": "What color should the mockup use?"}],
    extra_body={"session": "support-ticket-4821"},
)

Sessions are manageable over the API — GET /v1/sessions lists them, GET /v1/sessions/<id> shows one, DELETE removes it — and in the console's Memory tab.

One contract to remember: because the server owns the history, re-sending your full message list would duplicate turns. New messages only.

Why it's fast: sessions sit on the prefix cache

The server assembles session context stable parts first — system prompt, rolling summary, stored turns, then your new message. That ordering means every follow-up turn re-uses the server's prefix cache instead of re-ingesting the conversation: in our gate runs a follow-up turn that recalled earlier session facts answered in 622 ms end-to-end on a CPU box.

And because the prefix cache itself now persists (v1.1.0), session warmth survives restarts: a 4,400-token context that took 24.1 s to ingest cold re-loaded from disk in 2.9 s after a full server restart — token-exact. In a cluster, nodes advertise and pull each other's largest caches: in our two-node test, a node that died mid-conversation had its session continued by the surviving peer, full history intact, and the peer served the transferred cache in 268 ms — about 100× faster than recomputing it.

Compaction: old turns fold into a summary, in the background

Long conversations don't grow without bound. When a session exceeds a size threshold (sessions-compact-bytes, default 24000) or a turn count you choose (sessions-max-turns), the server folds the oldest half of the turns into a rolling summary — in the background, off the request path, using whichever model you point sessions-summary-model at. The 0.8B model group is ideal: summarization is exactly what a small model is good at, and it leaves your serving model alone.

Recent turns always stay verbatim — research on conversational memory is clear that verbatim recent context beats summaries for recall, so only the old tail is compacted.

Long-term memory: facts that outlive the session

During that same background pass, the server extracts durable facts — preferences, entities, standing constraints — into a per-user memory store. Scope memories to a user by the standard OpenAI user field and they follow that user across sessions:

# session A, Monday
-d '{"model":"q35-4b", "session":"onboarding-1", "user":"dana",
     "messages":[{"role":"user","content":"We are migrating Acme Corp to the enterprise plan this quarter."}]}'

# session B, a NEW session days later
-d '{"model":"q35-4b", "session":"checkin-7", "user":"dana",
     "messages":[{"role":"user","content":"Which customer am I migrating, and to what plan?"}]}'
# → "Acme Corp, to the enterprise plan" — recalled from long-term memory

You can also write and manage memories directly:

curl -X POST http://127.0.0.1:8081/v1/memories \
  -H "Authorization: Bearer $SEARCHAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"scope":"dana","text":"Always answer in metric units."}'

curl http://127.0.0.1:8081/v1/memories/dana \
  -H "Authorization: Bearer $SEARCHAI_API_KEY"      # list dana's memories
# DELETE /v1/memories/dana/<id> removes one

Recall is hybrid: the query is embedded (with the on-box embedding model) and matched by vector similarity, and ranked by exact BM25 keyword scoring — the two lists fused so that exact identifiers ("ticket ACME-4821") are found even when embeddings would miss them. Optionally, a cross-encoder reranker (memories-rerank-model) orders the final candidates. Recalled memories are injected into the context automatically — you just ask.

Setup

Fresh installs of v1.1.0 wire this up automatically. On an existing install, add to /etc/searchai/server.properties:

sessions-dir=/var/lib/searchai/sessions
memories-dir=/var/lib/searchai/memories
sessions-summary-model=q35-0.8b          # small model for background work
memories-embed-model=qwen3vl-embed-2b    # powers vector recall

Sessions work with no extra models at all; summarization needs the 0.8B group and memory recall needs the embed model (the same one that powers /v1/embeddings). Both are one installer group away.

Privacy note, because it's the point: session and memory files hold conversation content. They live on your disk, 0700, under the service user — treat them with the same care as logs. There is no second copy anywhere.

The bottom line

One request field gives your applications server-side conversation state; the user field gives them memory that crosses sessions; background summarization keeps context bounded; and the whole stack — storage, embeddings, keyword search, recall — runs on the hardware you already own, surviving restarts and following your cluster.

curl -fsSL https://inference-server.searchblox.com/install | sudo bash
# then: add "session": "my-first-session" to any chat request

Timings measured 2026-09-02/03 during our release gates: turn-2 session recall on an Apple M4 (CPU); restart reload (24.1 s → 2.9 s, 4,418-token context, q35-2b) on the same box; cluster cache transfer (268 ms) and session fail-over on a two-node AWS Graviton/x86 pair. Full details in the deployment guide.