---
title: "ADR-165: A resumed run re-gates its forced sources"
manual: "TYPO3 LLM Extension"
version: "0.35"
permalink: "https://docs.typo3.org/permalink/netresearch/nr-llm:adr-165@0.35"
source: "Adr/Adr165ResumeReGatesTheForcedSet.rst"
modified: "2026-09-16T22:09:16+00:00"
---

# ADR-165: A resumed run re-gates its forced sources

-   *Status:*

    Accepted (the re-load no longer skips a deactivated source — see
    [ADR-166](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-166@0.35))

-   *Date:* 2026-08-13
-   *Amends:*

    [ADR-164](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-164@0.35) (its "does not cover a resumed run" gap) and
    [ADR-084](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-084@0.35) (a further field on the suspended state)

-   *Amended:* 2026-08-13 by [ADR-166](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-166@0.35)
-   *Authors:* Netresearch DTT GmbH

## Context

[ADR-164](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-164@0.35) makes a run's forced snippets and skills bind against
the ADR-144 trust ceiling, and named the one place it did not reach:
`ToolLoopService::resume()` and `resumeWithInput()` re-enter the loop
with assembly skipped and no augmentation, because
`SuspendedRunState` did not carry one. Issue `#761`.

The bound was narrow and worth restating, because it decides how much machinery
this justifies. The forced text enters the transcript at assembly time, where
ADR-164 gates it, so a resume injects nothing new. What a resume could miss is a
ceiling that changed *while* the run was suspended: the configuration re-pointed
at a less trusted provider or given a fallback that reaches one, or
`dataClassEnforcement` switched from `observe` to enforcing. Both of those
already re-gate the configuration's *own* sources, because
`assertContextPermitted()` runs on every pipeline call against the live
configuration. Only the forced half was frozen out.

## Decision

**The suspended state carries the forced set as uids.**
`SuspendedRunState` gains `$forcedSnippetUids` and
`$forcedSkillUids`, written at both suspend sites and read on both resume
paths. A row persisted before this ADR has neither key and rehydrates with
none — the pre-ADR-165 behaviour, never a refusal to resume. That is the same
back-compat shape [ADR-136](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-136@0.35)'s call previews use, for the same
reason: a running installation has such rows in its database.

**Uids, and re-loaded on resume.** The alternative was to freeze the
*classification* at suspend time and re-compare it against the live zone. That
would also have closed the two risks above, without a repository lookup. Uids
win because the resume then reflects the record as it is now: an operator who
raises a snippet's class while a run is suspended means it, and a frozen copy
would ignore them. It is the same identity-over-snapshot choice
`AgentRunRequestCodec` already makes for a queued run's entities.

**The state, not the queued request.** `AgentRunRequestCodec::rehydrate()`
can already recover a queued run's forced set from `queuedRequest`, which
looks like it would do the job with no new field. It would not:
`queuedRequest` is null for a run started synchronously from the playground —
and that is precisely the path that motivated ADR-164. The suspended state is
written by both.

**Re-loaded in the loop, not in the coordinator.** `ToolLoopService` takes
the two repositories as optional collaborators and rebuilds the augmentation
itself. `ResumeCoordinator` has two call sites into the loop and would have
had to thread it through both; the loop has one place where a resumed run
re-enters. Optional, like every other collaborator on that constructor: a
construction without them keeps the pre-ADR-165 behaviour rather than failing.

## What this does not do

**It does not resurrect a deleted source.** A uid that no longer resolves —
the snippet was deleted while the run was suspended — contributes nothing to the
classification. The transcript still carries its text, so for that one source
the answer degrades to the pre-ADR-165 one. Refusing the resume instead would
strand a run over a record an operator deliberately removed, and would make
deleting a snippet a way to break running work.

A source merely **deactivated** does not fall under that paragraph. The re-load
originally used an active-only lookup and dropped one, which was a defect, not a
decision; [ADR-166](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-166@0.35) states the distinction and fixes it.

**It does not re-gate anything else about a resume.** The transcript, the
allow-list and the options are restored exactly as [ADR-084](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-084@0.35)
established. Only the forced set is added to what the ceiling can see.

## Consequences

A run suspended for approval or typed input, whose configuration is re-pointed
at a less trusted provider before the approver answers, is now refused on resume
rather than sending. The refusal names the forced source, as
[ADR-164](https://docs.typo3.org/permalink/netresearch/nr-llm:adr-164@0.35) requires.

An installation that has classified nothing is unaffected, and a run that forced
nothing hands over null exactly as before — the gate keeps its
configuration-only path rather than folding an empty list.
