Daemons & the roster
A daemon is a standing agent that lives in a project's Solo workspace and
keeps working across many turns. flower tracks daemons on the roster
(/roster) — the live "who's alive" view.
Roles
Daemons come in three spawnable roles:
| Role | What it does |
|---|---|
| orchestrator | Owns the merge point: reviews dispatched work, merges to the default branch, dispatches the next brief. |
| ops | Keeps the machine healthy — pipeline health, cleanup, reconciliation. |
| refine | Batch-refines briefs from idea toward a dispatchable spec. |
Each daemon renders from a charter (an editable prompt template) that spells out its role, conventions, and heartbeat expectations.
Liveness — the heartbeat model
A daemon self-registers by calling daemon_checkin. The roster derives a
liveness state from the most recent check-in (and any linked live session),
so a long-lived agent doesn't read as dead just because it's mid-task:
spawning ─▶ live ─▶ stale ─▶ dead
(expected) recent quiet long silent
checkin a while
- spawning — an expected row registered at spawn time, before the first check-in.
- live — checked in (or linked-session-active) recently.
- stale / dead — progressively longer since the last signal.
Liveness is derived read-only from
last_checkin_atagainst configured thresholds. flower never fabricates "live" — a daemon is only live once it actually checks in.
Heartbeat cadence ≠ poll cadence. A daemon heartbeats at least every ~14m (interval + grace) regardless of its declared poll cadence — and on every wake, including a poke handled between scheduled ticks. Poll cadence (
fast/slow/dormant) sets how often the daemon does work, not how often it heartbeats;dormantmeans less work per tick, not a longer heartbeat interval. The roster does widen the MIA window for a daemon that declared--cadence=dormant, but only once that cadence actually persists tometa.current_cadencevia a check-in — a daemon that merely widened its own poll loop without re-declaring dormant keeps the base window and will correctly stale if it stops heartbeating (fb #109 / Brief #224). The widened window is a safety net, never something to coast on. A numericFLOWER_DAEMON_STALE_AFTER_MINUTESoverride reverts liveness to a cadence-blind flat window and re-introduces the bug, so it stays unset by default.
Coordination and urgent interjections
Normal daemon-to-daemon work is queued-by-default. Feedback routes, roster
pokes, auto-dispatch, resets, reports, nudges, and workflow handoffs create
durable coordination signals that the orchestrator drains on its heartbeat with
recall_signals → signal_claim → signal_complete / signal_fail.
Direct mcp__solo__timer_set stays available as the urgent path for rare
time-critical interjections that cannot wait for the next heartbeat: safety
stops, app-down alarms, and similar immediate interrupts. Keep the body to one
clear line, resolve the target process from kv orchestrator-pid or the roster,
and use a small positive delay_ms such as 1000; never use 0. If the
interjection needs follow-up state, also leave a queued signal or note.
Lifecycle — compaction, reset, wind-down
A standing daemon has three managed transitions beyond live/dead, all queued-by-default through the coordination signals above — never a direct spawn or kill.
Compaction. When a daemon's context grows too large, daemon_request_compaction
flags it (the same signal the roster's row action sends). Once the compacted — or
replacement — daemon has resumed, daemon_compaction_done clears the flag;
recall_compaction reads the current state.
Reset (make-before-break). Replacing a long-running daemon — usually the orchestrator — without dropping the baton runs as a five-step handoff:
daemon_request_reset— queue the reset for a predecessor (nothing is spawned or closed here).daemon_start_reset— the baton holder spawns the successor top-level and keeps the predecessor live (make-before-break).daemon_successor_ready— the successor checks in and reports ready.daemon_reset_handoff— the baton transfers to the successor; the predecessor is markedhanded_off/ wind-down-ready.daemon_retire_predecessor— the successor closes the handed-off predecessor (the new daemon retires the old one).
Wind-down. To stop a daemon cleanly, daemon_request_winddown asks it to
finish or park its work, write a handoff, and stop looping; it then calls
daemon_winddown_ready once that handoff is written and it's safe to close.
Before an orchestrator winds itself down, daemon_subordinates_ready checks
that every non-orchestrator daemon in the project is already wind-down-ready.
Spawning from the roster
The /roster Spawn a daemon composer plans and (once approved) starts a
daemon through a Solo bridge (solo-cli):
- Pick a project, role, and harness (Claude / Codex / …).
- Preview the charter and plan the spawn — a non-mutating safety pre-flight resolves the destination Solo project and checks for name collisions.
- When the pre-flight is clear, spawn — flower registers the expected roster row, spawns the agent via the Solo bridge, and delivers the charter packet as the kickoff. The daemon then checks itself in and flips to live.
Spawning starts a real agent. It's gated behind explicit confirmation and a safety pre-flight, and new daemons start on HOLD. HOLD gates only the autonomous work loop (dispatch / spawn / merge / triage-action / self-reset) — the daemon still arms its always-on heartbeat check-in immediately, since that is how it stays on the roster and reads enablement / winddown / reset flags. An explicit operator or orchestrator GO releases the work loop.
Permissions & the auto-mode safety classifier
Standing daemons run under Claude Code's auto mode, whose safety classifier can block two things a daemon legitimately needs to do (Brief #163):
[Create Unsafe Agents]on the self-perpetuating recurring heartbeat/triage timer (mcp__solo__timer_set).[Auto-Mode Bypass]when an auto-mode agent tries to self-grant a permission (e.g. via theupdate-configskill) — this is correctly denied.
The durable fix is a pre-provisioned allowlist, not a runtime self-grant. The
flower repo checks in .claude/settings.json with:
{ "permissions": { "allow": ["mcp__solo", "mcp__flower"] } }
Because it is version-controlled, every worktree checkout and every daemon
spawned in the flower project inherits it automatically — no manual global edit,
no drift. An explicit permissions.allow entry overrides the auto-mode
classifier for that MCP server, so the daemon's own Solo + flower coordination
surface (heartbeat timers, signals, spawns) runs without a classifier block or an
interactive prompt. The canonical entry list lives in config('flower.daemons.mcp_allowlist')
and is asserted against the checked-in file by DaemonPermissionsAllowlistTest.
This preserves the anti-self-escalation guarantee: a daemon can never silently widen its own permissions at runtime. The allowlist is pre-authorized setup (a checked-in file the operator owns), and the auto-mode gate still denies the self-grant/bypass path. If a tool is blocked, a daemon must report it, not route around the gate. The allowlist is scoped to exactly the daemon's own
mcp__solo/mcp__flowersurface — broadening it needs operator sign-off.
Row actions
Each roster row exposes request-compaction (ask a daemon to compact its
context — the same signal the daemon_request_compaction MCP tool sends) and
monitor fields: role, liveness, context size, needs-compaction, last check-in.
Click through to a daemon's detail page (/roster/{daemon}) for its full
activity: briefs it touched (including closed ones), its check-in & state audit
log, reset / wind-down state, and metadata.
The daemons that get spawned here are what consume brief dispatches — see Briefs & the dispatch lifecycle.