Context
While investigating MAX mode on an M1 Max, the immediate backend issue was that M1–M4 fan control needs the global Ftst gate before per-fan mode/target writes are effective.
Reviewing the surrounding lifecycle exposed a broader concern that I intentionally did not include in the focused backend PR: desired profile state, applied SMC state, power transitions, and daemon auto-suspend are currently coordinated by separate components.
Current risk areas
ProfileStore can persist System mode independently of a confirmed hardware release.
- Profile selection/deletion, polling, sleep/wake, startup/shutdown, and auto-suspend do not share one transition owner.
SMCController.resetAllFans() is fail-fast, so one failed reset can prevent later fans from being attempted.
- Apple Silicon
Ftst is global, while the public control API is per-fan.
- Safety paths sometimes treat profile state as proof of applied hardware state.
- Sleep is a multi-message protocol (
CanSystemSleep, WillSleep, WillNotSleep, wake), not a single callback.
These can produce partial/manual/unknown fan states even when the persisted profile says System.
Suggested direction
Introduce a daemon-level FanControlCoordinator (an actor or one serial executor) as the only mutation gateway for:
- profile activation and active-profile deletion;
- periodic target application;
- startup and shutdown recovery;
- sleep cancellation, sleep entry, and wake;
- daemon auto-suspend eligibility.
The coordinator should track applied state explicitly, for example:
automatic
manual(profileID)
releasing(reason)
sleepPending
sleeping
faulted/unknown
Transaction ordering
For a transition to System:
- quiesce new fan writes;
- record a pending release;
- release hardware control and attempt every fan where control is per-fan;
- only after success persist/report System and permit auto-suspend;
- on failure remain active in a visible fault/retry state rather than exiting.
For Apple Silicon, the backend should expose whole-operation semantics such as applyFanTargets and releaseAllControl, so global Ftst ownership is not composed accidentally through repeated per-fan calls.
Acceptance criteria
- System is reported and eligible for auto-suspend only after hardware release succeeds.
- A failed release cannot later trigger daemon exit merely because the persisted profile is System.
- Deleting the active profile follows the same System transition.
- Reset attempts continue across independently controlled fans and aggregate failures.
- No target writes occur while releasing, sleeping, or in unknown state.
CanSystemSleep -> WillNotSleep resumes the prior desired state.
- Restart recovers an interrupted release before applying another manual profile.
- Fault-injection tests cover each fan failing, partial apply/reset, profile persistence failure, sleep cancellation, and auto-suspend during a failed release.
This is deliberately proposed as a separate architecture change rather than expanding the focused M1–M4 Ftst fix.
Context
While investigating MAX mode on an M1 Max, the immediate backend issue was that M1–M4 fan control needs the global
Ftstgate before per-fan mode/target writes are effective.Reviewing the surrounding lifecycle exposed a broader concern that I intentionally did not include in the focused backend PR: desired profile state, applied SMC state, power transitions, and daemon auto-suspend are currently coordinated by separate components.
Current risk areas
ProfileStorecan persist System mode independently of a confirmed hardware release.SMCController.resetAllFans()is fail-fast, so one failed reset can prevent later fans from being attempted.Ftstis global, while the public control API is per-fan.CanSystemSleep,WillSleep,WillNotSleep, wake), not a single callback.These can produce partial/manual/unknown fan states even when the persisted profile says System.
Suggested direction
Introduce a daemon-level
FanControlCoordinator(an actor or one serial executor) as the only mutation gateway for:The coordinator should track applied state explicitly, for example:
automaticmanual(profileID)releasing(reason)sleepPendingsleepingfaulted/unknownTransaction ordering
For a transition to System:
For Apple Silicon, the backend should expose whole-operation semantics such as
applyFanTargetsandreleaseAllControl, so globalFtstownership is not composed accidentally through repeated per-fan calls.Acceptance criteria
CanSystemSleep -> WillNotSleepresumes the prior desired state.This is deliberately proposed as a separate architecture change rather than expanding the focused M1–M4
Ftstfix.