Long-Term Memory
Long-Term Memory is the platform capability that lets users resume prior conversations and lets the AI use prior turns as context while keeping final answers grounded in retrieved source documents.
Memory types
| Memory type | Stored in | Purpose |
|---|---|---|
| Conversation memory | conversations, messages | Resume sessions, persist user/assistant/tool turns, preserve citations and metadata |
| Long-term user memory | user_memory | A short per-user profile (preferences, work context) carried into every later conversation; see below |
| Retrieval memory | documents, chunks, embeddings, lexical text | Long-lived corpus memory used by retrieval |
| Audit memory | audit_log | Compliance trail for queries, answers, policy guard events, and future review/admin actions |
| Prompt/eval memory | Langfuse | Prompt versions, traces, eval datasets, LLM-as-judge records |
Per-user long-term memory
Conversation memory ends with the conversation. Long-term memory carries a short profile of each user into every later conversation (ADR-0047; KAK §2 "secure, semantic-based long-term memory"). It is stored in user_memory (migrations 0004/0005). Each row holds one memory:
- the owner's
userId type:preferenceorfact- one sentence of
text - a pgvector
embedding importancestatus:active,supersededordeleted- provenance:
sourceConversationIdandsourceMessageIds
What it stores:
- preferences: answer language, length, format, level of detail
- work context: the regulations, sectors or datasets a user keeps returning to
- facts the user chose to share about themselves
What it never stores:
- facts about documents or the law (those are retrieved and cited)
- the user's claims about the law
- anything about other people
- sensitive personal data
When it writes. A conversation counts as ended after IDLE_MINUTES without a message.
- The sweeper runs inside the API process (production runs one API replica).
- It records
memory_consolidated_throughinconversations.metadata, so a resumed conversation feeds only its new turns. - Extraction sees the asker's own turns and the replies to them that passed the Policy Guard, never the retrieved passages themselves. Replies are fed truncated to 500 characters, and they can quote corpus text: the extraction prompt forbids storing corpus facts, and the recall judge is instructed to reject them.
- A malformed reply, an unknown memory id or a breached cap writes nothing.
- A model outage leaves the conversation for the next sweep, up to
MAX_EXTRACTION_ATTEMPTStimes, so nothing said during a short outage is lost and a conversation that always fails cannot block the queue. - Only one process sweeps a database at a time (a Postgres advisory lock), and each conversation's memory writes commit in one transaction. A sweeper scoped by
SUBJECT_ALLOWLISTholds a lock of its own, so it never delays the unscoped one.
Who gets memory. Memory is keyed to who asked each message (messages.metadata.author_id).
- The shared sentinel subject used by callers without identity gets none.
- A conversation with more than one asker, or in a workspace with more than one member, gets none unless
SHARED_CONVERSATIONSis on, because every member reads the thread. - In production, memory requires a JWT-verified principal (the
jwtresolver;require_authwith forgeable claim headers is refused).
How answers use it. The memories go into the single system message as background for tone, format and scope. They are not evidence. A user with nothing saved yet gets a short note instead, so "what do you remember about me?" is answered "nothing saved yet" rather than "I cannot remember between conversations". While writes are on, a name or work detail that is in neither the notes nor the conversation is answered "may not be saved yet" (notes are written only after a chat goes idle), never "I do not store names"; sensitive personal data is never saved. Memories are written in the language the user wrote in. A separate judge checks any sentence that restates a memory; it sees only the sentence and the memory records.
- A supported sentence is cited as
kind: "memory"in the claim map, never incitations. - A rejected one is withheld.
User control. GET /memories lists a user's active memories; DELETE /memories/{id} and DELETE /memories erase them permanently (API). Erasure removes superseded rows too. Telling the assistant to forget something only closes the row; the text stays until it is erased here. Deleting a conversation does not remove the memories written from it.
What a tester sees. A memory exists only after its conversation has been idle for IDLE_MINUTES plus up to one SWEEP_INTERVAL_SECONDS, about 35 minutes by default. For a demo or UAT, set IDLE_MINUTES=2 and SWEEP_INTERVAL_SECONDS=30. Telling the assistant "forget X" in chat likewise takes effect only after that conversation goes idle; DELETE /memories/{id} is immediate.
For database administrators.
- Rows live in
user_memory. Active rows havestatus = 'active';supersededanddeletedrows keep their provenance until a user erases them. - The sweeper's state is two keys in
conversations.metadata:memory_consolidated_through(the newest message time processed) andmemory_extraction_failures. A scoped sweeper keeps its own pair,memory_scoped_consolidated_throughandmemory_scoped_extraction_failures, because an unscoped sweeper on the same database marks conversations it extracts nothing from. - Migration
0027addsmessages_createdAt_idxfor the sweeper's lookback query. - Erase one user:
DELETE /memoriesas that user, orDELETE FROM user_memory WHERE "userId" = '<uuid>'. - Monitor:
SELECT status, count(*) FROM user_memory GROUP BY status- the API log lines
memory consolidated,memory extraction rejected,memory extraction unavailableandmemory consolidation abandoned ai_engine_audit_logrows with"entityType" = 'user_memory'
- Before enabling on a database that ran the deleted #110 feature, check for leftover rows:
SELECT type, status, count(*) FROM user_memory GROUP BY 1, 2.reflectionrows are never served but are erased with the user.
Configuration. Every setting is an environment variable; memory is off by default. Production has it on (write and read) since 19 October 2026. Partner JWT is now live: memory uses the token's verified subject and requires ACEH_RAG_MEMORY__REQUIRE_VERIFIED_PRINCIPAL=true. Checked-in production deployment defaults preserve these settings.
Env var (ACEH_RAG_MEMORY__…) | Default | Meaning |
|---|---|---|
ENABLED | false | Master switch. Off: no sweeper, no recall; the /memories routes still work. |
WRITE_ENABLED / READ_ENABLED | true / true | Split the switch. Write-only is shadow mode: memories are written and inspectable, no answer uses them. |
IDLE_MINUTES | 30 | Minutes without a message before a conversation counts as ended. |
SWEEP_INTERVAL_SECONDS | 300 | How often the sweeper looks for ended conversations. |
SWEEP_BATCH_SIZE | 20 | Conversations processed per sweep. |
LOOKBACK_HOURS | 168 | Only conversations active this recently are swept; switching on does not backfill history. |
MAX_CONCURRENT_WRITES | 1 | Extraction calls in flight; one per user regardless. |
EXTRACT_MODEL | (empty) | Model for the extraction call; empty = the main LLM. |
EXTRACT_TIMEOUT_SECONDS | 60 | A timed-out extraction writes nothing. |
MAX_EXTRACTION_ATTEMPTS | 3 | Sweeps that may fail on one conversation before it is marked done without extraction, so a conversation that always fails cannot block the queue. |
EXTRACT_MAX_MESSAGES / EXTRACT_MAX_CHARS_PER_MESSAGE | 20 / 2000 | Transcript bounds per asker. Every question since the last sweep is read, newest first within half of EXTRACT_MAX_MESSAGES × EXTRACT_MAX_CHARS_PER_MESSAGE characters (20,000 by default; each cut to EXTRACT_MAX_CHARS_PER_MESSAGE); replies only within the last EXTRACT_MAX_MESSAGES messages. |
MAX_NEW_PER_CONVERSATION | 3 | New memories one conversation may add. |
MAX_CHARS_PER_MEMORY | 300 | Length cap of one memory (about 40 words). |
MAX_ACTIVE_PER_USER | 30 | Active memories per user; above it extraction must update or delete. |
RECALL_TOP_K | 5 | Memories carried by one question. |
RECALL_MAX_CHARS | 1500 | Character budget of the injected block (about 400 tokens). |
WEIGHT_RELEVANCE / WEIGHT_RECENCY / WEIGHT_IMPORTANCE | 1.0 / 0.3 / 0.5 | Recall ranking weights. |
RECENCY_HALF_LIFE_DAYS | 30 | Recency halves every this many days. |
SHARED_CONVERSATIONS | false | Allow memory in conversations with more than one asker or member. |
REQUIRE_VERIFIED_PRINCIPAL | false (prod template: true) | Memory only for JWT-verified askers; refuses to start with memory on unless require_auth is on with the jwt resolver. |
SUBJECT_ALLOWLIST | [] | JSON list of canonical UUIDs. Set: only these subjects get memory (recall, writes, and the sweeper, which then keeps its own progress marker and lock). An unlisted subject sees no memories on GET /memories and erases nothing. Narrows only; never lifts REQUIRE_VERIFIED_PRINCIPAL. Memory without REQUIRE_VERIFIED_PRINCIPAL refuses to start unless this is set, in production or with SECURITY__REQUIRE_AUTH=false. Used by the JWT-less aistudio test API (ADR-0047 amendment). |
Conversation memory schema
conversations (
id,
user_id,
title,
metadata,
created_at,
updated_at
)
messages (
id,
conversation_id,
trace_id nullable,
role,
content,
source_citations,
metadata,
created_at
)
The platform persists conversations, user messages, assistant messages, answer citations, and metadata for Policy Guard and synthesis.
What is stored
| Field | Purpose |
|---|---|
user_id | Associates a conversation with an authenticated or application-level user |
title | Human-readable conversation title or generated label |
role | Message role: user, assistant, system, or tool |
content | Message body stored as text |
source_citations | JSON array of document/chunk citations attached to assistant answers |
metadata | Retrieval settings, policy results, synthesis faithfulness data, tool-call data, channel info, and future session attributes |
| timestamps | Creation and update ordering for resume and retention |
Organization model
Memory is organized by user, conversation, and ordered messages. The database indexes conversations.user_id and (messages.conversation_id, messages.created_at) so session lists and chronological message loading are efficient.
Target behavior:
The memory builder uses a configurable last-N context window, with a default of 10 messages.
Retrieval and indexing strategy
Conversation memory is not the same as corpus retrieval. Corpus retrieval indexes document chunks in pgvector and PostgreSQL full-text search. Conversation memory is primarily chronological and user-scoped, with optional future summarization or semantic indexing if conversations become large.
Recommended retrieval order:
- Load conversation metadata by
conversation_idand authorizeduser_id. - Load recent messages by
created_at. - Include only the last configured window of turns in the prompt context.
- Retrieve corpus chunks separately through hybrid retrieval.
- Attach citations from corpus chunks, not from uncited memory text.
- Persist the new user and assistant messages with retrieval, policy, and faithfulness metadata.
This keeps long-term memory useful without letting uncited chat history become an unsupported source of truth.
Update lifecycle
Confirmed query publication stores the conversation turn, assistant answer, source citations, and audit events for policy, query, and answer handling. Timeout persistence exceptions follow ADR-0041.
Retention policy
Retention policy for conversation history and audit logs is configurable and conservative: store enough for session resume, audit, and evaluation while allowing policy-driven deletion or archival.
Recommended policy dimensions:
- Retention duration by data class: conversation content, citations, audit events, traces, uploads, OCR review history.
- Legal hold and audit exceptions for government compliance.
- User deletion or anonymization behavior when policy allows it.
- Archival path to OBS for old conversations if PostgreSQL storage pressure grows.
- Clear separation between deleting a conversation view and retaining immutable audit events.
Security and access
Long-Term Memory must respect RBAC and ABAC. A user should only see conversations and messages they are authorized to access. Document citations in old messages must remain authorization-checked because document-level access may change after the answer was created.
Security rules:
- Scope conversation listing by authenticated user and role.
- Check document access before rendering stored citations or reusing chunks.
- Keep audit logs append-only from the application perspective.
- Store metadata as structured JSON, not prompt text blobs that bypass policy checks.
- Do not treat conversation history as trusted instructions; ingested documents and prior turns can still contain prompt injection.
Integration with the AI platform
Long-Term Memory integrates at the service layer, not as a separate frontend-only feature. The chatbot UI calls API endpoints; the backend loads memory, retrieves corpus chunks, calls models, applies Policy Guard, stores messages, and audits results.
The Synthesizer must still return source citations from retrieved chunks. Memory can clarify the user's intent, but it does not replace source-grounded retrieval.