From 1c5936febba7a54a6dc8f891ef4737f5b9696b4d Mon Sep 17 00:00:00 2001 From: damocles Date: Thu, 4 Jun 2026 09:47:36 +0200 Subject: [PATCH] fix: recover from M_UNKNOWN_TOKEN in hive-matrix-daemon when the homeserver rejects the token (stale session after state wipe or expiry), the daemon now deletes the token file + matrix-sdk state dir and exits cleanly instead of crash-looping. hive-c0re's matrix::ensure_all sweep now runs periodically every 30 minutes (in addition to startup) so deleted token files get re-provisioned without requiring a hive-c0re restart. the systemd.paths watcher on the token file path then restarts the daemon with a fresh token. --- hive-c0re/src/main.rs | 18 ++++++++++++++++++ hive-matrix-mcp/src/main.rs | 38 ++++++++++++++++++++++++++++++++++--- 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/hive-c0re/src/main.rs b/hive-c0re/src/main.rs index 273de9ac..e01d0835 100644 --- a/hive-c0re/src/main.rs +++ b/hive-c0re/src/main.rs @@ -267,8 +267,26 @@ async fn cmd_serve( // access_token persisted to `/matrix-token`. No-op when // the hive-matrix container isn't running. Backgrounded because // UIAA is a two-roundtrip dance per agent. + // + // Runs once at startup AND periodically every 30 minutes so that + // token files deleted by `hive-matrix-daemon` (stale-token + // recovery — `M_UNKNOWN_TOKEN`) get re-provisioned without + // requiring a hive-c0re restart. + let mut matrix_shutdown = coord.shutdown_rx(); tokio::spawn(async move { + let interval = std::time::Duration::from_mins(30); matrix::ensure_all().await; + loop { + tokio::select! { + () = tokio::time::sleep(interval) => { + matrix::ensure_all().await; + } + _ = matrix_shutdown.changed() => { + tracing::info!("matrix ensure_all: shutdown signal received"); + break; + } + } + } }); // Periodic broker vacuum: drop fully-acked messages older // than 30 days. Delivered-but-unacked rows (recoverable via diff --git a/hive-matrix-mcp/src/main.rs b/hive-matrix-mcp/src/main.rs index d8bcda2e..8066c3df 100644 --- a/hive-matrix-mcp/src/main.rs +++ b/hive-matrix-mcp/src/main.rs @@ -13,6 +13,15 @@ //! Standalone-degraded boot: missing token file → exit 0 cleanly so //! systemd's `ConditionPathExists=` doesn't have to be perfectly //! synced with hive-c0re's token-provisioning timing. +//! +//! Stale-token recovery: when the homeserver rejects the token with +//! `M_UNKNOWN_TOKEN` (e.g. after a homeserver state wipe or session +//! expiry), the daemon deletes both the token file and the matrix-sdk +//! sqlite state dir, then exits 0. hive-c0re's periodic +//! `matrix::ensure_all` sweep detects the missing token file and +//! re-provisions the account (login with the stored password → new +//! token). The systemd.paths watcher on the token file path then +//! restarts this daemon with a valid token. use anyhow::{Context, Result}; use matrix_sdk::config::SyncSettings; @@ -57,9 +66,32 @@ async fn main() -> Result<()> { "hive-matrix-daemon starting" ); - let matrix_client = client::build_and_restore(&homeserver, &token_file, &state_dir) - .await - .context("build matrix client")?; + let matrix_client = + match client::build_and_restore(&homeserver, &token_file, &state_dir).await { + Ok(c) => c, + Err(e) => { + let msg = format!("{e:#}"); + if msg.contains("M_UNKNOWN_TOKEN") { + // Homeserver rejected our token — stale session after a + // homeserver state wipe or token expiry. Delete the token + // file (and the matrix-sdk sqlite state which is keyed to + // the now-invalid session) so hive-c0re's next + // `ensure_all` sweep re-provisions the account. Exit 0 so + // systemd's Restart=on-failure doesn't loop us here — + // the systemd.paths watcher will restart us once the new + // token file appears. + tracing::warn!( + path = %token_file.display(), + "matrix token rejected (M_UNKNOWN_TOKEN); deleting stale \ + token + sdk state for re-provisioning" + ); + let _ = tokio::fs::remove_file(&token_file).await; + let _ = tokio::fs::remove_dir_all(&state_dir).await; + return Ok(()); + } + return Err(e.context("build matrix client")); + } + }; timeline::install_message_handler(&matrix_client, hyperhive_socket.clone()); timeline::install_invite_handler(&matrix_client, hyperhive_socket);