Daemon Operations
This page covers the internal architecture, debugging, and troubleshooting of the bm daemon system.
Architecture overview
The daemon uses a two-process model:
bm daemon start— the launcher. Validates configuration, spawns the daemon process, writes the PID file, and exits.bm daemon-run— the long-lived daemon process. Runs the event loop (webhook or poll), launches members one-shot, and handles signals.
bm daemon start bm daemon-run (long-lived)
┌──────────────┐ ┌──────────────────────────────┐
│ Validate cfg │──spawn──▶│ Event loop │
│ Write PID │ │ ├─ Receive event │
│ Write config │ │ ├─ Launch members one-shot │
│ Exit │ │ └─ Wait for completion │
└──────────────┘ └──────────────────────────────┘
The parent (bm daemon start) redirects the child's stdout/stderr to the daemon log file at ~/.botminter/logs/daemon-{team}.log, then exits after confirming the child is alive.
Modes of operation
Webhook mode
Listens on a configured port (default: 8484) for GitHub webhook HTTP POST requests. Events flow:
- GitHub sends a POST to
http://<host>:<port>/webhookwith anX-GitHub-Eventheader - If a webhook secret is configured, the daemon validates the
X-Hub-Signature-256HMAC-SHA256 signature - The daemon checks if the event type is relevant (
issues,issue_comment,pull_request) - If relevant, it launches all members one-shot and waits for them to complete
- Irrelevant events receive a 200 response but do not trigger member launches
Best for: production deployments with a publicly reachable endpoint or a webhook relay.
Poll mode
Polls the GitHub Events API at a configured interval (default: 60s). Events flow:
- The daemon calls
gh api repos/{owner}/{repo}/events - New events since the last poll are filtered by type
- If any relevant events are found, members are launched one-shot
- Poll state (last event ID, last poll timestamp) is persisted to
~/.botminter/daemon-{team}-poll.json
Best for: development, firewalled environments, or when webhook delivery is unreliable.
One-shot execution model
Unlike bm start (which launches members as persistent background processes), the daemon uses a one-shot model:
- An event arrives (webhook POST or poll detection)
- The daemon discovers all hired members and their workspaces
- Each member is launched with
ralph run -p PROMPT.md— the ralph process does its work and exits - The daemon waits for all members to complete (interruptible by shutdown signals)
- The cycle repeats on the next event
This eliminates idle token burn — members only run when there is work to do.
Runtime files
| File | Path | Purpose | Lifecycle |
|---|---|---|---|
| PID file | ~/.botminter/daemon-{team}.pid |
Daemon process ID | Created on start, removed on stop |
| Config JSON | ~/.botminter/daemon-{team}.json |
Mode, port, interval, start time | Created on start, removed on stop |
| Poll state JSON | ~/.botminter/daemon-{team}-poll.json |
Last event ID, last poll timestamp | Created on first poll, removed on stop |
| Daemon log | ~/.botminter/logs/daemon-{team}.log |
Daemon process output and structured log entries | Persistent, rotated at 10 MB |
| Member logs | ~/.botminter/logs/member-{team}-{member}.log |
Per-member ralph output (stdout/stderr) | Persistent, appended on each launch |
Log files & debugging
Daemon log
The daemon writes structured log entries to ~/.botminter/logs/daemon-{team}.log. Each entry has the format:
[2026-02-22T10:30:00Z] [INFO] Daemon starting in poll mode
[2026-02-22T10:30:05Z] [INFO] Found 2 relevant event(s)
[2026-02-22T10:30:05Z] [INFO] architect-alice: launched (PID 12345)
[2026-02-22T10:30:05Z] [INFO] architect-alice: log file at ~/.botminter/logs/member-my-team-architect-alice.log
Log rotation happens automatically when the file exceeds 10 MB. The previous log is renamed to daemon-{team}.log.old.
Per-member logs
Each member's ralph output (stdout and stderr) is redirected to its own log file:
This prevents output from multiple members from being interleaved. The daemon log notes the path to each member's log file when it launches.
Tailing logs in real-time
# Watch daemon log
tail -f ~/.botminter/logs/daemon-my-team.log
# Watch a specific member's log
tail -f ~/.botminter/logs/member-my-team-architect-alice.log
# Watch all logs at once
tail -f ~/.botminter/logs/*.log
Signal handling
The daemon handles two signals for graceful shutdown:
| Signal | Source | Behavior |
|---|---|---|
SIGTERM |
bm daemon stop, kill -TERM <pid> |
Sets shutdown flag, exits event loop |
SIGINT |
Ctrl+C (if running in foreground) | Same as SIGTERM |
Shutdown sequence
When a shutdown signal is received:
- The daemon's event loop detects the shutdown flag on its next iteration
- If members are currently running (one-shot launch in progress):
- SIGTERM is forwarded to each child process
- The daemon waits up to 5 seconds for each child to exit
- If a child doesn't exit within 5 seconds, it is sent SIGKILL
- The daemon logs "Daemon stopped" and exits
bm daemon stop flow
- Reads the PID file to find the daemon process
- Sends SIGTERM to the daemon
- Waits up to 30 seconds for the daemon to exit (polling every second)
- If the daemon is still alive after 30 seconds, sends SIGKILL
- Cleans up PID, config, and poll state files
Troubleshooting
Daemon won't start
"Daemon already running"
: Another daemon instance is running for this team. Run bm daemon stop -t <team> first, or check for a stale PID file.
"Failed to bind to 0.0.0.0:8484"
: The port is already in use (another daemon or another service). Use --port <other-port> to pick a different port.
"requires schema 1.0"
: The team repo was created with an older version of bm. Run bm upgrade to migrate.
Daemon won't stop
If bm daemon stop hangs or doesn't work:
# Find the daemon process
ps aux | grep 'bm daemon-run'
# Send SIGTERM manually
kill -TERM <pid>
# If that doesn't work (after 30s), send SIGKILL
kill -KILL <pid>
# Clean up stale files
rm ~/.botminter/daemon-{team}.pid
rm ~/.botminter/daemon-{team}.json
Stale PID files
If the daemon crashed (e.g., the machine rebooted), the PID file may point to a dead process. Both bm daemon start and bm daemon status detect and clean up stale PID files automatically:
bm daemon start: detects the stale PID, removes the file, and starts a fresh daemonbm daemon status: reports "not running (stale PID file)" and removes the file
Members not launching
Check these in order:
- Event types: The daemon only triggers on
issues,issue_comment, andpull_requestevents. Other events (push, star, fork) are ignored. - GitHub events: In poll mode, verify events exist with
gh api repos/{owner}/{repo}/events | head. - gh auth: The daemon runs
ghcommands. Verifygh auth statussucceeds with the configured token. - Member workspaces: Run
bm teams syncto ensure workspaces are provisioned. - Daemon log: Check
~/.botminter/logs/daemon-{team}.logfor error messages.
Finding the right log file
| Symptom | Check |
|---|---|
| Daemon itself misbehaving | ~/.botminter/logs/daemon-{team}.log |
| A specific member failing | ~/.botminter/logs/member-{team}-{member}.log |
| Member never launched | Daemon log (look for "no workspace found" or "failed to launch") |
Process management reference
Quick commands for operators:
# Check daemon state
bm daemon status -t <team>
# Find daemon process
ps aux | grep 'bm daemon-run'
# Watch daemon log
tail -f ~/.botminter/logs/daemon-<team>.log
# Watch member log
tail -f ~/.botminter/logs/member-<team>-<member>.log
# Graceful stop
bm daemon stop -t <team>
# Manual graceful stop
kill -TERM <pid>
# Manual force stop
kill -KILL <pid>
Related topics
- CLI Reference — Daemon — command syntax and flags
- Launch Members — choosing between persistent and daemon modes
- Configuration Files — credential fields used by the daemon