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; AgentRunReference::correlationId() 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. CallOutcome 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 CallOutcome, 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.
  • 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   → UNKNOWN
    Copied!

    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_UNCHANGED
    
    Copied!

    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_UNCHANGED may 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, EDITED and DISCARDED acquire a writer, and CallOutcome::isImplemented() starts returning true for them.
  • ✅ 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_history has 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.