---
title: "ADR-258: A model hints at the cause of a logged error"
manual: "LLM Foundation"
version: "0.44"
permalink: "https://docs.typo3.org/permalink/netresearch/nr-llm:adr-258@0.44"
source: "Adr/Adr258AModelHintsAtTheCauseOfALoggedError.rst"
rendered: "2026-11-12T17:35:48+00:00"
---

# ADR-258: A model hints at the cause of a logged error {#adr-258}

-   *Status:* Accepted
-   *Date:* 2026-11-04
-   *Authors:* Netresearch DTT GmbH

## Context {#context}

[ADR-044](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-044@0.44) gave the assistant `get_last_exception`: an
administrator who asks in the chat gets the newest logged error with its
stack and source, and the model explains it. Outside the chat nothing did:
an administrator on the command line, or one who opens the backend to look
at what failed, had the log and no hint. The gap analysis of 2026-09-23
listed "hints for exceptions by AI, for administrators and on the command
line" as missing.

Three constraints shape it. The hint must not show anyone more than the
person already sees: the log files are readable by whoever runs the CLI and,
in the backend, only by administrators. An exception message sits next to
secrets (connection strings, tokens; [ADR-094](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-094@0.44) classes it
`SECRET_ADJACENT`), and a provider may be allowed less than that. And a
model asked for a cause always names one, so a reader must be able to tell a
hint anchored in the stack from a guess.

## Decision {#decision}

One service, `ExceptionHintService`, two thin surfaces.

-   **What is sent**

    The exception class, the message after the sanitizer that masks
    credentials in URLs, the component, and up to 20 frames as file, line
    and function name. TYPO3's exception handlers log no trace by default;
    for their entries `LogExceptionReader` now takes the class and the
    throw site from the entry's own `exception_class`, `file` and
    `line` keys; that class beats one the message quotes. An entry with no class, no frame and a message the
    provider may not receive is not sent at all. Call
    arguments are cut from each frame (PHP writes argument values into a
    trace unless `zend.exception_ignore_args` is on). No source line is
    sent; `get_last_exception` remains the way to read code with a model.

-   **What the provider may receive**

    The configuration's trust zone decides, through the same ladder the tool
    loop applies (`TrustZoneResolver::ceilingFor()`). Class and frames
    are system diagnostics, the message is secret-adjacent. Below
    `SYSTEM_DIAGNOSTICS` no model is asked and the refusal says why; below
    `SECRET_ADJACENT` the message is withheld and the hint says it was made
    without it. With the default zones this means a hint is asked only of a
    local or privately hosted provider; an operator who wants another one
    moves that provider's zone, which is the documented switch. A
    criteria-mode configuration has no provider of its own and counts as
    external, as it does for the tool loop; name a fixed-model configuration
    instead.

-   **When a hint is a guess**

    The model answers with a cause, a next step and the number of the frame
    the cause sits in. The extension, not the model, labels the hint: it is
    anchored only when the cited frame exists in the entry's stack and lies in
    the project's code (inside the project root, not under `vendor/`,
    `var/` or `typo3temp/`, which hold generated files, and, in a
    classic-mode installation, not under `typo3/sysext/`). Every other hint
    is shown as a guess.

-   **Surfaces**

    `nrllm:exception:hint` (`--index`, `--search`, `--configuration`;
    default configuration otherwise) prints the entry and the hint. It is not
    schedulable: a scheduler task would send every logged error out on a
    timer. The backend module *AI > Operation > Error hints* is
    administrator-only and lists the ten newest entries; a hint is asked for
    per entry by POST, so a page view or a prefetch never sends an error. The
    form posts a key of the entry, not its position, so an error logged in
    between is never sent in its place. The hint action checks the
    administrator again. The module used the default configuration; the
    amendment of 2026-11-06 lets each administrator choose. The
    call bypasses the answer cache and is attributed as
    `exception_hint` in the usage data.

-   **Not done**

    No hook in TYPO3's exception handlers. A model call while an exception is
    rendered would add a provider's latency and an outbound request to every
    error page, and a failing provider would fail inside the handler.

## Consequences {#consequences}

-   An administrator gets a cause and a next step without opening the chat,
    and can tell an anchored hint from a guess by its heading.
-   On an installation whose providers are all external with default zones,
    both surfaces refuse with the zone named. That is the data-class rule
    applied, not a defect.
-   The failure signature of a regression in the guess rule is a hint shown
    as anchored whose frame is under `vendor/` or missing.

## Amendment (2026-11-06): each administrator chooses the configuration {#amendment-2026-11-06-each-administrator-chooses-the-configuration}

Where the default configuration reaches an external provider, every hint in
the module was refused, in English inside a German backend, and the module
offered no way to ask another configuration; only the command line had
`--configuration`.

-   **Where the choice is kept**

    In the backend user's module data for `nrllm_exceptions`, under
    `configuration`, written by a POST to the module's `choose` action
    and only for an active configuration; an empty value means the default
    configuration. An extension setting, as `decision.configuration` is for
    the decision service, was the alternative. It was not taken: the module
    would have to write `settings.php`, which no nr-llm module does and
    which a deployment that versions that file overwrites, and a setting is
    one answer for every administrator although the module is a per-person
    tool. The key is deliberately not declared as `moduleData` default in
    the module registration: core's `BackendModuleValidator` stores any
    declared key it finds in a query string, so a link could change the
    choice. A stored identifier is checked on every read; a deleted or
    deactivated one falls back to the default configuration and the page says
    so. The command line keeps `--configuration` and the default
    configuration; it reads no backend user's choice.

-   **What the list shows**

    Every active configuration with what a hint may send it, from
    `ExceptionHintService::admission()`, the same answer
    `ExceptionHintService::hint()` applies, fallback hops included. A
    configuration that may receive nothing stays selectable and is marked; a
    hint against it is refused with what to do (choose a configuration that,
    like each of its fallbacks, has a fixed model whose provider is local or
    privately hosted, or move the provider's zone) and links to
    *Configurations* and *Providers*.

-   **Language**

    A refusal carries an `ExceptionHintRefusal` reason and its
    arguments. The module words it from `locallang_mod_exceptions.xlf` in the
    administrator's language; the command prints the English text. The model
    is asked to write the cause and next step in the administrator's backend
    language, English on the command line.

-   **Switching the module off**

    No extension setting: none of nr-llm's administrator modules has one, and
    the module sends nothing until an administrator clicks. User or group
    TSconfig `options.hideModules = nrllm_exceptions` removes it from the
    module menu, for administrators too; the route itself stays reachable for
    administrators.
