Skip to content

Audit fixes: the Close timeout and the interrupted picture - #93

Merged
Saffsanity merged 24 commits into
Saffsanity:mainfrom
addhumal:audit-fixes
Oct 26, 2026
Merged

Saffsanity merged 24 commits into
Saffsanity:mainfrom
addhumal:audit-fixes

Conversation

@addhumal

@addhumal addhumal commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Why

Two problems from addhumal's audit (#101):

  • Closing a window could stall the Mac's host for seconds. The Close button kAXCloseButtonAttribute returns 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.
  • A still window could stay blank or frozen after an interruption. When iOS stops the display layer (Sill in the background, another app taking the decoder) or a frame fails to decode, HEVCDisplayView noticed only status == .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.

  • Close (host; the contributor's change, kept). The button is type-checked (CFGetTypeID, then the cast, as MenuReader does) and given a 1 s timeout. WindowSizer.perform now 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 close builds again (since A window Sill sized goes back to its own frame when it stops streaming #75 WindowSizer needs WindowRestore.swift) and presses Close on the menus' test app's Slow Close window, whose close holds its main thread for 4 s.
  • Kind 44, keyframeRequest (device → host, empty payload; the number Noah gave this pull request on Apple Pencil Features (pressure, tool swap, etc) #81). The host answers with HEVCEncoder.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.
  • The device. HEVCDisplayView.checkLayer flushes 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.
  • When to ask: KeyframeRecovery (pure, Tests/checks/keyframe-recovery, new):
    • once per interruption while its keyframe is due, and again when none came within max(4 s, 4 × the worst round trip), so a Mac that never answers is asked after 4, 8 and 16 s, then every 32 s;
    • only while the picture's window is foreground active (each Sill window has its own client) and something streams;
    • at least 2 s apart; 4, 8, 16, then 32 s for a picture that fails again within 10 s of its keyframe, so a decoder that cannot keep up never costs every device a keyframe every 2 s;
    • a trip to the background owes an ask that was out again and starts the spacing at 2 s again, and a keyframe shown while Sill is away never makes the next interruption a repeat.
  • One mechanism (Noah on this pull request, 2026-10-15: "One recovery mechanism should serve all of them"): Settings panel: Picture Smoothing (Automatic, None, Low, Medium, High) #107's renderer already calls the same onNeedsKeyframe; it can drop PictureSmoothing.DecoderRecovery and its re-pick and call onPictureBack where its renderer takes a keyframe.
  • Test hooks. DEBUG: -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) and SILL_TEST_KEYFRAME_IGNORE=<n> (the first n keyframe requests ignored, as an older Mac ignores every one); sillclient.py --keyframe@T.
  • Docs. CLAUDE.md (Current step, Layout, Build and run, the compatibility floor's kinds now 0–28, 32 and 44), docs/DEVELOPMENT.md ("If the picture freezes", the window lights' outcomes, the harness and test hooks), the checks' README, and site/privacy.html's list of what a device sends its Mac, between <!-- Keyframe and <!-- /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:

  1. A keyframe the flush threw away left the picture frozen (80e982f). If the keyframe an ask brought arrived while the layer still read failed, the flush discarded it, the interruption stayed open with its ask counted as out, and nothing was asked again while Sill stayed active. Now that keyframe counts as the picture back and lost again, and every ask has a deadline (max(4 s, 4 round trips)), after which it is made again under the backoff.
  2. A trip to the background kept the backoff (80e982f). The verifier's probe held an ask 24 s after the return. Now a trip starts the spacing at 2 s again, and a keyframe shown while Sill is away keeps the trip, so keyframes iOS may refuse in the background never build a streak.
  3. DEVELOPMENT.md on older Macs. A Mac from before kind 44 brings the picture back with the stream's next keyframe (the encoder makes one every fps × 4 encoded frames, none while the window stays still), not when the window next changes.
  4. Minimize and full screen said "asking about unsaved changes" (a45a523). Each action now has its own words.
  5. Noah's steps. TextEdit asks about unsaved changes in a sheet, which returns at once, so its Close most likely logs "done"; either outcome is fine. Against Sill for Mac 0.5.1 the device does send kind 44, which 0.5.1 skips with no line.
  6. Merge notes (CLAUDE.md): The Mac's sound on the device: Send Audio, kind 43, AAC-ELD (#83) #94's compatibility check asserts StreamMessageKind(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's pointer-control mutant "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 compiles StreamClient.swift without HEVCDisplayView's new hooks, the scene observers or KeyframeRecovery.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 release on the merged tree: complete, no new warning.
  • The iOS app for the simulator with CI's command (Xcode 27.1 27A9275, clean derived data): succeeded, only the known StreamClient.swift:3819 capture 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.
  • The host alone (sillclient.py, loopback, software encoder): with SILL_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).
  • Live on a private iPad simulator (iOS 27.0) against loopback synthetic hosts on the software encoder, the Desktop still from 3 s:
    • -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).
    • With 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.
  • This build against Sill for Mac 0.5.1's host (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 (requiresFlushToResumeDecoding and 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:

  1. A still window (TextEdit, nothing changing), Sill in the background for 2 s and back: the console's "display: the picture was interrupted …", one "Keyframe for …" in Sill.log, the picture back at once, no "Capture started", the window not raised.
  2. Ten quick trips: a line each, each ask at once, never one held for 4 s or more after a trip.
  3. A video playing in Split View beside Sill.
  4. Two Sill windows, one swapped out and back.
  5. Close on a TextEdit window with unsaved changes: its sheet on the Mac, input and picks unaffected meanwhile; Sill.log most likely says "Window N close: done" (a document's sheet returns at once). An app that asks in an alert of its own, or a hung one, gives "no answer in time …" after 1 s. Either is fine.
  6. This iPad against Sill for Mac 0.5.1: the device sends kind 44, which 0.5.1 skips with no line and no disconnect; the console's asks again after 4, 8 and 16 s, then every 32 s; the picture back only with the stream's next keyframe (a still window's never, as before).

Towards #101 (the trip to the background, the issue's own case, needs a device).

🤖 Generated with Claude Code

🤖 Generated with Claude Code

@addhumal addhumal left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).
@addhumal addhumal changed the title Audit fixes: the Close timeout, trackpad and wheel scrolling, the interrupted picture, CLAUDE.md slimmed Audit fixes: the Close timeout and the interrupted picture Oct 14, 2026
# Conflicts:
#	.github/workflows/ci.yml
@Saffsanity

Copy link
Copy Markdown
Owner

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 (PictureSmoothing.DecoderRecovery) for its Metal renderer, and as #66 (with #72 and #73), which moves this code into HEVCSampleFeed.swift. One recovery mechanism should serve all of them, with whichever lands second reusing the first. Thanks for the AC11/FC11 renumbering; the remaining overlaps with #94, #95, #99 and #117 are the ci.yml mutants line and neighbouring project-file entries.

# Conflicts:
#	.github/workflows/ci.yml
#	iOSClient/StreamClient.swift
addhumal added a commit to addhumal/sill that referenced this pull request Oct 15, 2026
@addhumal

addhumal commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor Author

Thanks. Merged main in (d821e1a, after #78, #79, #80 and #98). In #107 (7c7651e), HEVCDisplayView now exposes the same var onNeedsKeyframe: (() -> Void)? hook and routes MetalStreamRenderer's keyframe request through it in StreamClient.swift, so whichever of #93 and #107 lands second connects both paths to one recovery rule.

Saffsanity and others added 2 commits October 19, 2026 22:51
…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>
Saffsanity and others added 3 commits October 19, 2026 22:52
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>
Saffsanity and others added 5 commits October 20, 2026 00:49
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
HunterAhlquist pushed a commit to HunterAhlquist/sill that referenced this pull request Oct 19, 2026
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>
Saffsanity and others added 4 commits October 20, 2026 08:33
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>
@Saffsanity

Copy link
Copy Markdown
Owner

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 harness builds again: since A window Sill sized goes back to its own frame when it stops streaming #75 WindowSizer needs WindowRestore.swift, so Scripts/menu-check/build.sh compiles it too. run.sh close passes: against Slow Close the press comes back in 1.01 s, the close reaches the window, and the front app never changes.
  • WindowSizer.perform says what came of a press (CommandOutcome). A close that times out logs "no answer in time (the app is busy, or asking about unsaved changes); it may still act" instead of "refused", since the press did reach the window. A minimize or full screen with no answer says only that the app is busy.

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:

  • Kind 44, keyframeRequest (device → host, empty payload; the number Noah gave this pull request on Apple Pencil Features (pressure, tool swap, etc) #81). The host answers with HEVCEncoder.requestKeyframe, which encodes a still window's last frame again, at most once a second per connection, and logs "Keyframe for ‹device›: its picture was interrupted." A Mac from before kind 44 skips it without a line; the picture there waits for the stream's next keyframe, as before.
  • checkLayer also listens to the layer's failed-to-decode and needs-a-flush notifications, so a still window recovers without waiting for a frame. A keyframe the flush throws away (the layer still read failed when it came) counts as the picture back and lost again.
  • KeyframeRecovery decides when to ask: once per interruption while its keyframe is due, again when none came within max(4 s, 4 × the worst round trip) (so an older Mac is asked after 4, 8 and 16 s, then every 32 s, which costs it nothing); only while the picture's window is active and something streams; at least 2 s apart, and 4, 8, 16, then 32 s for a picture that fails again right after its keyframe; a trip to the background starts the spacing at 2 s again. Its check has 95 cases plus 20,000 random sessions against a model, and catches 39 of 39 mutants.

For #107: its renderer already calls onNeedsKeyframe, so it can drop PictureSmoothing.DecoderRecovery and its re-pick and call onPictureBack where its renderer takes a keyframe, so one rule serves both.

For testing: DEBUG -SillLayerFail <s>[,<s>…] and -SillKeyframeLost <n>, and on a synthetic host SILL_TEST_STILL_AFTER (a still Desktop) and SILL_TEST_KEYFRAME_IGNORE (asks ignored, as an older Mac ignores them), reproduce each path in a simulator. Live there, each failure gave one ask and one host line with the source never picked again; an ignored ask was made again 4 s later; and a trip to the background ended a 32 s hold at once. The simulator's display layer never stops for a trip to the background, though, so the issue's own case still needs a device run, and the description now says "Towards #101". It also still describes the first version (the kind 6 re-pick); a replacement is drafted.

Saffsanity and others added 2 commits October 26, 2026 14:57
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>
@Saffsanity
Saffsanity merged commit 396c64e into Saffsanity:main Oct 26, 2026
2 checks passed
Saffsanity added a commit that referenced this pull request Oct 26, 2026
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>
Saffsanity added a commit to cinminori/sill that referenced this pull request Oct 26, 2026
…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>
Saffsanity added a commit to cinminori/sill that referenced this pull request Oct 26, 2026
… 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>
Saffsanity added a commit to addhumal/sill that referenced this pull request Oct 27, 2026
…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>
Saffsanity added a commit to ArfiArfath21/sill that referenced this pull request Oct 27, 2026
…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>
Saffsanity added a commit to addhumal/sill that referenced this pull request Oct 27, 2026
…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 added a commit to Code-Tomato/sill that referenced this pull request Oct 27, 2026
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>
Saffsanity added a commit to Code-Tomato/sill that referenced this pull request Oct 27, 2026
…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>
Saffsanity added a commit to addhumal/sill that referenced this pull request Oct 27, 2026
…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>
Saffsanity added a commit to scripsweave/sill that referenced this pull request Oct 27, 2026
…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>
Saffsanity added a commit to addhumal/sill that referenced this pull request Oct 27, 2026
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>
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