Skip to content

Daemon Lifecycle and Autostart

Ivan Seredkin edited this page May 23, 2026 · 2 revisions

Daemon Lifecycle and Autostart

Covers what budi-daemon does on the laptop after it is installed: how it starts, how it survives going offline, how it stays in sync with the CLI, and how the platform-native service contract works on each OS.

This page is the operational sibling of JSONL Tailing as Live Ingestion (ADR-0089). That ADR pins the what of live ingestion; this page documents the how of the process that runs it.

What the daemon is

A single Rust binary (budi-daemon) that runs as a per-user service on each engineer's laptop. One process, port 7878, loopback-only. It owns:

  • A single SQLite database in WAL mode (the canonical local store)
  • A filesystem tailer that watches the directories each Provider::watch_roots() returns
  • An HTTP server (axum) that the CLI, the Cursor extension, and the Claude Code statusline call into
  • Background workers for pricing refresh and (when configured) cloud sync

Everything the daemon does, it does locally. The only outbound traffic is the LiteLLM pricing refresh and the optional cloud sync — both governed by ADRs (Model Pricing – Embedded Baseline and Runtime Refresh, Cloud Data Contract and Privacy Boundary).

Autostart

Three platforms, three native mechanisms, one CLI surface to install and remove them:

Platform Mechanism Service file
macOS launchd LaunchAgent ~/Library/LaunchAgents/dev.getbudi.budi-daemon.plist
Linux systemd user service ~/.config/systemd/user/budi-daemon.service
Windows Task Scheduler BudiDaemon task (created via schtasks)

The CLI surface is consistent across platforms:

  • budi init — creates the data dir, starts the daemon, installs autostart, wires recommended integrations idempotently
  • budi autostart status — reports service state
  • budi autostart install — installs the platform service
  • budi autostart uninstall — removes it
  • budi uninstall — full removal: service, Claude/Cursor integrations, managed legacy-proxy residue
  • budi doctor — reports service-installation status alongside its other checks

Crash recovery is the platform's job: launchd restarts on exit, systemd has Restart=on-failure, Task Scheduler retries on failure. Budi does not implement its own supervisor.

Daemon ↔ CLI version contract

The CLI and the daemon must come from the same build. The contract is enforced on the first CLI command after upgrade:

  • The first command after upgrade detects the daemon's version, and auto-restarts a stale daemon when needed.
  • If automatic restart fails, the CLI prints the platform-specific manual command (pkill -f budi-daemon on Unix, taskkill /IM budi-daemon.exe /F on Windows) and exits non-zero.
  • budi init warns when multiple budi binaries are found on PATH — version mismatch breaks daemon restarts.

The "only one copy on PATH" invariant is the most common source of failed upgrades. Do not mix Homebrew (/opt/homebrew/bin) with ~/.local/bin/.

Daemon takeover (port 7878)

When budi init or a release-binary swap starts a new daemon, it must replace any existing listener on port 7878. The takeover path is platform-specific:

  • macOS / Linux: lsof to identify the owning PID, ps to confirm it's a budi-daemon, kill to send SIGTERM. The new process binds once the port is free.
  • Windows: PowerShell Get-NetTCPConnection + taskkill for the same flow.

Unsupported or minimal environments (some hardened CI runners, locked-down dev containers) may skip the automatic takeover. In that case, stop the old daemon manually and re-run budi init. The daemon logs explicitly what it tried; check the launchd / systemd journal first.

What "offline" means

The daemon survives three flavors of offline; in all three, the live JSONL tailer keeps running and the CLI / statusline keep reading from local SQLite:

State What stops What keeps working
No network Cloud sync worker, pricing-refresh worker Tailer, CLI, statusline, every analytics route
Cloud auth failure (401) Cloud sync only Tailer, CLI, statusline, pricing refresh
Schema-version mismatch with cloud (422) Cloud sync (parked until upgrade) Everything else

Cloud sync re-attempts with exponential backoff (1 s → 2 s → … → 5 min cap) on 429 / 5xx. Pricing refresh retries on its 24 h cadence; failure does not block ingest because the embedded LiteLLM baseline keeps serving lookups.

Installation paths

budi init is the canonical install entry point. It is idempotent — re-running merges with existing state instead of clobbering it.

Three install routes:

  1. Homebrew (macOS / Linux): brew install siropkin/budi/budi && budi init. The tap lives at siropkin/homebrew-budi.
  2. Install script: ./scripts/install.sh — builds release, installs to ~/.local/bin/.
  3. Cargo-bin fallback (when AV blocks the install scripts):
    cargo install --path crates/budi-cli --bin budi --force --locked
    cargo install --path crates/budi-daemon --bin budi-daemon --force --locked
    budi --version && budi init

budi init --no-integrations skips the recommended-integrations step (Claude Code statusline, Cursor extension) for CI runs, containers, and hand-rolled setups.

Schema and DB

The daemon owns SQLite exclusively. The CLI never opens the DB directly — every CLI query goes through the daemon HTTP API. Two consequences:

  • A daemon that won't start blocks the CLI. The fix is budi doctor (which has its own DB open path for diagnostics) → budi autostart status → manual restart.
  • Schema drift is repaired via budi db check --fix. New schema versions migrate forward on daemon startup; the daemon refuses to start against a DB version it doesn't recognize and emits a structured error.

Failure modes and recovery

Symptom Cause Fix
budi --version works but budi stats returns "daemon unreachable" Daemon not running or stuck budi autostart status; budi init to reinstall the service
budi init warns about multiple binaries Mixed Homebrew + Cargo install Pick one prefix; budi uninstall from the other
First CLI after upgrade hangs Daemon version mismatch and auto-restart failed Manual kill (pkill -f budi-daemon / taskkill /IM budi-daemon.exe /F), then budi init
Anti-virus blocks scripts/install.sh Common on Windows, occasionally on macOS Cargo-bin fallback above
Port 7878 occupied by a non-budi process Some other dev tool bound to it Either reconfigure the other tool or kill it; budi has no port override yet

Reference implementations

Out of scope here

Clone this wiki locally