Watch
0
0
Fork
You've already forked hyperhive
0

start the forge and github pollers when their token lands late

Both pollers wait about ten minutes for the agent's token, then exit 0.
Under Restart=on-failure that exit is final, and nothing started them
again, so a token minted or placed after the window left the agent
without notifications until its container restarted.

Add a path unit per poller with PathChanged on the token file(s) the
poller reads, the shape forge-avatar-sync already uses. A path unit
starts its service only when it is not running, so a poller that found
its token is untouched. The github path unit is gated on
github.enable like its service.

Container-side: it ships with the agent's next rebuild, and an agent on
an older rev keeps the old behaviour.

Closes #6179
This commit is contained in:
atlas 2026-11-07 06:25:38 +01:00 • committed by mara
commit fae9398295
6 changed files with 70 additions and 1 deletions

View file

@ -133,6 +133,12 @@ unread-only filter), formats each notification as a broker
`Wake { from: "forge" }` message, and delivers it to the agent's own
inbox so claude's normal turn loop picks it up.
The daemon waits about ten minutes for the agent's forge token, then exits 0,
which its `Restart=on-failure` doesn't restart. A path unit
(`hive-forge-notify.path`) starts it again when the fetched token file or the
state-dir `forge-token` changes, so a token minted later still starts the
polling.
The crate builds a second, independent binary for a different host — see
[github.md](github.md#notifications).

View file

@ -110,7 +110,10 @@ crate its build cache.
At runtime the poller needs the PAT above. No PAT, no polling: the unit
logs why and exits 0, which is why it's `Restart = on-failure` and
never `always` — a clean exit on a PAT-less agent must not become a
restart loop. Once polling, it re-reads `<state>/github-token` on every
restart loop. The poller waits about ten minutes for the PAT to appear
before it gives up. A path unit (`hive-github-notify.path`) starts the poller
again when `<state>/github-token` changes, so a PAT that lands later still
enables polling. Once polling, it re-reads the file on every
tick, so it picks up a rotated PAT on the next tick without a restart.
Forgejo models its notifications API on GitHub's, so one tolerant

View file

@ -181,6 +181,21 @@ in
};
};
# Starts the poller when a token appears after it gave up waiting for one
# (the poller waits about ten minutes, then exits 0, which `on-failure`
# does not restart). A path unit starts its service only when it is not
# running, so it costs nothing while the poller runs. Both files the poller
# reads are watched. `PathChanged=` for the reason given on
# `forge-avatar-sync` below.
systemd.paths.hive-forge-notify = {
description = "start hive-forge-notify when the forge token appears";
wantedBy = [ "multi-user.target" ];
pathConfig.PathChanged = [
fetchedTokenFile
stateTokenFile
];
};
# Path-trigger sibling: re-fires forge-avatar-sync when the fetched
# forge token (./forge-token.nix) appears or is replaced. On first
# agent deployment the container can boot before the swarm has minted

View file

@ -127,5 +127,18 @@ in
Group = userName;
};
};
# Starts the poller when the token appears after it gave up waiting for one
# (about ten minutes, then exit 0, which `on-failure` does not restart).
# The operator places the PAT through the swarm after the agent runs, so
# that is the usual order. A path unit starts its service only when it is
# not running. `PathChanged=` because the fetch renames the file into place,
# and a `PathExists=` that already holds would re-fire on every re-arm.
# Gated on the service it triggers.
systemd.paths.hive-github-notify = lib.mkIf config.services.hyperhive.agent.github.enable {
description = "start hive-github-notify when the GitHub token appears";
wantedBy = [ "multi-user.target" ];
pathConfig.PathChanged = "/agents/${userName}/state/github-token";
};
};
}

View file

@ -195,6 +195,23 @@ let
agentForgeBao.systemd.paths.forge-avatar-sync.pathConfig.PathChanged == tokenFile agentForgeBao
&& builtins.elem "hive-agent-forge-token.service" agentForgeBao.systemd.services.forge-avatar-sync.after;
}
{
# The poller waits for the token about ten minutes and then exits 0, which
# `on-failure` does not restart; the path unit starts it when either file
# it reads appears. Control: the avatar watcher beside it, in the same set.
name = "a token that lands after the forge poller gave up starts it again";
ok =
let
path = agentForgeBao.systemd.paths.hive-forge-notify;
in
path.pathConfig.PathChanged == [
(tokenFile agentForgeBao)
"/agents/${agentForgeBao.services.hyperhive.agent.user.name}/state/forge-token"
]
&& path.wantedBy == [ "multi-user.target" ]
&& agentForgeBao.systemd.services ? hive-forge-notify
&& agentForgeBao.systemd.paths ? forge-avatar-sync;
}
{
# Both readers take the fetched token first and fall back to the state
# file, which is still the only copy for an agent with no store identity.

View file

@ -102,6 +102,21 @@ let
name = "the fetch runs before the github poller starts";
ok = builtins.elem "hive-github-notify.service" (fetchUnit agentGithubBao).before;
}
{
# The poller waits for the token about ten minutes and then exits 0, which
# `on-failure` does not restart; the path unit starts it when the token lands.
name = "a token that lands after the github poller gave up starts it again";
ok =
let
name = agentGithubBao.services.hyperhive.agent.user.name;
path = agentGithubBao.systemd.paths.hive-github-notify;
in
path.pathConfig.PathChanged == "/agents/${name}/state/github-token"
&& path.wantedBy == [ "multi-user.target" ]
&& agentGithubBao.systemd.services ? hive-github-notify
&& !(agentGithubOff.systemd.paths ? hive-github-notify)
&& !(agentGithubOff.systemd.services ? hive-github-notify);
}
{
name = "the fetch re-runs every two minutes";
ok = agentGithubBao.systemd.timers.hive-agent-github-token.timerConfig.OnUnitInactiveSec == "2min";