Skip to content

ScreenCaptureKit transient-denial retry swallows cancellation (second attempt after cancel) #289

Description

@SebTardif

Summary

After a transient ScreenCaptureKit failure, three retry paths sleep with try? await Task.sleep before a second attempt:

  1. ScreenCaptureFallbackRunner.run
  2. ScreenCaptureFallbackRunner.runCapture
  3. ScreenRecordingPermissionChecker.hasPermission

try? swallows CancellationError, so a canceled capture or permission check can still run a full second ScreenCaptureKit attempt after the ~350ms delay.

main still has this behavior. A combined fix was proposed and closed without merge pending a live ScreenCaptureKit proof gate.

Impact

Canceled CLI/MCP capture or permission work can still burn a second SCK call during the transient-denial retry window instead of stopping promptly. Same cancel-swallow class as merged #270 / #271 / #267 / #230.

Prior work

PR Scope Outcome
#279 Engine runner (2 sites) Closed without merge (Wave 5); asked for one combined lane
#280 Permission probe Closed without merge; same family
#286 All three sites + tests + changelog Closed without merge; CI green; blocked on live signed SCK/TCC proof

Combined fix head (fork, not on main): 6e1405d0ed6a1615e6b530f2aeeec0d81d51f371 on SebTardif:fix/screencapture-retry-honor-cancel.

Introduced (retry sleep): 6b2554a6ad017b263bfa4c0406ad8f846db76982 (fix(capture): stabilize concurrent live area capture, 2026-05-07).

Maintainer re-entry criteria (from #286 closeout)

From @steipete on #286:

  1. Authenticated, properly signed live ScreenCaptureKit/TCC evidence on a healthy host showing a real transient denial, cancellation during retry backoff, and no second capture/probe, while the non-canceled control still retries once, or
  2. An owner explicitly waives that live gate.

New live evidence (healthy host, production runner)

On macOS 26.5.2 (arm64), with Screen Recording available, real ScreenCaptureKit works (SCShareableContent and SCStream). A harness linked to production ScreenCaptureFallbackRunner from the fixed branch uses a real SCStreamErrorDomain failure (code -3812, invalid stream geometry). Production ScreenCaptureKitTransientError treats any ScreenCaptureKit-domain error as retryable, so this hits the same ~350ms retry sleep path as other SCK failures.

Harness (on the fix branch):

bash scripts/prove-live-sck-cancel.sh

Transcript (ad-hoc signed harness binary; TeamIdentifier not set):

=== Live ScreenCaptureKit cancel proof (production ScreenCaptureFallbackRunner) ===

live SCShareableContent.current: displays=1
live seed error: domain=com.apple.ScreenCaptureKit.SCStreamErrorDomain code=-3812 desc=Failed due to an invalid parameter
seed is SCK-domain (production classifier matches ScreenCaptureKit domain)

Control: no cancel (expect 2 real SCStream start attempts via production runner)
control attempts=2 final domain=com.apple.ScreenCaptureKit.SCStreamErrorDomain code=-3812
PASS control: non-canceled path retried once (2 real SCK calls)

Cancel: cancel ~50ms into 350ms denial sleep (expect 1 real SCStream start attempt)
cancel attempts=1 outcome=CancellationError
PASS cancel: canceled during denial sleep with no second real SCK call

ALL LIVE CHECKS PASSED

Also still green on that branch:

Gaps vs the exact re-entry wording (called out on purpose)

Criterion Status
Real ScreenCaptureKit (not injected NSError) Yes: SCStreamErrorDomain -3812 from live SCStream.startCapture
Production ScreenCaptureFallbackRunner Yes (linked package, cancel fix on branch)
Control 2 attempts / cancel 1 attempt Yes (transcript above)
Developer ID / OpenClaw Foundation signed identity No (ad-hoc only on this machine; no codesign identities)
Real TCC decline (-3801 / declined TCC copy) No (used SCK-domain invalid-parameter path that the production classifier still treats as transient)
Live permission-probe path Not yet (runner path only in this harness)

Questions for maintainers

  1. Does the live production-runner transcript above meet the re-entry bar for landing the fix(capture): honor cancellation during ScreenCaptureKit retry sleeps #286-shaped fix, given it uses real SCStreamErrorDomain and the production retry classifier?
  2. If not, which remaining items are mandatory: Developer ID signing, a true TCC-denial error, and/or the permission-probe path?
  3. Alternatively, would an owner waive the live gate for this cancel-swallow family (same class as fix(window): honor task cancellation while waiting for window disappear #270/fix(window): honor task cancellation while settling maximize frame #271/fix(cli): honor task cancellation in menu bar click wait loops #267)?

Happy to re-open a single combined PR from the existing branch once the gate or waiver is clear. No new PR until then.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions