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.
This commit is contained in:
damocles 2026-06-04 09:47:36 +02:00 committed by mara
commit 1c5936febb
2 changed files with 53 additions and 3 deletions

View file

@ -267,8 +267,26 @@ async fn cmd_serve(
// access_token persisted to `<state>/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

View file

@ -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);