なぜ lands on a window that explains nothing when an approval is outstanding #72

Open
opened 2026-07-31 13:09:28 +00:00 by aiko · 0 comments
Owner

Found on 2026-07-31 by the cross-seam review of the #62#68 batch.

The sequence

  1. An approval-request arrives and the card is drawn.
  2. The client socket drops.
  3. twoHopStatus yields socket-closed. The pal bar goes 不通 and offers なぜ — pal-bar.tsx draws that key off the fault alone, with no reference to an outstanding ask.
  4. Press it. #64 wires it to setMode('window'), so window mode mounts.
  5. The fault screen is not there. receiver-chrome.tsx withholds FaultScreen while approval !== null — a suspended turn outranks a fault screen, which is a deliberate #59-era rule.

What the operator gets instead is the approval card with 送信, ハンズフリー and 文 all disabled by #65's socketHeld, and no statement anywhere of why they are dead. socketHeld is keyed on the fault existing, not on its screen being drawn, so the keys go inert exactly when the explanation is suppressed.

Why it does not clear itself

This state is durable, not a frame of jitter.

  • The ask cannot be answered away. use-approvals.ts's send short-circuits on a non-OPEN socket and calls restoreAsk(..., 'offline'), putting the same ask straight back at the head.
  • #62's dismiss cannot clear it either. An approval-dismiss frame can only arrive over the socket that is shut.

So the fault screen stays unreachable until the socket returns — which is precisely the thing the operator pressed なぜ to find out about.

Why nothing caught it

#64's own test asserts that every fault putting なぜ on the bar has a non-null faultFor(hops) behind it. That is true and insufficient: it proves a fault screen exists for that state, never that receiver-chrome will render it. The approval === null guard is not in the test's world at all.

Each ingredient is defensible alone. #64 created the affordance; #65 made the destination's keys inert for a good reason; #62 defined the only remote way a card leaves the screen; the approval-outranks-fault rule predates all three. Only the combination produces a window with a dead dock, no explanation, and no way out.

Ways it could go

Not prescribing one — this is a design call about what outranks what.

  • Draw both. The fault as a band or strip above the card rather than a full screen it suppresses.
  • Let the fault win when the socket is what broke the ask. An ask that cannot be answered is arguably not a suspended turn worth protecting.
  • Say it on the card. Cheapest: when socketHeld holds keys under a suppressed fault, the card itself carries the reason.
  • Do not offer なぜ when it leads nowhere. Weakest — it hides the problem rather than answering it, and the operator still has a dead dock.

Acceptance

  • Under a shut socket with an outstanding approval, the operator can find out why the keys are dead
  • なぜ never lands on a window that explains nothing
  • The decision about what outranks what is written down where the next reader finds it
  • A test covers the fault-plus-approval overlap, not just the fault alone

denpa#64, denpa#65, denpa#62, denpa#59, ADR-0012

Found on 2026-07-31 by the cross-seam review of the `#62`–`#68` batch. ## The sequence 1. An `approval-request` arrives and the card is drawn. 2. The client socket drops. 3. `twoHopStatus` yields `socket-closed`. The pal bar goes 不通 and offers なぜ — `pal-bar.tsx` draws that key off the fault alone, with no reference to an outstanding ask. 4. Press it. `#64` wires it to `setMode('window')`, so window mode mounts. 5. **The fault screen is not there.** `receiver-chrome.tsx` withholds `FaultScreen` while `approval !== null` — a suspended turn outranks a fault screen, which is a deliberate `#59`-era rule. What the operator gets instead is the approval card with 送信, ハンズフリー and 文 all disabled by `#65`'s `socketHeld`, and **no statement anywhere of why they are dead**. `socketHeld` is keyed on the fault existing, not on its screen being drawn, so the keys go inert exactly when the explanation is suppressed. ## Why it does not clear itself This state is durable, not a frame of jitter. - **The ask cannot be answered away.** `use-approvals.ts`'s `send` short-circuits on a non-OPEN socket and calls `restoreAsk(..., 'offline')`, putting the same ask straight back at the head. - **`#62`'s dismiss cannot clear it either.** An `approval-dismiss` frame can only arrive over the socket that is shut. So the fault screen stays unreachable until the socket returns — which is precisely the thing the operator pressed なぜ to find out about. ## Why nothing caught it `#64`'s own test asserts that every fault putting なぜ on the bar has a non-null `faultFor(hops)` behind it. That is true and insufficient: it proves a fault screen *exists* for that state, never that `receiver-chrome` will *render* it. The `approval === null` guard is not in the test's world at all. Each ingredient is defensible alone. `#64` created the affordance; `#65` made the destination's keys inert for a good reason; `#62` defined the only remote way a card leaves the screen; the approval-outranks-fault rule predates all three. Only the combination produces a window with a dead dock, no explanation, and no way out. ## Ways it could go Not prescribing one — this is a design call about what outranks what. - **Draw both.** The fault as a band or strip above the card rather than a full screen it suppresses. - **Let the fault win when the socket is what broke the ask.** An ask that cannot be answered is arguably not a suspended turn worth protecting. - **Say it on the card.** Cheapest: when `socketHeld` holds keys under a suppressed fault, the card itself carries the reason. - **Do not offer なぜ when it leads nowhere.** Weakest — it hides the problem rather than answering it, and the operator still has a dead dock. ## Acceptance - [ ] Under a shut socket with an outstanding approval, the operator can find out why the keys are dead - [ ] なぜ never lands on a window that explains nothing - [ ] The decision about what outranks what is written down where the next reader finds it - [ ] A test covers the fault-plus-approval overlap, not just the fault alone ## Related `denpa#64`, `denpa#65`, `denpa#62`, `denpa#59`, ADR-0012
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
aiko/denpa#72
No description provided.