ADR-083: Conversation sessions and memory
- Status
-
Accepted (ownership and configuration binding superseded by ADR-091)
- Date
-
2026-07-18
- Authors
-
Netresearch DTT GmbH
Context
Completion is stateless per call: Completion builds a fresh
[system?, user] message array every time. Anything conversational — a backend
assistant, a multi-turn task — had to re-assemble and re-send the whole history
itself, and there was nowhere to persist it. Consumers were re-implementing
conversation memory badly and inconsistently.
Decision
Add an explicit, persisted session model in two UI-less log tables,
tx_ (one row per conversation) and
tx_ (one row per turn, ordered by sequence), read
and written by a raw-SQL Ai — the telemetry pattern
(ADR-058), no Extbase and no TCA. The turns are read back as
Ai / Ai value objects.
Add a `ConversationService` (a public feature service beside
Completion) that turns the stateless path into a conversation:
startSession()opens a session owned by the current backend user (resolved through the existingBackend, not a rawUser Context Resolver Interface $GLOBALSread),send()loads the prior turns, replays them plus the new user message to the provider via the unchangedLlm, and persists the user turn and the assistant reply (with the reply's model and token usage).Service Manager:: chat ()
The provider call is untouched — this only assembles the message array and records the turns around it. The user turn is persisted before the call, so a provider failure still leaves an honest record of what the user asked.
Retention is explicit and by inactivity. Ai
deletes sessions (and their messages) whose last_activity predates a window,
driven by a nrllm:session:purge command that mirrors nrllm:telemetry:purge.
There is no implicit, unbounded "the model remembers everything": memory is a
named session, scoped, purgeable, and cost-attributed (token counts per turn).
Consequences
- Consumers get a conversation primitive instead of hand-rolling history. The
stateless
complete*()methods are unchanged for one-shot callers. - Message rows store the conversation content (prompts and replies). That is the point (replayable memory), but it is privacy-relevant: retention is bounded by the purge command, and sessions are owned/attributed to a backend user. A scheduled purge task registration is a follow-up.
- The system prompt is prepended on every turn: the session history stores only user and assistant turns, so re-adding it does not duplicate it, and omitting it would drop the system instructions from the second turn onward.
- The user turn advances the session's message count immediately (before the provider call), so a failed call cannot leave the next turn reusing the same sequence number.
- Context-window management (summarising or truncating a long history before replay) is not in this change — the full history is replayed. A windowing strategy is a follow-up once real conversation lengths are observed.
Conversationdepends onService Ai, so it is unit-tested against a double; the raw SQL and schema are covered by a functional round-trip.Session Repository Interface - Public-service policy (ADR-028 count authority). This change adds two
public: trueoverrides — theConversationServiceconcrete and theConversationServiceInterfacealias — a Category-A documented downstream LLM-API feature pair, exactly like the Completion/Vision/Embedding services. The session repository stays private. The audited count therefore rises from 30 to 32 (Category A 17 → 19);PublicServicesPolicyTestis updated to match, and this ADR supersedes ADR-075 as the count authority.