From eb63c7ebb1ac496b1f0d0da936b47ca80ec66765 Mon Sep 17 00:00:00 2001 From: damocles Date: Thu, 21 May 2026 22:07:28 +0200 Subject: [PATCH 1/5] forge_notify: auto-unsubscribe from repo watches on subscribed notifications --- hive-ag3nt/src/forge_notify.rs | 48 ++++++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/hive-ag3nt/src/forge_notify.rs b/hive-ag3nt/src/forge_notify.rs index 2a97547a..35785bb8 100644 --- a/hive-ag3nt/src/forge_notify.rs +++ b/hive-ag3nt/src/forge_notify.rs @@ -17,6 +17,7 @@ //! `PATCH /notifications/threads/{id}` so it does not re-fire. If delivery //! fails the thread is left unread so it resurfaces next tick. +use std::collections::HashSet; use std::path::{Path, PathBuf}; use std::time::Duration; @@ -80,9 +81,13 @@ pub async fn run(socket: PathBuf, is_manager: bool) { // socket becoming available right at boot. interval.tick().await; + // Repos we have already unsubscribed this process lifetime. Persists + // across polls so we don't hammer DELETE on every cycle. + let mut unsubbed_repos: HashSet = HashSet::new(); + loop { interval.tick().await; - poll_once(&client, &forge_url, &token, &socket, is_manager).await; + poll_once(&client, &forge_url, &token, &socket, is_manager, &mut unsubbed_repos).await; } } @@ -222,7 +227,14 @@ async fn format_notification( } } -async fn poll_once(client: &reqwest::Client, forge_url: &str, token: &str, socket: &Path, is_manager: bool) { +async fn poll_once( + client: &reqwest::Client, + forge_url: &str, + token: &str, + socket: &Path, + is_manager: bool, + unsubbed_repos: &mut HashSet, +) { let url = format!("{forge_url}/api/v1/notifications?all=false&limit=50"); let resp = match client .get(&url) @@ -310,5 +322,37 @@ async fn poll_once(client: &reqwest::Client, forge_url: &str, token: &str, socke debug!(%id, "forge_notify: marked read"); } } + + // Auto-unsubscribe from broad repo watches. If the notification + // reason is "subscribed" (agent is watching the whole repo) and + // the agent has no personal stake (was just watching), drop the + // watch subscription so we don't accumulate firehose noise from + // repos we contributed to but are no longer actively working in. + // Thread-level subscriptions (specific issue/PR) are unaffected. + let reason = notif["reason"].as_str().unwrap_or(""); + if reason == "subscribed" { + if let Some(repo) = notif["repository"]["full_name"].as_str() { + if !unsubbed_repos.contains(repo) { + let unsub_url = format!("{forge_url}/api/v1/repos/{repo}/subscription"); + match client + .delete(&unsub_url) + .header("Authorization", format!("token {token}")) + .send() + .await + { + Ok(r) if r.status().is_success() || r.status().as_u16() == 404 => { + debug!(%repo, "forge_notify: unsubscribed from repo watch"); + unsubbed_repos.insert(repo.to_owned()); + } + Ok(r) => { + debug!(%repo, status = %r.status(), "forge_notify: unsub non-2xx (ignored)"); + } + Err(e) => { + debug!(%repo, error = ?e, "forge_notify: unsub request failed (ignored)"); + } + } + } + } + } } } From 717086b02df2dedd6db98e1f20af3639edbc45e8 Mon Sep 17 00:00:00 2001 From: damocles Date: Thu, 21 May 2026 22:14:41 +0200 Subject: [PATCH 2/5] forge_notify: HIVE_FORGE_KEEP_SUBSCRIPTIONS=1 disables auto-unsubscribe --- hive-ag3nt/src/forge_notify.rs | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/hive-ag3nt/src/forge_notify.rs b/hive-ag3nt/src/forge_notify.rs index 35785bb8..d36e1bcd 100644 --- a/hive-ag3nt/src/forge_notify.rs +++ b/hive-ag3nt/src/forge_notify.rs @@ -81,13 +81,28 @@ pub async fn run(socket: PathBuf, is_manager: bool) { // socket becoming available right at boot. interval.tick().await; + // HIVE_FORGE_KEEP_SUBSCRIPTIONS=1 disables auto-unsubscribe for agents + // that intentionally consume the full repo notification firehose (e.g. triage). + let keep_subscriptions = std::env::var("HIVE_FORGE_KEEP_SUBSCRIPTIONS") + .map(|v| v == "1" || v.eq_ignore_ascii_case("true")) + .unwrap_or(false); + // Repos we have already unsubscribed this process lifetime. Persists // across polls so we don't hammer DELETE on every cycle. let mut unsubbed_repos: HashSet = HashSet::new(); loop { interval.tick().await; - poll_once(&client, &forge_url, &token, &socket, is_manager, &mut unsubbed_repos).await; + poll_once( + &client, + &forge_url, + &token, + &socket, + is_manager, + keep_subscriptions, + &mut unsubbed_repos, + ) + .await; } } @@ -233,6 +248,7 @@ async fn poll_once( token: &str, socket: &Path, is_manager: bool, + keep_subscriptions: bool, unsubbed_repos: &mut HashSet, ) { let url = format!("{forge_url}/api/v1/notifications?all=false&limit=50"); @@ -323,14 +339,12 @@ async fn poll_once( } } - // Auto-unsubscribe from broad repo watches. If the notification - // reason is "subscribed" (agent is watching the whole repo) and - // the agent has no personal stake (was just watching), drop the - // watch subscription so we don't accumulate firehose noise from - // repos we contributed to but are no longer actively working in. - // Thread-level subscriptions (specific issue/PR) are unaffected. + // Auto-unsubscribe from broad repo watches when the notification + // reason is "subscribed" (agent watching the whole repo). Skipped + // when HIVE_FORGE_KEEP_SUBSCRIPTIONS=1 — triage and other firehose + // consumers set this to retain broad repo visibility. let reason = notif["reason"].as_str().unwrap_or(""); - if reason == "subscribed" { + if !keep_subscriptions && reason == "subscribed" { if let Some(repo) = notif["repository"]["full_name"].as_str() { if !unsubbed_repos.contains(repo) { let unsub_url = format!("{forge_url}/api/v1/repos/{repo}/subscription"); From 1adfd604b65efb707c126ef527389576da388987 Mon Sep 17 00:00:00 2001 From: damocles Date: Thu, 21 May 2026 22:27:58 +0200 Subject: [PATCH 3/5] forge_notify: hyperhive.forge.keepSubscriptions NixOS option --- nix/templates/harness-base.nix | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/nix/templates/harness-base.nix b/nix/templates/harness-base.nix index e8f43d12..06b2d6ad 100644 --- a/nix/templates/harness-base.nix +++ b/nix/templates/harness-base.nix @@ -159,6 +159,19 @@ ''; }; + options.hyperhive.forge.keepSubscriptions = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + When true, the forge notification poller will NOT auto-unsubscribe + from repo watches after delivering a "subscribed"-reason notification. + Set this for agents (e.g. triage) that intentionally consume the full + repo notification firehose and must retain broad watch subscriptions. + By default agents auto-unsubscribe from repos they no longer actively + work in to avoid accumulating firehose noise. + ''; + }; + options.hyperhive.claudeMarketplaces = lib.mkOption { type = lib.types.listOf lib.types.str; default = [ "anthropics/claude-plugins-official" ]; @@ -283,6 +296,8 @@ # Zero watermark disables proactive compaction; the reactive path # (compact-on-overflow) still fires when the session is truly full. HIVE_COMPACT_WATERMARK_TOKENS = "0"; + } // lib.optionalAttrs config.hyperhive.forge.keepSubscriptions { + HIVE_FORGE_KEEP_SUBSCRIPTIONS = "1"; }; boot.isNspawnContainer = true; From d58a3a3303cb976d152998f1acab5e8992016007 Mon Sep 17 00:00:00 2001 From: damocles Date: Thu, 21 May 2026 22:29:25 +0200 Subject: [PATCH 4/5] manager: keepSubscriptions = true by default --- nix/templates/manager.nix | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nix/templates/manager.nix b/nix/templates/manager.nix index f88e2fba..3cd1827f 100644 --- a/nix/templates/manager.nix +++ b/nix/templates/manager.nix @@ -2,6 +2,10 @@ { imports = [ ./harness-base.nix ]; + # Manager keeps broad repo watch subscriptions — needs full visibility. + # Sub-agents default to false and auto-unsubscribe from firehose repos. + hyperhive.forge.keepSubscriptions = true; + # HIVE_PORT/HIVE_LABEL/gitconfig are also injected by the generated # `applied/hm1nd/flake.nix` (see `lifecycle::setup_applied`); the values # here are the base config so the container stays sensible if anyone From 03fec39405163968c98099779ef162da0c1833ba Mon Sep 17 00:00:00 2001 From: damocles Date: Thu, 21 May 2026 22:32:44 +0200 Subject: [PATCH 5/5] =?UTF-8?q?forge=5Fnotify:=20flip=20keepSubscriptions?= =?UTF-8?q?=20default=20=E2=80=94=20agents=20keep,=20manager=20auto-unsubs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nix/templates/harness-base.nix | 15 ++++++++------- nix/templates/manager.nix | 6 +++--- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/nix/templates/harness-base.nix b/nix/templates/harness-base.nix index 06b2d6ad..e1bd2879 100644 --- a/nix/templates/harness-base.nix +++ b/nix/templates/harness-base.nix @@ -161,14 +161,15 @@ options.hyperhive.forge.keepSubscriptions = lib.mkOption { type = lib.types.bool; - default = false; + default = true; description = '' - When true, the forge notification poller will NOT auto-unsubscribe - from repo watches after delivering a "subscribed"-reason notification. - Set this for agents (e.g. triage) that intentionally consume the full - repo notification firehose and must retain broad watch subscriptions. - By default agents auto-unsubscribe from repos they no longer actively - work in to avoid accumulating firehose noise. + When true (the default), the forge notification poller will NOT + auto-unsubscribe from repo watches after delivering a + "subscribed"-reason notification. Sub-agents keep their broad + subscriptions so they stay informed about repos they contribute to. + Set to false for agents (e.g. the manager) that use reason-based + filtering and do not need firehose-level repo visibility — they will + auto-unsubscribe after receiving a watched-repo notification. ''; }; diff --git a/nix/templates/manager.nix b/nix/templates/manager.nix index 3cd1827f..c040cae5 100644 --- a/nix/templates/manager.nix +++ b/nix/templates/manager.nix @@ -2,9 +2,9 @@ { imports = [ ./harness-base.nix ]; - # Manager keeps broad repo watch subscriptions — needs full visibility. - # Sub-agents default to false and auto-unsubscribe from firehose repos. - hyperhive.forge.keepSubscriptions = true; + # Manager auto-unsubscribes from repo watches (uses mention-only filtering + # via HIVE_FORGE_NOTIFY_REASONS). Sub-agents default to keepSubscriptions=true. + hyperhive.forge.keepSubscriptions = false; # HIVE_PORT/HIVE_LABEL/gitconfig are also injected by the generated # `applied/hm1nd/flake.nix` (see `lifecycle::setup_applied`); the values