fix: the receiver erased the one thing that explained the failure - #70
Merged
Merged
Conversation
Reported from a real session: the laptop showed "Connecting…", then "Disconnected. Reconnecting…", on a loop, forever. It reads as a broken app. What was actually happening is that another receiver held the connection, the Mac refused the second one with `busy`, and said so. The receiver displayed that refusal correctly — and about a hundred milliseconds later the close that followed it fired `ds://disconnected`, whose handler wrote "Disconnected. Reconnecting…" straight over the top. The information needed to fix it in one click was on screen and was erased by the next event. Phase 5 hit this exact shape and fixed it with a boolean, `releasedBySender`, guarding one message. That was the wrong shape: it fixes the reason someone noticed and leaves every other reason exposed. `busy` was the next one, and it cost a session. So the flag is now a value. One `stickyStatus`, set by whatever the sender last explained, and one `disconnectStatus()` that decides whether a disconnect may overwrite what is on screen. A new refusal cannot be added without deciding what the user sees, because the decision lives in one function instead of being spread across handlers. `refusalExplanation` deliberately returns null for anything without a considered answer, so a transient failure cannot pin a stale reason to the screen after it stops being true. The busy text says what to do rather than what happened. "Another receiver is already connected" describes plumbing; the user needs to know they can close the other one or simply wait, since retrying continues underneath. Covered by verify-refusals.mjs in both CI jobs. None of this is reachable without two receivers and one Mac, which is exactly how it reached a user. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Reported from a real session: the laptop showed "Connecting…", then "Disconnected. Reconnecting…", on a loop, forever. It reads as a broken app.
What was actually happening: another receiver held the connection, the Mac refused the second one with
busy, and said so. The receiver displayed that refusal correctly — and about a hundred milliseconds later the close that followed firedds://disconnected, whose handler wrote "Disconnected. Reconnecting…" straight over the top.The information needed to fix it in one click was on screen, and was erased by the next event.
Why the previous fix didn't cover it
Phase 5 hit this exact shape — the "screen released" message being overwritten — and fixed it with a boolean,
releasedBySender, guarding that one message. That was the wrong shape. It fixes the reason someone noticed and leaves every other reason exposed.busywas the next one, and it cost a session.So the flag is now a value:
stickyStatus, set by whatever the sender last explained.disconnectStatus()that decides whether a disconnect may overwrite what's on screen.A new refusal can't be added without deciding what the user sees, because that decision now lives in one function instead of being spread across handlers.
refusalExplanationreturnsnullfor anything without a considered answer, so a transient failure can't pin a stale reason to the screen after it stops being true.The wording
The busy text says what to do, not what happened. "Another receiver is already connected" describes plumbing; the user needs to know they can close the other one or just wait, since retrying continues underneath.
Verification
verify-refusals.mjs, wired into both CI jobs — 4 checks covering the regression, the ordinary disconnect, and that transient codes never become sticky.None of this is reachable without two receivers and one Mac, which is exactly how it reached a user.
🤖 Generated with Claude Code