"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.