Fixes#358, a regression from #346. That PR's sticky_bypass_fn released
the Finish/100 hold on any concrete non-Finish progress code, ungated on
machine_state, reasoning that a new cycle's own progress can appear
before state catches up. But the reporting DA_WM_TP1_21_COMMON dryer
replays a running stage on the way *out* of a cycle: the issue's history
shows Cooling -> +60s Finish -> +24s 'Drying' -> +4s settled, twice,
identically. The bypass read that tail as a new cycle, dropped the hold,
and republished 'Drying' -- so progress read Drying, Cooling, Finish,
Drying, Idle instead of ending at Finish, Idle.
The tail is not new: rep_fn has always masked progress while state isn't
active, which is why it was invisible before #346. What surfaced it was
sticky_live_fn, a second, ungated view of the same field that the bypass
returned in rep_fn's place -- letting the hold publish a value the entity
otherwise never shows.
Both halves are fixed:
- The bypass (now _new_cycle_running) requires state == 'active'
alongside the progress code. The arm condition stays ungated -- failing
to arm loses the Finish entirely (#345), while releasing late costs
nothing, since the hold expires on its own.
- sticky_live_fn is gone. rep_fn is the only definition of a live value;
the hold decides only whether to freeze, and the bypass returns rep_fn's
own result.
Also stop an already-open window from being restarted by a progress that
flaps in and out of Finish, so sticky_seconds is measured from the first
Finish of a cycle and the documented bound actually holds.
A paused new cycle no longer cuts the hold short (it did under the old
ungated bypass). Nothing live is withheld by that: rep_fn shows Idle
while paused with or without a hold, so the only change is a stale Finish
expiring on schedule -- and 'paused' cannot be told apart from this tail.