feat(attended): record what kind of decider answered, not only which identity - #301
Open
abrichr wants to merge 2 commits into
Open
feat(attended): record what kind of decider answered, not only which identity#301abrichr wants to merge 2 commits into
abrichr wants to merge 2 commits into
Conversation
…identity An agreement rate drawn from the attended journal is a claim about what PEOPLE concluded when they read the evidence. The journal could not support that claim, and nothing said so. `AttendedDecision.operator` records WHICH identity a decision is attributed to. It has never recorded WHAT KIND of decider produced it, and the two are different questions. The MCP bridge in `openadapt-agent` exposes `continue_attention` and `skip_attention` as model-callable tools, and derives its operator from the same `_local_operator_identity()` a person's own console uses. So a decision a MODEL submitted and one a PERSON submitted from the same machine carried an IDENTICAL operator string. Every rate computed over this journal mixed the two populations with no way to separate them afterwards. `decided_by` is a closed enum -- human / automation / unknown -- beside `operator`, asserted by the trusted caller in exactly the same way. It is deliberately NOT a field on `AttendedActionRequest`. That is what a relay or an MCP client puts on the wire and `request_digest` commits to it, so putting provenance there would let the party whose nature is in question be the party declaring it. Pinned by test. The default is `unknown`, not `human`, and that is the safety property. A caller that does not declare is EXCLUDED from a human rate rather than silently counted into it, and every decision written before this field existed reads back as `unknown` -- which is true, because the bridge predates the field. Defaulting to `human` would have re-created the same bias one level down, where nobody would look for it. The console and the authenticated AAL2 relay therefore assert `human` explicitly, and a test pins that they do: if they ever stopped, genuine operator answers would fall silently out of the sample and the failure would look like nothing. `AttendedActionService.execute` relays the value unchanged. It owns the live executor, not the question of who decided, and inventing an answer there would attribute a decision on behalf of a caller that never made the claim. No free text is added anywhere, and the PHI-free `HumanDecisionReceiptV1` is untouched: it carries no operator identity by design, and provenance belongs to the local append-only audit record that already carries one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NyCHrzA1psrKMFfroYbzaM
abrichr
enabled auto-merge (squash)
July 28, 2026 03:24
abrichr
disabled auto-merge
July 28, 2026 05:06
Member
Author
|
Launch-train status (2026-07-28): this PR remains intentionally held behind the exact fresh-frame/OpenEMR campaign gate. Development continues in parallel; this is only a merge-order constraint on Flow main. Before merge, it needs one fresh-main rebase plus the schema-v1 digest/backward-compatibility check called out in the release review. Do not rerun the broad matrix until that bounded correction is ready; we will use one authoritative exact-head run. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds
decided_by— a closedhuman/automation/unknownenum — to Flow's append-only attended decision journal.The defect
An agreement rate drawn from this journal is a claim about what people concluded when they read the evidence. The journal could not support that claim, and nothing said so.
AttendedDecision.operatorrecords which identity a decision is attributed to. It has never recorded what kind of decider produced it. Those are different questions, and only one of them was answerable.The MCP bridge in
openadapt-agentexposescontinue_attentionandskip_attentionas model-callable tools, and derives its operator from the same_local_operator_identity()a person's own console uses. So a decision a model submitted and one a person submitted from the same machine carried an identicaloperatorstring. There is no query that separates them, and there never was — including retroactively, over every decision already recorded.This is the same shape as the gap
reject(#295) was added to close, one level down: a number that cannot be qualified, presented as if it could.Why not on the receipt, and why not in the request
Not
HumanDecisionReceiptV1. That contract carries no operator identity by design — it is the PHI-free artifact that crosses to a phone or a relay. Provenance belongs to the local append-only audit record, which already carries a principal on purpose. The receipt is untouched.Not
AttendedActionRequest. That is what a relay or an MCP client puts on the wire, andrequest_digestcommits to it. Putting provenance there would make the party whose nature is in question the party declaring it. It sits besideoperatorinstead — asserted by the trusted caller, in the same way, for the same reason. Pinned bytest_a_submitter_cannot_describe_its_own_nature.No free text anywhere. A closed enum from the start, so a new member is additive.
The default is the safety property
unknown, nothuman.A caller that does not declare is excluded from a human rate rather than silently counted into it. And every decision written before this field existed reads back as
unknown— which is true, because the bridge predates the field. Defaulting tohumanwould have re-created the identical bias one level down, in a place nobody would look for it.The consequence is that the human paths must say they are human, and they do:
console/app.py)humanconsole/human_decisions.py)humanAttendedActionService.executetest_the_console_and_the_phone_route_assert_a_person_decidedpins the assertion, because if the console ever stopped making it, genuine operator answers would fall silently out of the sample and the failure would look like nothing at all.The measurement consequence, stated as the query
test_a_human_answer_and_an_automated_answer_are_told_apart_in_the_journaldrives both answers into one journal with the same operator string, asserts thatoperatorcannot separate them, and then does what an analyst would actually do:and asserts the automated answer is genuinely excluded, not merely labelled. If it could not be, the field would be decoration.
Mutation proof
Each mutation is caught by the test written for it, and by no other.
unknown→humantest_an_undeclared_caller_is_excluded_from_a_human_rate_not_counted_into_it, alonehumantest_the_console_and_the_phone_route_assert_a_person_decided+ the told-apart testtest_a_submitter_cannot_describe_its_own_nature, aloneInterface note
AttendedActionService.executegains a defaulted keyword. Callers are unaffected; implementers must accept it, and there is exactly one real implementer — Flow's own class.openadapt-agentconstructs that class rather than its own. The two fakes in this repo's tests are updated, and the console fake now records the value so it can pin what the console asserts.Local verification
tests/test_attended_actions.py+tests/test_mobile_attended_decision_e2e.py: 88 passed.ruff check openadapt_flow,ruff format --check openadapt_flow tests,mypy(218 files): clean.Next
openadapt-agentsetsdecided_by="automation"and gainsreject_attention. It depends onopenadapt-flow>=1.18.1, so it needs the release that carries this — batched with #294 and #296.🤖 Generated with Claude Code
https://claude.ai/code/session_01NyCHrzA1psrKMFfroYbzaM