autopilot --install uses a global job name, so a second brain silently replaces the first brain's daemon
Summary
The autopilot runtime is per-brain since #1226 — the lock file, the disabled/paused/strike markers and the generated wrapper all live under GBRAIN_HOME — but autopilot --install is not. It writes a fixed job name:
- launchd:
~/Library/LaunchAgents/com.gbrain.autopilot.plist(autopilotLaunchdLabel()returns the constantcom.gbrain.autopilot); - systemd: the constant
AUTOPILOT_SYSTEMD_UNIT = 'gbrain-autopilot.service'; - and the plist hardcodes its log sinks at
$HOME/.gbrain/autopilot.log/.err, ignoringGBRAIN_HOME.
autopilot --install accepts --repo, --interval and --target only, so there is no supported way to ask for a second, brain-scoped job.
Repro (0.51.0.0, macOS, two PGLite brains, isolated HOME)
GBRAIN_HOME=/tmp/brainA gbrain autopilot --install --force --no-inject --repo /tmp/repoA
# → ~/Library/LaunchAgents/com.gbrain.autopilot.plist → /tmp/brainA/.gbrain/autopilot-run.sh
GBRAIN_HOME=/tmp/brainB gbrain autopilot --install --force --no-inject --repo /tmp/repoB
# → the SAME plist, now → /tmp/brainB/.gbrain/autopilot-run.shObserved after the second install:
- one plist, not two: brain A is silently no longer supervised;
GBRAIN_HOME=/tmp/brainA gbrain autopilot --status --jsonstill reports{"installed": true}— brain A believes it has a daemon while the job runs brain B;- both installs print
Log: ~/.gbrain/autopilot.log, so the two brains would interleave into one log file; --uninstallfrom either brain removes the single shared job.
Why it matters
A host with two brains is a supported shape (#1226 exists precisely because a second brain's daemon collided with the first's lock), and it is the natural answer when one brain publishes canonical memory and another only indexes a repository it does not own. Today only one of them can be supervised, and the failure is silent: no error at install, and a status that says installed.
The one escape hatch is GBRAIN_AUTOPILOT_LABEL, which autopilot-paths.ts documents as a test seam rather than a user knob ("it exists so the real-launchd lifecycle e2e can load a genuinely unique job on a dev Mac"), and which has to be exported identically for install, status, uninstall and the wrapper's self-disable or the pair silently disagrees. We would rather not run production supervision on a documented test seam.
Expected
Scope the job identity to the brain — derive the label / unit name (and the log sinks) from the brain id or GBRAIN_HOME, keeping today's name for the default brain so existing installs are untouched — or add a first-class --service-name / --label install flag that --status and --uninstall resolve the same way.
Related: #5058 is the same class of global-state assumption on the install path.
Source: garrytan/gbrain