Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
03fec39405 | ||
|
|
d58a3a3303 | ||
|
|
1adfd604b6 | ||
|
|
717086b02d | ||
|
|
eb63c7ebb1 |
3 changed files with 80 additions and 2 deletions
|
|
@ -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,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<String> = 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,
|
||||
keep_subscriptions,
|
||||
&mut unsubbed_repos,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -222,7 +242,15 @@ 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,
|
||||
keep_subscriptions: bool,
|
||||
unsubbed_repos: &mut HashSet<String>,
|
||||
) {
|
||||
let url = format!("{forge_url}/api/v1/notifications?all=false&limit=50");
|
||||
let resp = match client
|
||||
.get(&url)
|
||||
|
|
@ -310,5 +338,35 @@ 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 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 !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");
|
||||
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)");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -159,6 +159,20 @@
|
|||
'';
|
||||
};
|
||||
|
||||
options.hyperhive.forge.keepSubscriptions = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = true;
|
||||
description = ''
|
||||
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.
|
||||
'';
|
||||
};
|
||||
|
||||
options.hyperhive.claudeMarketplaces = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ "anthropics/claude-plugins-official" ];
|
||||
|
|
@ -283,6 +297,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;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@
|
|||
{
|
||||
imports = [ ./harness-base.nix ];
|
||||
|
||||
# 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
|
||||
# here are the base config so the container stays sensible if anyone
|
||||
|
|
|
|||
Loading…
Reference in a new issue