Skip to content

fix(human-decision): give operator disagreement a closed wire channel - #15

Merged
abrichr merged 1 commit into
mainfrom
feat/attended-reject-action
Jul 28, 2026
Merged

fix(human-decision): give operator disagreement a closed wire channel#15
abrichr merged 1 commit into
mainfrom
feat/attended-reject-action

Conversation

@abrichr

@abrichr abrichr commented Jul 28, 2026

Copy link
Copy Markdown
Member

Why

The attended decision vocabulary is closed at continue / skip / teach / escalate. When the engine is correct to halt — the record genuinely says Ineligible — the operator's only one-tap options are to assert they fixed something (verify_and_resume) or to hand it to a colleague (escalate). There is no way to say "you were right, stop."

Consequences:

  • Disagreement has no channel, so the answer distribution can never report how often halts are correct — data needed before tuning halt rates.
  • The absence of a visible reject option communicates that disagreement is not an expected outcome.
  • The only one-tap resolution is the agreeable one, which is the shape that produces rubber-stamping.

What

  • HumanDecisionAction.REJECT = "reject".
  • HumanDecisionReceiptState.REJECTED + HumanDecisionReceiptReason.REJECTED_BY_OPERATOR, pinned as a pair in HUMAN_DECISION_RECEIPT_REASONS.
  • allowed_actions max_length 4 → 5.
  • Regenerated human-decision-task-v1.json / human-decision-receipt-v1.json.

Design notes

reject is not a relabelled escalate. escalate parks the run — the durable pause stays intact and a colleague can continue it. reject terminates it. Collapsing them destroys the signal the action exists to obtain.

reject is scoped to one run. It asserts that this run must not proceed, not that the saved workflow is wrong. The latter changes future runs and belongs to teach, which carries the demonstration and requalification gate that authority requires.

No free text anywhere. The cause is a closed enum with exactly one member today. A reason taxonomy would make the distribution far more informative, but there is no evidence yet for its members — the reject rate is the data that would design them — and a wrong taxonomy is worse than none. Adding members later is additive; a free-text reason could never be closed again.

max_length 4 → 5 is the load-bearing part of the additive change. The bound is the size of the vocabulary, not a policy. A pause that is skippable, re-verifiable, rejectable, teachable and escalatable is legitimate, and a stale bound would have refused to issue that task at all — a refusal in exactly the case where the operator has the most choice. Covered by a new test.

Release

fix: on purpose, not feat:. major_on_zero = false means feat: would cut 0.7.0 and fall outside openadapt-flow's openadapt-types>=0.6.2,<0.7.0 pin. This lands as 0.6.3, inside the pin. Flow's floor is raised to >=0.6.3 in its own PR, because a floor that predates a new symbol fails at import, not at resolve.

Precedent: #13 (fix: close the attended terminal-outcome contract gap with HumanDecisionReceiptV1) added the entire receipt model under fix:.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NyCHrzA1psrKMFfroYbzaM

The attended vocabulary was closed at continue/skip/teach/escalate. An
operator who looked at the live application and concluded the engine was
right to stop had no way to say so: escalate PARKS the run for a colleague
and reads as "I don't know", and continue asserts a repair that did not
happen. Disagreement therefore had no channel, so the recorded answer
distribution could not report how often a halt was correct -- the exact
measurement needed before anyone tunes a halt rate.

Add `reject` to `HumanDecisionAction` and the terminal `rejected` /
`rejected_by_operator` pair to the receipt. Reject and escalate stay
separate members because they do opposite things to the run: escalate
leaves the durable pause resumable, reject ends it. Collapsing them is
cheaper and destroys the signal.

`allowed_actions` widens from four to five entries, which is the size of
the vocabulary rather than a policy: a pause that is skippable,
re-verifiable, rejectable, teachable and escalatable is legitimate, and a
stale bound would have refused to issue that task at all.

The cause of a rejection is a closed enum with one member. A reason
taxonomy would be more informative, but there is no evidence yet for what
its members should be -- the reject rate is the data that would design
them -- and adding members later is additive, whereas a free-text reason
could never be closed again.
@abrichr
abrichr merged commit 995d4d0 into main Jul 28, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant