Skip to content

fix: the receiver erased the one thing that explained the failure - #70

Merged
nbkdoesntknowcoding merged 1 commit into
mainfrom
fix/refusal-survives-disconnect
Aug 26, 2026
Merged

fix: the receiver erased the one thing that explained the failure#70
nbkdoesntknowcoding merged 1 commit into
mainfrom
fix/refusal-survives-disconnect

Conversation

@nbkdoesntknowcoding

Copy link
Copy Markdown
Owner

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 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.

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. 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.
  • One 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.

refusalExplanation returns null for 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

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>
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