UNPKG

relay-companion

Version:

Install Relay for Claude Code, Cowork, and Codex, then sign in from the Relay pill.

1,186 lines • 56.2 kB
// Canonical signed-runtime update discovery and orchestration. Legacy installed
// clients use their already-shipped npm updater once to reach the migration
// bridge; every new bridge/runtime uses only the signed canonical transaction.

import os from "node:os";
import path from "node:path";
import fs from "node:fs";
import { spawn } from "node:child_process";
import { fileURLToPath } from "node:url";
import { createRequire } from "node:module";
import {
  UPDATE_CHANNEL_STABLE,
  UPDATE_CHANNEL_DEV,
  UPDATE_CHANNEL_STAGING,
  normalizeUpdateChannel,
  updateChannel,
} from "./config.js";
import { storeDir } from "./host-paths.js";
import { readAutostartDaemonRoot } from "./autostart-registration.js";
import {
  isCanonicalPackageRoot,
  readCanonicalRuntime,
  readCanonicalRuntimeState,
} from "./canonical-runtime.js";
import {
  exactRuntimeHealth,
  reconcileUpdateWorkerJobs,
  spawnCanonicalUpdate,
  waitForUpdateRequestTerminal,
} from "./canonical-updater.js";

export const PACKAGE_NAME = "relay-companion";
const DEFAULT_REGISTRY = "https://registry.npmjs.org";
const STABLE_RUNTIME_MANIFEST = "https://api.sendrelays.com/v1/companion-releases/stable/manifest.json";
const releaseTrust = createRequire(import.meta.url)("./release-trust.json");
const { RELEASE_ALGORITHM, verifyReleaseEnvelope } = createRequire(import.meta.url)("../bootstrap/release-signature.cjs");
const { systemdRunEnvironmentArgs } = createRequire(import.meta.url)("../bootstrap/linux-systemd.cjs");
// A production publish should reach an idle machine promptly. The registry request is
// a tiny dist-tags document and the daemon is the only checker, so one minute gives us
// fast fleet convergence without meaningful registry load.
const DEFAULT_CHECK_INTERVAL_MS = 60 * 1000;
const DEFAULT_CHECK_FAILURE_RETRY_MS = 15 * 1000;
const MAX_CHECK_FAILURE_RETRY_MS = 5 * 60 * 1000;
// A normal npm + Electron install takes seconds. If this old daemon is still alive two
// minutes after launch, the detached install/restart did not finish successfully; retry.
const RETRY_COOLDOWN_MS = 2 * 60 * 1000;
// Candidate npm/Electron staging is allowed to run for 15 minutes. An old daemon
// remains alive throughout immutable staging, so its ordinary two-minute failed-
// update heuristic must not launch duplicate workers while the first is healthy.
export const CANONICAL_TRANSACTION_IN_FLIGHT_MS = 20 * 60 * 1000;
// ...but do not retry at that rate forever. A machine whose update can never complete
// (field report: Scheduled Tasks registered by hand under different names) used to
// re-download and re-install every two minutes indefinitely — ~2,679 attempts for a
// single version, three identical log lines each time, growing daemon.log to 40 MB.
// Back off exponentially from RETRY_COOLDOWN_MS up to this ceiling, so a permanently
// stuck machine still checks hourly (it self-heals the moment the cause is fixed)
// without burning bandwidth, disk, and battery in a hot loop.
const MAX_RETRY_COOLDOWN_MS = 60 * 60 * 1000;
// Consecutive failed attempts at the SAME version before we stop treating this as a
// transient hiccup and start saying so where a human will actually see it.
export const UPDATE_FAILURE_ESCALATION_THRESHOLD = 3;
// Fleet-restart politeness: every update RESTARTS the pill in the user's face, and a
// publish spree (12 versions went out on 2026-08-05 alone) once restarted every user's
// pill every few minutes. The original one-hour cooldown over-corrected: a user could
// report a production bug, watch its fix publish, and remain on the broken build for
// nearly an hour (Sven, 2026-08-13). Ten minutes still collapses a release burst into
// one hop to @latest without making an urgent shipped fix look undeployed.
// Manual `relay update` bypasses it (explicit user intent).
export const DEFAULT_RESTART_COOLDOWN_MS = 10 * 60 * 1000;
const UPDATE_STATE_FILE = "update-state.json";

// ---- pure, unit-tested helpers ------------------------------------------

// Parse "x.y.z" (tolerating a leading v and any build/prerelease suffix) into
// numeric parts, or null if it doesn't look like a release version.
export function parseVersion(v) {
  const m = /^\s*v?(\d+)\.(\d+)\.(\d+)/.exec(String(v || ""));
  return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
}

// True iff `candidate` is a strictly higher release than `current`. Conservative:
// any unparseable input returns false, so a garbage registry reply never triggers
// an update. Numeric-per-component so 0.1.10 correctly beats 0.1.9.
export function isNewerVersion(candidate, current) {
  const a = parseVersion(candidate);
  const b = parseVersion(current);
  if (!a || !b) return false;
  for (let i = 0; i < 3; i += 1) {
    if (a[i] > b[i]) return true;
    if (a[i] < b[i]) return false;
  }
  return false;
}

// Map a release channel to the npm dist-tag it follows. Stable is `latest`;
// dev and staging follow their matching deliberately promoted tags.
export function distTagForChannel(channel) {
  const normalized = normalizeUpdateChannel(channel);
  if (normalized === UPDATE_CHANNEL_DEV) return "dev";
  if (normalized === UPDATE_CHANNEL_STAGING) return "staging";
  return "latest";
}

export function latestFromRegistryDoc(doc, channel = UPDATE_CHANNEL_STABLE) {
  // The targeted /-/package/<name>/dist-tags endpoint returns the tags at the top
  // level, while the full packument nests them under "dist-tags". Accept both.
  const tags = doc && typeof doc === "object" ? { ...(doc["dist-tags"] || {}), ...doc } : {};
  const tag = distTagForChannel(channel);
  // A non-stable machine falls back to `latest` when its tag does not exist yet
  // (for example before staging's first promotion): stable beats frozen.
  const version = tags[tag] || tags.latest;
  return typeof version === "string" && version ? version : null;
}

export function versionFromSignedRuntimeManifest(envelope, trustStore = releaseTrust) {
  try {
    if (typeof trustStore === "string") {
      trustStore = {
        schema: 2,
        activeKeyId: envelope?.keyId,
        keys: [{ keyId: envelope?.keyId, algorithm: RELEASE_ALGORITHM, publicKeyPem: trustStore }],
      };
    }
    const payloadBytes = verifyReleaseEnvelope(envelope, trustStore);
    const payload = JSON.parse(payloadBytes.toString("utf8"));
    return payload?.product === "Relay" && parseVersion(payload?.version) ? payload.version : null;
  } catch {
    return null;
  }
}

// Only ever self-update an npm-managed copy. A dev checkout lives at
// .../packages/companion (NOT under node_modules), so an npm install would update a
// separate tree the launch agents don't own and confuse the developer.
export function isManagedInstall(packageRoot) {
  return /[\\/]node_modules[\\/]relay-companion[\\/]?$/.test(String(packageRoot || ""));
}

// Resolve the npm prefix which OWNS this exact package tree. Global npm installs live
// at <prefix>/lib/node_modules (macOS/Linux) or <prefix>\node_modules (Windows);
// Relay's no-sudo fallback lives at <home>/.relay/lib/node_modules. Passing the
// explicit prefix to npm prevents an nvm/fnm/volta daemon from accidentally updating
// Homebrew's copy (or the reverse).
export function managedInstallInfo(
  packageRoot,
  { home = os.homedir(), platform = process.platform, exists = fs.existsSync } = {},
) {
  if (platform === "win32") {
    // Decide with path.win32 so the same logic is exact both on a real Windows
    // daemon and when unit tests exercise it from POSIX. Windows paths are
    // case-insensitive; compare accordingly.
    const win = path.win32;
    const root = win.resolve(String(packageRoot || ""));
    const nmSuffix = win.sep + win.join("node_modules", PACKAGE_NAME);
    if (!root.toLowerCase().endsWith(nmSuffix.toLowerCase())) return null;
    const prefix = root.slice(0, root.length - nmSuffix.length) || win.parse(root).root;
    const relayLocalSuffix = win.sep + win.join(".relay", "lib");
    if (prefix.toLowerCase().endsWith(relayLocalSuffix.toLowerCase())) {
      return { packageRoot: root, prefix, global: false };
    }
    // A Windows global prefix has the same <dir>\node_modules\<pkg> shape as an
    // arbitrary project checkout, which is NOT a safe rollback unit (the project
    // hoists Relay's dependencies beside it). Only adopt a prefix that carries
    // npm's global bin shims: `npm i -g relay-companion` always writes
    // <prefix>\relay.cmd, and custom prefixes that own npm itself carry npm.cmd /
    // node_modules\npm. The darwin branch refuses arbitrary node_modules for the
    // same reason.
    for (const marker of ["relay.cmd", "npm.cmd", win.join("node_modules", "npm")]) {
      try {
        if (exists(win.join(prefix, marker))) return { packageRoot: root, prefix, global: true };
      } catch {}
    }
    return null;
  }
  // Mirror of the win32 branch above: decide with path.posix so the logic is exact
  // both on a real macOS/Linux daemon and when the Windows gate exercises it.
  const posix = path.posix;
  const root = posix.resolve(String(packageRoot || ""));
  const globalSuffix = posix.join("lib", "node_modules", PACKAGE_NAME);
  const localSuffix = posix.join("node_modules", PACKAGE_NAME);
  // install.js's no-sudo fallback deliberately uses `npm install --prefix
  // ~/.relay/lib` (non-global), whose resulting path happens to LOOK like a global
  // <prefix>/lib/node_modules tree. Recognize that one explicit Relay-owned prefix
  // before applying the generic global rule.
  const relayLocalRoot = posix.resolve(home, ".relay", "lib", "node_modules", PACKAGE_NAME);
  if (root === relayLocalRoot) {
    return { packageRoot: root, prefix: posix.resolve(home, ".relay", "lib"), global: false };
  }
  if (root.endsWith(posix.sep + globalSuffix)) {
    return { packageRoot: root, prefix: root.slice(0, -(globalSuffix.length + 1)) || posix.parse(root).root, global: true };
  }
  // Do not take ownership of an arbitrary project's node_modules. Local npm
  // installs hoist Relay's dependencies beside it, so a correct rollback would
  // have to preserve that project's entire dependency tree. Only Relay's dedicated
  // no-sudo prefix above is safe for this updater to manage.
  return null;
}

export function autoUpdateEnabled(env = process.env) {
  const raw = String(env.RELAY_AUTO_UPDATE ?? "").trim().toLowerCase();
  return !(raw === "0" || raw === "false" || raw === "off" || raw === "no");
}

// ---- environment probes -------------------------------------------------

function moduleDir() {
  return path.dirname(fileURLToPath(import.meta.url));
}

export function companionPackageRoot() {
  return path.resolve(moduleDir(), "..");
}

export function currentCompanionVersion(packageRoot = companionPackageRoot()) {
  try {
    const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8"));
    return typeof pkg.version === "string" ? pkg.version : null;
  } catch {
    return null;
  }
}

// Ask the npm registry for the current lightweight dist-tags document and pick
// the version this machine's release channel follows (stable -> latest,
// dev -> dev, staging -> staging). Returns the version string, or null on any error
// (offline, timeout, unexpected shape) so the caller simply skips this cycle.
// The channel is re-read from config on every call, so `relay update-channel`
// takes effect on the daemon's next poll without a restart.
export async function fetchLatestVersion({
  fetchImpl = globalThis.fetch,
  timeoutMs = 10_000,
  registry = DEFAULT_REGISTRY,
  now = () => Date.now(),
  channel = updateChannel(),
  publicKeyPem = "",
  trustStore = releaseTrust,
  stableManifestUrl = STABLE_RUNTIME_MANIFEST,
} = {}) {
  if (typeof fetchImpl !== "function") return null;
  try {
    if (normalizeUpdateChannel(channel) === UPDATE_CHANNEL_STABLE && (publicKeyPem || trustStore?.keys?.length)) {
      const response = await fetchImpl(`${stableManifestUrl}?relay_update=${encodeURIComponent(now())}`, {
        cache: "no-store",
        headers: { Accept: "application/json", "Cache-Control": "no-cache, no-store, max-age=0", Pragma: "no-cache" },
        signal: AbortSignal.timeout(timeoutMs),
      });
      if (!response?.ok) return null;
      return versionFromSignedRuntimeManifest(await response.json(), publicKeyPem || trustStore);
    }
    const base = String(registry || DEFAULT_REGISTRY).replace(/\/+$/, "");
    // Query only the mutable dist-tags document. A unique query plus explicit
    // no-cache directives avoids a CDN/proxy/browser cache serving the pre-publish
    // tag for another full polling interval.
    const url = `${base}/-/package/${encodeURIComponent(PACKAGE_NAME)}/dist-tags?relay_update=${encodeURIComponent(now())}`;
    const res = await fetchImpl(url, {
      cache: "no-store",
      headers: {
        Accept: "application/json",
        "Cache-Control": "no-cache, no-store, max-age=0",
        Pragma: "no-cache",
      },
      signal: AbortSignal.timeout(timeoutMs),
    });
    if (!res || !res.ok) return null;
    return latestFromRegistryDoc(await res.json(), channel);
  } catch {
    return null;
  }
}

// ---- update execution ---------------------------------------------------

// Launch the install + restart in a FULLY DETACHED shell (its own session via
// `detached`), so it survives the daemon being killed when we restart it. The
// script is hardened against the ways an unattended npm install can go
// wrong on a fleet of machines:
//   - Single-writer lock (mkdir, with stale-steal) so a manual `relay update` and
//     the daemon can't run concurrent global installs into the same dir.
//   - Verify the freshly-written tree is loadable (electron binary present + the
//     bin + a valid package.json) BEFORE restarting anything, so a partial install
//     — e.g. electron's postinstall binary download failing mid-way — is never
//     launched. The pill plist hard-points at electron, so a broken tree would
//     otherwise brick the pill on its next relaunch.
//   - On install, verify, or restart failure, atomically restore a local backup of
//     the known-running tree (no registry/network dependency).
//   - Restart the pill first and the daemon LAST — kicking the daemon SIGKILLs this
//     process's parent, but this detached shell has already issued everything before
//     that and launchd relaunches the daemon with the freshly installed code.
// Prefer the npm that belongs to the SAME node runtime the daemon is running under.
// A version-manager user (nvm/fnm/volta) runs the daemon from e.g.
// ~/.nvm/versions/node/vX/bin/node; the matching npm sits beside it and installs to
// its expected CLI/runtime behavior. The install command separately pins the owning
// prefix, so even an unusual npm config cannot redirect the update to another copy.
export function readUpdateState(file) {
  try {
    const value = JSON.parse(fs.readFileSync(file, "utf8"));
    if (value && typeof value === "object" && !Array.isArray(value)) return value;
  } catch {}
  return null;
}

function writeUpdateState(file, value) {
  try {
    fs.mkdirSync(path.dirname(file), { recursive: true });
    fs.writeFileSync(file, `${JSON.stringify(value)}\n`);
  } catch {}
}

export function recordBootVersion(file, version, { now = () => Date.now() } = {}) {
  const stored = readUpdateState(file);
  if (stored && stored.version === version) {
    return Number.isFinite(stored.updatedAt) ? stored.updatedAt : 0;
  }
  // First-ever record (fresh install/older tree) must not delay a pending fix by
  // a full cooldown: only a VERSION CHANGE counts as "an update just landed".
  const updatedAt = stored ? now() : 0;
  // Booting on a different version means an update actually landed, so whatever was
  // failing is fixed. Drop the failure record rather than carrying a stale escalation.
  writeUpdateState(file, { version, updatedAt });
  return updatedAt;
}

/**
 * The durable "my updates are failing" record, kept beside the boot version so it
 * survives the daemon restarts and logons that the failure loop itself causes. This
 * is what makes a silently pinned machine diagnosable: `relay doctor` reads it, and
 * the daemon escalates from it. Cleared automatically when an update lands, because
 * `recordBootVersion` rewrites the file on any version change.
 */
export function readUpdateFailure(file) {
  const stored = readUpdateState(file);
  const failure = stored && typeof stored.failure === "object" && stored.failure ? stored.failure : null;
  if (!failure || !Number.isFinite(Number(failure.count)) || Number(failure.count) <= 0) return null;
  return {
    target: String(failure.target || ""),
    count: Number(failure.count),
    firstAt: Number(failure.firstAt) || 0,
    lastAt: Number(failure.lastAt) || 0,
  };
}

export function recordUpdateFailure(file, target, { now = () => Date.now() } = {}) {
  const stored = readUpdateState(file) || {};
  const prior = readUpdateFailure(file);
  // Count attempts at THIS version. A newly published version is a fresh problem and
  // deserves a fresh fast retry — the old target's failures say nothing about it.
  const continuing = prior && prior.target === String(target || "");
  const t = now();
  const failure = {
    target: String(target || ""),
    count: continuing ? prior.count + 1 : 1,
    firstAt: continuing ? prior.firstAt || t : t,
    lastAt: t,
  };
  writeUpdateState(file, { ...stored, failure });
  return failure;
}

// The update-path record is written BEFORE the launch (see launchPending), so a
// LANDED update leaves a failure record for its own target behind. The daemon that
// boots running that target is the success signal; it drops the record here.
export function clearUpdateFailure(file) {
  const stored = readUpdateState(file);
  if (!stored || !stored.failure) return false;
  const rest = { ...stored };
  delete rest.failure;
  writeUpdateState(file, rest);
  return true;
}

/**
 * The canonical-runtime MIGRATION needs its own durable attempt record, in its own
 * slot, because it is the one update path whose in-memory guard cannot work:
 * launching a migration quiesces and exits this daemon, launchd restarts it, and
 * `state` comes back empty — so a migration that never lands retries every few
 * seconds forever. On David's Mac that produced 35,440 failed installs and 11,015
 * leaked lock directories in a single day while the machine stayed pinned at
 * 0.1.265. Separate from `failure` above so an upgrade backoff and a migration
 * backoff can never overwrite each other's counts.
 *
 * RECOVERY needs a slot of its own for the same reason it needs a record at all.
 * Both paths quiesce this daemon and both write here, but they carry DIFFERENT
 * targets — `canonical-migration:<v>` and `canonical-recovery:<v>` — and a record
 * whose target changed is a fresh attempt, count 1. Sharing one slot therefore let
 * the two paths reset each other every time they alternated: the count never left
 * the two-minute base, the exponential ceiling was never reached, and a machine
 * whose activation could not succeed retried for 17.5 hours (104 activations, 100
 * of them the same launchd EIO, 22 GB of leaked release trees) while the stored
 * count still read 3. Keyed slots keep each path's backoff its own.
 */
const MIGRATION_FAILURE_SLOT = "migrationFailure";
const RECOVERY_FAILURE_SLOT = "recoveryFailure";
const AUTOSTART_REPOINT_FAILURE_SLOT = "autostartRepointFailure";
const ATTEMPT_FAILURE_SLOTS = [MIGRATION_FAILURE_SLOT, RECOVERY_FAILURE_SLOT, AUTOSTART_REPOINT_FAILURE_SLOT];

export function readMigrationFailure(file, { slot = MIGRATION_FAILURE_SLOT } = {}) {
  const stored = readUpdateState(file);
  const failure = stored && typeof stored[slot] === "object" && stored[slot]
    ? stored[slot]
    : null;
  if (!failure || !Number.isFinite(Number(failure.count)) || Number(failure.count) <= 0) return null;
  return {
    target: String(failure.target || ""),
    count: Number(failure.count),
    firstAt: Number(failure.firstAt) || 0,
    lastAt: Number(failure.lastAt) || 0,
  };
}

export function recordMigrationFailure(file, target, { now = () => Date.now(), slot = MIGRATION_FAILURE_SLOT } = {}) {
  const stored = readUpdateState(file) || {};
  const prior = readMigrationFailure(file, { slot });
  const continuing = prior && prior.target === String(target || "");
  const t = now();
  const record = {
    target: String(target || ""),
    count: continuing ? prior.count + 1 : 1,
    firstAt: continuing ? prior.firstAt || t : t,
    lastAt: t,
  };
  writeUpdateState(file, { ...stored, [slot]: record });
  return record;
}

// Doctor needs the recovery slot without owning the slot names.
export function readRecoveryFailure(file) {
  return readMigrationFailure(file, { slot: RECOVERY_FAILURE_SLOT });
}

// Migration succeeded the moment this process boots from the canonical runtime, so
// the record is dropped there rather than by any explicit success signal. That boot
// retires the recovery record too: recovery exists only to get the runtime onto the
// canonical tree, which is exactly what has just been observed to have happened.
export function clearMigrationFailure(file) {
  const stored = readUpdateState(file);
  if (!stored) return false;
  const present = ATTEMPT_FAILURE_SLOTS.filter((slot) => stored[slot]);
  if (!present.length) return false;
  const rest = { ...stored };
  for (const slot of present) delete rest[slot];
  writeUpdateState(file, rest);
  return true;
}

// Exponential backoff over consecutive failed attempts at the same version, from the
// two-minute base up to the hourly ceiling. `failures` of 0 or 1 keeps the original
// prompt retry, so a genuinely transient failure still recovers within minutes.
export function updateRetryCooldownMs(failures, { baseMs = RETRY_COOLDOWN_MS, maxMs = MAX_RETRY_COOLDOWN_MS } = {}) {
  const n = Math.max(0, Math.floor(Number(failures) || 0));
  if (n <= 1) return baseMs;
  return Math.min(maxMs, baseMs * 2 ** (n - 1));
}

// Build a rate-limited auto-update checker. Call `tick()` freely (e.g. once per
// daemon poll); it no-ops cheaply until a check is due, then fetches the latest
// version and, if newer, launches the detached update-and-restart. Every effectful
// dependency is injectable so the decision logic is unit-testable without network,
// npm, or launchd. `tick()` never throws.
/**
 * Re-point the machine's autostart at the canonical tree, from the canonical bin.
 *
 * `repair-runtime` is trampoline-exempt by design, so it registers whichever tree
 * invokes it; running it from anywhere but canonical would only move the problem.
 * On macOS it goes through `launchctl submit` rather than a plain child, because
 * the repair unloads the daemon's own launchd job and a process descended from
 * that job loses its Mach bootstrap session when the ancestor is booted out
 * (field-observed on the 0.1.68 rollout). A submitted job is parented to the gui
 * domain and survives.
 */
export function submitCanonicalRepoint({
  canonical,
  platform = process.platform,
  env = process.env,
  log = () => {},
  spawnImpl = spawn,
  label = null,
}) {
  if (!canonical?.bin) return false;
  const node = canonical.node && fs.existsSync(canonical.node) ? canonical.node : process.execPath;
  try {
    if (platform === "darwin") {
      const jobLabel = label || `work.relay.autostart.repoint.${process.pid}`;
      const child = spawnImpl(
        "/bin/launchctl",
        ["submit", "-l", jobLabel, "--", node, canonical.bin, "repair-runtime"],
        { detached: true, stdio: "ignore" },
      );
      child?.unref?.();
      return true;
    }
    if (platform === "linux") {
      const jobLabel = label || `work.relay.autostart.repoint.${process.pid}`;
      const child = spawnImpl(
        "systemd-run",
        [
          "--user",
          "--quiet",
          "--collect",
          `--unit=${jobLabel}`,
          ...systemdRunEnvironmentArgs(env),
          node,
          canonical.bin,
          "repair-runtime",
        ],
        { detached: true, stdio: "ignore" },
      );
      child?.unref?.();
      return true;
    }
    const child = spawnImpl(node, [canonical.bin, "repair-runtime"], { detached: true, stdio: "ignore" });
    child?.unref?.();
    return true;
  } catch (err) {
    log(`autostart repoint launch failed: ${err && err.message ? err.message : String(err)}`);
    return false;
  }
}

export function createAutoUpdater({
  now = () => Date.now(),
  env = process.env,
  packageRoot = companionPackageRoot(),
  getCurrentVersion = () => currentCompanionVersion(packageRoot),
  getOnDiskVersion = () => getCurrentVersion(),
  getCurrentChannel = () => updateChannel(),
  getLatestVersion = (opts) => fetchLatestVersion(opts),
  spawnUpdate = (opts) => spawnCanonicalUpdate({
        version: opts.targetVersion,
        runningVersion: opts.currentVersion,
        runningPackageRoot: opts.packageRoot,
        node: process.execPath,
        homeDir: os.homedir(),
        platform: opts.platform,
        env,
      }),
  getCanonicalRuntime = () => readCanonicalRuntime({ platform }),
  readAutostart = () => readAutostartDaemonRoot({ platform }),
  repointAutostart = (opts) => submitCanonicalRepoint(opts),
  getCanonicalRuntimeState = () => readCanonicalRuntimeState({ platform }),
  getCanonicalRuntimeHealth = (target) => exactRuntimeHealth(target, { platform }),
  reconcileCanonicalWorkers = () => platform === "darwin" ? reconcileUpdateWorkerJobs() : { removedLegacy: 0, fixed: null },
  checkIntervalMs = Number(env.RELAY_UPDATE_CHECK_INTERVAL_MS) || DEFAULT_CHECK_INTERVAL_MS,
  checkFailureRetryMs = DEFAULT_CHECK_FAILURE_RETRY_MS,
  maxCheckFailureRetryMs = MAX_CHECK_FAILURE_RETRY_MS,
  retryCooldownMs = RETRY_COOLDOWN_MS,
  platform = process.platform,
  // Idle probe: while any host turn is in flight, defer the restart-bearing update
  // (kickstart -k would kill the running turn). Injected from the daemon; defaults to
  // "always idle" so the pure decision logic stays testable without runtime.
  hasActiveWork = () => false,
  // Fleet-restart politeness (see DEFAULT_RESTART_COOLDOWN_MS). 0 disables the
  // cooldown — used by manual `relay update`, where the user asked right now.
  restartCooldownMs = Number.isFinite(Number(env.RELAY_UPDATE_RESTART_COOLDOWN_MS))
    ? Number(env.RELAY_UPDATE_RESTART_COOLDOWN_MS)
    : DEFAULT_RESTART_COOLDOWN_MS,
  updateStatePath = path.join(storeDir(), UPDATE_STATE_FILE),
  // Honour the persisted failure backoff. false = attempt right now regardless of how
  // many times this version has failed — used by manual `relay update`, where the user
  // is present, asked explicitly, and is very likely retrying because they JUST fixed
  // whatever was broken.
  useFailureBackoff = true,
  log = () => {},
  explicitRepair = false,
} = {}) {
  // This is the version of the code loaded in THIS process, not a fresh read of the
  // package.json on every tick. If npm writes a new tree but launchctl restart fails,
  // the old daemon must keep retrying rather than see the new on-disk version and
  // falsely conclude that it is current.
  let runningVersion = null;
  try {
    runningVersion = getCurrentVersion();
  } catch {}
  let bootChannel = UPDATE_CHANNEL_STABLE;
  try {
    bootChannel = normalizeUpdateChannel(getCurrentChannel());
  } catch {}
  // When this daemon boots on a version different from the recorded one, an
  // update (self- or manual) just landed and already restarted the pill once.
  let lastUpdateLandedAt = 0;
  if (restartCooldownMs > 0 && runningVersion) {
    try {
      lastUpdateLandedAt = recordBootVersion(updateStatePath, runningVersion, { now });
    } catch {}
  }
  // Carried across daemon restarts: the failure loop restarts this process (logon,
  // KeepAlive), so an in-memory counter would reset to zero on every lap and the
  // backoff would never actually engage.
  const priorFailure = (() => {
    if (!useFailureBackoff) return null;
    try {
      const failure = readUpdateFailure(updateStatePath);
      // The record is written BEFORE each launch (see launchPending), because a
      // canonical update that fails at ACTIVATION rolls back by restarting this
      // daemon — the process that could count the failure afterwards is dead by
      // the time there is a failure to count. The flip side: an update that
      // LANDED leaves its own pre-launch record behind, and booting as the
      // recorded target is the success signal that retires it.
      if (failure && runningVersion && failure.target === runningVersion) {
        try { clearUpdateFailure(updateStatePath); } catch {}
        return null;
      }
      return failure;
    } catch {
      return null;
    }
  })();
  const state = {
    lastCheckAt: 0,
    nextCheckAt: 0,
    consecutiveCheckFailures: 0,
    checking: false,
    pendingVersion: priorFailure?.target || null,
    pendingChannel: priorFailure ? bootChannel : null,
    updateStartedAt: 0,
    updating: false,
    cooldownAnnouncedFor: null,
    updateFailures: priorFailure?.count || 0,
    failingTarget: priorFailure?.target || null,
    // Wall-clock gate for the next attempt, so the backoff survives the daemon
    // restart that a failed update tends to cause.
    nextAttemptAt: priorFailure
      ? priorFailure.lastAt + updateRetryCooldownMs(priorFailure.count, { baseMs: retryCooldownMs })
      : 0,
    escalatedFor: null,
    workersReconciled: false,
  };

  function admittedLaunch(result) {
    if (!result) return false;
    return typeof result.status !== "string" || result.status === "admitted";
  }

  function rejectedLaunchStatus(result, fallback = "launch-failed") {
    if (!result) return "worker-not-admitted";
    if (result?.status === "failed") return "worker-failed";
    if (result?.status === "busy") return "worker-busy";
    if (result?.status === "not-admitted") return "worker-not-admitted";
    return fallback;
  }

  function liveChannel() {
    try {
      return normalizeUpdateChannel(getCurrentChannel());
    } catch {
      return UPDATE_CHANNEL_STABLE;
    }
  }

  function onDiskVersion() {
    try {
      return getOnDiskVersion();
    } catch {
      return null;
    }
  }

  function busy() {
    try {
      return Boolean(hasActiveWork());
    } catch {
      return false;
    }
  }

  // Record one failed end-to-end attempt and decide how long to wait before the next.
  // "Failed" here means: we launched an updater and this daemon is still running the
  // old version well after it should have been replaced.
  function noteUpdateFailure(target, t) {
    if (state.failingTarget !== target) {
      state.failingTarget = target;
      state.updateFailures = 0;
      state.escalatedFor = null;
    }
    state.updateFailures += 1;
    let persisted = null;
    try {
      persisted = recordUpdateFailure(updateStatePath, target, { now: () => t });
    } catch {}
    if (persisted) state.updateFailures = persisted.count;
    // Compound from the CONFIGURED base, not the module default, so an injected or
    // env-tuned retry cadence still governs the backoff built on top of it.
    const wait = updateRetryCooldownMs(state.updateFailures, { baseMs: retryCooldownMs });
    // Measure the window from when the attempt STARTED, not from now. The in-flight
    // retry cooldown has already been served by the time we get here, so the first
    // failure retries immediately (unchanged behaviour) and only the second and later
    // failures actually hold the update back.
    state.nextAttemptAt = (state.updateStartedAt || t) + wait;
    const waitLabel = wait >= 60_000 ? `${Math.round(wait / 60_000)}m` : `${Math.round(wait / 1000)}s`;
    if (state.updateFailures >= UPDATE_FAILURE_ESCALATION_THRESHOLD && state.escalatedFor !== target) {
      // Escalate ONCE per stuck version, loudly and actionably. Before this, a stuck
      // machine emitted the same three lines forever and surfaced nothing anywhere a
      // human looks — the only way to notice was diffing package.json against npm.
      state.escalatedFor = target;
      log(
        `auto-update STUCK: ${state.updateFailures} consecutive failed attempts to install ${target} ` +
          `(still running ${runningVersion}). Updates are not reaching this machine. ` +
          `See ${path.join(os.homedir(), ".relay", "update.log")} for the cause, ` +
          `and run \`relay doctor\` for a summary or \`relay repair-installation\` if autostart is broken.`,
      );
    } else {
      log(`auto-update: attempt ${state.updateFailures} to install ${target} did not land; retrying in ~${waitLabel}`);
    }
  }

  function launchPending(t) {
    const latest = state.pendingVersion;
    if (!latest || !isNewerVersion(latest, runningVersion)) return null;
    const channel = liveChannel();
    // Pending versions are channel-scoped. A daemon may discover @dev or @staging,
    // remain alive through an updater failure, and later see the user switch.
    // Never deliver a pending build after the configured channel changed.
    if (!state.pendingChannel || state.pendingChannel !== channel) {
      state.pendingVersion = null;
      state.pendingChannel = null;
      state.nextCheckAt = 0;
      return { status: "stale-channel", current: runningVersion, channel };
    }
    // This process identity is the code it booted, not whatever package.json says
    // now. A different on-disk version means another updater/manual install won the
    // race; this daemon is retired and must never mutate that tree.
    const diskVersion = onDiskVersion();
    if (diskVersion && runningVersion && diskVersion !== runningVersion) {
      return { status: "stale-process", current: runningVersion, onDisk: diskVersion, channel };
    }
    // Exponential backoff between failed attempts at the same version.
    if (useFailureBackoff && state.nextAttemptAt > 0 && t < state.nextAttemptAt && state.failingTarget === latest) {
      return {
        status: "deferred-backoff",
        current: runningVersion,
        latest,
        failures: state.updateFailures,
        retryAt: state.nextAttemptAt,
      };
    }
    // Never kill an active host turn. Unlike the old hard deadline, this waits as
    // long as necessary, but because pending updates are checked on EVERY daemon
    // loop it launches within seconds of the final turn becoming idle.
    if (busy()) return { status: "deferred-busy", current: runningVersion, latest };
    // Restart politeness: one update restart per cooldown window. The pending
    // version keeps tracking @latest, so waiting never strands users on an
    // intermediate build — it only spaces out the restarts.
    if (restartCooldownMs > 0 && lastUpdateLandedAt > 0) {
      const sinceLanded = t - lastUpdateLandedAt;
      if (sinceLanded >= 0 && sinceLanded < restartCooldownMs) {
        if (state.cooldownAnnouncedFor !== latest) {
          state.cooldownAnnouncedFor = latest;
          const waitMin = Math.ceil((restartCooldownMs - sinceLanded) / 60000);
          log(`auto-update: ${latest} is ready; deferring the restart ~${waitMin}m (an update landed recently)`);
        }
        return { status: "deferred-cooldown", current: runningVersion, latest };
      }
    }
    log(`auto-update: ${runningVersion} -> ${latest}; installing exact version and restarting`);
    state.updating = true;
    state.updateStartedAt = t;
    let launch = null;
    try {
      launch = spawnUpdate({
        log,
        currentVersion: runningVersion,
        currentChannel: channel,
        targetVersion: latest,
        packageRoot,
        platform,
      });
    } catch (err) {
      state.updating = false;
      log(`auto-update launch failed: ${err && err.message ? err.message : String(err)}`);
      return { status: "launch-failed", current: runningVersion, latest };
    }
    if (!admittedLaunch(launch)) {
      state.updating = false;
      const status = rejectedLaunchStatus(launch);
      log(`auto-update worker was not admitted (${status})`);
      return { status, current: runningVersion, latest, channel };
    }
    // Persist only after the worker has acquired the canonical transaction lock.
    // A successful `launchctl submit` is not admission and must never be counted as
    // a running update or a failed end-to-end attempt.
    try { recordUpdateFailure(updateStatePath, latest, { now: () => t }); } catch {}
    return { status: "updating", current: runningVersion, latest, channel, launch };
  }

  async function tick() {
    if (!autoUpdateEnabled(env)) return { status: "disabled" };
    // Every admitted platform has an independent supervisor-owned worker and a
    // verified restart path: launchd, Scheduled Tasks, or a Linux systemd user
    // unit. Anything else remains fail-closed.
    if (!["darwin", "win32", "linux"].includes(platform)) return { status: "unsupported-platform" };
    const t = now();
    let canonical = null;
    let canonicalState = null;
    if (!state.workersReconciled && platform === "darwin") {
      try { reconcileCanonicalWorkers(); } catch {}
      state.workersReconciled = true;
    }
    try {
      canonical = getCanonicalRuntime();
      canonicalState = getCanonicalRuntimeState();
    } catch {}
    const api = platform === "win32" ? path.win32 : path.posix;
    const sameRoot = (left, right) => {
      const a = api.resolve(String(left || ""));
      const b = api.resolve(String(right || ""));
      return platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
    };
    const runningFromCanonical = Boolean(canonical && sameRoot(canonical.packageRoot, packageRoot));
    // Booting from the canonical runtime IS the migration's success signal; nothing
    // else reports it, and the version never changes across a migration, so the
    // durable attempt record has to be dropped here or it would back off forever.
    if (runningFromCanonical) {
      try { clearMigrationFailure(updateStatePath); } catch {}
    }
    if (canonical && !runningFromCanonical) {
      const newerGlobal = isNewerVersion(runningVersion, canonical.version);
      let equalExplicitRepair = false;
      if (explicitRepair && runningVersion === canonical.version) {
        try { equalExplicitRepair = getCanonicalRuntimeHealth(canonical)?.ok === false; } catch {}
      }
      if ((newerGlobal || equalExplicitRepair) && managedInstallInfo(packageRoot, { platform })) {
        if (busy()) return { status: "deferred-rescue-busy", current: runningVersion, onDisk: canonical.version };
        let launch = null;
        try {
          launch = spawnUpdate({
            log,
            currentVersion: runningVersion,
            currentChannel: liveChannel(),
            targetVersion: runningVersion,
            packageRoot,
            platform,
            canonicalRescue: true,
          });
        } catch (err) {
          log(`canonical runtime rescue launch failed: ${err && err.message ? err.message : String(err)}`);
          return { status: "rescue-launch-failed", current: runningVersion, onDisk: canonical.version };
        }
        if (!admittedLaunch(launch)) {
          return { status: rejectedLaunchStatus(launch, "rescue-launch-failed"), current: runningVersion, onDisk: canonical.version };
        }
        state.updating = true;
        state.updateStartedAt = t;
        return { status: "rescuing-runtime", current: runningVersion, onDisk: canonical.version, launch };
      }
      // `stale-process` describes a LEFTOVER: some other process is the machine's
      // real daemon and this one should simply die. That is not this situation when
      // the autostart registration names THIS tree — then this process IS the
      // machine's registered daemon, merely pointed at a stale release, and no
      // other actor exists to start canonical. Returning the no-op status there
      // leaves the machine with a daemon that can never reach canonical and, paired
      // with the exit-on-stale path, loops forever without delivering anything.
      let registration = null;
      try { registration = readAutostart(); } catch { registration = null; }
      if (registration?.root && sameRoot(registration.root, packageRoot)) {
        const repointTarget = `autostart-repoint:${canonical.version}`;
        const priorRepoint = readMigrationFailure(updateStatePath, { slot: AUTOSTART_REPOINT_FAILURE_SLOT });
        if (priorRepoint && priorRepoint.target === repointTarget) {
          const wait = updateRetryCooldownMs(priorRepoint.count, { baseMs: retryCooldownMs });
          if (t - priorRepoint.lastAt < wait) {
            return {
              status: "deferred-autostart-repoint",
              current: runningVersion,
              onDisk: canonical.version,
              failures: priorRepoint.count,
            };
          }
        }
        if (busy()) return { status: "deferred-repoint-busy", current: runningVersion, onDisk: canonical.version };
        let launched = false;
        try {
          launched = repointAutostart({ canonical, platform, log });
        } catch (err) {
          log(`autostart repoint failed: ${err && err.message ? err.message : String(err)}`);
          launched = false;
        }
        // Record before reporting success: the repair replaces this very process,
        // so there is no later moment in which to write the attempt down. A boot
        // from canonical clears the slot; anything else backs off.
        try { recordMigrationFailure(updateStatePath, repointTarget, { now: () => t, slot: AUTOSTART_REPOINT_FAILURE_SLOT }); } catch {}
        if (launched) {
          log(
            `autostart still names this stale tree (${registration.source}); ` +
              `repointing it at ${canonical.version} from the canonical bin`,
          );
          state.updating = true;
          state.updateStartedAt = t;
          return { status: "repointing-autostart", current: runningVersion, onDisk: canonical.version };
        }
        return { status: "autostart-repoint-failed", current: runningVersion, onDisk: canonical.version };
      }
      return { status: "stale-process", current: runningVersion, onDisk: canonical.version };
    }

    if (runningFromCanonical && explicitRepair) {
      let health = null;
      try { health = getCanonicalRuntimeHealth(canonical); } catch {}
      if (health && !health.ok) {
        if (busy()) return { status: "deferred-repair-busy", current: runningVersion, health };
        let launch = null;
        try {
          launch = spawnUpdate({
            log,
            currentVersion: runningVersion,
            currentChannel: liveChannel(),
            targetVersion: runningVersion,
            packageRoot,
            platform,
            canonicalRepair: true,
          });
        } catch (err) {
          log(`canonical runtime repair launch failed: ${err && err.message ? err.message : String(err)}`);
          return { status: "repair-launch-failed", current: runningVersion, health };
        }
        if (!admittedLaunch(launch)) {
          return { status: rejectedLaunchStatus(launch, "repair-launch-failed"), current: runningVersion, health };
        }
        state.updating = true;
        state.updateStartedAt = t;
        return { status: "repairing-runtime", current: runningVersion, health, launch };
      }
    }
    if (!canonical && ["activating", "recovery-required"].includes(canonicalState?.state)) {
      const preparedAt = Number(canonicalState?.preparedAt) || 0;
      if (canonicalState?.state === "activating" && preparedAt > 0 && t - preparedAt >= 0 && t - preparedAt < retryCooldownMs) {
        return {
          status: "activation-in-flight",
          current: runningVersion,
          recovery: canonicalState,
          recoverAfter: preparedAt + retryCooldownMs,
        };
      }
      if (busy()) return { status: "deferred-recovery-busy", current: runningVersion, recovery: canonicalState };
      // Same in-flight window as migration. This was retryCooldownMs (2 minutes),
      // which an npm install of a ~650MB Electron tree routinely exceeds — so a
      // second recovery worker was spawned on top of a live one, each building its
      // own release dir against the same lock.
      if (state.updating && t - state.updateStartedAt < CANONICAL_TRANSACTION_IN_FLIGHT_MS) {
        return { status: "recovery-in-flight", current: runningVersion, recovery: canonicalState };
      }
      // Recovery quiesces and exits this daemon exactly as migration does, so its
      // in-memory guard is just as empty on the way back. A transaction that dies
      // LATE — activation, not install — leaves a journal that sends every restart
      // straight back here, which is how a fixed install turned into a hot recovery
      // loop instead of a hot migration loop. Same durable record, same backoff.
      const recoveryTarget = `canonical-recovery:${canonicalState.candidate?.version || runningVersion}`;
      const priorRecovery = readMigrationFailure(updateStatePath, { slot: RECOVERY_FAILURE_SLOT });
      if (priorRecovery && priorRecovery.target === recoveryTarget) {
        const wait = updateRetryCooldownMs(priorRecovery.count, { baseMs: retryCooldownMs });
        if (t - priorRecovery.lastAt < wait) {
          return { status: "deferred-recovery-backoff", current: runningVersion, recovery: canonicalState, failures: priorRecovery.count };
        }
      }
      let launch = null;
      try {
        launch = spawnUpdate({
          log,
          currentVersion: runningVersion,
          currentChannel: liveChannel(),
          targetVersion: canonicalState.candidate?.version || runningVersion,
          packageRoot,
          platform,
          canonicalRecovery: true,
        });
      } catch (err) {
        log(`canonical runtime recovery launch failed: ${err && err.message ? err.message : String(err)}`);
        return { status: "recovery-launch-failed", current: runningVersion, recovery: canonicalState };
      }
      if (!admittedLaunch(launch)) {
        return { status: rejectedLaunchStatus(launch, "recovery-launch-failed"), current: runningVersion, recovery: canonicalState };
      }
      try { recordMigrationFailure(updateStatePath, recoveryTarget, { now: () => t, slot: RECOVERY_FAILURE_SLOT }); } catch {}
      state.updating = true;
      state.updateStartedAt = t;
      return { status: "recovering-runtime", current: runningVersion, recovery: canonicalState, launch };
    }
    // First move a healthy npm/global/Hermes-owned install into Relay's immutable
    // runtime at the exact version currently executing. No update is attempted in
    // this transaction, and the external tree remains the explicit rollback target.
    if (!runningFromCanonical && !isCanonicalPackageRoot(packageRoot, { platform })) {
      if (!managedInstallInfo(packageRoot, { platform })) return { status: "unmanaged" };
      if (busy()) return { status: "deferred-migration-busy", current: runningVersion };
      if (state.updating && t - state.updateStartedAt < CANONICAL_TRANSACTION_IN_FLIGHT_MS) {
        return { status: "migration-in-flight", current: runningVersion };
      }
      // The in-memory guard above cannot hold this path: launching quiesces and exits
      // this daemon, launchd restarts it, and `state` comes back empty. Only a record
      // on disk survives the restart the attempt itself causes — see
      // readMigrationFailure. Written BEFORE the launch, and read back only by a
      // daemon that returned still-not-canonical, i.e. only after a failure.
      const migrationTarget = `canonical-migration:${runningVersion}`;
      const priorMigration = readMigrationFailure(updateStatePath);
      if (priorMigration && priorMigration.target === migrationTarget) {
        const wait = updateRetryCooldownMs(priorMigration.count, { baseMs: retryCooldownMs });
        if (t - priorMigration.lastAt < wait) {
          return { status: "deferred-migration-backoff", current: runningVersion, failures: priorMigration.count };
        }
      }
      state.updating = true;
      state.updateStartedAt = t;
      state.pendingVersion = runningVersion;
      state.pendingChannel = liveChannel();
      let launch = null;
      try {
        launch = spawnUpdate({
          log,
          currentVersion: runningVersion,
          currentChannel: state.pendingChannel,
          targetVersion: runningVersion,
          packageRoot,
          platform,
          canonicalMigration: true,
        });
      } catch (err) {
        state.updating = false;
        log(`canonical runtime migration launch failed: ${err && err.message ? err.message : String(err)}`);
        return { status: "migration-launch-failed", current: runningVersion };
      }
      if (!admittedLaunch(launch)) {
        state.updating = false;
        return { status: rejectedLaunchStatus(launch, "migration-launch-failed"), current: runningVersion };
      }
      try { recordMigrationFailure(updateStatePath, migrationTarget, { now: () => t }); } catch {}
      return { status: "migrating-runtime", current: runningVersion, launch };
    }
    // An update was launched but we're still alive (install failed, or restart is
    // pending) — hold off briefly, then retry the exact pending version. A successful
    // daemon restart creates a fresh updater, so reaching the cooldown is itself
    // evidence that this attempt did not complete end-to-end.
    if (state.updating && t - state.updateStartedAt < retryCooldownMs) return { status: "in-flight" };
    if (state.updating) {
      state.updating = false;
      // This daemon outliving the attempt IS the failure signal: a successful update
      // replaces this process. Count it, back off, and escalate once it is chronic —
      // rather than re-attempting at a flat two-minute cadence forever.
      noteUpdateFailure(state.pendingVersion || "update", t);
    }

    // A discovered update is retained across network failures and busy turns. If the
    // host is now idle, install immediately without waiting for another registry poll.
    // Deferred states fall through to the (interval-gated) registry check so the
    // pending version keeps tracking @latest while we wait.
    const pending = launchPending(t);
    if (
      pending &&
      pending.status !== "deferred-busy" &&
      pending.status !== "deferred-cooldown" &&
      pending.status !== "deferred-backoff"
    ) {
      return pending;
    }

    if (state.checking) return { status: "check-in-flight", current: runningVersion };
    if (t < state.nextCheckAt) return pending || { status: "not-due", current: runningVersion };
    state.lastCheckAt = t;
    state.checking = true;

    let latest = null;
    const checkedChannel = liveChannel();
    try {
      latest = await getLatestVersion({ channel: checkedChannel });
    } catch (err) {
      log(`auto-update registry check failed: ${err && err.message ? err.message : String(err)}`);
    } finally {
      state.checking = false;
    }
    if (!latest) {
      state.consecutiveCheckFailures += 1;
      const backoff = Math.min(
        maxCheckFailureRetryMs,
        checkFailureRetryMs * 2 ** Math.max(0, state.consecutiveCheckFailures - 1),
      );
      state.nextCheckAt = t + backoff;
      // Keep an already-discovered update pending through a transient check failure.
      if (pending) return { ...pending, checkFailed: true, retryAt: state.nextCheckAt };
      return { status: "check-failed", current: runningVersion, retryAt: state.nextCheckAt };
    }
    state.consecutiveCheckFailures = 0;
    state.nextCheckAt = t + Math.max(0, checkIntervalMs);

    if (!isNewerVersion(latest, runningVersion)) {
      state.pendingVersion = null;
      state.pendingChannel = null;
      return { status: "up-to-date", current: runningVersion, latest };
    }
    state.pendingVersion = latest;
    state.pendingChannel = checkedChannel;
    return launchPending(t);
  }

  return { tick, state, runningVersion, bootChannel };
}

// One-shot for the `relay update` CLI: check once and, if a newer version exists,
// launch the update. Prints a human-readable line. Bypasses the interval gate.
export async function runUpdateOnce({
  log = (m) => console.log(`[relay] ${m}`),
  waitForTerminal = false,
} = {}) {
  // checkIntervalMs 0: bypass the interval gate. restartCooldownMs 0: an explicit
  // `relay update` is user intent — never make the human wait out fleet politeness.
  // useFailureBackoff false: likewise never make them wait out a backoff they are
  // very likely retrying precisely because they just repaired the cause.
  const updater = createAutoUpdater({
    env: { ...process.env, RELAY_ALLOW_SANDBOX_AUTHORIZATION: "1" },
    checkIntervalMs: 0,
    restartCooldownMs: 0,
    useFailureBackoff: false,
    explicitRepair: true,
    log,
  });
  const result = await updater.tick();
  switch (result.status) {
    case "updating":
      log(`updating ${result.current} -> ${result.latest}; the pill and daemon will restart shortly.`);
      break;
    case "up-to-date":
      log(`already on the latest version (${result.current}).`);
      break;
    case "unmanaged":
      log("not an npm-managed install (dev checkout?) — skipping self-update.");
      break;
    case "disabled":
      log("auto-update is disabled (RELAY_AUTO_UPDATE).");
      break;
    case "check-failed":
      log("could not reach the npm registry to check for updates.");
      break;
    case "unsupported-platform":
      log("self-update is supported on 64-bit macOS, Windows, and Linux — update this unsupported platform with `npm i -g relay-companion@latest`.");
      break;
    case "deferred-busy":
      log(`an update to ${result.latest} is ready but deferred while an agent turn is active; it will install once idle.`);
      break;
    case "deferred-cooldown":
      log(`an update to ${result.latest} is ready but a recent update just landed; it will install after the cooldown.`);
      break;
    case "deferred-backoff":
      log(
        `an update to ${result.latest} is ready but the last ${result.failures} attempt(s) failed; ` +
          "backing off. Check ~/.relay/update.log for the cause.",
      );
      break;
    case "migrating-runtime":
      log(`moving the current ${result.current} install into Relay's safe runtime; services will restart shortly.`);
      break;
    case "rescuing-runtime":
      log(`moving the newer ${result.current} install into Relay's safe runtime; services will restart shortly.`);
      break;
    case "repairing-runtime":
      log(`repairing Relay ${result.current}; services will restart shortly.`);
      break;
    case "worker-busy":
      log("an update worker already owns the runtime transaction.");
      break;
    case "worker-not-admitted":
      log("the update worker did not acquire the runtime transaction; no update was started.");
      break;
    case "deferred-recovery-backoff":
      log(
        `recovering the interrupted runtime activation has failed ${result.failures} time(s); backing off. ` +
          `See ${path.join(os.homedir(), ".relay", "update.log")} for the cause.`,
      );
      break;
    case "deferred-migration-backoff":
      log(
        `moving ${result.current} into Relay's safe runtime has failed ${result.failures} time(s); backing off. ` +
          `See ${path.join(os.homedir(), ".relay", "update.log")} for the cause.`,
      );
      break;
    case "recovering-runtime":
      log("recovering an interrupted runtime activation before checking for updates.");
      break;
    case "activation-in-flight":
    case "migration-in-flight":
    case "recovery-in-flight":
      log("a Relay runtime transaction is already in progress.");
      break;
    default:
      log(`update check: ${result.status}`);
  }
  if (waitForTerminal && result.launch?.requestPath) {
    const terminal = await waitForUpdateRequestTerminal(result.launch.requestPath);
    return { ...result, terminal };
  }
  return result;
}