Repository navigation
Conversation
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.
# Conflicts: # Tests/checks/README.md
|
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: |
|
From a pass over all the open pull requests (nothing is decided yet): #117 also adds a line to |
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.
|
Thanks. Wrapped the |
…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>
…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>
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>
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>
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
--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.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".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.HostGoodbyeandSleepWindow(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
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 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
kAEQuitReasonis an attribute, just before applicationShouldTerminate. With the reason as a parameter, or with no reason, it posts nothing.Changed from the pull request as opened
The contributor's commits are untouched; these are on top.
git cherry-pick 8aee786).told.outcomesignature.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.<!-- Goodbyemarkers, with a perl line indocs/release-checklist.mdpart 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 itselfCLAUDE.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 (
toldbeside #94's audio fields) andTests/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.swift build -c releasefrom a cleangit archiveof the head: "Build complete! (49.42 sec)", only the known CaptureProbe warning.** BUILD SUCCEEDED **, only the known StreamClient capture warning (StreamClient.swift:3962, the autoDesktop closure).--syntheticon loopback, masked and sorted): idle 30 s identical; with a Desktop client 15 s identical in three alternating runs but forcursor.shape, which follows this Mac's own cursor and came in both builds' runs.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.capture-scale(79),pairing-sharing(18) andtext-composition(178).Tests/checks/run-all.sh: "All passed.", all 54 on the head.SillMenuBar --synthetic, the identity in a scratchSILL_TEST_REMOTE_DIR,sillclient.py --tls --identity=…):-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", nodisp.wakeDryand 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.-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.-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".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:
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:
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