Runner Daemon
The runner daemon lets you spawn agent sessions on demand — from the web UI or programmatically via the spawn_session tool — without needing a terminal open. It’s the key piece that makes PizzaPi work like a proper remote agent platform.
How It Works
Section titled “How It Works”┌──────────────────────────────────────────────┐│ PizzaPi Web UI / spawn_session tool │└─────────────────────┬────────────────────────┘ │ HTTP request to relay ▼┌──────────────────────────────────────────────┐│ Relay Server ││ → routes spawn request to registered runner │└─────────────────────┬────────────────────────┘ │ WebSocket command ▼┌──────────────────────────────────────────────┐│ Runner Supervisor (pizzapi runner) ││ → spawns daemon child process │└─────────────────────┬────────────────────────┘ │ fork ▼┌──────────────────────────────────────────────┐│ Runner Daemon (_daemon) ││ → spawns Worker for each session request │└─────────────────────┬────────────────────────┘ │ fork ▼┌──────────────────────────────────────────────┐│ Session Worker (_worker) ││ → runs pizzapi session, streams events │└──────────────────────────────────────────────┘The supervisor manages crash recovery — if the daemon dies, the supervisor restarts it. Each session runs in an isolated worker process so a crash never affects other sessions.
Starting the Runner
Section titled “Starting the Runner”# Start in the foreground (Ctrl+C to stop)pizzapi runner
# Start in the backgroundpizzapi runner &
# Stop the runnerpizzapi runner stopRunner Identity
Section titled “Runner Identity”On first start, the runner acquires a persistent identity and process lock and saves them to ~/.pizzapi/runner.json. Treat this file as a credential — it contains the long-lived runnerSecret used to re-authenticate the runner with the relay.
// ~/.pizzapi/runner.json (auto-generated, do not edit){ "pid": 12345, "supervisorPid": 12344, "startedAt": "2025-01-15T10:30:00.000Z", "runnerId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "runnerSecret": "64-char-hex-secret"}runnerId— stable UUID that identifies this runner to the relay.runnerSecret— 32-byte hex secret used to re-authenticate. Do not commit or share this file.pid/supervisorPid/startedAt— process lock fields that detect and prevent duplicate runners.
The runner appears in the web UI under its display name (from PIZZAPI_RUNNER_NAME or the hostname) and is targeted by spawn_session calls using runnerId.
Spawning Sessions Programmatically
Section titled “Spawning Sessions Programmatically”Agents can spawn sub-sessions using the built-in spawn_session tool:
// Inside a pi agent session:const { sessionId, shareUrl } = await spawn_session({ prompt: "Fix all TypeScript errors in packages/server/src/", cwd: "/path/to/project", runnerId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890", // optional — defaults to current runner model: { provider: "anthropic", id: "claude-sonnet-4-20250514" }, // optional});The sub-session streams to the same relay and appears as a child in the web UI session tree.
Process Hierarchy
Section titled “Process Hierarchy”pizzapi runner ← supervisor (PID tracked in ~/.pizzapi/runner.json) └── pizzapi _daemon ← daemon (restarted on crash by supervisor) ├── pizzapi _worker ← session A ├── pizzapi _worker ← session B └── pizzapi _worker ← session CRunning as a System Service
Section titled “Running as a System Service”systemd (Linux)
Section titled “systemd (Linux)”The installer generates the user unit and can activate it for you:
pizzapi runner install --activateTo preview without writing:
pizzapi runner install --dry-runThe generated unit is equivalent to:
[Unit]Description=PizzaPi RunnerAfter=network-online.target
[Service]Type=simpleExecStart=/usr/local/bin/pizzapi runnerRestart=on-failureRestartSec=5WorkingDirectory=/path/to/project
[Install]WantedBy=default.targetsystemctl --user daemon-reloadsystemctl --user enable --now pizzapi-runner.servicesystemctl --user status pizzapi-runner.servicemacOS (launchd)
Section titled “macOS (launchd)”The generated LaunchAgent includes the install-time PATH and working directory:
<key>EnvironmentVariables</key><dict> <key>PATH</key><string>/your/install-time-shell-path</string></dict><key>WorkingDirectory</key><string>/path/to/project</string><key>RunAtLoad</key><true/><key>KeepAlive</key><dict> <key>SuccessfulExit</key><false/> <key>Crashed</key><true/></dict>See the dedicated macOS Setup guide for a full walkthrough, including the LaunchAgent plist and load/unload commands. The key points:
- Use a LaunchAgent (
~/Library/LaunchAgents/), not a LaunchDaemon - LaunchAgents run inside your login session with keychain access
- LaunchDaemons cannot access the keychain (
gh, SSH keys, etc. will fail)
pm2 start "pizzapi runner" --name pizzapi-runnerpm2 savepm2 startup # generate startup scriptDocker
Section titled “Docker”To run the runner as a container instead of a host process, see the dedicated Runner Container guide — it covers the Compose service, the persistent data volume, workspace mounts, UID/GID remapping, and sandbox posture.
Stopping the Runner
Section titled “Stopping the Runner”pizzapi runner stopThis sends SIGTERM to the supervisor and polls for up to 10 seconds, then force-kills with SIGKILL if it has not exited. The daemon disconnects from the relay and releases its lock immediately; workers are independent processes that may continue running or be reaped by the system.
Checking Status
Section titled “Checking Status”pizzapi runner status # human-readablepizzapi runner status --json # machine-readableExits 0 if the process is alive and currently registered with the relay, 1 otherwise (dead process, alive but disconnected, waiting on pairing approval, or rejected by the relay — reason distinguishes these). This is what the runner container image’s Docker HEALTHCHECK runs. See the CLI Reference for the JSON field reference, and Runner Container → Credentials for what auto-pairing does when no PIZZAPI_API_KEY is set.
Re-pairing On Demand
Section titled “Re-pairing On Demand”pizzapi runner pair # refuses if a credential already resolvespizzapi runner pair --force # pairs and overwrites any existing credentialRuns the same headless device-claim flow the daemon runs automatically on a credential-less boot, but on demand — useful after revoking a runner’s API key in the web UI. Refuses without --force if a credential already resolves (env var or config.json), and refuses (or, with --force, warns loudly) if PIZZAPI_RUNNER_API_KEY / PIZZAPI_API_KEY / PIZZAPI_API_TOKEN is set, since that would keep shadowing the freshly paired key at runtime. If a daemon is already running, pairing still writes the new credential but the daemon keeps using the old one in memory until you restart it — the command tells you so. See the CLI Reference for the full guard-rail and exit-code reference.
Session Linking & Triggers
Section titled “Session Linking & Triggers”When sessions are spawned via spawn_session, the runner daemon automatically links them as parent-child. This enables the conversation trigger system. See the Multi-Agent Sessions guide for the full list of trigger types, response actions, and delivery semantics.