Watch
0
0
Fork
You've already forked hyperhive
0

Compare commits

..
4 changed files with 10 additions and 95 deletions

View file

@ -50,24 +50,6 @@ hive-c0re maintains the local clone at
restarted between pushes). The pull is best-effort — a failure restarted between pushes). The pull is best-effort — a failure
logs a warning and does not affect the rest of the daemon. logs a warning and does not affect the rest of the daemon.
Both paths share the same `knowledge::pull()` function, which also
handles the change notice below — neither path can forget to wire it
in since the broadcast logic lives once, in `pull()` itself, not at
each call site.
### Change notice
When a pull actually moves the local clone's `HEAD` (a real change,
not a no-op — e.g. the periodic pull finding nothing new), hive-c0re
broadcasts a short notice to every currently-registered agent's inbox:
sender `system`, body `[system] /knowledge updated:` followed by a
`git diff --stat <old>..<new>` summary of what changed (or a generic
"see the repo" fallback if computing the diff itself fails). This is
the same broadcast mechanism used for other hive-wide notices — inbox
message only, no forced wake, and it carries the standard "this was a
broadcast" hint. A diff or per-agent send failure is logged but never
blocks the pull itself.
The local clone is created (or refreshed) once at startup via The local clone is created (or refreshed) once at startup via
`knowledge::ensure_local_clone`. If the repo is brand new and `knowledge::ensure_local_clone`. If the repo is brand new and
empty, `ensure_local_clone` seeds it with the default README empty, `ensure_local_clone` seeds it with the default README

View file

@ -129,9 +129,8 @@ pub(super) async fn post_webhook_knowledge(
return (StatusCode::OK, "ignored").into_response(); return (StatusCode::OK, "ignored").into_response();
} }
tracing::info!("webhook/knowledge: pull triggered by push to {expected_repo}"); tracing::info!("webhook/knowledge: pull triggered by push to {expected_repo}");
let coord = state.coord.clone(); tokio::spawn(async {
tokio::spawn(async move { if let Err(e) = crate::knowledge::pull().await {
if let Err(e) = crate::knowledge::pull(&coord).await {
tracing::warn!(error = ?e, "webhook/knowledge: pull failed"); tracing::warn!(error = ?e, "webhook/knowledge: pull failed");
} }
}); });

View file

@ -376,12 +376,11 @@ async fn cmd_serve(
// missed (e.g. hive-c0re was down during a push). First fires at // missed (e.g. hive-c0re was down during a push). First fires at
// startup (immediate pull after the clone is already present). // startup (immediate pull after the clone is already present).
let mut knowledge_shutdown = coord.shutdown_rx(); let mut knowledge_shutdown = coord.shutdown_rx();
let knowledge_coord = coord.clone();
tokio::spawn(async move { tokio::spawn(async move {
// Initial pull — reconcile any commits that landed while c0re // Initial pull — reconcile any commits that landed while c0re
// was offline. Not fed to the health tracker: a startup miss is // was offline. Not fed to the health tracker: a startup miss is
// expected (the clone may not exist yet) and is logged at debug. // expected (the clone may not exist yet) and is logged at debug.
if let Err(e) = knowledge::pull(&knowledge_coord).await { if let Err(e) = knowledge::pull().await {
tracing::debug!(error = ?e, "knowledge: startup pull skipped (no clone yet?)"); tracing::debug!(error = ?e, "knowledge: startup pull skipped (no clone yet?)");
} }
// Persistent-failure → banner. An hourly sweep that keeps failing for // Persistent-failure → banner. An hourly sweep that keeps failing for
@ -394,7 +393,7 @@ async fn cmd_serve(
loop { loop {
tokio::select! { tokio::select! {
() = tokio::time::sleep(interval) => { () = tokio::time::sleep(interval) => {
match knowledge::pull(&knowledge_coord).await { match knowledge::pull().await {
Ok(()) => health.record_ok(), Ok(()) => health.record_ok(),
Err(e) => { Err(e) => {
tracing::warn!(error = ?e, "knowledge: periodic pull failed"); tracing::warn!(error = ?e, "knowledge: periodic pull failed");

View file

@ -16,7 +16,6 @@ use std::collections::BTreeMap;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use forgejo_api::structs::{CreateHookOption, CreateHookOptionConfig, CreateHookOptionType}; use forgejo_api::structs::{CreateHookOption, CreateHookOptionConfig, CreateHookOptionType};
use crate::coordinator::Coordinator;
use crate::forge::forge_git_url; use crate::forge::forge_git_url;
pub const ORG: &str = "internal"; pub const ORG: &str = "internal";
@ -242,92 +241,28 @@ pub async fn ensure_webhook(
Ok(()) Ok(())
} }
/// Current `HEAD` sha of the local clone, best-effort. `None` on any
/// failure (used only to detect whether a pull actually moved `HEAD` —
/// worth skipping the broadcast over, not worth failing the pull for).
async fn head_sha() -> Option<String> {
let out = tokio::process::Command::new("git")
.args(["-C", LOCAL_DIR, "rev-parse", "HEAD"])
.output()
.await
.ok()?;
out.status
.success()
.then(|| String::from_utf8_lossy(&out.stdout).trim().to_owned())
}
/// Pull the latest changes in the local clone. Called from the webhook /// Pull the latest changes in the local clone. Called from the webhook
/// handler on every push to `internal/knowledge` main, and periodically /// handler on every push to `internal/knowledge` main, and periodically
/// from `main.rs` as a fallback. Uses `--ff-only` so a force-push to /// from `main.rs` as a fallback. Uses `--ff-only` so a force-push to
/// the knowledge repo never wedges the local copy silently. /// the knowledge repo never wedges the local copy silently.
/// pub async fn pull() -> Result<()> {
/// When the pull actually moves `HEAD` (a real change, not a no-op),
/// broadcasts a short `git diff --stat` summary to every live agent's
/// inbox via `coord` — inbox-only, no forced wake, and shared by both
/// call sites since the broadcast lives in here rather than in each
/// caller.
pub async fn pull(coord: &Coordinator) -> Result<()> {
// Sanity: if the clone is missing (e.g. storage was wiped), refuse // Sanity: if the clone is missing (e.g. storage was wiped), refuse
// to pull and let the caller decide whether to re-clone. // to pull and let the caller decide whether to re-clone.
let git_dir = std::path::Path::new(LOCAL_DIR).join(".git"); let git_dir = std::path::Path::new(LOCAL_DIR).join(".git");
if !git_dir.exists() { if !git_dir.exists() {
anyhow::bail!("knowledge: {LOCAL_DIR}/.git not found — clone first"); anyhow::bail!("knowledge: {LOCAL_DIR}/.git not found — clone first");
} }
let before = head_sha().await;
let out = tokio::process::Command::new("git") let out = tokio::process::Command::new("git")
.args(["-C", LOCAL_DIR, "pull", "--ff-only"]) .args(["-C", LOCAL_DIR, "pull", "--ff-only"])
.output() .output()
.await .await
.context("git pull knowledge")?; .context("git pull knowledge")?;
if !out.status.success() { if out.status.success() {
let stdout = String::from_utf8_lossy(&out.stdout).trim().to_owned();
tracing::info!(result = %stdout, "knowledge: pull succeeded");
Ok(())
} else {
let stderr = String::from_utf8_lossy(&out.stderr).trim().to_owned(); let stderr = String::from_utf8_lossy(&out.stderr).trim().to_owned();
anyhow::bail!("git pull {ORG}/{REPO} failed: {stderr}") anyhow::bail!("git pull {ORG}/{REPO} failed: {stderr}")
} }
let stdout = String::from_utf8_lossy(&out.stdout).trim().to_owned();
tracing::info!(result = %stdout, "knowledge: pull succeeded");
let after = head_sha().await;
if let (Some(before), Some(after)) = (before, after)
&& before != after
{
broadcast_change(coord, &before, &after).await;
}
Ok(())
}
/// Diff `before..after` in the local clone and broadcast a one-line-per-file
/// summary to every live agent. Best-effort — a diff or send failure is
/// logged, never propagated (the pull itself already succeeded).
async fn broadcast_change(coord: &Coordinator, before: &str, after: &str) {
let diff = tokio::process::Command::new("git")
.args([
"-C",
LOCAL_DIR,
"diff",
"--stat",
&format!("{before}..{after}"),
])
.output()
.await;
let stat = match diff {
Ok(out) if out.status.success() => String::from_utf8_lossy(&out.stdout).trim().to_owned(),
Ok(out) => {
let stderr = String::from_utf8_lossy(&out.stderr).trim().to_owned();
tracing::warn!(%stderr, "knowledge: diff --stat failed; broadcasting without it");
String::new()
}
Err(e) => {
tracing::warn!(error = ?e, "knowledge: diff --stat failed; broadcasting without it");
String::new()
}
};
let body = if stat.is_empty() {
"[system] /knowledge updated — see the repo for what changed.".to_owned()
} else {
format!("[system] /knowledge updated:\n{stat}")
};
let errors = coord.broadcast_send(hive_sh4re::SYSTEM_SENDER, &body);
if !errors.is_empty() {
tracing::warn!(?errors, "knowledge: broadcast had per-agent failures");
}
} }