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 generates a stable ID and saves it to ~/.pizzapi/runner.json. This ID is used to register the runner with the relay server.
// ~/.pizzapi/runner.json (auto-generated, do not edit){ "id": "runner_a1b2c3d4e5f6", "name": "my-macbook"}The runner appears in the web UI under its ID and can be targeted by spawn_session calls.
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: "runner_a1b2c3d4e5f6", // optional — defaults to current runner});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)”[Unit]Description=PizzaPi Runner DaemonAfter=network.target
[Service]Type=simpleUser=youruserExecStart=/usr/local/bin/pizzapi runnerRestart=on-failureRestartSec=5Environment=PIZZAPI_API_KEY=pk_live_abc123...Environment=PIZZAPI_RELAY_URL=wss://relay.example.com
[Install]WantedBy=multi-user.targetsudo systemctl daemon-reloadsudo systemctl enable --now pizzapi-runnersudo systemctl status pizzapi-runnermacOS (launchd)
Section titled “macOS (launchd)”See the dedicated macOS Setup guide for a full walkthrough. 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)
# Load / startlaunchctl load ~/Library/LaunchAgents/com.pizzapi.runner.plist
# Unload / stoplaunchctl unload ~/Library/LaunchAgents/com.pizzapi.runner.plistpm2 start "pizzapi runner" --name pizzapi-runnerpm2 savepm2 startup # generate startup scriptStopping the Runner
Section titled “Stopping the Runner”pizzapi runner stopThis sends a graceful shutdown signal. Active session workers are allowed to finish before the daemon exits.
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:
How It Works
Section titled “How It Works”- The
spawn_sessiontool includes the current session’s ID asparentSessionIdin the spawn request - The server stores the parent-child relationship in Redis
- The runner daemon passes
PIZZAPI_WORKER_PARENT_SESSION_IDto the child worker process - The child’s trigger system detects it’s a child session and routes events to the parent
Automatic Triggers
Section titled “Automatic Triggers”Child sessions fire triggers through the relay server when they need input:
| Event | Trigger Type | Behavior |
|---|---|---|
AskUserQuestion called | ask_user_question | Question and options sent to parent; parent responds with respond_to_trigger |
plan_mode called | plan_review | Plan sent to parent for approval; parent approves, rejects, or suggests edits |
| Session exits | session_complete | Completion summary sent to parent |
| Error occurs | session_error | Error details sent to parent |
Parent Tools
Section titled “Parent Tools”The parent session has three tools for interacting with child triggers:
respond_to_trigger(triggerId, response)— Respond to a pending triggerescalate_trigger(triggerId)— Pass a trigger to the human viewertell_child(sessionId, message)— Proactively send a message to a child
All trigger routing goes through the relay server via Socket.IO events (session_trigger and trigger_response).