Skip to content
C.W.K.
Lesson 04 of 05 · published

Proving a Non-Attempt — Failed Versus Never Happened

~14 min · change-not-attempted, uncertainty, error-types, review-pass

Level 0Idle Hands
0 XP0/39 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"A child that can prove it asked the OS for nothing throws ChangeNotAttempted; every other failure leaves its receipt uncertain." — Tinker architecture

Two Kinds of Failure That Look Alike

Picture the moment after the engine writes a prepared receipt and calls apply, and apply throws. Two very different worlds produce that same throw. In one, the app quit a millisecond earlier, so Tinker never asked macOS to hide anything. In the other, Tinker did ask, macOS accepted the request, and then the confirmation never arrived in time — the app may be hidden now, or half-way, or not at all. In the first world there is nothing to clean up. In the second, Tinker genuinely does not know whether it changed the screen, and the honest thing is to keep the receipt and show it to Dad as uncertain.

The Bug This Distinction Fixed

The first version of Tinker treated every apply failure the same way: keep the prepared receipt, mark it uncertain. That was safe and it was miserable. A hide refused because secure keyboard entry was on, or because an app had already been hidden by Dad a moment before, left an uncertain record that only Forget in the Access & Recovery tab could clear — and because uncertain records hold the engine stopped after a relaunch, one harmless refusal could disable Tinker until someone went and clicked. The comprehensive review pass on 2026-11-10 named the problem and split the error type.

A Type That Carries a Proof

ChangeNotAttempted is an ordinary error struct with a message. What makes it special is the contract around it: a capability may throw it only when it can prove no request reached the OS. HideApplications throws it from two places, both before the hide call — when the revalidation finds the app already hidden or gone, and when setHidden re-checks the refusal and the process identity just before asking. Once app.hide() has been called, every later failure — the app quitting mid-change, the confirmation timing out — is a plain error and leaves the receipt uncertain.

What the Engine Does With Each

On ChangeNotAttempted the engine removes that one prepared receipt (no intent reached the OS, so nothing is uncertain), persists the journal, and rethrows so the command still reports the refusal. On any other error it writes the error message into the receipt and leaves it prepared. In both cases the surrounding catch then restores the changes this owner had already confirmed, so a preset never stays half-applied because its third app misbehaved.

Why Not Just Be Optimistic?

Dropping every failed receipt would be simpler and would usually be fine. "Usually" is the problem: the one time an app really did get hidden after a timed-out confirmation, an optimistic Tinker would forget it owned that app, and the app would stay hidden after restore with nobody responsible for it. The error type lets Tinker be relaxed exactly where it has proof and strict everywhere else.

Code

The error type and where the engine sorts the two worlds·swift
/// Thrown by `apply` when the capability can prove it requested no change.
public struct ChangeNotAttempted: Error, LocalizedError, Sendable {
  public let message: String
  public init(_ message: String) { self.message = message }
  public var errorDescription: String? { message }
}

// Inside the engine's apply loop, after the prepared receipt is persisted:
do {
  try await capability.apply(change.payload)
  try markApplied(change.key)
} catch let refusal as ChangeNotAttempted {
  try removeReceipt(change.key)        // nothing reached the OS: nothing uncertain
  throw refusal
} catch {
  try? recordError(error, on: change.key)   // stays .prepared: shown in Recovery
  throw error
}
Where the line falls inside setHidden: before vs after the OS request·swift
public func setHidden(_ value: Bool, for identity: ApplicationIdentity) async throws {
  // Both refusals come before the request to the OS: nothing was attempted.
  if value, let refusal = hideRefusal() { throw ChangeNotAttempted(refusal) }
  guard let app = match(identity) else {
    throw ChangeNotAttempted("\(identity.name) stopped or its process identity changed; visibility was left alone.")
  }
  _ = value ? app.hide() : app.unhide()
  // From here on, a failure cannot prove that nothing changed.
  // ...confirmation loop; on timeout: throw TinkerError(...)  -> uncertain
}

External links

Exercise

For a capability that pauses a sync client by sending it a command over a local socket, list the failures that may throw ChangeNotAttempted and the ones that must stay uncertain. Where exactly in the code does the line fall?
Hint
Before the bytes are written — socket refused, client not running, input invalid — nothing was asked, so ChangeNotAttempted is honest. Once the command has been written, a lost reply or a timeout cannot prove the client did not pause, so it must stay uncertain. The line is the write call, the same way it is the hide() call in Tinker.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.