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

Status

Accepted (the re-load no longer skips a deactivated source — see ADR-166)

Date

2026-08-13

Amends

ADR-164 (its "does not cover a resumed run" gap) and ADR-084 (a further field on the suspended state)

Amended

2026-08-13 by ADR-166

Authors

Netresearch DTT GmbH

Context 

ADR-164 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'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 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 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 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.