Repository navigation
Audit fixes: the Close timeout and the interrupted picture - #93
Conversation
addhumal
left a comment
There was a problem hiding this comment.
Hey found few issues hope i can help. Got few more features planned.
…ault The close button is a new AX element, on the default messaging timeout, and the host presses it on its main actor: an app whose close runs a modal alert, or that hangs, held input, picks and settings for every device that long. The button is now type-checked and given a 1 s timeout, as every other AX element the host uses. Check: Scripts/menu-check/run.sh close, against menufixture's new Slow Close window (its close holds the fixture's main thread 4 s): the press must come back within 1.5 s. Built here; not run: this shell has no Accessibility permission (AXIsProcessTrusted false), which menu-check needs.
The display layer was only checked for `.failed`, and then waited for whatever keyframe came next: a layer that needs a flush to decode again (iOS sets it after the app was in the background) was never flushed, and a still window sends no keyframe, so the picture could stay frozen or black until the window changed, plus up to the 4 s safety keyframe. Now both states flush the layer, wait for a keyframe and ask for one by picking the source already streaming (kind 6), which every Mac answers with a restart; at most once per 2 s (KeyframeRecovery, pure; pbxproj AC01/FC01). The return to the foreground checks the layer too, as no frame may come to notice it. In regular mode the re-pick brings the streamed window forward on the Mac, as any pick does. Check: Tests/checks/keyframe-recovery, 10 checks and 7 of 7 mutants (red before the rule existed); in run-all.sh by its folder and in CI's mutants matrix. The iOS app compiles here except for DuoPostureReader.swift (this Mac's Xcode 27.0 lacks the iOS 27.1 SDK), StreamClient.swift and HEVCDisplayView.swift without an error. Not run: the app on a simulator or device (background 2 s and back against SillHost --synthetic should give one restart and the picture).
Review finding 1. In the background the display layer reads as needing a flush at every frame until iOS suspends the app, so the iPad picked its source again every 2 s there: a restart of the stream every device shares and, in regular mode, its window raised on the Mac over whatever its user was doing. The ask now goes out only while the app is active, the foreground check moves to didBecomeActive, and the view remembers a flush with no keyframe since (`interrupted`), so the return to the front still asks once when the background's flush already cleared the layer's flag. Also review finding 4: keyframeRecovery no longer sits between displayView and its doc comment. Checked: the iOS app compiles but for DuoPostureReader.swift (Xcode 27.0 here); keyframe-recovery as before.
Review finding 2. The label command took the first static text of the fixture's first window, and Slow Close, ordered in front of the label's window, has its title bar's. The reader and mirror checks read the label to see a press land. Compiled; not run here (no Accessibility).
# Conflicts: # .github/workflows/ci.yml
Saffsanity#66 both add AC01/FC01)
# Conflicts: # .github/workflows/ci.yml
|
From a pass over all the open pull requests (nothing is decided yet): this edits the same decode path as #107, which adds its own keyframe re-pick ( |
# Conflicts: # .github/workflows/ci.yml # iOSClient/StreamClient.swift
…affsanity#93; framebufferOnly false for snapshots
|
Thanks. Merged main in (d821e1a, after #78, #79, #80 and #98). In #107 (7c7651e), |
…ince Saffsanity#75 Scripts/menu-check/build.sh compiled WindowSizer.swift alone for the close step, and since Saffsanity#75 (issue Saffsanity#57) WindowSizer's resize calls WindowRestore (`fit`, `Access`, `Write`, `keep`), so the harness stopped at "cannot find 'WindowRestore' in scope". WindowRestore.swift needs only Foundation and CoreGraphics, so it goes into the same module. The harness builds again with no warning, and `Scripts/menu-check/run.sh close` ran here against the fixture's Slow Close window: the press back within its own 1 s, the close delivered, the front app unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With the button's 1 s timeout, a close that an app answers late (its close asks about unsaved changes, or it hangs) comes back as kAXErrorCannotComplete, and the host logged it as "Window 104 close: refused", though the press reached the window and the close may still happen. `WindowSizer.perform` now says what came of a light (`CommandOutcome`: done, refused, or no answer in time), for close, minimize and full screen alike, and the line reads "Window 104 close: no answer in time (the app is busy, or asking about unsaved changes); it may still act". The other callers, which discard the answer, are unchanged. The close harness checks the words too: against the fixture's Slow Close window it came back in 1.01 s as no answer, the fixture logged windowShouldClose, and the front app never changed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was no way for a device to ask for one: the host asked its encoder for a keyframe only for a device that joined and for one whose delta it dropped. A device whose display layer lost its picture (iOS stops it in the background; a frame it could not decode) waits for a keyframe, and a still window never sends one, so the picture stayed blank or frozen until the window changed (issue Saffsanity#101). This PR's first answer, a pick of the source already streaming (kind 6), restarts every device's stream and in regular mode raises the window on the Mac. Kind 44, `keyframeRequest`, device → host, empty payload. The host answers it with HEVCEncoder.requestKeyframe, as for a joining device: the next repaint carries a keyframe, or a still window's last frame is encoded again, and every device gets it. At most one a second per connection (a device asks once per interruption, 2 s apart at least; each costs every device a keyframe), the rest dropped with one line a minute (RefusalSummary); nothing while nothing streams. One line per ask: "Keyframe for ‹device›: its picture was interrupted." An older host maps 44 to .unknown and skips it, so a device's picture there waits for the window's next keyframe, as it always did. 44 is the first number no open pull request holds (29 to 31 and 33 to 43 are). Also, TEST ONLY: SILL_TEST_STILL_AFTER=<s> makes a synthetic host's Desktop deliver no frame from s seconds on, as a window that stopped changing, so a device's picture comes back only through its ask; and sillclient.py's --keyframe@T sends kind 44. The pointer-control check's kind table takes 44 (158 checks, 33 of 33 mutants). Checked against a loopback synthetic host on the software encoder, still from 1.5 s: --keyframe@3, @3.2, @3.4 and @5.5 gave keyframes 50 to 60 ms after the two taken, one "ignored" line, and a kind 44 with nothing streaming gave nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ked again Review of this PR (the triage of 2026-10-16 and 2026-10-19): the recovery picked the source again every 2 s, which restarts every device's stream and raises the window in regular mode, for ever if a layer kept failing. Now the device asks for one keyframe (kind 44), once per interruption. HEVCDisplayView.checkLayer, on the network queue, flushes a layer that failed (`status`, `error`), could not decode a frame (its failedToDecodeNotification) or needs a flush to decode again (requiresFlushToResumeDecoding, and its change notification): at each frame let through, at each notification (a still window sends no frame that would show it) and when the picture's window becomes active. It says so (`onNeedsKeyframe`, the hook Saffsanity#107's renderer calls too), and a keyframe that ends the wait on a healthy layer says the picture is back (`onPictureBack`). A decode failure posted before the picture last came back is about an earlier frame and is ignored, or it would throw the new picture away. The Mac sends its parameter sets before every keyframe and each sample carries the format, so nothing else is sent again. KeyframeRecovery (pure, main thread) decides when StreamClient sends the ask: one per interruption, owed again after a trip away (a keyframe that comes meanwhile is lost to it); only while the picture's window scene is foreground active (each Sill window has its own client) and something streams; at least 2 s after the last ask, an ask held back going when it is due; a picture that fails again within 10 s of its keyframe, Sill active throughout, waits 4, 8, 16, then 32 s, so a decoder that cannot keep up never costs every device a keyframe every 2 s; a trip away or 10 s back ends that. Tests/checks/keyframe-recovery: 51 checks and 20,000 random sessions against a model of the event history, 28 of 28 mutants. It found a real fault on the way: an ask held back until `last + spacing` was held again at that very time, (last + s) - last rounding below s; it compares with the due time now. DEBUG: the console's "display: …" and "keyframe: …" lines, and -SillLayerFail <s>[,<s>…], which enqueues a frame that cannot decode at those seconds after the first picture (ContentView's harness contract). Live, on a private iPad simulator (iOS 27.0) against a loopback synthetic host on the software encoder whose Desktop is still from 3 s: -SillLayerFail 6 gave the layer's own -11821, one ask, one "Keyframe for …" on the host, the picture back 75 to 108 ms after, and a single "Synthetic capture" line (nothing picked again); -SillLayerFail 6,6.6,12 asked at once, then 4.2 and 8.1 s after the ask before, three host lines. A trip to the background does not stop the simulator's layer: nothing was asked and the picture was as it was (a device has to show that case). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CLAUDE.md: the Current step entry for this pull request (what was kept, what replaced the re-pick, kind 44, the rule, the verification, the note for Saffsanity#107 and Saffsanity#66, and what is left for a device); the Layout (kind 44, the coordinator's answer, WindowSizer's CommandOutcome, KeyframeRecovery, HEVCDisplayView's checkLayer, SILL_TEST_STILL_AFTER, the fixture's Slow Close window and the close step, sillclient's --keyframe@T, the check); Build and run (the still host, --keyframe@T, -SillLayerFail); and kind 44 among the compatibility floor's kinds, empty and skipped by a host from before. docs/DEVELOPMENT.md: the picture's recovery under "If the picture freezes" (the host's line, the rule, an older Mac), the lights and Close's timeout after "Windows sized for a device", the menus' test app's Slow Close window and the close step (and that menu-check needs an unlocked screen), -SillLayerFail in the debug harness, the still host among the CLI's test variables, and --keyframe@T beside the test client's flags. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main gained Saffsanity#153 (a saved Mac no longer disappears after a force quit) and Saffsanity#142 (a typed destination survives a key change); neither touches the display path, the host's commands or the checks this pull request changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hat the app is busy CommandOutcome.noAnswer had one set of words for every light, so a slow minimize logged "Window N minimize: no answer in time (the app is busy, or asking about unsaved changes)", and only a close asks about unsaved changes. `words(for:)` now takes the action: a close keeps its words, a minimize or full screen says the app is busy and that the window may still minimize, or go in or out of full screen. The close harness (Scripts/menu-check/run.sh close) checks every action's words and how AX's answers map to done, no answer and refused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…me requests
TEST ONLY, honoured only by a --synthetic host: the first n keyframe requests (kind 44) it takes are
ignored with a line each ("Test keyframes: ignored a keyframe request from ..."), as a Mac from
before kind 44 skips every one, so the device's ask again once its keyframe is overdue can be run
for real against a loopback host (issue Saffsanity#101). One start line says it is on; on another host, or with
a value that is not a positive count, one line says it is ignored. The coordinator's comment on the
requests says a device asks again only when its keyframe has not come in 4 s or more.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…afresh after a trip away The verifier's two findings on KeyframeRecovery (issue Saffsanity#101): - A keyframe the layer's own flush threw away (the layer still read failed when it came) left its interruption open with its ask counted as out, so while Sill stayed active nothing was asked again: a still window stayed frozen for good, a moving one until its next periodic keyframe. Now HEVCDisplayView says the picture came back and was lost again (onPictureBack, then onNeedsKeyframe), and every ask has a wait: no keyframe within answerWait, max(4 s, 4 x the worst round trip), and the ask is owed again as a repeat. A Mac from before kind 44 skips every ask, so it is asked after 4, 8 and 16 s, then every 32 s, which costs it nothing. - A trip to the background kept a streak's spacing (in the verifier's probe 24 s more of a frozen picture after the return), and keyframes that came while Sill was away cleared the trip, so ones iOS then refused built a streak. Now leftForeground starts the spacing at 2 s, and a keyframe shown while the window is away keeps the trip, so the next interruption is not a repeat. KeyframeRecovery.consider takes the wait and says when an ask's keyframe is due (keyframeDue); StreamClient looks again then, and the DEBUG console says "keyframe: no keyframe 4.0 s after the last ask; asked again". DEBUG -SillKeyframeLost <n> throws the first n keyframes that end a wait away as though the layer still read failed, so the first finding's path runs in a simulator. Tests/checks/keyframe-recovery: 95 checks (a Mac that never answers is a bounded series of asks; a lost keyframe; the trip during a streak; keyframes shown while away), 20,000 random sessions against a model of the event history that knows waits, lost keyframes and trips (never stuck while open, active and streaming), and 39 of 39 mutants, three of them each finding put back. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main gained Saffsanity#136 (the Apps list finds apps in a folder of their own, and its installed-apps check); it touches the window catalog and the checks' list, not the display path, the host's commands or the keyframe check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> # Conflicts: # .github/workflows/ci.yml
Kind 29 was free on 985a733, where the pen was built; since, main's comment and checks keep 29 to 31 free (29 the audio plan's, 30 and 31 Saffsanity#66's side streams), 32 is the clipboard, and open pull requests hold 33 to 44 (Saffsanity#66's 33 to 42, Saffsanity#94's 43, Saffsanity#93's keyframeRequest 44, checked against every open head today). So `case pen = 45`, and everything that names the number follows: SessionLink.isInput (8 or 45), the comments of Pen.swift, Switcher, PenTablet, InputInjector and StreamServer (which match the enum, not the number), sillclient.py's --raw45 and its sends, and the docs (DEVELOPMENT.md, CLAUDE.md's entry, Layout and Build and run, the plan's design and gates, with a note that its results before the rebase say 29). The compatibility floor's kinds are 0–28, 32 and 45 again with 29 to 31 free as main has them; kind 26's `seen` counts kinds 8 and 45. The checks: compatibility names kind 45 and 46 unknown (157 checks; its mutant "kind 45 as 46"), protocol keeps main's two kind checks and has the pen at 45 with 46 unknown (193 + 8), pointer-control's table and sweep say 45 (158), fence's input and its two C10 mutants say 45; menus' line and mutant and pointer-control's two renumbering mutants are main's again, 29 being free once more. clipboard passes unchanged. Run here: compatibility 157, protocol 193 and the 8 cross-checks, pointer-control 158, menus 309, clipboard 259, pen 197 and 5,000 random sessions, all passing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main gained Saffsanity#110 (Shift+Return and Shift+Tab from a hardware keyboard: InputOverlay, KeyChords and the key-strokes check), Saffsanity#87 (the virtual display's BOOL encodings, its virtual-display-api check) and Saffsanity#88 (a universal Sill.app with SILL_UNIVERSAL=1). Only ci.yml's mutants matrix conflicted: both new checks kept, keyframe-recovery and virtual-display-api, in sorted order. None of it touches the display path, the host's window commands, kind 44 or the keyframe check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ac, marked for its release Kind 44 is a new message from the device (issue Saffsanity#101): when its picture was interrupted, it asks the Mac to send the picture again. privacy.html's list of what a device sends its Mac names it, between <!-- Keyframe and <!-- /Keyframe --> as the clipboard's item is, with the date October 20, since the builds out today never send it. docs/release-checklist.md part 1 §3 gains the perl line that cuts the block from the sill-site copy for a republish before the release that ships it, and its grep check (tried on a copy of site/: no "keyframe" left, nothing else changed but the date it puts back). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ent was cut off; see the handoff) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…rom git archive The verification bullet said `swift build -c release` ran from `git archive` with only the CaptureProbe warning; it ran in the worktree on the merge with main at 4d990e9 (complete, exit 0, no new warning). Everything else in the bullet is as measured: run-all.sh all 47, keyframe-recovery 95 checks and 39 of 39 mutants, pointer-control 158 and 33 of 33, the iOS app with only the known StreamClient warning, menu-check close, the simulator runs and the 0.5.1 host. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks for the audit and for both fixes. Your branch now has commits on top of yours; none of yours were rewritten, and main (4d990e9) is merged in. Close: kept as you wrote it (the type check, then the 1 s timeout on the button). Added:
The interrupted picture: picking the source again restarts every device's stream, raises the window on the Mac in regular mode, and with a layer that keeps failing it repeated every 2 s. The device now asks for a keyframe instead:
For #107: its renderer already calls For testing: DEBUG |
StreamCoordinator keeps both test hooks (SILL_TEST_USER_ACTIVE and SILL_TEST_KEYFRAME_IGNORE); ci.yml's matrix, CLAUDE.md, DEVELOPMENT.md and the release checklist keep both sides. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The coordinator keeps both test hooks: the keyframe-ignore hook of #93 and SILL_TEST_NO_SCREEN, their lines both among testHookLines. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…nto zhuyin-input Four files conflicted, each settled keeping both sides: - ci.yml's mutants matrix: main's display-wake and keyframe-recovery with this branch's text-composition, in order. - Tests/checks/README.md and docs/DEVELOPMENT.md: the lists of what the checks decide name the keyframe request and the display wake beside what an input method or dictation sends. - docs/release-checklist.md: main's paragraph on the site's markers had come out of Saffsanity#93's and Saffsanity#151's merges as two copies of one item run together; it is one item again naming all four markers (Clipboard, Keyframe, Display wake, Other languages) and the grep for each, and the perl lines run one per marker. Tried on a copy of site/: the four lines leave no marker, and the greps print only privacy.html's clipboard log line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the re-run - CLAUDE.md's Current step gains the pull request's entry, as Saffsanity#93's and Saffsanity#151's have theirs: what the PR does, the triage's four changes and what else was found, the docs, what was verified on the tree merged with main at 36e7212, and what waits for Noah and Saffsanity#138's testers on TestFlight. - docs/DEVELOPMENT.md, "Other languages and dictation": a shortcut pressed while an input method composes (Command-S, say) goes to the input method, not the Mac, as the simulator showed (Zhuyin's ㄘㄛ composing, Command-S: nothing reached the host); the simulator's runs repeated on the merged tree with the same results; a shortcut mid-composition joins what no device has tried yet. - README's How it works names Korean beside Chinese and Japanese, as the support page and What's New do, and its paragraph is wrapped again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nto connection-list Five files met; each keeps both sides: - ci.yml's mutants matrix names connection-list and keyframe-recovery, sorted. - DEVELOPMENT.md's harness list: the tiles bullet and the -SillHUD note, then Saffsanity#93's -SillLayerFail bullet. - The release checklist's paragraph on marked words names all four markers (Clipboard, Keyframe, Display wake, Tiles) once; main's copy repeated the Clipboard and Display wake sentences after the Keyframe ones. With the four perl lines on a copy of site/, the clipboard grep prints only privacy.html's log line and the keyframe, display wake and tiles greps print nothing. - The project file: ConnectionList (AF01/FF01) beside KeyframeRecovery (AC11/FC11). - HEVCDisplayView.swift: Saffsanity#93 replaced enqueue's failed-layer branch with checkLayer, which flushes an interrupted picture from a frame, the layer's notifications and the window becoming active. The snapshot's showsPicture now goes false in checkLayer after its flush and true after the keyframe that ends a wait goes onto the layer (git put that line in the DEBUG breakLayer, where `resuming` is not in scope). Live on a private simulator with -SillLayerFail 6 against a synthetic host that ignores keyframe requests: a session that ends while the picture waits keeps no picture, and the one kept before stays. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…nto reverse-scrolling Three docs files met, each keeping both sides: CLAUDE.md's Layout (the StreamProtocol paragraph with ScrollDirection beside Saffsanity#93's kind 44, and the TrackpadView and HEVCDisplayView paragraph beside the keyframe recovery), the checks README's lede, and the release checklist's paragraph on marked words, which now names Clipboard, Keyframe, Display wake and Reverse Scrolling once each, with all four perl lines. 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>
Saffsanity#93's kind 44 (keyframeRequest: a device's ask for a keyframe after its picture was interrupted) and its Close timeout, and Saffsanity#227's SILL_TEST_NO_SCREEN, beside the sound's kind 43. Saffsanity#151 has been in since 7b4c313, the merge base; its display-wake check is where Saffsanity#93's 44 meets the sound's 43. Nine files conflicted, each resolved by keeping both sides: - .github/workflows/ci.yml: the mutants matrix, kept sorted, keyframe-recovery added beside the sound's audio-codec, audio-packetizer, audio-playout and ledger (51 checks). - CLAUDE.md, five hunks: the Current step's entries (the sound's first, then Saffsanity#93's, then Saffsanity#151's); the Layout's StreamProtocol paragraph (Audio.swift, and kind 44 with no type of its own), its Scripts paragraph (the sound harness, and the restore probe's close step) and its checks list (keyframe-recovery and ledger); the compatibility floor's kinds (0–28, 32 and 44 kept, 43 the sound's below, 29 to 31 and 33 to 42 free). - Sources/StreamProtocol/StreamMessage.swift: 43 audio, then 44 keyframeRequest. - Tests/checks/README.md, four hunks: the opening list (the keyframe ask and the sound's packets), display-wake's row (43 and 44 never declare; 196 checks, 62 mutants), keyframe-recovery's row beside ledger's (the sound's 99 and 12), phone-portrait's row as the sound's (495 and 80), and pointer-control's with 43 and 44 (159). - Tests/checks/display-wake/main.swift and mutants.py: 43 and 44 both among the kinds that never declare, and both mutants, M61 (the Mac's sound counts) and M21b (a keyframe request counts). - Tests/checks/pointer-control/main.swift: the kind table with 32, 43 and 44, every other kind (29–31, 33…42, 45…254) unknown. - docs/release-checklist.md: the paragraph on marked words written once, for the Clipboard, Keyframe, Display wake and Sound blocks, where main's copy repeated itself (Saffsanity#93's with Saffsanity#151's), with the four perl lines. - iOSClient/StreamClient.swift: init ends with sound.setMuted, onRouteMuted, then observePictureActivation. Four checks git merged without a conflict failed on the result, fixed here: compatibility (main.swift:258), menus (main.swift:26) and protocol (main.swift:370) asserted that kind 44 is unknown, and it is .keyframeRequest now; display-wake (main.swift:104) counted 32 kinds, and there are 33. Now display-wake has 196 checks and 62 mutants, pointer-control 159, and Tests/checks/run-all.sh passes all 51. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he row-1 Sound button - CLAUDE.md, the Layout's PhonePortraitLayout: with Larger Picture, Menus in row 1 and no Sound button, the Settings panel's switch alone (6effec7 took the row-1 Sound and `row1Beside` out): `sound` and `stripBesideBoth` are the strip's empty place, `menusBesideSound` is `menus`. - docs/audio-plan.md: "Since the beta" notes where the body still had the design before it, as §4.1 and §7.7 have: Controls' Send Audio (Q8: the Mac's alone, a device's change refused and named in the log, the panel's row read-only) and its Sound button (Q10: none in Larger Picture's row 1, the panel's switch alone there); §5's "A device's change", which there is none of now; §13's hard rule (only Menus joins Larger Picture's row 1); and "Merged with main at 5c323e6", which left the row-1 Sound to Noah, says the beta took it out again. - docs/app-store-metadata.md: "Sill doesn’t play sound from your Mac." goes from any version whose build carries the sound, with or without the sound's bullet, which waits for P1–P16: a device plays the sound once the Mac turns on Send Audio, so a version that shipped the beta before the device tests passed would have kept a false line (Before you submit, the description's lead-in, the next version's note). The counts gain the block without that line and before the bullet: 3,003 characters. - Tests/checks/README.md: since the merge with main at 36e7212 (Saffsanity#93), the audio bullet's kind checks expect 44 to be the keyframe request, and 45 and up unknown; it still said 44 unknown. Docs only: no check reads these files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e is taken The keyframe kept can be from long before the session's end: the encoder makes one at a new source and every 4 x fps frames it encodes, so none while a window stays still, and the changes since are in later frames only. When the session's end is Sill's own (Disconnect, the only one a connected session has: another Mac is picked from the connect screen) and its end keeps a picture, Disconnect now asks the Mac for a keyframe (kind 44, Saffsanity#93's request: a still window's last frame encoded again) and ends the session when it comes or after 300 ms (ConnectionList.freshKeyframeWait), whichever is first; the stream screen stays meanwhile and a second tap does nothing. The wait is told of the keyframe on the network queue (`keyframeWanted`, set there before the ask goes out); a session that ends otherwise during it ends as the Disconnect did, with no reconnect. A session lost or ended by the Mac decodes the keyframe kept. ConnectionList.freshPicture (pure): only when the end keeps a picture (takesPreview), something streams (the host asks its encoder for nothing then) and the Mac takes the request; within the picture's recovery's spacing of its last ask (KeyframeRecovery.spacing, 2 s, so the Mac's one a second never refuses one) no other ask: wait for that ask's keyframe while it is due, else the keyframe kept is the one it brought. Which Macs take kind 44 (ConnectionList.takesKeyframeRequests): nothing on the wire says so, and Saffsanity#93's recovery sends it to every Mac because an older one skips it; but a skipped ask here would hold Disconnect for the whole 300 ms, and the request must never go to a Mac that does not take it. So only a Mac whose window list says a version after 0.5.1, the last Sill for Mac without it, is asked. Main's Sill.app builds take it and still say 0.5.1 until the next release, and SillHost says no version: they are not asked, and their picture is the keyframe kept. DEBUG `-SillFreshKeyframe 1` asks any Mac (the live tests' SillHost). Tests/checks/connection-list: 218 checks (the rule over every combination, the spacing's boundary at 2 s, eighteen versions), compiled with KeyframeRecovery.swift and StreamProtocol's Compatibility.swift; 64 of 64 mutants (16 new). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…affsanity#93, Saffsanity#151) into fix/remote-desktop-qr-code Main brought Send Audio as a beta (Saffsanity#94), Reverse Scrolling (Saffsanity#159), SILL_TEST_NO_SCREEN (Saffsanity#227), the Close timeout and the interrupted picture (Saffsanity#93) and a sleeping display woken for a device (Saffsanity#151). Six files conflicted, each resolved keeping both sides: - .github/workflows/ci.yml: the mutants matrix is main's 51 checks with pairing-sharing, sorted: 52, exactly the folders of Tests/checks with a mutants.py. - Sources/SillMenuBar/HostSettings.swift: Saffsanity#94's sendAudio beside showPairingWindowToScreenSharing in Key and in the registered defaults (sendAudio first, a HostConfig knob next to shareClipboard), as Noah's comment of 2026-10-19 settled the overlap: whichever lands second keeps both. HostConfig's sendAudio, read and saved, and this switch's property merged by themselves. - Sources/SillMenuBar/DebugHooks.swift: -SillSetAfter's list names both keys (sendAudio, and showPairingWindowToScreenSharing=0|1; shareClipboard, which both sides had added, once); both cases of apply merged by themselves. - Tests/checks/README.md: main's rewrapped introduction with this PR's "whether screen sharing shows the pairing window"; the pairing-sharing row beside main's phone-portrait and pointer-control rows; the history's list of checks and their pull requests ends with pairing-sharing and Saffsanity#157. - docs/release-checklist.md: the marker paragraph names every marker once (Clipboard, Keyframe, Display wake, Sound, Reverse Scrolling, Pairing window), the screen sharing grep among those that print nothing, and all six perl lines. - CLAUDE.md: both Current step entries, this PR's first, then main's three; Build and run's defaults list with sendAudio and showPairingWindowToScreenSharing; this PR's entry names this merge, and its verification is this merge's (Tests/checks/run-all.sh, all 52; the previews against main's 74d099c, the Devices pane's eight differ, two new, the other 136 byte for byte). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Noah's decision, from his iPad: his Mac says 0.5.1, so the version rule never asked it, and the tile kept the session's first keyframe and missed the window he had brought forward since. Disconnect now asks every Mac, as Saffsanity#93's picture recovery does: "Any Mac from main onwards answers within a frame; an App Store 0.5.1 Mac ignores it and the only cost is a 300 ms pause before the connect screen appears." The 300 ms wait stays, and so does cancelling the recovery's pending ask while Disconnect waits. ConnectionList.freshPicture loses its Mac's-version input (ask only when the end keeps a picture and something streams; the recovery's 2 s spacing as before), and takesKeyframeRequests, its 0.5.1 constant and the SillVersion import go; so does DEBUG -SillFreshKeyframe 1, which only forced the ask (no other use). Tests/checks/connection-list compiles without Compatibility.swift: 154 checks (the freshness grid over every combination, every Mac asked), 72 of 72 mutants (the version mutants gone; "no Mac asked" new). DEVELOPMENT.md's tiles paragraph and harness list (SILL_TEST_KEYFRAME_IGNORE stands in for a Mac that skips the request), CLAUDE.md's Layout, the harness contract, the checks README, and privacy.html's Tiles item ("a request for one more picture"). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Why
Two problems from addhumal's audit (#101):
kAXCloseButtonAttributereturns is a new Accessibility element on the system's default messaging timeout, and the host presses it on its main actor. An app whose close asks about unsaved changes in an alert of its own, or a hung one, held up input, window picks and settings for every connected device for that long.HEVCDisplayViewnoticed onlystatus == .failed, only when a frame arrived, and never told the Mac. A window that is not changing sends no frame, so nothing brought the picture back until it did.What
The first version of this pull request picked the source again (kind 6) every 2 s while the layer read interrupted. That restarts every device's stream, raises the window on the Mac in regular mode, and repeats for as long as a layer keeps failing. It now asks for one keyframe instead, through a new message.
CFGetTypeID, then the cast, asMenuReaderdoes) and given a 1 s timeout.WindowSizer.performnow says what came of a window's light (CommandOutcome): "Window 104 close: done", "refused", or after 1 s "no answer in time (the app is busy, or asking about unsaved changes); it may still act". A minimize or full screen with no answer says only that the app is busy.Scripts/menu-check/run.sh closebuilds again (since A window Sill sized goes back to its own frame when it stops streaming #75WindowSizerneedsWindowRestore.swift) and presses Close on the menus' test app's Slow Close window, whose close holds its main thread for 4 s.keyframeRequest(device → host, empty payload; the number Noah gave this pull request on Apple Pencil Features (pressure, tool swap, etc) #81). The host answers withHEVCEncoder.requestKeyframe, which encodes a still window's last frame again; every device gets that keyframe, as when one joins. At most one a second per connection ("Keyframe for ‹device›: its picture was interrupted."; the rest one line a minute), nothing while nothing streams, never a restart or a raise. A Mac from before (Sill for Mac 0.5.1) skips the kind without a line, and the picture there waits for the stream's next keyframe, as it always has.HEVCDisplayView.checkLayerflushes a layer that failed, could not decode a frame (failedToDecodeNotification) or needs a flush to decode again (requiresFlushToResumeDecoding): at each frame, at the layer's notifications (a still window sends no frame that would show it) and when the picture's window becomes active. It says so (onNeedsKeyframe), and a keyframe that ends the wait says the picture is back (onPictureBack). A keyframe the flush throws away, because the layer still read failed when it came, counts as the picture back and lost again.KeyframeRecovery(pure,Tests/checks/keyframe-recovery, new):onNeedsKeyframe; it can dropPictureSmoothing.DecoderRecoveryand its re-pick and callonPictureBackwhere its renderer takes a keyframe.-SillLayerFail <s>[,<s>…](a frame that cannot decode goes onto the layer at those seconds) and-SillKeyframeLost <n>(the first n keyframes that end a wait are thrown away by the flush). TEST ONLY, synthetic hosts only:SILL_TEST_STILL_AFTER=<s>(the Desktop still from then on) andSILL_TEST_KEYFRAME_IGNORE=<n>(the first n keyframe requests ignored, as an older Mac ignores every one);sillclient.py --keyframe@T.<!-- Keyframeand<!-- /Keyframe -->with a perl line in docs/release-checklist.md part 1 §3 to cut it for a republish before the release that ships it. No release words yet (What's New, README, support): they wait for a device run of the trip to the background.The review, fixed
A verifier re-ran everything on 09549b3 and found six problems. Each is fixed in a commit of its own:
compatibilitycheck assertsStreamMessageKind(rawValue: 44) == nil, and Sill Viewer: a Mac controlling another Mac #66's, Sill Viewer: Dock badges, bounces and minimised windows in the remote apps' Dock icons; full speed in the background (builds on #66) #72's and Background windows paused while hidden and at 1×; stream rate capped by size (builds on #72 and #71) #73'spointer-controlmutant "kind 26 numbered 44" stops compiling once 44 exists (their mutants.py then counts it as not caught); whichever lands second changes the check, never kind 44. Sill Viewer for Mac, and Rust hosts for Windows and Linux #128's Mac viewer compilesStreamClient.swiftwithoutHEVCDisplayView's new hooks, the scene observers orKeyframeRecovery.swift.Verified
On the merge with main at 4d990e9, on a Mac shared with other agents (load average in the hundreds, so times are slow):
Tests/checks/keyframe-recovery: 95 checks and 20,000 random sessions against a model of the event history (never stuck while open, active and streaming; a re-ask only once the last ask's keyframe was overdue; no two asks less than 2 s apart); 39 of 39 mutants, three of them each finding put back.Tests/checks/pointer-control(its kind table has 44): 158 checks, 33 of 33 mutants.Tests/checks/run-all.sh: all 47 passed (28 min under that load).swift build -c releaseon the merged tree: complete, no new warning.StreamClient.swift:3819capture warning.Scripts/menu-check/run.sh close: 9 checks, back in 1.01 s as no answer, the close delivered to the window, every action's words, the front app unchanged.sillclient.py, loopback, software encoder): withSILL_TEST_KEYFRAME_IGNORE=1, requests at 4 and 6 s: the first ignored with its line, the second answered (its keyframe 0.09 s later).-SillLayerFail 6: the layer's own -11821, one ask, one "Keyframe for …" on the host, the picture back, and one "Synthetic capture" line (no re-pick).SILL_TEST_KEYFRAME_IGNORE=1: the first ask ignored, "keyframe: no keyframe 4.0 s after the last ask; asked the Mac for a keyframe (spacing 4 s)" 4.2 s later, the host's line, the picture back 0.15 s after it.-SillKeyframeLost 1: the keyframe that ended the wait thrown away ("flushed with the keyframe it waited for"), the next ask 4 s after the first (a repeat's spacing), the picture back.-SillLayerFail 6,9,13,21.5,37.5: asks 4.05, 8.1 and 16.0 s apart; the fifth interruption held 28.5 s by a 32 s spacing; then Settings in front for 2 s and Sill back: asked at once ("spacing 2 s"), 21 s before the hold would have let it.git archive v0.5.1, its Desktop moving) with-SillLayerFail 6: asks at +0, +4.1 and +12.3 s, no line on the host and no disconnect, pongs and frames throughout, the picture back 15.2 s after the interruption with the stream's periodic keyframe.Not run here: anything on a device. The simulator's display layer never stops for a trip to the background, so the issue's own case (
requiresFlushToResumeDecodingand a failed status after Sill was away) has run only through the pure check.Untested, for Noah
On the iPad and the iPhone with this build and a Sill.app from this branch:
Towards #101 (the trip to the background, the issue's own case, needs a device).
🤖 Generated with Claude Code
🤖 Generated with Claude Code