diff --git a/hive-c0re/src/matrix.rs b/hive-c0re/src/matrix.rs index 1a4aca74..ed019656 100644 --- a/hive-c0re/src/matrix.rs +++ b/hive-c0re/src/matrix.rs @@ -1234,16 +1234,44 @@ fn state_needs_write(current: Option<&serde_json::Value>, desired: &serde_json:: /// Read the room's current content for one state event. `None` on any /// failure — absent state, transport error, or an unparseable body are /// all "we don't know", and [`state_needs_write`] turns that into a write. +/// +/// One failure is louder than the rest, and it is the only one that hides: +/// **a non-404 HTTP failure means the read broke while the write still +/// works**, so the guard degrades to writing every time and the sweep +/// resumes waking the hive with nothing else to show for it. A transport +/// error needs no warning of its own — the PUT immediately after it fails +/// too, loudly — and a 404 is the expected first-setup case. async fn current_room_state( client: &reqwest::Client, admin_token: &str, url: &str, ) -> Option { - let resp = client.get(url).bearer_auth(admin_token).send().await.ok()?; - if !resp.status().is_success() { + let resp = match client.get(url).bearer_auth(admin_token).send().await { + Ok(resp) => resp, + Err(e) => { + tracing::debug!(error = ?e, url, "matrix: state read unreachable; writing"); + return None; + } + }; + let status = resp.status(); + if !status.is_success() { + if status != reqwest::StatusCode::NOT_FOUND { + tracing::warn!( + %status, + url, + "matrix: state read failed while writes still work — the \ + skip-if-unchanged guard is off and every sweep will re-emit" + ); + } return None; } - resp.json::().await.ok() + match resp.json::().await { + Ok(body) => Some(body), + Err(e) => { + tracing::debug!(error = ?e, url, "matrix: state read body unparseable; writing"); + None + } + } } /// PUT a state event into `room_id` using the admin token, **skipping the