ADR 043 — A halt payload records the attempts that produced the result¶
Status¶
Superseded by 045. The attempt summary
survives; the non_verdicts count and the abstained outcome were removed
with the abstention state. See 045.
Context¶
The halt payload is the machine-readable account of what happened to a task: an orchestrator, the CLI, and snodo cloud read it. It carried the final validator results and the final policy decision, and nothing about how the task reached them. A task that passed first time and a task that passed on the fourth attempt after three silent judges produced payloads a reader could not distinguish.
Observed on a real project: a task took four attempts over forty-five minutes,
three acceptance judgements abstained, and three coder dispatches produced no
changes. The payload read status: completed, unanimous pass,
abstain_count: 0. Every failure was absent; the only surviving trace was a
sentence buried in another validator's prose.
The information was not missing. Task.prior_failures already accumulates
failures across recovery attempts, tagged with 1-based attempt numbers and
including abstentions, because it is what the recovery spec is built from.
LoopState.abstention_retries already counts the in-place abstention re-judges
introduced by ADR 042. Both reached the recovery spec and the audit events and
stopped there.
Decision¶
1. The halt payload carries an attempts summary.
attempts.total is the number of judged attempts the task took;
attempts.non_verdicts is how many of them ended without a verdict;
attempts.coder_dispatches is how many coder runs the chain cost; and
attempts.history enumerates each attempt as {attempt, outcome} with a
canonical outcome (passed, warned, blocked, abstained, error). The
tokens are the same across projects, unlike the prose in validator_results,
so payloads aggregate: a consumer can count non-verdicts and coder dispatches
without reading a judge's words.
2. An attempt is one judged pass over a code state.
The root is attempt 1, each recovery subtask is the next attempt, and each in-place abstention re-judge is an attempt of its own — it re-runs a judge on unchanged work and dispatches no coder. The two invisible-to-the-results categories, prior recovery attempts and in-place re-judges, are exactly what the summary recovers.
3. The counts are exact; the enumerated history is bounded.
total, non_verdicts and coder_dispatches are counts over the whole chain.
history keeps the most recent _MAX_ATTEMPT_HISTORY entries and reports the
number omitted in attempts.omitted, so a pathological run cannot grow the
payload without bound.
4. Halt types and final results are untouched.
No halt type is added or changed, and the final results stay in
validator_results / policy_decision. The summary is the history that
precedes them, not a second name for them.
Consequences¶
- A clean pass reports one attempt and zero non-verdicts; a hard-won pass reports the chain, so the two are distinguishable from the payload alone.
- The cost of a completed task — judge retries and coder dispatches — is answerable from payloads, which is what the cloud and any dashboard count.
- The summary is derived, not journaled: it is reconstructed from
prior_failuresandabstention_retries. A prior attempt's internal re-judges are not individually retained (only the attempt's final verdict is), so the history is a faithful attempt-level account rather than a judge-call-level one.
Alternatives¶
- Add a mutable attempt journal to
Task, appended at each judge run. Rejected: it would carry the same information the engine already accumulates, in a second place that can drift, and widen the core model and the closure serde for no structural gain. - Restate the final results under an
attempts.finalkey. Rejected: the payload already carriesvalidator_results; the summary is the preceding history, and a second copy is the drift this ADR avoids. - Emit the full history unbounded. Rejected: the payload is read by orchestrators and the cloud, and a long chain must not inflate it; the exact counts preserve the aggregation while the list stays bounded.