ADR-185: An observed outcome is derived from history, inside a window
- Status
-
Proposed
- Date
-
2026-08-31
- Authors
-
Netresearch DTT GmbH
Context
ADR-176 split the per-call outcome into two sources and
shipped the explicit one. The observed one — did the generated text survive
contact with an editor — was blocked on a fact nobody persisted: which record a
tool write produced. ADR-182 persists it, as a tool_write
step naming a table and a uid.
So the join exists now. What it joins to, and what the answer MEANS, is not decided, and three things surfaced on the way here that decide it.
The correlation id is the run's uuid. tx_nrllm_agentrun has no
correlation_id column; Agent returns the
run uuid, and every provider round-trip of that run reports under it. An
observed outcome therefore attaches to a RUN, not to one round-trip. ADR-176
says "per-call" and this is narrower than that phrase suggests; a five-round run
that wrote once yields one observed row covering all five.
There is no UNKNOWN. Call has five cases and none of them says
"looked, could not tell". The absence of a row cannot stand in for it, because
absence also means "the window has not closed yet" — two facts one storage state
cannot hold, which the engineering constitution's sixth principle forbids.
The write target names a record, not the fields it wrote. ADR-182 kept it to two scalars deliberately. That is enough to ask whether a record changed and not enough to ask whether OUR text changed.
Decision
An observed outcome is derived once, after a window, from ``sys_history``, and says UNKNOWN whenever it cannot say better.
- A new ``UNKNOWN`` case on
Call, in the observed source. It is written, not implied: a derivation that ran and could not decide is a different fact from one that has not run.Outcome - A window, configurable, defaulting to seven days. Derivation happens after it closes, once per write, and the answer is final. Before it closes there is no row — which is why UNKNOWN has to be a value rather than an absence.
-
The classification, from history rows AFTER the write's own:
no row, record still present → ACCEPTED_UNCHANGED MODIFY → EDITED DELETE, or the record is gone → DISCARDED history trimmed past our write → UNKNOWNCopied!The last line is the subtle one. History is trimmed oldest-first, so what proves our write's row survived is not that SOME row exists at or before it — an older survivor answers that for a write whose own row is long gone — but that the OLDEST retained row is at or before it.
-
One outcome per RUN, combined from every record it wrote, by an explicit precedence:
DISCARDED > EDITED > UNKNOWN > ACCEPTED_UNCHANGEDCopied!A run can call more than one write tool and they share a correlation id, so judging only the first would let a run whose second write was deleted be recorded as accepted. A known negative outranks an unknown; and
ACCEPTED_UNCHANGEDmay only be claimed when every write of the run is known to have survived untouched. - The already-answered runs are excluded in the QUERY, not filtered after it. A fixed page of the oldest writes would stop advancing the moment a full page of them had been answered, and the command would then report no work while writes piled up behind it.
- A CLI command, schedulable, like the four purge commands this extension already ships. Never the request path — ADR-174's rule for the cost signal holds here for the same reason.
What EDITED means, exactly, and what it does not
Any modification by a human, in the window, after our write. Not "a modification to the text we generated" — that question needs the field list, and ADR-182 deliberately does not carry one.
The consequence is over-reporting, and it is stated rather than hidden: an editor who fixes an unrelated field on the same page inside the window makes our write read as EDITED. On a busy page that is wrong about the model and right about the record.
The alternative — widen the write target to name the fields — is a real option and is not taken here, because ADR-182's revisit clause is explicit that the shape changes when a reader asks, and this reader can begin without it. The first measured rate of EDITED is what says whether it must.
What this does not build
No promotion rule. ADR-156's second criterion becomes computable; whether a delta justifies promoting a canary is a decision this record does not make.
No editor-level readout. ADR-176's rule stands: no be_user on the
outcome row, no per-person view. The question is how the model did, not who
edited.
Approval is still not quality. ADR-176 separated them and ADR-184 added a refusal that is explicitly not a signal. A stale-refused approval produces no outcome at all, because no write happened.
No second definition of "changed". ADR-184 compares preview lines to decide whether an approval still holds. This compares history rows to decide what became of a write. Same word, different question, different data — and they must not be folded together, for the reason ADR-136 gave when it refused to fold the turn digest into a resource check.
Consequences
- ✅
ACCEPTED_UNCHANGED,EDITEDandDISCARDEDacquire a writer, andCallstarts returning true for them.Outcome:: is Implemented () - ✅ A derivation that cannot decide says so, so "no signal" and "no answer" stay apart in the data.
- ⚠️ The outcome is per RUN, because the correlation id is. Any readout that calls it per-call is describing something narrower than its name.
- ⚠️ EDITED over-reports by construction until the write target names fields. A readout that ignores this will read an editorial habit as a model deficit.
- ⚠️
sys_historyhas its own retention, set by the installation and not by this extension. A window longer than that retention yields UNKNOWN rather than ACCEPTED_UNCHANGED — which is the whole reason UNKNOWN exists.
Revisit when
- EDITED is measured and high. That is the trigger for carrying the written field names, and the number is what justifies it rather than the suspicion.
- The precedence proves wrong in practice. A run whose several writes end differently is recorded by the worst of them, which is a choice and not a measurement. If the mix turns out to matter, the outcome has to become one row per write — and that needs a key the outcome table does not have.
- A caller needs the outcome per round-trip. That needs a per-call identifier the runtime does not have today, and inventing one is a larger change than this record.