yothere is local-first: the runner, its watchdog, and the cockpit all run on your own
machine, and the network-facing surface binds loopback by default. This page installs the
always-on services from your live settings and validates them with doctor. For the
variables these services read, see Configuration; for the setup
flow that precedes them, see Onboarding.
Overview
Three always-on pieces keep a fleet moving: a runner that advances every thread, a watchdog
that alerts you if the runner goes stale, and an optional cockpit and voice server. An
optional cloudflared tunnel fronts the cockpit when you want to reach it from a phone or
Twilio. Everything roots at YOTHERE_HOME and is generated from your live settings, so two
hosts on one machine differ only by config.
Install the services
python -m yothere.service install generates the wrappers and units from your settings and
loads them: launchd user agents on macOS, systemd user units on Linux. The plan builder is
pure, so --dry-run prints every file path and command it would run, verbatim.
# print the exact artifacts, change nothing
python -m yothere.service install --cockpit --dry-run
# install the runner and watchdog (plus the cockpit/voice unit with --cockpit)
python -m yothere.service install --cockpit
Flags: --cockpit also installs the cockpit and voice unit (needs the [voice] extra),
--dry-run prints the plan and runs nothing, --env-file <path> sets the env file the
wrappers source (default $YOTHERE_HOME/relay.env), and --python <path> pins the
interpreter the services run with.
python -m yothere.service uninstall # stop and remove units and wrappers; keeps relay.env and threads/
python -m yothere.service status # liveness of each installed unit
yothere service or yothere doctor subcommand. Both run as modules: python -m yothere.service and python -m yothere.doctor.The four services
| Service | Module | Role |
|---|---|---|
| runner | yothere.runner loop |
the headless advance loop; keep-alive (launchd KeepAlive / systemd Restart=always) |
| runner-watchdog | yothere.runner_watchdog |
a one-shot heartbeat check that runs about every 60 seconds; sends ONE alert if the runner heartbeat goes stale, then stays quiet for a suppression window. It deliberately does not self-heal |
| voice | yothere.voicecall.pipeline serve |
the /overview cockpit plus voice surface; binds 127.0.0.1:8767; needs the [voice] extra; installed only with --cockpit |
| tunnel | cloudflared (host adapter) | a cloudflared tunnel fronting the voice surface at a public wss:// host for Twilio inbound; optional |
service install generates the first three units (runner, watchdog, and the voice unit
with --cockpit). The tunnel is a separate cloudflared unit you add through the host
adapter, not something service install writes. The watchdog exists because launchd
KeepAlive silently respawns a crash-looping runner forever: a stale heartbeat means
restarts are not sticking, which is exactly the failure you need surfaced rather than
papered over.
Validate
python -m yothere.doctor runs read-only checks and reports pass, warn, or fail with a
one-line fix for anything not passing. It exits non-zero on any failure, so scripts can
gate on it.
python -m yothere.doctor
python -m yothere.doctor --json # feed the cockpit setup card
python -m yothere.doctor --probe-brain # also check the brain is reachable
python -m yothere.doctor --bundle # write a redacted diagnostics tarball for an issue
The checks cover the interpreter, the package, the home layout, the brain, the runner
service and its heartbeat age, the cockpit, the notifier, MCP registration, and which
credentials are present. The one hard security gate is exposure: if the server binds a
non-loopback address with auth off and no bearer token, doctor returns FAIL with the fix
(set YOTHERE_VOICECALL_BEARER, or YOTHERE_AUTH_MODE=hosted, or bind 127.0.0.1).
The cockpit tunnel
The cockpit and voice server binds 127.0.0.1:8767, so by default it is reachable only
from the machine it runs on. Two ways to reach it from elsewhere:
- Public wss for Twilio inbound. Front the loopback server with a cloudflared tunnel to
a public host, then set
YOTHERE_VOICE_PUBLIC_WSSto thatwss://hostname. This is the tunnel service in the table above. - Tailnet. Keep the server loopback and tailnet-only, and set
YOTHERE_TRUST_TAILNET_IDENTITY=1so a browser on your tailnet connects without putting the bearer in the URL.
Any non-loopback bind requires YOTHERE_VOICECALL_BEARER (the bearer gate fails closed
when it is unset), or doctor’s exposure check will FAIL.
Logs and gotchas
- Never put logs under
~/Documents. On macOS, TCC revokes launchd access to~/Documentsafter OS updates, which makes the worker die on every start (an exit-78 crash loop) with no signal that the fleet has stopped. Use~/Library/Logsor another non-Documents path. On macOS,service installdefaults launchd logs to~/Library/Logs/relayand refuses aYOTHERE_LOG_DIRunder~/Documents, warning and using the safe default instead. - No ephemeral interpreters. A service templated against a
uvxorpipx runcache dies silently when that cache is garbage-collected. Install persistently (uv tool install yothereorpipx install yothere), then re-run install. doctor flags an ephemeral interpreter as a failure. - Linux headless. systemd user services need a login session; on a headless box run
loginctl enable-linger $USER(install warns whenXDG_RUNTIME_DIRis unset).
macOS and Linux
| Platform | Units | Location |
|---|---|---|
| macOS | launchd user agents (.plist) |
~/Library/LaunchAgents |
| Linux | systemd user units (a .service per role, plus a .timer for the watchdog) |
~/.config/systemd/user |
Windows is out of scope; use WSL2, which takes the systemd path.
The full host-adapter contract (the five pieces: a venv with the wheel, a config env file,
service wrappers, service units, and a notifier bridge) is documented in the product repo
at docs/HOST-ADAPTER.md (private repo, available to beta hosts). service install
automates the wrappers and units; the venv, the env file, and the notifier bridge are yours
to provide.