- "Rescheduled adapter" log fires with **correct** calculated next_poll
- Actual poll occurs at OLD cadence time
- Subsequent polls continue at OLD cadence
### Contrast: Increase 60s → 90s (WORKS)
```
Tlast: 03:16:34Z
Config change: 03:16:36Z
Expected next poll: 03:18:04Z (Tlast + 90s)
Actual next poll: 03:18:04Z ✅
```
---
## 2. Root Cause
### Location
`supervisor.py` lines 395-450 (`_reschedule_adapter`) and lines 144-181 (`_run_adapter_loop`)
### The Bug
The `cancel_event.set()` call in `_reschedule_adapter` does not reliably wake the `asyncio.wait_for()` in the adapter loop when the cadence is **decreased**.
await self._reschedule_adapter(adapter_name, new_config) # sets cancel_event here
```
2.**Reschedule updates config then signals:**
```python
# _reschedule_adapter
state.config = new_config # Line 420
state.adapter.cadence_s = new_cadence # Line 423
# ... logging ...
state.cancel_event.set() # Line 450 - inside lock context
```
3.**Asyncio event delivery delay:**
The `asyncio.Event.set()` queues a wakeup for waiting tasks, but the signal delivery is subject to asyncio's task scheduler. When called from within an `async with` block, the event may not be processed until the current task yields or the lock context exits.
4.**Timing difference between increase and decrease:**
- **Increase (60→90):** Loop has ~30-50s remaining sleep. Event signal arrives well before timeout.
- **Decrease (90→60):** Loop may be ~10s from timeout. By the time event signal is processed, timeout has already fired.
5.**Why subsequent polls use old cadence:**
When the loop times out naturally (rather than being woken by event), it proceeds to poll. After poll completes, `state.last_completed_poll` is updated. The loop then reads `state.config.cadence_s` for the NEXT iteration — but if `state.config` was somehow not durably updated (or there's a stale reference), it uses the old value.
**Alternative theory:** The `state.config = new_config` assignment creates a new config object, but the loop may be reading from a captured reference to the old object if there's any closure behavior we're not seeing.
---
## 3. Proposed Fix
### Option A: Force immediate reschedule (Recommended)
Move the cancel logic OUTSIDE the lock, and use a more aggressive wake pattern: