From 84e468dadb56abc009d0533eaf26fffc8728e553 Mon Sep 17 00:00:00 2001 From: damocles Date: Thu, 4 Jun 2026 21:15:03 +0200 Subject: [PATCH] fix(#1309): retry forge token read at startup instead of giving up immediately --- hive-ag3nt/src/forge_notify.rs | 42 ++++++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/hive-ag3nt/src/forge_notify.rs b/hive-ag3nt/src/forge_notify.rs index 544ebdf7..e16598bc 100644 --- a/hive-ag3nt/src/forge_notify.rs +++ b/hive-ag3nt/src/forge_notify.rs @@ -20,6 +20,13 @@ const POLL_INTERVAL_SECS: u64 = 30; const HTTP_TIMEOUT_SECS: u64 = 10; /// Maximum characters of a body/comment to include in the wake message. const BODY_TRUNCATE: usize = 500; +/// How long to wait between token-read retries when the token file is +/// missing or unreadable at startup (e.g. hive-priv hasn't provisioned +/// it yet, or a chown race left it temporarily root-owned). +const TOKEN_RETRY_SECS: u64 = 30; +/// Give up waiting for the token after this many retries (~10 minutes). +/// Avoids an infinite wait on agents that genuinely have no forge account. +const TOKEN_RETRY_MAX: u32 = 20; /// Spawn point: called once from `hive-ag3nt serve` (agent) or /// `hive-m1nd serve` (manager). Returns immediately if the forge is not @@ -37,18 +44,33 @@ pub async fn run(socket: PathBuf) { let state_dir = std::env::var("HYPERHIVE_STATE_DIR").unwrap_or_default(); let token_path = format!("{state_dir}/forge-token"); - let token = match tokio::fs::read_to_string(&token_path).await { - Ok(t) => { - let t = t.trim().to_owned(); - if t.is_empty() { - debug!("forge_notify: empty forge token at {token_path} — disabled"); + // Retry reading the token to handle races where hive-priv provisions the + // token after the harness starts, or where a parent-container chown briefly + // makes the file unreadable (see #1304 / #1309). We wait up to + // TOKEN_RETRY_MAX * TOKEN_RETRY_SECS before giving up. + let token = { + let mut attempts = 0u32; + loop { + match tokio::fs::read_to_string(&token_path).await { + Ok(t) => { + let t = t.trim().to_owned(); + if !t.is_empty() { + break t; + } + debug!("forge_notify: empty forge token at {token_path}"); + } + Err(e) => { + debug!("forge_notify: cannot read token at {token_path}: {e}"); + } + } + attempts += 1; + if attempts >= TOKEN_RETRY_MAX { + debug!( + "forge_notify: token not available after {TOKEN_RETRY_MAX} retries — disabled" + ); return; } - t - } - Err(e) => { - debug!("forge_notify: no forge token at {token_path} ({e}) — disabled"); - return; + tokio::time::sleep(Duration::from_secs(TOKEN_RETRY_SECS)).await; } };