Skip to content

Goodbye: the Mac says when it goes to sleep or shuts down - #100

Open
addhumal wants to merge 46 commits into
Saffsanity:mainfrom
addhumal:goodbye-sleep
Open

addhumal wants to merge 46 commits into
Saffsanity:mainfrom
addhumal:goodbye-sleep

Conversation

@addhumal

@addhumal addhumal commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

The Mac says when it goes to sleep, restarts or shuts down

When the Mac went to sleep, restarted or shut down while a device streamed, the device found out only when its liveness gave up on a Mac gone silent. It then said "‹Mac› disconnected" and started its reconnect at once, dialling a row that a sleep proxy may still list for a Mac asleep (#105).

With this change Sill for Mac tells every connected device why before it goes, and the device says so and reconnects once the Mac is back. From the sleep goodbye until the Mac wakes, no device connects.

Additive on the wire: kind 22 (goodbye) gains four reasons, "sleep", "shutdown", "restart" and "poweroff". Each carries the Mac's words and "reconnect":true. A device from before them shows the Mac's words as it shows any reason it does not know, and reconnects as after any end.

What it does

On the Mac

  • Sleep. NSWorkspace's willSleep, under the AppKit loop (Sill.app, and the CLI with --virtual-display). Every device gets "sleep" ("Noah’s MacBook Pro is going to sleep."), handed to the network within 0.1 s. Each connection then closes the way the device gate closes a refused one: the goodbye, this side's end, then what the device still sends is read and dropped until it closes, 1 s at most. Sill.log: "The Mac is going to sleep: 2 devices told." The CLI's default path (dispatchMain) registers nothing: NSWorkspace says nothing there, and a power client nobody answers holds the Mac's sleep up to 30 s.
  • No device connects until the Mac wakes. From willSleep until NSWorkspace's didWake, every door (both TLS doors and the CLI's plain one) refuses a session with the same goodbye.
    • The refusal closes as the device gate's refusals close, and the session is never registered: no catalog, no capture, no encoder, and no user activity declared. Wake the host display for active streams #151 declares user activity at every connect, which could light the display of a Mac someone has just put to sleep, or keep it awake.
    • 0.5.1, the App Store build, dials the still-listed row as soon as its session ends, inside the sleep's transition. A source refused five times in a minute hears the next refusal 2 s late, as at the device gate.
    • DisplayWake declares nothing while the window runs.
    • The window ends at didWake, opens again at a willSleep, and lapses ten minutes of the Mac's awake time after willSleep, so a didWake that never comes shuts no door for good.
    • Sill.log: "The Mac is going to sleep: refused a session from … with the same goodbye." (once a source a minute), then "The Mac woke: devices can connect again (2 sessions refused while it slept)."
  • Log out, restart, shut down. The first signal is NSWorkspace's willPowerOff. For the next 10 minutes it makes the goodbye Sill's end sends "poweroff", in the words "‹Mac› is shutting down or restarting.". Nothing is sent at willPowerOff itself: it also comes for a log out, and someone may still cancel it.
    • It goes out when Sill goes, by a SIGTERM (where that sent "quit") or by a quit Apple event.
    • That event's kAEQuitReason, read as a parameter or an attribute, decides: a restart says "restart" ("… is restarting."), a shut down "shutdown" ("… is shutting down."), a log out "quit".
    • Sill's own Quit from its menu stays "quit". Without willPowerOff or a reason (a signal alone, another app's quit), it is "quit", byte for byte as before.
  • One goodbye per connection, the first, and nothing after it (StreamServer's told): a Quit right after the sleep's goodbye, or after Remove, says nothing more, and no frame, tick, pong, pointer report, state or sound packet follows a goodbye on its connection.
  • HostGoodbye and SleepWindow (pure) make the choices and the log lines. Sill.log is flushed before a signal's or the Quit's exit, so the line saying what the devices heard survives a restart's SIGTERM.

On the device

  • The words (GoodbyePolicy):
    • "‹Mac› went to sleep. This ‹iPad› reconnects when it wakes."
    • "‹Mac› is shutting down. This ‹iPad› reconnects when it’s back."
    • "‹Mac› is restarting. This ‹iPad› reconnects when it’s back."
    • "‹Mac› is shutting down or restarting. This ‹iPad› reconnects when it’s back." ("poweroff")
    • The device closes its end as soon as one of these comes, so the session ends at once even from a Mac already asleep.
  • The reconnect's waits (DiscoveryPolicy):
    • After "sleep", a row listed at the goodbye counts as asleep, as Find the Mac again after hours idle: the browsers made again, the reconnect kept alive (#69) #77's dials would have found after three tries. Its first dial comes 20 s later, then one every 30 s, with Couldn't get the app to work after sitting dormant for a few hours. Had to login to my Mac and close and restart the app on my iPad before it would reconnect. #69's lines from that dial on, and the goodbye's words until then. A row listed afresh (the Mac awake) is dialled at once. A refusal while the Mac is asleep is the same goodbye, so it starts that count again, at either door.
    • After "shutdown", "restart" or "poweroff", a row listed since before the goodbye is the registration on its way out. It is left alone until the first of: listed afresh (the Mac back with Sill open), listed by the browser made again at a return from the background, or 10 minutes. Then it is dialled, with the words kept until that dial.
    • A goodbye read only as the app came back is dated at the return, though the Mac may have said it long before (it waited in the connection while the app was suspended). So what the browser made at that return lists counts as listed afresh, after "sleep" too, and is dialled at once.
    • Remote dials happen only after a session that came through the remote door, and not before 20 s.
  • The first ping tick after the app runs again judges nothing. A suspended app's ping timer fires once, overdue, as it runs again, and its liveness check used to run before the return's reset reached the session's queue, ending the session as "connection silent … lost" before a goodbye that waited in the connection could be read. Now a tick a second or more after the one before counts the silence afresh (DiscoveryPolicy.liveness). A connection iOS reclaimed while the app was suspended still loses its goodbye: "‹Mac› disconnected…" and a dial at once, as before.
  • A goodbye belongs to its connection. StreamClient keeps it only for the connection it came on: a move's hand-over drops it, a rescue is refused only for that connection, and a new session starts without one.

A wire decision for Noah: "poweroff"

willPowerOff's goodbye used to be "shutdown" in the words "… is shutting down or restarting.", so "shutdown" meant two things, and this build worded even a known shut down "is shutting down or restarting". The compatibility floor would freeze that once it shipped. The maintainer's default: a reason of its own for the case Sill cannot tell, "poweroff", so "shutdown" is only ever a shut down Sill knows. An older device shows the Mac's message as it is. To go back to one reason for both, it is one line in Remote.swift's shuttingDownOrRestarting, before any release ships either.

Restart and shut down: what was seen, what is inferred

  • Seen. A restart begun from another account, while Sill.app ran in this one, ended Sill.app with SIGTERM during the system shutdown. launchd logged "exited due to exit(143) during system shutdown", and there was no log out in Sill's own session, so its goodbye was "quit".
  • Seen in loginwindow's log and strings. loginwindow quits a menu bar (UI-element) app only after the point of no return: a kill attempt, then a quit Apple event to an app it could not kill.
  • Seen in a probe on this Mac. AppKit posts willPowerOff as it handles a quit Apple event whose kAEQuitReason is an attribute, just before applicationShouldTerminate. With the reason as a parameter, or with no reason, it posts nothing.
  • Inferred, not seen. A log out, restart or shut down begun in Sill's own session sends Sill.app such a quit event, since Sill turns sudden termination off and so cannot simply be killed. Only Noah's real Restart and Shut Down can settle this, which is why they are this pull request's merge gate. If Sill.log shows only "Shutting down: …", with no willPowerOff line and no "Quit: macOS asked Sill to quit for …", the inference is wrong.
  • Not known. Whether a dark wake (a Bonjour Sleep Proxy waking the Mac for a device's dial, Power Nap) posts didWake. If it does not, the window holds through it and the dial is refused; if it does, the dial is admitted and Wake the host display for active streams #151 may light the display. Noah's sleep-proxy item says which.

Changed from the pull request as opened

The contributor's commits are untouched; these are on top.

  • Background and lock wording. 8aee786's wording for an end in the background or while locked is out (7425327), for a pull request of its own read with Keep active streams responsive briefly in the background #152 (git cherry-pick 8aee786).
  • One goodbye per connection, the right reason. The Quit's "quit" used to overwrite a "shutdown" on the same connection 45 ms later; now every connection gets only its first goodbye.
  • The sleep closes the session. The connection used to stream on after the sleep goodbye; now it closes as the gate's refusals close, with no reset.
  • The reconnect waits as the Mac said. The listed row used to be redialled at once.
  • willPowerOff is the first signal, refined by the quit's reason. A SIGTERM after it keeps its goodbye.
  • The holds are bounded. A device that missed the old registration's removal used to wait for good after a restart.
  • After the review of 2026-10-26:

Docs

  • docs/update-notice-plan.md §7.3: the four rows, how the host sends them, the window, the first tick after a return, what was seen and what is inferred, and the words that wait for a device.
    • README's Good to know, the support page and the next What's New say nothing of these goodbyes until Noah's test passes. §7.3 has their lines, to go in then.
    • When they go in, the site's line goes between <!-- Goodbye markers, with a perl line in docs/release-checklist.md part 1 §3, as the Clipboard's does.
  • docs/DEVELOPMENT.md: Sill.app's goodbyes, the window and their log lines, the device's waits, and the TEST ONLY hooks:
    • -SillSleepAfter <s>
    • -SillWakeAfter <s>: what didWake does
    • -SillPowerOffAfter <s>: willPowerOff posted to the bare app alone
    • -SillQuitAfter '<s> restart|shutdown|logout[ parameter]': a quit Apple event to itself
  • CLAUDE.md's entry, with Noah's list, and the compatibility floor, which keeps kind 22's reasons with their meanings; Tests/checks/README.md.

Verified

This is the tree merged with main at 74d099c (#94, Send Audio as a beta), then at 76bc100 (#157, the pairing window to screen sharing) and at b4caa91 (#147, Chinese input and dictation; #233, capture at the display's own scale). At 74d099c only CLAUDE.md (two Current-step entries), StreamServer.swift's Client fields (told beside #94's audio fields) and Tests/checks/README.md (two neighbouring rows) conflicted, and at 76bc100 and b4caa91 only CLAUDE.md; both sides were kept whole each time. #233's capture paths in StreamCoordinator and #147's caret handling in StreamClient merged by themselves and meet none of this pull request's code. Every check and build ran under nice and started only while the Mac's load was under 20 (the mutant runs' compiles were stopped while it was above). Every host ran on 127.0.0.1 with the software encoder, no router and no screen.

  • Builds.
    • swift build -c release from a clean git archive of the head: "Build complete! (49.42 sec)", only the known CaptureProbe warning.
    • The iOS app as CI builds it (Xcode 27.1 27A9275, Debug, generic simulator, arm64, unsigned, fresh derived data): ** BUILD SUCCEEDED **, only the known StreamClient capture warning (StreamClient.swift:3962, the autoDesktop closure).
    • The CLI against main's at b4caa91 (--synthetic on loopback, masked and sorted): idle 30 s identical; with a Desktop client 15 s identical in three alternating runs but for cursor.shape, which follows this Mac's own cursor and came in both builds' runs.
  • Checks.
    • goodbye: 169 checks; run.sh --mutants: every mutant caught (116 of 116).
    • policy: ALL PASS (526); run.sh --mutants: every mutant caught (211 of 211).
    • display-wake: 211 checks; run.sh --mutants: every mutant caught (67 of 67).
    • door-policy: ALL PASS (128); DoorPolicy and Door are unchanged.
    • main's capture-scale (79), pairing-sharing (18) and text-composition (178).
    • Tests/checks/run-all.sh: "All passed.", all 54 on the head.
    • The mutants ran on 697e6c0 (display-wake), aed225a (goodbye) and 66d7121 (policy); nothing those checks compile has changed since.
  • The bare app on loopback, two TLS clients paired through a pairing window (SillMenuBar --synthetic, the identity in a scratch SILL_TEST_REMOTE_DIR, sillclient.py --tls --identity=…):
    • The sleep window. -SillSleepAfter 6 -SillWakeAfter 12, both clients streaming the Desktop with sound: each got "sleep" and nothing more; "The Mac is going to sleep: 2 devices told.". A third client that dialled 0.3 s after the goodbye, and a fourth 2 s after, got only the sleep goodbye, with no window list, settings, kind 18 or frame; no "Client connected", no disp.wakeDry and no capture start between the goodbye and the wake; "The Mac woke: devices can connect again (2 sessions refused while it slept)."; a client 0.5 s after the wake was admitted and streamed. On 697e6c0's build, aed225a's, a921209's and the head's, alike.
    • No sound after a goodbye (-sendAudio 1, clients with --audio, one counting every message after its goodbye). The sleep's goodbye, three runs: none on aed225a's build (nor on the head's, in its sleep run); the merge's own build (7789575) sent 100 sound packets on each connection in the second until the cancel, 600 in all. The Quit's goodbye: none over 22 connections on 697e6c0's and aed225a's builds; the merge's build sent a packet after it on 3 of 22. The told rule and the sound's send path are unchanged since.
    • The goodbyes at Sill's end. -SillPowerOffAfter 3, then SIGTERM: exit 143, "poweroff" ("… is shutting down or restarting.") and "The Mac is shutting down or restarting: 2 devices told.". -SillQuitAfter '5 shutdown': "shutdown" ("… is shutting down."), after willPowerOff's line, "Quit: macOS asked Sill to quit for a shut down." and "The Mac is shutting down: 2 devices told."; as a parameter, the same without willPowerOff's line. -SillQuitAfter '5 restart': "restart".
  • Earlier rounds (on b9ec80e, and after the review of 2026-10-20 with main at 9a8e30c), on private iPad simulators (iOS 27.0, deleted after) against the bare app under the test service type: a sleep mid-session, the words at once, the dial 20 s later and the session back; a restart held, then a return from the background dialling at once; a paired Mac back after a restart dialled at once.
  • Not done: no simulator this round (the review's run pushed the Mac's load past 90), so the first tick after a return is checked by the policy check's model of the session's queue, not live. CI has not run on this head. No real Mac sleep, restart or shut down, and no device.

For Noah: the merge gate

With Sill.app from this branch (release-signed with the embedded profile) and the iPad on this build:

  • Sleep (Apple menu › Sleep, and the lid) while streaming:

    • "‹Mac› went to sleep…" at once, and "The Mac is going to sleep: 1 device told." in Sill.log.
    • After waking it, the session back by itself, and "The Mac woke: devices can connect again." in Sill.log.
    • Close the lid for a few seconds with a window picked (Aa set, and once with Virtual Display on). Every sleep ends every session, so what Sill does when the last device leaves runs as the Mac sleeps: capture stops, a sized window goes back, the virtual display goes and its window home. With 0.5.1 it can come at the wake instead. Note what the Mac's window does and what the device shows after.
  • 0.5.1 streaming, Apple menu › Sleep: the Mac stays asleep, its display dark. 0.5.1 dials again as soon as its session ends; Sill.log may show "The Mac is going to sleep: refused a session from …" before the Mac sleeps.

  • Behind an Apple TV or HomePod sleep proxy, the Mac left asleep: whether the device's dials (the first 20 s after the goodbye, then every 30 s) wake it, and what Sill.log says then. "refused a session" means a dark wake posts no didWake and the window held, so the Mac should go back to sleep with its display dark. "The Mac woke" then "Woke the display for …, which connected." means a dark wake posts didWake and Wake the host display for active streams #151 lit the display: send the lines.

  • Restart and Shut Down from the Apple menu while streaming. Sill.log should have:

    • "macOS says the Mac is about to log out, restart or shut down."
    • "Quit: macOS asked Sill to quit for a restart."
    • "The Mac is restarting: 1 device told."
    • For Shut Down, "… for a shut down." and "The Mac is shutting down: 1 device told."

    Only "Shutting down: …" means the inference is wrong: send the lines. Then check the device's words ("is restarting", "is shutting down"), and the session back once the Mac is up with Sill open.

  • A restart begun from another account: "quit", as seen. Log Out: "quit".

  • The iPad locked, or Sill in the background, through a sleep and through a restart, back a minute later: the Mac's own words, not "disconnected" ("disconnected" means iOS reclaimed Sill's connection while it was suspended, the known limit), and the Mac dialled as soon as it is listed, not after 10 minutes.

  • 0.5.1 from the App Store against this Sill.app: the Mac's message, then its own Reconnecting.

  • Through Tailscale, a sleep: the remote dials from 20 s on.

  • A decision: willPowerOff's goodbye keeps its own reason, "poweroff", or goes back to "shutdown" for both.

🤖 Generated with Claude Code

Towards #105

Windows App's "your session was disconnected because…", from the roadmap (why a session ended):
Sill.app, told by NSWorkspace that the Mac is about to sleep, or to shut down, restart or log out,
sends every connected device kind 22 with a new reason, "sleep" or "shutdown", the Mac's own
words and reconnect true, at most 0.1 s, and logs one line ("Going to sleep: told 2 devices
why."). This build says "Mac mini went to sleep. This iPad reconnects when it wakes." (or "is
shutting down or restarting… when it's back") and reconnects, the remote door allowed for a saved
Mac, as after "quit". A device from before these reasons shows the Mac's message and reconnects,
as for any reason it does not know. Wire: additive, two reason values with a message, as the
compatibility floor allows. The CLI sends neither (no AppKit loop). The device's own reasons (its
lock, the background) wait for Saffsanity#77, which changes that code.

Bare binary only: -SillSleepAfter '<s> sleep|shutdown' calls the same path (Sill.app ignores it
with one line, as its other timed hooks).

Checks: goodbye 53 (sleep and shutdown for this build and an older one) and 22 of 22 mutants (four
new; three moved with the reasons' list); Tests/checks/run-all.sh all passed. End to end: the bare
binary on loopback (software encoder, a test identity directory, Require pairing off) with
-SillSleepAfter '8 sleep': sillclient over TLS got 'goodbye: sleep; message: "… went to sleep.
Sill reconnects when it wakes."; reconnect: true' and left. Not run: a real sleep or shutdown.
Review findings 4 and 5. A sleep or shutdown goodbye in the second between a remote dial and its
first window list went to the remote dial rules, which read every goodbye they do not know as
"refused" ("answered, but Sill isn't accepting remote connections there…"). Before these reasons
existed such a goodbye was a notice and said why; they are notices again (out of knownReasons, as
"update" is), still in this build's words and always with a reconnect, so they are said whatever
the session's state. knownReasons is main's list again. And macOS sends the same "will power off"
for a log out as for a shut down, so the words say "is shutting down, restarting or logging out",
the Mac's message and this build's.

Checks: goodbye 53 (red first: four) and 23 of 23 mutants (the reasons' list mutants back on main's
list, two new for the notice, one for the words); Tests/checks/run-all.sh all passed; the bare
binary's -SillSleepAfter '7 shutdown' reached sillclient with the new words.
Found in the review before this push: GoodbyePolicy's and Goodbye's doc comments ran past their
width, and the coordinator's left out a log out, which sends the same notice as a shut down.
# Conflicts:
#	Sources/SillMenuBar/DebugHooks.swift
…ile locked

Now that Saffsanity#77 is in, a session that ends without a goodbye from the Mac after Sill
went to the background (or the device locked: protectedDataWillBecomeUnavailable)
before a pong for a ping sent since the return says so: "The connection to ‹Mac›
ended while Sill was in the background." or "…while this iPad was locked.", and
"Reconnecting to ‹Mac›… (the connection ended while …)" carries the reason on the
network, Direct and remote reconnect lines until the session is back or the line
moves on to "…isn’t answering" (Saffsanity#105). Pure in GoodbyePolicy (Away, AwaySpell,
enteredBackground, heardPong, awayEnd, awayNote) and DiscoveryPolicy.reconnectLine:
goodbye 70 checks and 34 mutants, policy 448 and 160.
@addhumal

addhumal commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor Author

Now that #77 is on main, pushed 8aee786 (and merged main in 485149e) with the device's own reasons: when a session ends without a goodbye after Sill went to the background (or the device locked: protectedDataWillBecomeUnavailable) before a pong for a ping sent since the return (GoodbyePolicy.AwaySpell, enteredBackground, heardPong), the status line says "The connection to ‹Mac› ended while Sill was in the background." or "…while this iPad was locked.", and the "Reconnecting to ‹Mac›…" lines carry "(the connection ended while …)" until the session is back or the line moves on to "…isn’t answering". goodbye is now 70 checks and 34 of 34 mutants, policy 448 and 160.

@Saffsanity

Copy link
Copy Markdown
Owner

From a pass over all the open pull requests (nothing is decided yet): #117 also adds a line to StreamClient.tearDown right after resetHomeMove() (its preview save, beside this PR's awaySpell = nil), so whichever lands second keeps both, and #158 edits StreamClient.swift as well. #66 (and #72 and #73, built on it) compiles StreamClient.swift into a Mac client, where the new UIApplication.protectedDataWillBecomeUnavailableNotification observer would need its ClientPlatform layer or a guard, and #128's viewer shims lack that notification too. The rest (#94, #124, #158, #159) is rows of Tests/checks/README.md; separately, #152 keeps a session alive for a while in the background, which changes when the new background wording would show, so the two are worth reading together.

Saffsanity and others added 2 commits October 19, 2026 22:21
8aee786 added the device's own reasons for an end with no goodbye ("The connection to ‹Mac›
ended while Sill was in the background", "…while this iPad was locked", and the note on the
Reconnecting lines). Issue Saffsanity#105 asks for the Mac's goodbyes only, and the maintainer's triage
(2026-10-16 and 2026-10-19) asked for the wording to be split out: it is a change of its own,
and Saffsanity#152 (keeping a session alive in the background) changes when it would show. This takes it
out again, whole: GoodbyePolicy's away spell, StreamClient's background and protected-data
observers, the pong's ping time, the reconnect line's note and their checks and mutants.
StreamClient.swift, StreamClient+Remote.swift, DiscoveryPolicy.swift and the policy check are
main's again. 8aee786 stays in this branch's history, so `git cherry-pick 8aee786` brings it
back for a pull request of its own.

Checks: goodbye 53 and 23 of 23 mutants; policy 446.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…f canImport(UIKit)

Mac client targets (Saffsanity#66, Saffsanity#128) compile StreamClient.swift without UIKit's
protectedDataWillBecomeUnavailableNotification.
@addhumal

addhumal commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor Author

Thanks. Wrapped the UIApplication.protectedDataWillBecomeUnavailableNotification observer in #if canImport(UIKit) (9b59e0d), so Mac client targets (#66, #128) that compile StreamClient.swift need no shim for it, and pushed 34872a5 so goodbyeBeforeSleep closes the connections once the goodbye is handed over (preventing shutdownForExit from sending a second "quit" goodbye on top of "shutdown") and endWithNotice keeps the sleep/shutdown reason on the "Reconnecting…" line (goodbye: 76 checks, 38 of 38 mutants). Also noted on #117 (tearDown keeps both lines) and #152 (with a background task keeping the session alive briefly, the background wording only appears if the session still ends before a post-return pong arrives).

Saffsanity and others added 5 commits October 19, 2026 22:52
…s choice is HostGoodbye

The Mac's own going, as kind 22 reasons (additive: three strings, no new field). Their words, which
a device from before them shows as a reason it does not know, now say what happened in plain words
("Noah’s MacBook Pro is going to sleep.", "… is shutting down.", "… is restarting."): the earlier
"went to sleep. Sill reconnects when it wakes." spoke of a reconnect the device decides, and
"shutting down, restarting or logging out" guessed between three. A restart is its own reason now,
because Sill.app learns which from the quit's reason (next commit).

HostGoodbye (Sources/SillHost, pure): the goodbye a Quit sends from the quit Apple event's
kAEQuitReason ('rest'/'rrst' restart, 'shut'/'rsdn' shut down; a log out, the menu or a signal stay
"quit"), and the log's lines.

GoodbyePolicy: this build's words for the three ("‹Mac› went to sleep. This ‹iPad› reconnects when
it wakes.", "‹Mac› is shutting down." / "is restarting." "This ‹iPad› reconnects when it’s back."),
still notices with a reconnect; `went` (quit, asleep, off) tells the reconnect how the Mac went, for
its waits (the device's commit); the remote door only for a session that came through it (`remote`):
at home a remote dial would only replace the words while the Mac sleeps or restarts.

The goodbye check now builds HostGoodbye too (-package-name sill) and holds its quit reasons to the
SDK's constants: 107 checks; its mutants cover GoodbyePolicy, HostGoodbye and Remote.swift's goodbyes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d the sleep's closes the session

The triage's first two faults, on the Mac's side (reproduced on 485149e with the bare binary and
sillclient):
- A restart, a shut down or a log out runs Sill.app's Quit right after willPowerOff, and the Quit's
  "quit" went out on the same connections after the "shutdown" (45 ms later there), so the device
  kept the quit's words. Now every connection gets one goodbye, the first (StreamServer's `told`,
  in goodbyeAll and in goodbye(_:to:)), and willPowerOff is not used at all: it fires for a log out a
  person may still cancel, which left a stale goodbye, and says neither which nor when. The Quit
  reads the reason in the quit Apple event instead (applicationShouldTerminate, kAEQuitReason), so a
  restart or a shut down says so (HostGoodbye.forQuit), with a line ("Quit: macOS asked Sill to quit
  for a restart.", "The Mac is restarting: 1 device told."). A quit from the menu or a signal is
  "quit", byte for byte as before.
- After the sleep goodbye the connection stayed open (in a test it streamed on for 5 s), so a sleep
  that did not come, or a short one, left the device a goodbye for a session that carried on. Now
  the sleep's goodbye closes each connection once it is handed to the network (goodbyeAll's
  `close`), and the device's session ends at once with those words, not when its liveness gives up.

The sleep observer moved from AppModel into the coordinator (`observeSleep`), registered only under
the AppKit loop: Sill.app, and the CLI with --virtual-display, which gets it too. The CLI's
dispatchMain gets none: NSWorkspace says nothing there, and a power client nobody answers would hold
the Mac's sleep up to 30 s. The observer runs synchronously on the main thread before the Mac
sleeps and reads no main-actor state (a Sendable copy of the server and the Mac's name).

TEST ONLY on the bare binary: -SillSleepAfter <s> (the sleep's goodbye), and -SillQuitAfter
'<s> restart|shutdown|logout' (a quit Apple event to the app itself carrying that reason, read where
Sill.app reads it; an event to its own process asks nobody's permission).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…or its connection

The triage's third fault, and the device's side of the second.

After "sleep" the reconnect dialled the Mac's listed row at once (a notice's reconnect knew no
wait): the registration that is going, or a Bonjour Sleep Proxy's record, whose dial may wake a Mac
someone put to sleep, and Saffsanity#69's dials then ran out while it slept. Now the Mac's word counts: a row
listed at the goodbye starts as Saffsanity#69's dials would have got to, asleep (DiscoveryPolicy.listingStart:
three that did not connect, the last at the goodbye), so its first dial comes 20 s later
(`asleepWait`), then every 30 s, nine in all, with Saffsanity#69's lines from that dial on; until it the
goodbye's words stay (`waitingLine`). A row listed afresh (the Mac awake, registered again) is dialled
at once, and the return from the background starts the count where a fresh look would. After
"shutdown" or "restart" a row listed at the goodbye is never dialled (`reconnectRow`'s `offAt`), one
listed afresh is, at once. Nothing dials the Mac's addresses remotely sooner than 20 s after it said
it went (`firstRemoteDial`). The Reconnect's `afterQuit` became `went` (GoodbyePolicy.Went).

The goodbye was kept for the session, whatever connection it came on: a move's hand-over carried an
old connection's goodbye to the next end, and a rescue was refused for any connection while one
stood. Now it is kept with the connection it came on (`goodbyeOn`): a hand-over drops it, a rescue is
refused only for that connection, and a new session starts without one.

The policy check: 478 (the quit's hold, uncovered until now, among the new cases), with a model of
the reconnect after each goodbye (a sleep proxy, no proxy and the Mac waking, the return from the
background inside and after the 20 s, a restart and the Mac back, a row that never leaves, a quit).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he reconnect line

goodbyeBeforeSleep closes each client's connection once the goodbye is handed over
(and sets saidPowerOffGoodbye so shutdownForExit does not send a second 'quit'
goodbye on top of 'shutdown'). endWithNotice gives sleep and shutdown the 3 s
quitWait before dialling a row listed since before the loss, and carries
'(‹Mac› went to sleep)' / '(‹Mac› was shutting down…)' on the Reconnecting line
(goodbye: 76 checks, 38 of 38 mutants).
…dbye

Mutant G14 (a row with no sighting time counted as listed afresh after "shutdown" or "restart") went
uncaught: every case gave a listed row its time. Two cases now hold such a row back, for good after a
shut down or restart and for quitWait after a quit, as the rule does. 480 checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Saffsanity and others added 10 commits October 19, 2026 23:23
…o test them

Nothing a device sees changes without the docs saying so:
- docs/update-notice-plan.md §7.3: rows for "sleep", "shutdown" and "restart" (this build's words,
  its waits, the remote door) and a note on how the host sends them, one per connection, and how a
  device from before them reads them (0.5.1 shows the Mac's words until its first dial).
- docs/DEVELOPMENT.md: Sill.app's goodbyes (the sleep's, the Quit's reason, one per connection; the
  CLI with --virtual-display only), the device's words and waits beside Saffsanity#69's reconnect, and the
  bare binary's -SillSleepAfter and -SillQuitAfter '<s> restart|shutdown|logout' with the simulator's
  test setup (both missing from the docs, the 10-19 triage noted).
- CLAUDE.md: a Current step entry (the design, the three faults and their fixes, what was verified,
  what is left for a device), and the Layout's HostGoodbye, StreamServer's `told`, the coordinator's
  sleep observer, DiscoveryPolicy's waits and GoodbyePolicy's `went`.
- Tests/checks/README.md: goodbye 107 checks and 61 mutants (now with HostGoodbye), policy 480 and
  177; the policy check's header.
- DiscoveryPolicy: asleepWait's comment said nothing dials such a Mac sooner, but a row listed
  afresh is dialled at once; it says so now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ished): kept as above

9b59e0d and 34872a5 arrived on addhumal:goodbye-sleep while the commits above were made from
485149e. Merged, not rebased, and every conflict resolved to the commits above, whose tree this
is, because each change in them is done there in the way the maintainer's triage and task ask:
- 9b59e0d guards the protected-data observer for the Mac clients (Saffsanity#66, Saffsanity#128): that observer left
  with the background wording (7425327), so nothing needs the guard.
- 34872a5 closes each connection after the sleep and shutdown goodbyes and keeps a "quit" from
  following a "shutdown" (saidPowerOffGoodbye, set by willPowerOff): here every connection gets one
  goodbye, the first (StreamServer's `told`), the sleep's closes the connections, and the Quit takes
  restart or shut down from the quit's own Apple event, so a log out another app cancels sends
  nothing early and leaves no flag behind.
- It gives "sleep" and "shutdown" the quit's 3 s before a row listed at the goodbye is dialled, and
  a note on the Reconnecting line: here a Mac that said it went to sleep counts as asleep (Saffsanity#77's rule,
  its first dial 20 s later, Saffsanity#69's lines from then on, the goodbye's words until it), and after a
  shut down or a restart a row listed at the goodbye is never dialled, only one listed afresh, so a
  sleep proxy's record is not dialled 3 s after the Mac went to sleep. The note grew from the
  background wording's, which is out of this pull request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ge's entry

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main gained Saffsanity#153 (the checks' build.sh quote their paths) and Saffsanity#142
(saved Macs keep a typed destination across a key change). Neither
touches a file this branch changes but Tests/checks/README.md, whose
rows merged by themselves (remote-rules' new counts beside this
branch's goodbye and policy rows).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main gained Saffsanity#136 (the Apps list finds apps in folders of their own, at
any depth: InstalledApps, and its check installed-apps). It touches no
file this branch changes but CLAUDE.md's Layout and Tests/checks/README.md,
whose lines merged by themselves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ad on the return counts the Mac as it is now

The device left a row listed at a "shutdown" or "restart" goodbye alone until the row was listed
afresh, for good. A device that missed the old registration's removal keeps the row's sighting
through Saffsanity#69's rebuild, which keeps a Mac by name, so it never dialled a Mac that was back. Such a
device was suspended within the second the record outlives the goodbye, read the goodbye only on
its return from the background, or lost the mDNS goodbye. The review of b9ec80e showed it with a
scratch driver: take=nil at 400, 3600 and 86400 s.

Now the hold ends at the first of:
- the row listed afresh;
- the row listed by a browser made again at a return from the background that counts for the
  goodbye (DiscoveryPolicy.returnRenews and renewed). After "shutdown" or "restart" that is any
  return since the goodbye, as the browser made then lists the network as it is now, not the
  registration on its way out;
- offHold, ten minutes after the goodbye, when the row is looked at again.
The goodbye's words stay until the first dial (waitingLine's wentAt).

A goodbye read only as the app came back may have been said long before: read in the
background just before the return, or within 3 s after it on a session that was up before the
return. For such a goodbye, what the browser made at that return lists counts as listed afresh,
after "sleep" too. So a sleep goodbye read on the return no longer waits 20 s for a Mac that is
awake and listed (listingStart's renewed). A goodbye on a session dialled after the return (the
reconnect's own dial at it) is the Mac's now and holds as usual (returnRenews' sessionSince, from
the new Session.startedAt): without that, a Mac that said "restart" to each dial was dialled again
at once until the 3 s were up.

StreamClient keeps:
- the return (returnedAt);
- the renewal (renewedFrom, endedInBackground);
- whether the listing it dials is renewed (rowRenewed);
- when a session was dialled (Session.startedAt).

The policy check covers the bounded hold, the renewal, the read on the return and a session dialled
after it, and its model now has returns. "A row that never leaves is never dialled" became its first
dial at 10 minutes. 24 new mutants (G20 to G43), and the hold's older ones on their new lines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… an attribute, and a SIGTERM after willPowerOff keeps its goodbye

The restart and shutdown goodbyes rested on a quit Apple event with kAEQuitReason that nobody
had seen, and the reason was read as an attribute only. What has been seen:
- A real restart begun from another account (2026-10-19) ended Sill.app with SIGTERM during
  the system shutdown, so devices would have heard "quit".
- loginwindow handles a UI-element app such as Sill after the point of no return: a kill
  attempt, then a quit event to one it could not kill.
- A probe on this Mac shows AppKit posts NSWorkspace's willPowerOff as it handles a quit Apple
  event whose reason is an attribute. It posts nothing for a parameter, or for no reason.

What changes:
- willPowerOff is the first signal (StreamCoordinator.observePowerOff, under the AppKit loop).
  For ten minutes (HostGoodbye.powerOffStands) the goodbye Sill's end sends is "shutdown" in the
  words "‹Mac› is shutting down or restarting." (Goodbye.shuttingDownOrRestarting).
  - It goes out when Sill goes: by a SIGTERM, where HostShutdown's signal path sent "quit", or
    by a quit Apple event without a reason.
  - It never goes out by Sill's own Quit from its menu (byMenu: no Apple event), so a
    cancelled log out or restart leaves nothing behind.
  - Nothing is sent at willPowerOff itself, which comes for a log out too and may be cancelled.
- A quit Apple event's reason, read as a parameter (AERegistry.h's word) or an attribute, and
  only in a quit event (HostGoodbye.quitReason), decides as before: restart, shut down, or
  "quit" for a log out.
- HostGoodbye chooses with Going. Its header says what was observed and what is inferred.
- This build's device words "shutdown" as "‹Mac› is shutting down or restarting.", since a
  Mac sends it for willPowerOff, which says neither.
- TEST ONLY: -SillPowerOffAfter <s> posts willPowerOff to the bare app alone, and
  -SillQuitAfter '<s> restart parameter' sends the reason as a parameter.

The goodbye check covers the choice with willPowerOff, the menu's Quit and the ten minutes, the
reason read both ways and only from a quit event, and the new goodbye's words and lines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d the device closes its end at once

goodbyeAll(close:) cancelled each connection as soon as the sleep goodbye was handed over. A
streaming device keeps sending pings, stats and input. Closing with that still unread makes TCP
answer with a reset, which can reach the device before the goodbye, as closeWithGoodbye's own
comment says.

Now it closes as closeWithGoodbye does:
1. the goodbye;
2. this side's end;
3. what the device still sends, read and dropped by its own receive loop until the device
   closes too, DeviceGate.closeWait at most.
Nothing else is sent to a connection after its goodbye (told): no frame, tick, pong, pointer
report or state. What the device sends after it is no longer acted on.

Over TLS that end goes only at the cancel, a second later, and a Mac that has gone to sleep sends
it only once it wakes. The device would have waited for its liveness to give up (6 s). So the
device now closes the connection itself as soon as it reads "sleep", "shutdown" or "restart"
(GoodbyePolicy.closesAtOnce), and its session ends at once with those words. The Mac's drain then
sees its end.

Live on loopback, two TLS clients sent a pointer move every 50 ms through a sleep goodbye: both read
it, then the end a second later, with no reset. On a simulator the iPad's session ended with the
words at once, and the Mac logged "Client left" 2 ms after the goodbye. The goodbye check covers
closesAtOnce, with three mutants.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The log file is written on a queue of its own, and HostShutdown's signal path exits right after
the goodbye. On loopback, a SIGTERM after a staged willPowerOff printed "The Mac is shutting
down or restarting: 2 devices told." on stdout but not in the log file. That line says what the
devices heard at a real restart.

HostLog.flush now waits, 0.5 s at most, for the lines recorded so far. It runs before the signal
path's exit and at the end of the app's Quit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed, the bounded hold, and Noah's list

docs/DEVELOPMENT.md:
- The Sill.app paragraph says what was observed of a log out, restart or shut down: a restart
  begun from another account ended Sill with SIGTERM, loginwindow's kill attempt and quit
  event, and AppKit posting willPowerOff with a quit event's reason attribute.
- It says what is inferred: that a log out, restart or shut down begun in Sill's own session
  sends Sill such an event. It also gives the willPowerOff goodbye, and notes that every sleep
  now ends every session.
- The reconnect paragraph: the bounded hold, and the goodbye read on the return (on a session
  from before it).
- The test arguments: -SillPowerOffAfter, and -SillQuitAfter's parameter form.

docs/update-notice-plan.md §7.3: the rows and the paragraph to match.

CLAUDE.md:
- The entry: the design after the review, its findings and fixes, the verification, and the
  list for Noah. Every sleep now ends every session, with what the host does as the last device
  leaves; it adds a restart begun from another account and the iPad locked or Sill in the
  background through a sleep or a restart, and drops the moot cancelled restart.
- Layout, and Build and run.

Tests/checks/README.md: the goodbye and policy rows and their counts (141 and 89; 507 and 201).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Saffsanity and others added 23 commits October 20, 2026 05:05
Main gained Saffsanity#110 (Shift+Return and Shift+Tab reach the Mac from an iPad:
InputOverlay, KeyChords and the key-strokes check) and Saffsanity#87 (VirtualDisplay's
BOOL return spelled "B" or "c", with the virtual-display-api check). Neither
touches a file this branch changes but CLAUDE.md's Layout, docs/DEVELOPMENT.md
and Tests/checks/README.md, whose lines merged by themselves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main brought Saffsanity#88, a universal Sill.app on request (SILL_UNIVERSAL=1): make-app.sh,
release.sh, docs/DEVELOPMENT.md and CLAUDE.md, merged by git with both sides kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d main at 4d990e9 in the entry

docs/update-notice-plan.md §7.3: README's Good to know, the support page and the next What's New
say nothing of the Mac's goodbyes yet. The sleep's has shown only through the bare app's test hook
and a simulator, and the restart's and shut down's rest on an inference. The section now holds the
lines they gain once Noah's real Sleep, Restart and Shut Down pass, and says the site's line goes
between Goodbye markers, with a perl line in the release checklist, as the Clipboard's does.

CLAUDE.md: the entry names main at 4d990e9. Noah's list says what a Sill.log with only "Shutting
down…" means (a signal or a quit without a reason, with no willPowerOff before it: the inference
is wrong), and that the waiting lines go in as each step passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nto goodbye-sleep

Main gained the display wake (Saffsanity#151: user activity declared for a device's connect and a person's
input, DisplayWake and DisplayActivity), the Close timeout and the interrupted picture (Saffsanity#93: kind 44,
KeyframeRecovery) and TEST ONLY SILL_TEST_NO_SCREEN (Saffsanity#227). Only CLAUDE.md conflicted: both sides
added entries at the top of Current step, and both are kept, this pull request's first.
StreamCoordinator (the sleep and power-off observers beside the display wake's), StreamClient (the
goodbye's connection beside the keyframe recovery), docs/DEVELOPMENT.md and Tests/checks/README.md
merged by themselves. This pull request adds no message kind; kind 44 stays main's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…at the probe adds

The entry said a restart begun from another account ended Sill.app by SIGTERM with "no
willPowerOff", as if that had been watched. What was watched is launchd's "exit(143) during system
shutdown" and no log out in Sill's own session; that no willPowerOff came follows from the probe
(AppKit posts it only as it handles a quit Apple event whose reason is an attribute), and the
sentence now says so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…h behind a sleep proxy

Since Saffsanity#151 the host declares user activity at every device's connect, so
a dial through an Apple TV's or HomePod's sleep proxy that wakes the Mac
and connects after a sleep goodbye would also light its display, with
nobody at it. Noah's sleep test now says to note whether the dials wake
the Mac and whether Sill.log then says "Woke the display for …, which
connected." The entry names main at 36e7212, merged in at 2fb87c8.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Verified bullet now says what ran on this head: a clean release build
from a git archive, the iOS app as CI builds it from fresh derived data,
goodbye's and policy's mutants each by its own run.sh --mutants, one run
at a time as CI's matrix runs them (89 of 89, 201 of 201), run-all.sh
(48 of 48), the other checks' mutant patterns against the changed files
(179 of 181, the two duo's generic one that aims at DuoPosture.swift),
the bare app's ten goodbye runs on loopback and the DiscoveryPolicy
driver. The earlier round's parallel mutant counts are gone; its
simulator runs and the willPowerOff probe stay, marked as that round's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sleep

Only Tests/checks/README.md conflicted, two neighbouring rows each side
changed alone: this branch's `policy` row (issue Saffsanity#105's cases, 507 and
201) and main's `protocol` row (Reverse Scrolling's cases, 205 + 8 and
32), both kept as their side wrote them. CLAUDE.md, docs/DEVELOPMENT.md
and iOSClient/StreamClient.swift merged by themselves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On the head with Saffsanity#159 merged in: a clean release build from a git
archive, the iOS app as CI builds it from fresh derived data (the known
StreamClient warning now at line 3904), run-all.sh's 48, goodbye's 89 of
89 mutants again (its check compiles every StreamProtocol source, which
the merge changed), the bare app's ten goodbye runs again, alike. Policy's
201 of 201 stand from 36e7212's merge: the merge leaves DiscoveryPolicy.swift
and the check as they were. The entry names main at c215514.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e-sleep

Three files conflicted, each kept whole on both sides:
- CLAUDE.md: both Current step entries, this branch's first, then main's
  "The Mac's sound on the device".
- Sources/SillHost/StreamServer.swift: Client's fields, `told` beside
  Saffsanity#94's audioCodec, audioEpochSent and audioUnsent.
- Tests/checks/README.md: this branch's policy row (507, 201) beside
  main's protocol row (234 + 8, 42).

The sound's own send path (broadcastAudio(packets:), sendAudio) does not
check `told` yet; the next commit makes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Saffsanity#94's sound has a send path of its own (broadcastAudio(packets:) and
sendAudio, a direct connection.send) that checked no `told`, so after the
merge a connection told why it closes (the Mac's sleep, a Quit, Remove)
could still be sent kind 43 behind its goodbye, against `told`'s rule
and, after the sleep's goodbye, behind its .finalMessage. The packets'
loop now skips a told client, and sendAudio guards as `send` does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The verifier's review of 2026-10-26 (P1, blocking): from the sleep goodbye
until the Mac actually slept, and on until it woke, the doors kept
admitting sessions, and with Saffsanity#151 merged every connect declares user
activity (DisplayWake), which could light the display of a Mac someone
has just put to sleep, or keep it awake. 0.5.1, the App Store build,
reads "sleep" as a reason it does not know with "reconnect":true and
dials the still-listed row as soon as its session ends, the Mac's cancel
a second after the goodbye: on loopback a TLS client that dialled 0.3 s
after the goodbye was admitted, served and counted as a display wake.

- HostGoodbye's SleepWindow (pure): asleep from willSleep until didWake,
  opened again by a willSleep, and lapsing ten minutes of the Mac's awake
  time (systemUptime) after willSleep, so a didWake that never comes
  shuts no door for good; its refusal is the sleep's own goodbye; its
  log lines.
- StreamServer keeps it under a lock. Every door's session passes `gate`
  before it is registered (both TLS doors through Door, the plain door at
  .ready), so while the Mac is asleep the gate closes it with the sleep
  goodbye (`refuseAsleep`, as the gate's refusals close: the goodbye, FIN,
  the drain), never registered: no catalog, no capture, no encoder, no
  declaration. A source refused five times in a minute hears the next 2 s
  late, the device gate's slowdown shared. One line per source a minute,
  "The Mac woke: devices can connect again (N sessions refused while it
  slept)." at didWake, and a line when a window lapses.
- StreamCoordinator: the window opens before the sleep's goodbyes are
  sent (the check and the registration are one turn of the network
  queue, so a session registered before it is told and one after it is
  refused); NSWorkspace's didWake ends it; DisplayWake.decide's
  `macAsleep` declares and counts nothing while it runs (a session
  registered just before willSleep has its connect heard after it).
- This build's device reads the refusal as the sleep's: a notice, so at
  either door, a remote dial's before its first window list too, with
  the Mac counted as asleep from it (its first dial 20 s later).
- TEST ONLY, the bare binary (Sill.app ignores it): -SillWakeAfter <s>.
- Checks: goodbye 160 (the window, its limit, a willSleep again, didWake,
  a clock gone back, the refusal as this build reads it, the lines, and
  20,000 random sequences against the rule stated on its own), with 17
  new mutants; display-wake 211 (nothing while asleep, the interval left
  as it was, a test host counting nothing, and 5,000 random sessions with
  spans asleep beside a rule that never sees them), with 5 new mutants
  and M6 and M7 following the guard they mutate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The verifier's review of 2026-10-26 (P3): a goodbye that waited in the
connection while Sill was suspended (the Mac went to sleep or restarted
while the iPad was locked) was never read. On the return the suspended
app's ping timer, on the session's queue, fires once, overdue, and its
liveness check ran before willEnterForeground's resetLiveness hop reached
that queue: the time suspended counted as silence, "connection silent for
N s: lost", and the session ended as main's does ("Mac disconnected…" and
a dial at once) before its reader got to the goodbye.

DiscoveryPolicy.liveness (pure) is the verdict at each tick: a tick a
second or more after the one before (livenessGap, four pings) is `resumed`
and judges nothing, and StreamClient.checkLiveness then counts the silence
and the viability afresh from it, as resetLiveness does; then the silence,
then a remote path's viability, as before. A connection dead across the
suspension is still lost, 6 s after the return. One iOS reclaimed while
the app was suspended loses its goodbye anyway (DEVELOPMENT.md, §7.3).

Checks: policy 526 (19 new: the verdict at its edges, and a model of the
session's queue across a suspension, which loses the goodbye to the
silence without the rule), and 10 new mutants (L1-L10). The iOS app
builds as CI builds it, with the known StreamClient capture warning only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eaning

The verifier's review of 2026-10-26 (P6, a wire decision): "shutdown"
carried two meanings, a shut down Sill knows ("‹Mac› is shutting down.")
and willPowerOff's "shutting down or restarting", so this build worded
even a known shut down "is shutting down or restarting", vaguer than the
Mac's own message 0.5.1 shows, and the compatibility floor would freeze
that once it shipped.

The maintainer's default, for Noah to reverse before the merge: a reason
of its own for the case Sill cannot tell, "poweroff", in the words
"‹Mac› is shutting down or restarting." (Goodbye.shuttingDownOrRestarting;
HostGoodbye's `.powerOff`). "shutdown" is now only a shut down a quit
Apple event's reason said. This build words each ("is shutting down.",
"is shutting down or restarting."), waits on both as on a restart (went
.off) and closes its end at once on either; an older device shows the
Mac's message as it is, with its reconnect. The floor in CLAUDE.md now
keeps kind 22's reasons with their meanings, these four among them.

Checks: goodbye 169 (each reason's words and wait, and a sweep of every
path to Sill's end: "shutdown" exactly when a quit's reason said a shut
down, "poweroff" exactly when willPowerOff stands and nothing said
which), 116 mutants (the shutdown ones that now name poweroff's block
renamed, shutdown's own added, and poweroff's). Docs: §7.3's table and
prose, §13's note, DEVELOPMENT.md, CLAUDE.md's entry, Layout and floor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The verifier's review of 2026-10-26 (P4, P5):
- DEVELOPMENT.md and Noah's lid-close item in CLAUDE.md said what Sill
  does when the last device leaves (capture stopped, a sized window back,
  the virtual display gone) runs as the Mac sleeps. It does when the
  device closes its end on the goodbye, as this build's does; 0.5.1 only
  stores the goodbye and waits for the Mac's close, which goodbyeAll makes
  a second later by a DispatchTime timer whose clock stops while the Mac
  sleeps, so that work can come at the wake instead.
- docs/update-notice-plan.md §7.3 gave the rule as
  GoodbyePolicy.outcome(goodbye, mac:, device:, saved:) -> Outcome (text,
  reconnect, remoteAllowed, storeLink, isNotice): it takes `remote:` and
  returns `went` since this pull request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ring) into goodbye-sleep

Main moved on while the review's fixes were made. Only CLAUDE.md
conflicted: both Current step entries kept whole, this branch's first,
then Saffsanity#157's. DebugHooks.swift merged by itself, this branch's
-SillWakeAfter in the timed hooks beside Saffsanity#157's
showPairingWindowToScreenSharing; Tests/checks/README.md, DEVELOPMENT.md
and ci.yml's matrix (pairing-sharing added, still sorted) likewise.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…fsanity#233, capture at the display's own scale) into goodbye-sleep

Only CLAUDE.md conflicted: this branch's Current step entry first, then
main's two new ones in main's order (Saffsanity#233's, Saffsanity#147's), then the rest as
main has them, each once. StreamCoordinator.swift merged by itself:
Saffsanity#233's capture paths (CaptureScale, streamDisplayScale, catalogPolled)
meet none of this branch's code there (observeSleep and the wake, the
goodbyes, deviceActive's macAsleep). StreamClient.swift likewise: Saffsanity#147's
onCaretMayMove and sendAfterInput beside this branch's liveness verdict.
Tests/checks/README.md (54 rows, one a check), ci.yml's matrix (54,
sorted) and DEVELOPMENT.md merged by themselves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd's numbers

The entry: main at b4caa91; the sleep window in the Host bullet and
-SillWakeAfter; a bullet for the review's P1 to P6, each with its
commit; this round's Verified bullet (the earlier round's kept, marked);
Noah's list gains 0.5.1 streaming through a Sleep (the Mac stays asleep,
its display dark), what Sill.log says behind a sleep proxy (whether a
dark wake posts didWake), the Mac's own words after a locked iPad (with
"disconnected" the known limit), the words for each going, and the
"poweroff" decision. Tests/checks/README.md: goodbye 169 and 116,
display-wake 211 and 67, policy 526 and 211.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The three sleep runs counting sound after the goodbye were on aed225a's
build (the head's own sleep run showed none too), the Quit's on 697e6c0's
and aed225a's; the told rule and the sound's send path are unchanged
since e1e73cf.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Run again against main's CLI at b4caa91 after its merge: idle identical,
and with a Desktop client identical but for cursor.shape, which follows
this Mac's own cursor and came in both builds' runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

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

2 participants