---
title: "ADR-083: Conversation sessions and memory"
manual: "TYPO3 LLM Extension"
version: "0.35"
permalink: "https://docs.typo3.org/permalink/netresearch/nr-llm:adr-083@0.35"
source: "Adr/Adr083ConversationSessions.rst"
modified: "2026-09-16T22:09:16+00:00"
---

# ADR-083: Conversation sessions and memory

-   *Status:* Accepted (ownership and configuration binding superseded by [ADR-091](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-091@0.35))
-   *Amended:* 2026-07-20 by [ADR-091](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-091@0.35)
-   *Date:* 2026-07-18
-   *Authors:* Netresearch DTT GmbH

## Context

Completion is stateless per call: `CompletionService::complete()` 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_nrllm_ai_session` (one row per conversation) and
`tx_nrllm_ai_session_message` (one row per turn, ordered by `sequence`), read
and written by a raw-SQL `AiSessionRepository` — the telemetry pattern
([ADR-058](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-058@0.35)), no Extbase and no TCA. The turns are read back as
`AiSession` / `AiSessionMessage` value objects.

**Add a \`ConversationService\`** (a public feature service beside
`CompletionService`) that turns the stateless path into a conversation:

-   `startSession()` opens a session owned by the current backend user (resolved
    through the existing `BackendUserContextResolverInterface`, not a raw
    `$GLOBALS` read),
-   `send()` loads the prior turns, replays them plus the new user message to the
    provider via the unchanged `LlmServiceManager::chat()`, and persists the user
    turn and the assistant reply (with the reply's model and token usage).

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.** `AiSessionRepository::purgeInactiveSince()`
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.
-   `ConversationService` depends on `AiSessionRepositoryInterface`, so it is
    unit-tested against a double; the raw SQL and schema are covered by a functional
    round-trip.
-   **Public-service policy (ADR-028 count authority).** This change adds two
    `public: true` overrides — the `ConversationService` concrete and the
    `ConversationServiceInterface` alias — 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); `PublicServicesPolicyTest` is updated to
    match, and this ADR supersedes ADR-075 as the count authority.
