Skip to content

fix(portal): teach the phone the receipt shape and say what actually broke - #79

Merged
abrichr merged 1 commit into
mainfrom
fix/portal-decision-detail
Jul 27, 2026
Merged

fix(portal): teach the phone the receipt shape and say what actually broke#79
abrichr merged 1 commit into
mainfrom
fix/portal-decision-detail

Conversation

@abrichr

@abrichr abrichr commented Jul 27, 2026

Copy link
Copy Markdown
Member

The founder paired a phone to a real halted run and could not tell what to do. The decision view asked "Is the live application ready for OpenAdapt to verify and continue?", offered I fixed it, verify and continue / Teach the correction / Needs more help, and said neither what broke nor what any of the three answers would do. The engine knew: that pause carried category: resolution and "could not resolve target for step_000 (click 'Open'): all resolution rungs failed".

BLOCKING: the receipt shape

engine/portal/shell/app.js read body.status / body.message. openadapt-flow PR #290 replaces that reply with the closed HumanDecisionReceiptV1, which carries state / reason_code and deliberately no message. Every terminal outcome therefore fell through to the else branch and rendered as "That decision was refused" — a real halted was observed doing exactly that.

Every (state, reason_code) pair Flow can emit now maps to its own operator copy in RECEIPT_COPY. A pair this build does not recognise is reported as an unknown outcome, never as a refusal: showing a real terminal outcome as some other outcome is the whole failure this table exists to prevent. A runner still returning the older decision record is mapped through Flow's own _RECEIPT_STATE table so it renders identically, and the engine's free-text message is never rendered even when present.

openadapt-flow#290 is unblocked by this PR.

Say what broke, and what "fixed" means

Flow's local decision detail (presentation.halt) carries closed values only: the typed halt category, the step's ordinal, its action kind, the target's role, and the target's label only when the engine proved that label is static control chrome rather than record content. The shell composes the sentence, because the wire carries no prose — that is what keeps a typed value off it:

Step 1 of 6 could not start: OpenAdapt could not find the button labelled “Open” on the screen, so it did not click anything.

A withheld label degrades to the role noun alone, so a TYPE step reads "could not find the text field", never the value. The view also lists which resolution rungs were tried and their verdicts (rungs with no recorded evidence are shown as never attempted, not as failures), and what a continue will re-check before anything happens.

Distinguish the three answers by consequence

Which is what could not be told apart. Each button carries its consequence, and the card states it in full:

  • check and continue — this run only; the saved workflow is unchanged, so the same drift stops the next run
  • teach the correction — changes future runs; nothing continues now
  • needs more help — the run stays paused exactly where it is

The two cosmetic defects, causes established

  1. The action bar stayed visible after a terminal receipt. .actions { display: grid } outranks the user-agent [hidden] { display: none } rule, so actionBar.hidden = true set an attribute that nothing acted on. Fixed with an explicit .actions[hidden] { display: none }.
  2. The outcome text sat under the fixed bar. The page reserved a constant 200px, which three two-line buttons exceed. The bar's real height is now measured into --action-bar-height and released when it is hidden.

Also: a fetch that never returns is now status 0 — "the result is uncertain" — instead of an unhandled rejection leaving "Submitted, waiting…" on screen forever.

Verification

src/portalShell.test.ts drives the shipped app.js in a DOM rather than grepping it, because every existing source-text assertion passed while the surface a human reads showed every outcome as a refusal. 53 frontend tests, 737 Python tests, tsc && vite build, and ruff check pass locally; the loop was also driven end to end against a real halt over the live tunnel from a mobile device profile.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NyCHrzA1psrKMFfroYbzaM

…broke

The founder paired a phone to a real halted run and could not tell what to do.
The decision view asked "is the live application ready for OpenAdapt to verify
and continue?" and offered three answers, and it said neither what broke nor
what any of the three would do.

BLOCKING: the receipt shape. `app.js` read `body.status`/`body.message`. Flow
PR #290 replaces that reply with the closed `HumanDecisionReceiptV1`, which has
`state`/`reason_code` and deliberately no message, so every terminal outcome
fell through to the else branch and rendered as "That decision was refused" --
a real `halted` was observed doing exactly that. Every `(state, reason_code)`
pair now maps to its own copy, and a pair this build does not recognise is
reported as an unknown outcome rather than a refusal, because showing a real
terminal outcome as some other outcome is the failure this table prevents. A
runner still returning the older decision record is mapped through Flow's own
status table so it renders identically; Flow #290 is no longer blocked on
Desktop.

Say what broke. Flow's local decision detail carries a typed halt category, the
step's ordinal, its action kind, the target's role, and the target's label only
when the engine proved that label is static control chrome. The shell composes
the sentence -- "Step 1 of 6 could not start: OpenAdapt could not find the
button labelled Open on the screen, so it did not click anything" -- because
the wire carries closed enums and no prose, which is what keeps a typed value
off it. A withheld label degrades to the role noun, so a TYPE step reads "could
not find the text field". It also lists which resolution rungs were tried and
what continuing will re-check.

Distinguish the three answers by consequence, which is what could not be told
apart: check and continue is this run only and the same drift stops the next
run; teach the correction changes future runs and continues nothing now; needs
more help leaves the run paused and untouched.

Two cosmetic defects, both with established causes. The action bar stayed
visible after a terminal receipt because `.actions { display: grid }` outranks
the user-agent `[hidden] { display: none }` rule, so setting the attribute did
nothing; an explicit `.actions[hidden]` fixes it. The outcome text sat under
the fixed bar because the page reserved a constant 200px, which three
two-line buttons exceed; the bar's real height is now measured and released
when it is hidden.

Also: a fetch that never comes back is now status 0 -- "the result is
uncertain" -- instead of an unhandled rejection leaving "Submitted, waiting"
on screen forever.

`src/portalShell.test.ts` drives the shipped `app.js` in a DOM rather than
grepping it, because every existing source-text assertion passed while the
surface a human reads showed every outcome as a refusal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NyCHrzA1psrKMFfroYbzaM
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