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 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.


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.


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

The installer generates the user unit and can activate it for you:

Terminal window
pizzapi runner install --activate

To preview without writing:

Terminal window
pizzapi runner install --dry-run

The generated unit is equivalent to:

~/.config/systemd/user/pizzapi-runner.service
[Unit]
Description=PizzaPi Runner
After=network-online.target
[Service]
Type=simple
ExecStart=/usr/local/bin/pizzapi runner
Restart=on-failure
RestartSec=5
WorkingDirectory=/path/to/project
[Install]
WantedBy=default.target
Terminal window
systemctl --user daemon-reload
systemctl --user enable --now pizzapi-runner.service
systemctl --user status pizzapi-runner.service

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)
Terminal window
pm2 start "pizzapi runner" --name pizzapi-runner
pm2 save
pm2 startup # generate startup script

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.


Terminal window
pizzapi runner stop

This 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.


Terminal window
pizzapi runner status # human-readable
pizzapi runner status --json # machine-readable

Exits 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.


Terminal window
pizzapi runner pair # refuses if a credential already resolves
pizzapi runner pair --force # pairs and overwrites any existing credential

Runs 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.


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.