relay init: start the desktop app's core before the public one

Merged
KJ
9d ago
+550-14

What changes

  • relay init (the message-injection setup path): when the Agent Relay desktop app is installed and no core answers on the bridge socket, init first asks the desktop app to start its own core and attaches to it. "No core answers" includes the case where init has just retired its own outdated core.
    • macOS: open -g -j -b com.agentrelay.desktop (background, hidden, no focus steal). The app registers and starts its core LaunchAgent (com.agentrelay.desktop.core) as it does on every launch.
    • Linux: systemctl --user start agent-relay.service when systemctl --user is-enabled --quiet agent-relay.service succeeds. Otherwise, only in a desktop session (DISPLAY or WAYLAND_DISPLAY set), a runnable agent-relay-desktop launcher (/usr/bin, then per-user updater trees, newest first) is started detached with --background (tray). It is never started with --headless: a headless start removes the person's GUI autostart entry, so a relay init over SSH into their desktop would cost them the tray app at login. With no enabled unit and no display, init skips the desktop start and falls back to the public core with the warning (reason: the desktop app's core needs agent-relay.service enabled, or a desktop session).
    • Windows: no desktop start, so behaviour is unchanged. The logon task is off unless the person turned it on.
    • A moved socket (AGENT_RELAY_CORE_SOCKET set to anything other than the default) also skips the desktop start, because the desktop core never serves it.
  • After asking the desktop app, init shows Asked the Agent Relay desktop app to start its core; using the core now running on this machine (agent-relay-probe <v>, pid <n>). No core.json is written for it.
  • PROBE_VERSION goes from 15.0.7 to 15.0.8. The Cursor row in docs/probe-move.md now says the editor route needs core 15.0.8 or newer.

Fallback rule

desktopInstalled can be stale (markers left after an uninstall, or a disabled service), so a failed desktop start never fails init. The desktop start counts as failed if the start command throws or exits non-zero, or if no core answers within CORE_START_TIMEOUT_MS (30s, wall-clock). In that case init installs and starts the public core exactly as before. Once the public core is up, init prints one warning as a ! line in the message-injection summary, so it shows in plain output and also in the guided flow's note after the spinner stops:

! The Agent Relay desktop app is installed, but its core could not be started (). relay init started the public Agent Relay core instead. To use the desktop app's core, open the Agent Relay desktop app, then run relay init --stop and relay init again.

If the public core fails too, the desktop failure is added to the usual "Message injection is not available (...)" reason.

The existing "one core per machine; attach, never double" rule still holds:

  • A core that is already running is attached to. The desktop app is never asked, and nothing is stopped.
  • A core that answers after the probe download, or that takes the socket while the public core starts, is attached to. Only the process this run spawned is stopped (new: before this PR it was left to exit on the socket lock). The core that answered is never stopped.
  • After a switch off the local workspace, a desktop core this run asked for gets running sessions moved to the new workspace, the same as a public core this run started.
  • --uninstall's temporary core still uses today's path. It never opens the desktop app.

Tests

  • init.test.ts, new starting the desktop app core block (fake deps only):
    • desktop installed and no core: the desktop start is attempted, its core is attached, there is no probe download and no core.json
    • the desktop start throws (including Linux with no enabled unit and no display), or its core never answers: the public core starts, core.json records it, and the warning prints exactly once
    • desktop not installed: the desktop start is never called (unchanged path)
    • a desktop core appears while the public core starts: it is attached, our pid 5151 is stopped, the foreign pid 777 is never stopped, there is no daemon.shutdown and no core.json
    • a desktop core appears during the download: it is attached and startCore is never called
    • a foreign core is already running: it is untouched and the desktop app is not asked
    • the public core also fails: one reason names both failures
    • init's own outdated core is retired: the desktop app is asked next
    • a switch to a team with a desktop core this run asked for: sessions are moved
  • desktop-presence.test.ts: which command startDesktopCore picks per platform, with fake effects (macOS open and its failure, Linux enabled unit, launcher --background with DISPLAY or WAYLAND_DISPLAY, no unit and no display throws without launching anything, newest per-user tree, nothing runnable found, Windows runs nothing).
  • The setup default in init.test.ts fakes startDesktopCore to return undefined. Tests also already stub AGENT_RELAY_CORE_SOCKET, so the real default would skip the desktop start as well.
  • CLI typecheck and lint pass (no new warnings; ensureCore complexity is back to main's 16). The whole CLI suite passes: 2684 passed, 17 skipped. ci-standalone-smoke hit its 5s timeout once under full-suite load and passes on its own.

Not covered

  • Merge after probe-v15.0.8 is published. Until it exists, relay init on a machine with no core binary cannot download the pinned core and falls back to MCP delivery. For the same reason, the Flows v2 scenario-core-init-parity step fails on this PR, because that lane installs the pinned release.

  • CI never runs a real desktop start (no app, launchd or systemd), and neither did I by hand. The exact commands are only checked against fake effects.

  • The macOS LaunchAgent can wait on a Login Items approval prompt. In that case init falls back after 30s with the warning.

  • On Linux without an enabled agent-relay.service and without a display (a headless server, or SSH into a desktop), init never starts the desktop core; it falls back to the public core with the warning. The desktop's headless launcher is deliberately not used, because of its autostart side effect.

🤖 Generated with Claude Code


Autofix with [code]smith
Open in [code]smith · Need help on this PR? Tag @codesmith-bot with what you need.


Agent Relay sessions

  • claude session a994d8be · opened via gh pr create · last active 2026-11-03