Summary
collie update --check reports the service row as amber
no service manager on this host — the bridge is unsupervised, so restart it by hand after the update
on a host where the bridge is running under systemd --user, is enabled, and has been for weeks.
The false negative depends only on the environment of the process running the check, not on the state of the service. Any spawner that does not pass a session environment triggers it — notably a Herdr plugin action, which herdr-plugin.toml documents as injecting "HERDR_SOCKET_PATH / HERDR_PLUGIN_CONFIG_DIR into these invocations and nothing else — no login shell".
Reproduction
On a host with a healthy ~/.config/systemd/user/collie.service:
$ ./bin/collie update --check | tail -3
✓ upstream already current — v1.6.0 is the newest release of major 1
✓ service collie is active — the update can restart it
$ env -i HOME=$HOME PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
COLLIE_PLUGIN_ROOT=$PWD ./bin/collie update --check | tail -3
✓ upstream already current — v1.6.0 is the newest release of major 1
amber: service no service manager on this host — the bridge is unsupervised, so restart it by hand after the update
Nothing about the service changed between the two runs. systemctl --user is-active collie is active throughout.
Cause
supervisionTier() (cli/lifecycle.ts:107) gates the systemd tier on systemdUserReachable() (cli/lifecycle.ts:91), which is systemctl --user show-environment exiting 0. systemctl --user locates the user manager through XDG_RUNTIME_DIR (or DBUS_SESSION_BUS_ADDRESS); with neither set it cannot connect and exits non-zero. supervisionTier then falls through to "unsupervised" (cli/lifecycle.ts:117), and serviceCheck prints the amber line (cli/update-check.ts:591-595).
Either variable alone is sufficient for the probe to succeed:
$ env -i HOME=$HOME PATH=/usr/bin:/bin XDG_RUNTIME_DIR=/run/user/1000 systemctl --user show-environment >/dev/null; echo $?
0
$ env -i HOME=$HOME PATH=/usr/bin:/bin DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus systemctl --user show-environment >/dev/null; echo $?
0
The docstring on systemdUserReachable says the probe exists to distinguish "a container commonly ships the systemd package … without ever running a user instance or session bus" from a real user manager. That intent is right; the implementation cannot tell that case apart from "a user manager is running but this process was spawned without a session env", and reports both as unsupervised.
The consequence is not cosmetic: it tells an operator whose only interface is the phone to go restart a daemon by hand that systemd is about to restart correctly on its own.
Suggested fix
When XDG_RUNTIME_DIR is unset, derive it rather than concluding "unsupervised" — /run/user/$(id -u), and only report unsupervised if the probe still fails with it set. That preserves the container case (no user manager ⇒ no socket under that path ⇒ probe still fails) while removing the false negative on a normal Linux host.
Workaround
Adding one line to the Collie config .env fixes it for every spawner at once, since cli/context.ts merges that file into ctx.env and realExec(ctx.env, …) (cli/deps.ts:26, cli/sys.ts:270) hands it to every subprocess:
XDG_RUNTIME_DIR=/run/user/1000
Environment
- Ubuntu 24.04, kernel 6.8.0, x86_64
- Herdr 0.8.2, Herdr-managed Collie checkout,
linger enabled for the user
- Checkout detached at
v1.6.0; reproduced against the built binary reporting 1.5.1+ba39c05. The logic in cli/lifecycle.ts is unchanged between the two, and the line numbers above are from the v1.6.0 tree.
Summary
collie update --checkreports theservicerow as amberon a host where the bridge is running under
systemd --user, isenabled, and has been for weeks.The false negative depends only on the environment of the process running the check, not on the state of the service. Any spawner that does not pass a session environment triggers it — notably a Herdr plugin action, which
herdr-plugin.tomldocuments as injecting "HERDR_SOCKET_PATH/HERDR_PLUGIN_CONFIG_DIRinto these invocations and nothing else — no login shell".Reproduction
On a host with a healthy
~/.config/systemd/user/collie.service:Nothing about the service changed between the two runs.
systemctl --user is-active collieisactivethroughout.Cause
supervisionTier()(cli/lifecycle.ts:107) gates thesystemdtier onsystemdUserReachable()(cli/lifecycle.ts:91), which issystemctl --user show-environmentexiting 0.systemctl --userlocates the user manager throughXDG_RUNTIME_DIR(orDBUS_SESSION_BUS_ADDRESS); with neither set it cannot connect and exits non-zero.supervisionTierthen falls through to"unsupervised"(cli/lifecycle.ts:117), andserviceCheckprints the amber line (cli/update-check.ts:591-595).Either variable alone is sufficient for the probe to succeed:
The docstring on
systemdUserReachablesays the probe exists to distinguish "a container commonly ships the systemd package … without ever running a user instance or session bus" from a real user manager. That intent is right; the implementation cannot tell that case apart from "a user manager is running but this process was spawned without a session env", and reports both as unsupervised.The consequence is not cosmetic: it tells an operator whose only interface is the phone to go restart a daemon by hand that systemd is about to restart correctly on its own.
Suggested fix
When
XDG_RUNTIME_DIRis unset, derive it rather than concluding "unsupervised" —/run/user/$(id -u), and only reportunsupervisedif the probe still fails with it set. That preserves the container case (no user manager ⇒ no socket under that path ⇒ probe still fails) while removing the false negative on a normal Linux host.Workaround
Adding one line to the Collie config
.envfixes it for every spawner at once, sincecli/context.tsmerges that file intoctx.envandrealExec(ctx.env, …)(cli/deps.ts:26,cli/sys.ts:270) hands it to every subprocess:Environment
lingerenabled for the userv1.6.0; reproduced against the built binary reporting1.5.1+ba39c05. The logic incli/lifecycle.tsis unchanged between the two, and the line numbers above are from thev1.6.0tree.