Skip to content

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.


┌──────────────────────────────────────────────┐
│ 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.


Terminal window
# Start in the foreground (Ctrl+C to stop)
pizzapi runner
# Start in the background
pizzapi runner &
# Stop the runner
pizzapi runner stop

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.


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.


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 C

/etc/systemd/system/pizzapi-runner.service
[Unit]
Description=PizzaPi Runner Daemon
After=network.target
[Service]
Type=simple
User=youruser
ExecStart=/usr/local/bin/pizzapi runner
Restart=on-failure
RestartSec=5
Environment=PIZZAPI_API_KEY=pk_live_abc123...
Environment=PIZZAPI_RELAY_URL=wss://relay.example.com
[Install]
WantedBy=multi-user.target
Terminal window
sudo systemctl daemon-reload
sudo systemctl enable --now pizzapi-runner
sudo systemctl status pizzapi-runner

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)
Terminal window
# Load / start
launchctl load ~/Library/LaunchAgents/com.pizzapi.runner.plist
# Unload / stop
launchctl unload ~/Library/LaunchAgents/com.pizzapi.runner.plist
Terminal window
pm2 start "pizzapi runner" --name pizzapi-runner
pm2 save
pm2 startup # generate startup script

Terminal window
pizzapi runner stop

This sends a graceful shutdown signal. Active session workers are allowed to finish before the daemon exits.


When sessions are spawned via spawn_session, the runner daemon automatically links them as parent-child. This enables the conversation trigger system:

  1. The spawn_session tool includes the current session’s ID as parentSessionId in the spawn request
  2. The server stores the parent-child relationship in Redis
  3. The runner daemon passes PIZZAPI_WORKER_PARENT_SESSION_ID to the child worker process
  4. The child’s trigger system detects it’s a child session and routes events to the parent

Child sessions fire triggers through the relay server when they need input:

EventTrigger TypeBehavior
AskUserQuestion calledask_user_questionQuestion and options sent to parent; parent responds with respond_to_trigger
plan_mode calledplan_reviewPlan sent to parent for approval; parent approves, rejects, or suggests edits
Session exitssession_completeCompletion summary sent to parent
Error occurssession_errorError details sent to parent

The parent session has three tools for interacting with child triggers:

  • respond_to_trigger(triggerId, response) — Respond to a pending trigger
  • escalate_trigger(triggerId) — Pass a trigger to the human viewer
  • tell_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).