Skip to content

Reap a phone whose companion link died without a FIN - #298

Open
jiweiyuan wants to merge 1 commit into
mainfrom
fix/companion-link-resilience
Open

Reap a phone whose companion link died without a FIN#298
jiweiyuan wants to merge 1 commit into
mainfrom
fix/companion-link-resilience

Conversation

@jiweiyuan

Copy link
Copy Markdown
Collaborator

A phone that leaves coverage or sleeps never sends a FIN, so its half-open socket stays on the books for minutes — and its bridge still owns the PTY's winsize, leaving the Mac's window sized to a phone that is gone.

The Mac now enables TCP keepalive, records the last frame heard from each connection, and drops one silent past 50s. A tick that arrives late (the Mac slept, or the main runloop stalled) forgives the gap instead of reaping every phone at once.

The phone closes the same loop from its end. CompanionTransport gains the 15s ping timer and NWPathMonitor that CompanionClient already had, so a half-open terminal socket is noticed instead of looking frozen, and a network switch reconnects immediately instead of sitting out the 30s heartbeat. Its frame cap rises to the server's 16 MB, since a cap below the peer's turns any oversized frame into an unexplained reconnect.

Compatibility with shipped phones

The 50s reap counts any received frame as proof of life, so it must not kill a TestFlight 1.1 phone that predates the client-side ping. Verified against a Network.framework listener built to mirror CompanionServer.start() (autoReplyPing = true, same receive pump, same 20s ping), with a client that never initiates a ping and never sends data:

PING sent
RECEIVED opcode=pong bytes=2
PING sent
RECEIVED opcode=pong bytes=2

NWConnection surfaces the pong to the receive handler, so lastHeard refreshes every 20s on a link that sends nothing at all — two refreshes inside the 50s limit. A pong also falls through to default: break rather than being decoded as a control frame, and the pump re-arms outside the opcode switch, so it cannot end the receive loop.

Release Notes

  • The Mac now releases a session's window size when a phone drops off the network instead of holding it until the socket times out.
  • A phone whose terminal link goes half-open reconnects instead of sitting on a frozen screen, and reconnects immediately when the network comes back.

A phone that leaves coverage or sleeps never sends a FIN, so its
half-open socket stays on the books for minutes — and its bridge still
owns the PTY's winsize, leaving the Mac's window sized to a phone that
is gone. The server now enables TCP keepalive, records the last frame
heard from each connection, and drops one silent past 50s. A tick that
arrives late (the Mac slept, or the main runloop stalled) forgives the
gap instead of reaping every phone at once.

The phone closes the same loop from its end: the socket waits for
connectivity rather than failing instantly and burning a backoff step,
reconnects the moment the network path returns instead of sitting out
the heartbeat, and holds the "Reconnecting…" banner through the fast
retry burst so a blip does not read as an outage. Its frame cap rises
to match the server's 16 MB, since a cap below the peer's turns any
oversized frame into an unexplained reconnect.
@vercel

vercel Bot commented Aug 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
landing Ready Ready Preview Aug 13, 2026 11:01pm

Request Review

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