Single-fix release on the per-day mutable CalVer scheme.
Sonos — a single transport SOAP failure no longer reverts to local (Android)
When the phone was locked, a transport command (lock-screen / Bluetooth / watch media button, or a queue-resync play) issued during a WiFi power-save stall would hit the 2 s connect timeout and throw. handleSoapFailure then cancelled the poll loop and fired onDrop on that single failure — showing "Disconnected from <Sonos>" and reverting to local playback, even though the renderer was perfectly reachable. This bypassed the poll loop's deliberate 30-consecutive-failure tolerance (DROP_THRESHOLD, bumped from 3 precisely for screen-off WiFi sleep / Doze).
Fix — make the 1 Hz poll loop the sole drop arbiter:
Transport SOAP commands retry transient IO failures (retryTransientIo: 3 attempts, 400 ms backoff) so a brief WiFi stall lands the command once WiFi wakes instead of abandoning it. A SoapFaultException (renderer answered and rejected the action) is not retried — the device is alive.
handleTransportFailure no longer cancels the poll loop or fires onDrop; it logs and nudges an immediate poll so the UI reconciles to Sonos's actual state. If the renderer is truly gone, the poll loop trips the drop on its own via DROP_THRESHOLD.
retryTransientIo extracted as an internal top-level fn with a unit test (first-success, retry-then-succeed, exhaust-and-rethrow, no-retry-on-SoapFault).
Verify on device
Play to a Sonos speaker, lock the phone, let WiFi sleep, then hit a lock-screen / Bluetooth / watch transport button → the command lands (once WiFi wakes); the app stays routed to Sonos instead of flashing "Disconnected" and reverting to local.
Power off the Sonos mid-session → after ~30 s of failed polls the app drops to local on its own (the deliberate tolerance still fires).
Single-fix release on the per-day mutable CalVer scheme.
## Sonos — a single transport SOAP failure no longer reverts to local (Android)
When the phone was locked, a transport command (lock-screen / Bluetooth / watch media button, or a queue-resync `play`) issued during a WiFi power-save stall would hit the 2 s connect timeout and throw. `handleSoapFailure` then cancelled the poll loop and fired `onDrop` on that single failure — showing "Disconnected from \<Sonos\>" and reverting to local playback, even though the renderer was perfectly reachable. This bypassed the poll loop's deliberate 30-consecutive-failure tolerance (`DROP_THRESHOLD`, bumped from 3 precisely for screen-off WiFi sleep / Doze).
**Fix — make the 1 Hz poll loop the sole drop arbiter:**
- Transport SOAP commands retry transient IO failures (`retryTransientIo`: 3 attempts, 400 ms backoff) so a brief WiFi stall lands the command once WiFi wakes instead of abandoning it. A `SoapFaultException` (renderer answered and rejected the action) is **not** retried — the device is alive.
- `handleTransportFailure` no longer cancels the poll loop or fires `onDrop`; it logs and nudges an immediate poll so the UI reconciles to Sonos's actual state. If the renderer is truly gone, the poll loop trips the drop on its own via `DROP_THRESHOLD`.
- `retryTransientIo` extracted as an internal top-level fn with a unit test (first-success, retry-then-succeed, exhaust-and-rethrow, no-retry-on-SoapFault).
## Verify on device
- Play to a Sonos speaker, lock the phone, let WiFi sleep, then hit a lock-screen / Bluetooth / watch transport button → the command lands (once WiFi wakes); the app stays routed to Sonos instead of flashing "Disconnected" and reverting to local.
- Power off the Sonos mid-session → after ~30 s of failed polls the app drops to local on its own (the deliberate tolerance still fires).
When the phone was locked, a transport command (lock-screen/Bluetooth/
watch media button, or queue-resync play) issued during a WiFi power-save
stall would hit the 2s connect timeout and throw. handleSoapFailure then
cancelled the poll loop and fired onDrop on that single failure -- showing
"Disconnected from <Sonos>" and reverting to local playback, even though
the renderer was perfectly reachable. This bypassed the poll loop's
deliberate 30-consecutive-failure tolerance (DROP_THRESHOLD, bumped from 3
precisely for screen-off WiFi sleep / Doze).
Make the 1 Hz poll loop the sole drop arbiter:
- Transport SOAP commands retry transient IO failures (retryTransientIo:
3 attempts, 400ms backoff) so a brief WiFi stall lands the command once
WiFi wakes instead of abandoning it. A SoapFaultException (renderer
answered, rejected the action) is not retried -- the device is alive.
- handleTransportFailure no longer cancels the poll loop or fires onDrop;
it logs and nudges an immediate poll so the UI reconciles to Sonos's
actual state. If the renderer is truly gone, the poll loop trips the
drop on its own via DROP_THRESHOLD.
Extract retryTransientIo as an internal top-level fn + unit test covering
first-success, retry-then-succeed, exhaust-and-rethrow, and no-retry-on-
SoapFault. Refresh now-stale drop-heuristic comments.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Single-fix release on the per-day mutable CalVer scheme.
Sonos — a single transport SOAP failure no longer reverts to local (Android)
When the phone was locked, a transport command (lock-screen / Bluetooth / watch media button, or a queue-resync
play) issued during a WiFi power-save stall would hit the 2 s connect timeout and throw.handleSoapFailurethen cancelled the poll loop and firedonDropon that single failure — showing "Disconnected from <Sonos>" and reverting to local playback, even though the renderer was perfectly reachable. This bypassed the poll loop's deliberate 30-consecutive-failure tolerance (DROP_THRESHOLD, bumped from 3 precisely for screen-off WiFi sleep / Doze).Fix — make the 1 Hz poll loop the sole drop arbiter:
retryTransientIo: 3 attempts, 400 ms backoff) so a brief WiFi stall lands the command once WiFi wakes instead of abandoning it. ASoapFaultException(renderer answered and rejected the action) is not retried — the device is alive.handleTransportFailureno longer cancels the poll loop or firesonDrop; it logs and nudges an immediate poll so the UI reconciles to Sonos's actual state. If the renderer is truly gone, the poll loop trips the drop on its own viaDROP_THRESHOLD.retryTransientIoextracted as an internal top-level fn with a unit test (first-success, retry-then-succeed, exhaust-and-rethrow, no-retry-on-SoapFault).Verify on device