---
title: "ADR-166: Deactivating a source does not lower a ceiling"
manual: "TYPO3 LLM Extension"
version: "0.35"
permalink: "https://docs.typo3.org/permalink/netresearch/nr-llm:adr-166@0.35"
source: "Adr/Adr166DeactivationDoesNotLowerACeiling.rst"
modified: "2026-09-16T22:09:16+00:00"
---

# ADR-166: Deactivating a source does not lower a ceiling

-   *Status:* Accepted
-   *Date:* 2026-08-13
-   *Amends:* [ADR-165](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-165@0.35) (which lookup the resume re-gate uses)
-   *Amended:*

    2026-08-18 by [ADR-175](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-175@0.35) (its "the skill half was
    already correct" held for the resume path and not for the two composition
    paths)

-   *Authors:* Netresearch DTT GmbH

## Context

[ADR-165](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-165@0.35) rebuilds a resumed run's forced set from the uids the
suspend persisted, so the [ADR-164](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-164@0.35) ceiling still sees it. It did
that through `PromptSnippetRepository::findByUids()`, which filters
`is_active = true`.

That filter is the whole defect. The repository's `initializeObject()`
already sets `setIgnoreEnableFields(true)`, so TYPO3's `hidden` field
plays no part; the explicit `is_active` clause was the only thing deciding,
and an operator who switched a snippet off while a run was suspended made it
vanish from the re-gate. Both lists then came back empty,
`augmentationFrom()` returned null, and the send took the
configuration-only gate path — the pre-ADR-165 answer, silently.

The text was still there. It sits in the persisted transcript, it goes on the
wire on the very next send, and its classification is what the ceiling exists to
weigh. Nothing about deactivating a record removes it from a conversation that
is already carrying it.

## Decision

**"Inactive" means "not for new composition". It does not mean "already-injected
text loses its classification".** Those are two different questions, and the
repository now answers them with two methods.

`findByUids()` keeps its active-only semantics unchanged: it is the lookup
for a prompt being assembled *now*, and a snippet an operator switched off must
not enter one. `findExistingByUids()` is new and drops only the
`is_active` clause — same contract otherwise: input order preserved, unknown
uids silently skipped, a **deleted** record still resolving to nothing.
`ToolLoopService::augmentationFrom()` uses the second one, because the text
it is classifying is already in the transcript.

The two are deliberately not merged, and each carries a docblock saying which
question it answers.

**The skill half was already correct.** `SkillRepository` ignores enable
fields for the same reason the snippet repository does, and
`augmentationFrom()` filters `findAll()` by uid alone — no `enabled`
clause anywhere on that path. A skill disabled while a run was suspended
therefore always kept its classification on resume. Only the snippet branch had
an explicit filter to trip over, so only the snippet branch needed a change.

## What this does not do

**It does not reverse any of ADR-165's three decisions.** They stand exactly as
written:

-   the **live record wins** over a frozen classification (identity over
    snapshot), so a class raised *or lowered* while the run was suspended takes
    effect;
-   a **deleted** source resolves to nothing and contributes nothing — the
    deleted restriction stays on in `findExistingByUids()`, so refusing a
    resume over a record an operator removed is still not what happens;
-   a **legacy suspended row with no uids** rehydrates with none and resumes,
    never refuses.

**It does not make an inactive snippet usable again.** Nothing composes it into
a new prompt, nothing offers it in the playground, and a fresh run that forces
it gets it through the active-only lookup like any other. The only thing it
regains is being *counted* by a ceiling that is judging text already sent.

**It does not widen what the ceiling reads.** The forced set is the same set
ADR-164 defined; this changes which rows resolve, not which rows are asked for.

## Consequences

A run suspended for approval or typed input, one of whose forced snippets is
switched off before the approver answers, is re-gated against that snippet's
class exactly as if it were still active — and is refused when the ceiling has
meanwhile dropped below it. Before this, deactivating a snippet mid-run was a
way to quietly lower a run's ceiling while its text kept going out.

An installation that has classified nothing is unaffected, and a run that forced
nothing still hands over null.
