diff --git a/hive-matrix-mcp/src/client.rs b/hive-matrix-mcp/src/client.rs index efe73e29..56891bba 100644 --- a/hive-matrix-mcp/src/client.rs +++ b/hive-matrix-mcp/src/client.rs @@ -64,6 +64,7 @@ pub async fn build_and_restore( homeserver: &str, token_file: &Path, state_dir: &Path, + is_primary: bool, ) -> Result { let token = fs::read_to_string(token_file) .await @@ -80,31 +81,42 @@ pub async fn build_and_restore( 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 keyed to the now-invalid session) so - // hive-c0re's periodic `ensure_all` sweep re-provisions the account. - // Exit 0: systemd's Restart=on-failure must not loop us here; the - // systemd.paths watcher restarts us once the new token file appears. + // state wipe or token expiry. Delete the stale token file so we + // don't loop on it. The rest of the handling depends on whether + // this is the primary (hive-internal) account or a secondary one, + // because a single bad secondary token must NOT take down the + // whole daemon (and with it every healthy account). tracing::warn!( path = %token_file.display(), - "matrix token rejected (M_UNKNOWN_TOKEN); deleting stale token + \ - sdk state for re-provisioning" + is_primary, + "matrix token rejected (M_UNKNOWN_TOKEN); removing stale token" ); let _ = fs::remove_file(token_file).await; - if let Err(re) = fs::remove_dir_all(state_dir).await { - tracing::warn!( - path = %state_dir.display(), - err = %re, - "failed to remove sdk state dir; next startup may fail with stale state" - ); + if is_primary { + // Primary: also drop the matrix-sdk sqlite state keyed to the + // now-invalid session, then exit 0 so hive-c0re's `ensure_all` + // re-provisions the account and the systemd.paths watcher + // restarts us once the fresh token file appears. Exit 0 (not + // Err) keeps systemd's Restart=on-failure from looping; the + // call site is before any tasks are spawned so there's + // nothing to clean up. + if let Err(re) = fs::remove_dir_all(state_dir).await { + tracing::warn!( + path = %state_dir.display(), + err = %re, + "failed to remove sdk state dir; next startup may fail with stale state" + ); + } + std::process::exit(0); } - // Exit 0 rather than returning Err: `hive-matrix-daemon` is a - // single-purpose process binary; the call site is before any - // tasks are spawned so there are no resources to clean up. - // Using exit(0) (not Err) keeps systemd's Restart=on-failure - // from looping — the systemd.paths watcher re-launches us - // once hive-c0re writes a fresh token file. - std::process::exit(0); + // Secondary: token removed (so it's cleanly skipped next boot + // rather than re-erroring); leave the sdk state in place in case + // the operator re-provisions a fresh token for the same device. + // Return Err so the caller logs + skips this one account and the + // daemon keeps serving the primary and any other healthy account. + return Err(anyhow!( + "matrix token rejected (M_UNKNOWN_TOKEN); removed stale token, skipping account" + )); } return Err(e); } diff --git a/hive-matrix-mcp/src/main.rs b/hive-matrix-mcp/src/main.rs index 12efc512..6ac631a6 100644 --- a/hive-matrix-mcp/src/main.rs +++ b/hive-matrix-mcp/src/main.rs @@ -17,8 +17,12 @@ //! provisions it. A SECONDARY account missing its token is skipped (the //! daemon still serves the others). //! -//! Stale-token recovery: handled in `client::build_and_restore` — see -//! that module for the `M_UNKNOWN_TOKEN` detection + cleanup flow. +//! Stale-token recovery (`M_UNKNOWN_TOKEN`): handled in +//! `client::build_and_restore`, and it mirrors the missing-token policy +//! above — a rejected PRIMARY token drops the stale token + sdk state and +//! exits 0 for systemd re-provisioning, while a rejected SECONDARY token +//! is removed and that one account is skipped so the daemon keeps serving +//! the primary and any other healthy account. use std::sync::Arc; @@ -65,7 +69,7 @@ async fn main() -> Result<()> { // Account-tag the wakes only in multi-account mode so single- // account wake bodies stay byte-identical to the legacy format. let tag = multi.then(|| cfg.name.clone()); - match bring_up_account(&cfg, &hyperhive_socket, tag).await { + match bring_up_account(&cfg, &hyperhive_socket, tag, is_primary).await { Ok(Some((client, sync_loop))) => { registry.insert(cfg.name, client); sync_loops.push(sync_loop); @@ -137,6 +141,7 @@ async fn bring_up_account( cfg: &AccountCfg, hyperhive_socket: &std::path::Path, tag: Option, + is_primary: bool, ) -> Result> { let homeserver = cfg.homeserver(); if !tokio::fs::try_exists(&cfg.token_file) @@ -152,9 +157,10 @@ async fn bring_up_account( state_dir = %cfg.state_dir.display(), "bringing up matrix account" ); - let client = client::build_and_restore(&homeserver, &cfg.token_file, &cfg.state_dir) - .await - .with_context(|| format!("build matrix client for account {}", cfg.name))?; + let client = + client::build_and_restore(&homeserver, &cfg.token_file, &cfg.state_dir, is_primary) + .await + .with_context(|| format!("build matrix client for account {}", cfg.name))?; timeline::install_message_handler(&client, hyperhive_socket.to_path_buf(), tag.clone()); let sync_client = client.clone();