The pal keeps 文 and 受信 live under the shut socket the chrome holds them for #71

Closed
opened 2026-07-31 13:09:28 +00:00 by aiko · 1 comment
Owner

Found on 2026-07-31 by the cross-seam review of the #62#68 batch, not by a failing test.

What is true

#65 gave the window chrome a rule: a shut socket holds the keys that cannot work under it. receiver-chrome.tsx derives socketHeld from fault.kind === 'socket-closed' and threads it into Dock, TransmissionBand and KeyRow, where 送信, ハンズフリー and 文 go disabled.

PalBar never got the rule. It carries the same 文 and 受信 keys and takes the same fault (pal-bar.tsx's fault?: PalFaultKind prop), but 文 has no disabled at all and 受信 is disabled={micHeld} only. So in pet mode and in the Android overlay both keys stay pressable under the very socket the bar is drawing as 不通, and pressing 送信's equivalent raises the error #65 was filed to stop.

Both surfaces are deciding from the same fact. two-hop.ts's palFaultFor returns 'local-down' exactly when local.mark === 'down', and fault-spec.ts returns 'socket-closed' on the identical test. The disagreement is not about what is true; it is that only one surface was taught what to do about it.

Why it is worth its own ticket

The gap is #65's alone — pal-bar.tsx, pal-fault.ts and fault-spec.ts were untouched by that batch, and the two fault taxonomies predate it. What the batch added is reachability: #64 wired なぜ to setMode('window'), so the operator now walks from the pal, where the keys are live, straight into the window, where the same keys are dead under the same fault. Two surfaces disagreeing is a bug; two surfaces disagreeing with a button between them is one you will actually hit.

The fix, and the one to avoid

One shared predicate over TwoHopStatus — something shaped like stopsTransmission(fault) — consumed by both surfaces.

Not a socketHeld prop added to PalBar by hand. That is a third place deriving the same rule from the same fact, and the next fault kind added has to remember all three. The two fault vocabularies (ReceiverFaultKind = 'socket-closed' vs PalFaultKind = 'local-down') already cost #64 a cross-walk in its own test to prove なぜ always has a screen behind it; a shared predicate is the thing that stops that list growing.

Acceptance

  • One predicate decides whether a fault stops transmission, and both surfaces read it
  • Under a shut socket, the pal's 文 and 受信 are held the way the chrome's are
  • A test pins both surfaces against the same TwoHopStatus, so they cannot drift again
  • 5a is checked for the same question and answered deliberately, as #65 did for the chrome — her failing to answer is not the socket failing
  • The Android overlay bar is covered too, not just the desktop pal

denpa#65, denpa#64, denpa#62, denpa#59

Found on 2026-07-31 by the cross-seam review of the `#62`–`#68` batch, not by a failing test. ## What is true `#65` gave the window chrome a rule: a shut socket holds the keys that cannot work under it. `receiver-chrome.tsx` derives `socketHeld` from `fault.kind === 'socket-closed'` and threads it into `Dock`, `TransmissionBand` and `KeyRow`, where 送信, ハンズフリー and 文 go disabled. **`PalBar` never got the rule.** It carries the same 文 and 受信 keys and takes the same fault (`pal-bar.tsx`'s `fault?: PalFaultKind` prop), but 文 has no `disabled` at all and 受信 is `disabled={micHeld}` only. So in pet mode and in the Android overlay both keys stay pressable under the very socket the bar is drawing as 不通, and pressing 送信's equivalent raises the error `#65` was filed to stop. Both surfaces are deciding from the same fact. `two-hop.ts`'s `palFaultFor` returns `'local-down'` exactly when `local.mark === 'down'`, and `fault-spec.ts` returns `'socket-closed'` on the identical test. The disagreement is not about what is true; it is that only one surface was taught what to do about it. ## Why it is worth its own ticket The gap is `#65`'s alone — `pal-bar.tsx`, `pal-fault.ts` and `fault-spec.ts` were untouched by that batch, and the two fault taxonomies predate it. What the batch added is **reachability**: `#64` wired なぜ to `setMode('window')`, so the operator now walks from the pal, where the keys are live, straight into the window, where the same keys are dead under the same fault. Two surfaces disagreeing is a bug; two surfaces disagreeing with a button between them is one you will actually hit. ## The fix, and the one to avoid **One shared predicate over `TwoHopStatus`** — something shaped like `stopsTransmission(fault)` — consumed by both surfaces. **Not** a `socketHeld` prop added to `PalBar` by hand. That is a third place deriving the same rule from the same fact, and the next fault kind added has to remember all three. The two fault vocabularies (`ReceiverFaultKind = 'socket-closed'` vs `PalFaultKind = 'local-down'`) already cost `#64` a cross-walk in its own test to prove なぜ always has a screen behind it; a shared predicate is the thing that stops that list growing. ## Acceptance - [ ] One predicate decides whether a fault stops transmission, and both surfaces read it - [ ] Under a shut socket, the pal's 文 and 受信 are held the way the chrome's are - [ ] A test pins both surfaces against the same `TwoHopStatus`, so they cannot drift again - [ ] 5a is checked for the same question and answered deliberately, as `#65` did for the chrome — her failing to answer is not the socket failing - [ ] The Android overlay bar is covered too, not just the desktop pal ## Related `denpa#65`, `denpa#64`, `denpa#62`, `denpa#59`
Author
Owner

This was generated by AI during triage.

Merged to main.

Fix. stopsTransmission(status: TwoHopStatus) in two-hop.ts is the one place the rule lives. use-receiver-chrome.ts reads it for the chrome, pal-container.tsx and overlay-pal-container.tsx for the two pals. The chrome no longer derives fault.kind === 'socket-closed' — that was the second copy this issue named.

5a answered deliberately, not by omission, the way #65 answered it for the chrome: with both hops reachable and a turn unanswered, the socket is fine and the send is legitimate. Her failing to answer is not the socket failing. CONNECTING is not a fault either — the reconnect backoff sits there for most of every retry.

Acceptance, all five:

  • One predicate decides, both surfaces read it
  • 文 and 受信 are held on the pal the way the chrome holds them
  • keys-under-shut-socket.test.tsx renders the chrome, the desktop pal and the Android overlay from one TwoHopStatus, so a change that holds one and not the others fails whichever one it is
  • 5a checked and answered
  • The Android overlay is covered, not just the desktop pal

One thing this issue did not ask for, found by review. pal-composer-panel.tsx held the 文 key but not the composer's own 送信, so a composer already open when the socket dropped could still send — raising the exact error #65 was filed to stop. The send and its ⏎ are held now. The panel stays open and the draft is kept: closing it would throw away what the operator typed, and 不通 on the bar already says why it will not go.

Known trade. transmissionHeld is a prop that must agree with the fault prop beside it; passing TwoHopStatus into each surface would make disagreement structurally impossible. Kept as a boolean because the chrome is otherwise a pure render, there are three real call sites, and all three are pinned by the cross-surface test. Reviewed and flagged as a risk rather than a bug.

Gate: 66 test files, 878 tests, npm run typecheck exits 0.

> *This was generated by AI during triage.* Merged to `main`. **Fix.** `stopsTransmission(status: TwoHopStatus)` in `two-hop.ts` is the one place the rule lives. `use-receiver-chrome.ts` reads it for the chrome, `pal-container.tsx` and `overlay-pal-container.tsx` for the two pals. The chrome no longer derives `fault.kind === 'socket-closed'` — that was the second copy this issue named. **5a answered deliberately, not by omission**, the way `#65` answered it for the chrome: with both hops reachable and a turn unanswered, the socket is fine and the send is legitimate. Her failing to answer is not the socket failing. CONNECTING is not a fault either — the reconnect backoff sits there for most of every retry. **Acceptance, all five:** - One predicate decides, both surfaces read it - 文 and 受信 are held on the pal the way the chrome holds them - `keys-under-shut-socket.test.tsx` renders the chrome, the desktop pal and the Android overlay **from one `TwoHopStatus`**, so a change that holds one and not the others fails whichever one it is - 5a checked and answered - The Android overlay is covered, not just the desktop pal **One thing this issue did not ask for, found by review.** `pal-composer-panel.tsx` held the 文 *key* but not the composer's own 送信, so a composer already open when the socket dropped could still send — raising the exact error `#65` was filed to stop. The send and its ⏎ are held now. The panel stays open and the draft is kept: closing it would throw away what the operator typed, and 不通 on the bar already says why it will not go. **Known trade.** `transmissionHeld` is a prop that must agree with the `fault` prop beside it; passing `TwoHopStatus` into each surface would make disagreement structurally impossible. Kept as a boolean because the chrome is otherwise a pure render, there are three real call sites, and all three are pinned by the cross-surface test. Reviewed and flagged as a risk rather than a bug. **Gate:** 66 test files, 878 tests, `npm run typecheck` exits 0.
aiko closed this issue 2026-07-31 15:59:45 +00:00
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#71
No description provided.