fix(bootstrap): self-heal a systemd unit left on a retired plugin root
The residue an interrupted update leaves behind is invisible: the server
runs, every doctor row reads OK, and the unit quietly still names a cache
directory Claude Code deleted during the update. Nothing surfaces it until
a reboot, when the dashboard does not come back and nothing recorded why.
- fastPathOk now rejects a stale unit, so the next SessionStart repairs it
instead of short-circuiting. `unitIsStale` is pure fs on purpose: that
path runs on every session and may not spawn a process.
- Repairing the unit is not restarting the server. A current, healthy
server keeps running and the corrected unit takes effect at the next
start (`pendingSystemdRestart = systemdOwned && needsServer`), and the
detached-spawn fallback is guarded by needsServer so a stale unit alone
cannot start a second server onto a bound port.
- systemdOwned now also requires the unit to be ACTIVE. Installed-but-
stopped owns nothing: skipping the stop there and asking systemd to start
would bind an occupied port and crash-loop under Restart=always.
- /agenthub-doctor gains a systemd row naming the consequence ("will NOT
come back after a reboot") and the fix. WARN, never FAIL — the dashboard
in front of you works, and having no unit at all is a normal setup.
The fast-path test pins the regression against an otherwise fully healthy
install, live server included; without a live server it would have passed
against the unfixed code either way.
This commit is contained in:
@@ -83,6 +83,7 @@ function diagnose() {
|
||||
);
|
||||
|
||||
add(...cliStatus());
|
||||
add(...systemdStatus());
|
||||
add(...tmuxStatus());
|
||||
add(...skillPluginStatus());
|
||||
|
||||
@@ -220,6 +221,53 @@ function skillPluginStatus() {
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* The failure this row exists for is silent by construction: a unit still
|
||||
* naming a retired plugin cache directory keeps a running dashboard looking
|
||||
* perfectly healthy, and only bites at the next reboot — when the server does
|
||||
* not come back, from a path Claude Code deleted weeks earlier. Nothing else
|
||||
* in this report would show it.
|
||||
*
|
||||
* WARN, never FAIL: the dashboard in front of you is working, and on macOS,
|
||||
* Windows or a Linux box without systemd there is no unit to have an opinion
|
||||
* about.
|
||||
*/
|
||||
function systemdStatus() {
|
||||
const svc = require("./systemd-service");
|
||||
const unit = svc.unitPath("agenthub");
|
||||
|
||||
if (!svc.unitIsOurs("agenthub")) {
|
||||
if (fs.existsSync(unit)) {
|
||||
return ["warn", "systemd", `${unit} exists but was not written by AgentHub — left untouched`];
|
||||
}
|
||||
return [
|
||||
"ok",
|
||||
"systemd",
|
||||
svc.systemctlAvailable()
|
||||
? "no unit — the server is started per session (install one with `agenthub service install`)"
|
||||
: "not available on this platform — the server is started per session",
|
||||
];
|
||||
}
|
||||
|
||||
if (svc.unitIsStale("agenthub", PLUGIN_ROOT)) {
|
||||
return [
|
||||
"warn",
|
||||
"systemd",
|
||||
`${unit} still points at a previous plugin version — the dashboard will NOT come back after a reboot. ` +
|
||||
`Starting a new Claude Code session repairs it; or run: node ${path.join(PLUGIN_ROOT, "scripts", "plugin-bootstrap.js")} --force`,
|
||||
];
|
||||
}
|
||||
|
||||
if (!svc.unitIsActive("agenthub")) {
|
||||
return [
|
||||
"warn",
|
||||
"systemd",
|
||||
"unit is installed and current but not running — start it with `systemctl --user start agenthub`",
|
||||
];
|
||||
}
|
||||
return ["ok", "systemd", "unit is current and running — the dashboard survives reboots"];
|
||||
}
|
||||
|
||||
/**
|
||||
* WARN (never FAIL) when `tmux` is absent: every run the dashboard starts is a
|
||||
* tmux session (`server/lib/tmux.js`), so without the binary the run console,
|
||||
|
||||
Reference in New Issue
Block a user