forge: stop the avatar sync retriggering itself into the start limit

`forge-avatar-sync.path` used `PathExists=`. systemd.path(5): a
`PathExists=` condition that already holds activates the configured unit
immediately whenever the path unit is activated. A `Type=oneshot` unit
with `RemainAfterExit=false` deactivates after each run, which re-arms
the path, which fires again because the token is still there — an
unconditional loop that ends at `StartLimitBurst`.

Measured on a container boot carrying the previous fix: five starts and
`start-limit-hit` with the glob already removed and exactly one matching
file. So the count was never one-per-token; it was the start limit, and
scoping the watch (#4025) could not have fixed it.

`PathChanged=` does not fire on an already-present path, and hive-priv
writes this file in place — `write_state_file_nofollow` opens with
`O_TRUNC` and no rename — so close-after-write still triggers it. The
token-present-at-boot case stays covered by the service's own
`wantedBy = multi-user.target`.

Same directive and same reasoning as `swarm-controller.nix`'s
queue-credential watcher, which reached it first: "`PathChanged=` requires
a write, so it cannot do that and cannot spin."

The comment claiming the storm came from many agents' tokens arriving at
once is removed with it — that model is what let the defect survive the
previous fix.

Refs #3984.
This commit is contained in:
atlas 2026-09-03 20:29:48 +02:00
commit e9faa3896f

View file

@ -244,26 +244,32 @@ in
''; '';
}; };
# Path-trigger sibling: re-fires forge-avatar-sync the moment # Path-trigger sibling: re-fires forge-avatar-sync when
# `<state>/forge-token` appears. Mirrors the hive-matrix-daemon # `<state>/forge-token` is written. On first agent deployment the
# token-watcher pattern — on first agent deployment the container # container boots before hive-c0re has provisioned the forge-token, so
# boots before hive-c0re has provisioned the forge-token, so the # the service fires too early and exits with "no forge-token found".
# service fires too early and exits with "no forge-token found".
# Without this path unit, RemainAfterExit=true would prevent systemd # Without this path unit, RemainAfterExit=true would prevent systemd
# from ever re-running the service. See # from ever re-running the service. See
# docs/agent-lifecycle/persistence.md::forge-avatar-sync. # docs/agent-lifecycle/persistence.md::forge-avatar-sync.
#
# PathChanged=, not PathExists=: a PathExists= condition that already
# holds re-activates the unit immediately every time the path unit
# re-arms, and a oneshot re-arms it by deactivating — so it loops until
# systemd's start limit stops it. PathChanged= does not fire on an
# already-present path, and hive-priv writes this file in place
# (write_state_file_nofollow: O_TRUNC, no rename), so close-after-write
# still triggers it. Same directive and same reason as
# swarm-controller.nix's queue-credential watcher.
#
# ⚠️ This agent's own token, not a glob over `/agents/*/`. Every agent's # ⚠️ This agent's own token, not a glob over `/agents/*/`. Every agent's
# state dir is visible from inside every container, so a wildcard here # state dir is visible from inside every container, so a wildcard here
# watches paths this unit has no business reacting to: each sibling's # watches paths this unit has no business reacting to.
# token appearing re-fires *this* agent's sync, and enough of them
# arriving at once trips systemd's start rate limit — leaving a red
# `[FAILED]` on every boot after the upload has already succeeded.
# The service reads `$HYPERHIVE_STATE_DIR/forge-token`; this is the same # The service reads `$HYPERHIVE_STATE_DIR/forge-token`; this is the same
# file, spelled the way `tea-login` above already spells it. # file, spelled the way `tea-login` above already spells it.
systemd.paths.forge-avatar-sync = lib.mkIf (config.hyperhive.icon != null) { systemd.paths.forge-avatar-sync = lib.mkIf (config.hyperhive.icon != null) {
description = "trigger forge-avatar-sync when forge-token appears"; description = "trigger forge-avatar-sync when forge-token appears";
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
pathConfig.PathExists = "/agents/${userName}/state/forge-token"; pathConfig.PathChanged = "/agents/${userName}/state/forge-token";
}; };
# One-shot: hyperhive.icon → Forgejo profile avatar. Shape contract: # One-shot: hyperhive.icon → Forgejo profile avatar. Shape contract: