Daemon

caco daemon is the per-node service that orchestrates all Cacophony operations. One daemon runs on each machine in the cluster.

Responsibilities

Canonical Checkouts

For each project the daemon keeps a clean default-branch checkout at $CACOPHONY_DIR/daemon/checkouts/<project>. No agent can be created until it exists; agents are cloned from it and never edit it. It is fetched periodically (every 60 seconds is recommended) and refreshed before a spawn unless that happened in the last 10 seconds.

The daemon's canonical checkout of each project, the source of every agent clone, profile resolution and reintegration input. Absent or partial shapes (an empty directory, a placeholder, Git without origin, origin without HEAD, a dirty partial repository, non-Git evidence or a policy exclusion) are reported exactly; caco checkout refresh materialises a clean checkout of the configured remote and branch. A valid checkout is fetched periodically, and local commits on the tracked branch are rebased onto the upstream branch. If the recorded config fingerprint no longer matches, the daemon resets and reclones it before anything uses it. Arbitrary dirty or untracked files are never reset away: they raise a loud checkout-health error, requires_regenerate, and only caco checkout regenerate replaces the checkout.
Canonical checkout statesA disposable cache, never the place real work lives: unknown files stop it rather than being reset away.

Canonical checkout refresh is non-destructive for real work: local commits on the tracked branch are rebased onto the fetched upstream default branch when possible, while arbitrary dirty or untracked project files produce a loud checkout-health error instead of being reset away.

For recorded session summaries, the daemon may also warm a read-only clone of each project's configured cacophony-state branch at $CACOPHONY_DIR/state-branches/<project>/. Summary list/show/raw endpoints prefer that cache so interactive CLI, TUI, web, and Android viewers do not fetch the state branch on every request. The cache is refreshed by checkout init/refresh paths and is not an editable code checkout.

Auto-Branch Creation

When the configured branch does not exist on the remote (e.g. a new project or a branch that hasn't been pushed yet), the daemon automatically creates it:

  1. Detects the remote's actual default branch (via ls-remote --symref, falling back to main or master)
  2. Clones using the actual default branch
  3. Creates the configured branch locally from the default
  4. Pushes the new branch to the remote (warning on failure; local branch is still usable)

This also applies to fetch_and_reset: if a fetch fails because the branch doesn't exist on the remote, the daemon attempts to push the local branch before proceeding with the local HEAD.

Service Lifecycle

caco up

caco up

Loads and validates config, ensures the local daemon is running, starts the beads service (if this node is the beads host), launches sidecars, and starts declarative always-on agents. Safe to run repeatedly (idempotent).

The command now streams real lifecycle progress instead of a blind timer. Operators see service convergence, health/stabilization, and daemon background-startup phases such as profile discovery, persistent-agent reconciliation, full-state sync, and stale-agent repair, including degraded or potentially stuck follow-up work. Startup full-state pull only runs when sync.state.pull.on_connect.enabled is true, while the periodic pull loop is gated separately by sync.state.pull.periodic.enabled; disabling one trigger no longer accidentally disables or starts the other.

Service ownership and warmup

What goes in daemon-crash.log

Only the main caco-daemon writes stderr to daemon-crash.log. Child services keep their stderr in their own service logs, and routine diagnostics (HTTP 5xx records, sync notices, checkout lock messages, idle advisories, config reload errors) stay in daemon.log, the feed and the Errors view. The crash log is rotated about once a minute and keeps real panic and backtrace evidence; unknown stderr is still treated as crash evidence. Config reload failures carry a short hash you can find with caco log tail --service caco-daemon --json.

Warmup cache

The warmup cache holds redacted per-project/profile/model summaries for faster agent starts. Injecting a summary into a new agent's goal is off by default; when enabled, the block is age- and size-bounded, labelled advisory, and subordinate to the product contract, AGENTS.md, the profile and the operator's goal.

caco down

caco down

Stops services and agents for the current node only. Does not mutate peer nodes. Supports scoped shutdown by service or project.

caco status

caco status --json

Reports:

caco restart

  1. Accepted at once

    caco restart

    POST /api/v1/restart answers accepted: true before the restart subprocess starts, so the caller gets its 200 OK before the daemon goes down and polls health for the outcome.

  2. Phases you can watch

    Port release, service health and background startup are each reported, so healthy progress looks different from a stuck restart. Every restart records why it happened: caco status --json lists recent lifecycle_decisions[], and daemon.down_reason while the daemon is down.

  3. Roll back if it is unhealthy

    If health checks fail, the restart tries the single-slot previous launcher, then walks the launcher archive under ~/.cacophony/launcher/archive/ from newest to oldest until a daemon is healthy or the archive runs out. With previous_config, the prior config is staged too and restored on failure. caco launcher list and caco launcher rollback manage the archive; it keeps one previous generation by default (updates.launcher_archive_count: 1) plus any entry a live process still uses.

  4. Never loop on the same version

    If a restart for an update finds the binary version unchanged, because the new one could not be fetched or built, that target version is suppressed until a newer release appears.

API Endpoints

CLI daemon transport failures first probe the cheap local /api/v1/node endpoint before reporting broad daemon-down guidance. If that probe succeeds while the requested endpoint times out or resets, the JSON error is classified as daemon_endpoint_transport_error with the probed node/version so operators can treat it as endpoint-local backpressure or a focused daemon bug rather than reflexively running caco up. The mutating messages/send endpoint is stricter: it never retries the POST on transport failure, and it reports retryable msg_send_backpressured even when the bounded node probe is also unavailable, marking daemon_probe.ok:false so operators know both checks failed without risking duplicate accepted sends.

Safety and compact-status floors remain bounded under daemon-store contention. Incident-hold reads, mutations, and admission fail closed with HTTP 503 incident_hold_store_backpressured; current-choice reads return choices_store_backpressured rather than an unsafe empty set. Both include Retry-After, normalized endpoint/reason, lock-wait/timeout diagnostics, and state_known_clear:false. /api/v1/agents/summary uses the background agent read model and independently bounds optional assignment, planned-outage, peer-health, and message-delivery collectors, returning base counts with explicit degraded_sections when enrichment is unavailable. CACO_SAFETY_STORE_LOCK_TIMEOUT_MS tunes the shared default 1000ms store-lock budget.

Discovery

MethodEndpointPurpose
GET/api/v1/nodeNode identity, version, config hash, listeners, and startup-progress status
GET/api/v1/nodesAll configured nodes with liveness
GET/api/v1/nodes/<node>Single node detail
GET/api/v1/projectsAll configured projects
GET/api/v1/projects/<project>Single project detail

Feed

MethodEndpointPurpose
GET/api/v1/feedFeed history
GET/api/v1/feed/streamFeed SSE stream
GET/api/v1/ui/streamUI aggregate stream (for TUI/choices)

Agent Management

MethodEndpointPurpose
POST/api/v1/projects/<project>/agentsSpawn agent
GET/api/v1/projects/<project>/agentsList agents
GET/api/v1/projects/<project>/agents/<id>Project-validated agent detail
GET/api/v1/agents/<id>Compatibility detail alias for /api/v1/agents/<id>/status, including remote-owner forwarding
POST/api/v1/agents/<id>/stopStop agent (global; forwards to the owning node)
POST/api/v1/projects/<project>/agents/<id>/messageSend message
GET/api/v1/agents/<id>/statusGlobal status/detail endpoint with remote-owner forwarding.
GET/api/v1/agents/<id>/logs / /log / /diffRead-only diagnostics: structured logs, raw tmux scrollback, and checkout diff. Diff supports summary_only=true and max_diff_bytes=<N> for lighter polling.
GET/api/v1/agents/<id>/files/tree / /files/readRead-only agent filesystem browser.
GET/api/v1/agents/<id>/ptyStructured interactive PTY WebSocket.
POST/api/v1/agents/<id>/{stop,restart,fork,pause,resume,discard,recreate,nudge}Global lifecycle controls resolved/forwarded by owner node.

Use project-scoped endpoints when project membership must be validated, and global /api/v1/agents/... endpoints for cross-node read/lifecycle clients. There is currently no daemon /api/v1/agents/<id>/reintegrate route; reintegration is still driven by first-party lifecycle/CLI surfaces.

Audio

MethodEndpointPurpose
POST/api/v1/audio/speechText-to-speech via upstream provider; uses the bounded synthesis timeout (CACO_AUDIO_SPEECH_REQUEST_TIMEOUT_SECS, default 90s) and classified upstream error codes such as upstream_provider_5xx, upstream_provider_4xx, upstream_provider_timeout, and upstream_provider_transport_error
POST/api/v1/audio/transcriptionSpeech-to-text via provider, local, or configured command-template STT backend; advertised command-template model labels are accepted as model values

Config

MethodEndpointPurpose
GET/api/v1/config/hashLocal config hash
GET/api/v1/config/peersPeer config hashes
GET/api/v1/config/diffConfig comparison report

Cluster (Cluster Listener Only)

MethodEndpointPurpose
POST/api/v1/feed/eventsAccept events from peer

UI Stream Events

The daemon persists feed events, messages and agent rows, and collects bead state (through a short-lived cache on nodes that are not the beads primary), choices, speech and health. Changes are published on an in-process broadcast channel. Each client loads /api/v1/ui/snapshot once, which is built under a single-flight guard and cached briefly so many dashboards do not each rebuild it, then follows /api/v1/ui/stream, a server-sent event stream with keepalives every five seconds. A client that falls behind skips the missed events and refetches the snapshot when it reconnects. The TUI, choices TUI and apps connect directly; the browser goes through caco-web, which bounds the snapshot request and proxies the stream. The headless speech daemon follows the separate raw feed stream.
How surfaces stay currentEvery client loads one snapshot and then follows the event stream; the browser reaches both through caco-web.

The /api/v1/ui/stream endpoint produces a synthesized event stream consumed by both the main TUI and the choices frontend:

Periodic Snapshots

The daemon periodically emits daemon_state snapshots persisted at $CACOPHONY_DIR/daemon/state/<node>.json, containing node health, running agents, project inventory, and config hash.

UI snapshots are bounded-staleness read models, not beads authorities. Non-owner nodes fetch only interactive states through a short-lived, single-flight mesh cache; the proxy coalesces identical project reads and trims legacy unfiltered requests so concurrent clients cannot multiply full-board scans.

Structured Performance Events

First-party HTTP request_duration performance events keep the human-readable METHOD /path summary for direct inspection and also attach bounded-cardinality labels for method:, status:, and normalized route: family. Project, agent, bead, scratchpad, perf-record, numeric, and long hex/UUID-like path segments are replaced with placeholders such as {project}, {agent}, {bead_id}, or {id}, so Performance views can aggregate hot daemon routes without parsing freeform summaries or retaining per-operator identifiers as label cardinality. Transcription requests add bounded audio:transcription plus latency-bucket labels (latency:normal_lt_5s, latency:slow_ge_5s, or latency:slow_ge_10s) so STT/provider latency investigations can filter slow calls without scraping freeform summaries. The daemon also emits bounded process telemetry once per minute by default into the same performance store: CPU percent/seconds, RSS and max-RSS memory bytes, request/5xx throughput, and cumulative request count. Set CACO_DAEMON_PERF_TELEMETRY_INTERVAL_SECS=0 only for an intentional opt-out, and inspect samples with caco log perf-list --metric daemon_cpu_percent or the Performance dashboards.

Pre-Merge Validation

Agent profiles can define before_reintegration hooks that execute pre-merge smoke tests during caco agent complete. These run after before_complete hooks succeed but before any git merge or PR operations begin. If any hook fails, reintegration is blocked and the agent is notified.

Typical uses:

The --strict flag on caco config validate treats null-resolving template expressions as errors, which catches bugs like undefined variable references that would silently produce empty values in normal validation. Agents can also use --project-config-dir (or CACOPHONY_PROJECT_CONFIG_DIR) to validate config from an agent checkout without modifying the real project config.

The daemon runs no validation gate of its own before landing: profile before_reintegration hooks still run in the CLI, and GitHub branch protection merges a pull request only after its required checks pass. The projects[].integration.gate config block and the --force-skip-gate flag were removed. See the validation guide.

Beads Routing & Standalone Daemon

All beads operations route to the active primary. With service-object beads.primary, independently managed caco-bd-daemon exclusively owns every project beads.db and WAL. The main daemon proxies over same-node loopback or remote mTLS and fails closed instead of opening a store. Inspect the service with caco bd daemon status / restart.

The standalone store is SQLite-first: mutations commit transactionally and mark durable export-dirty state; a bounded pass later writes deterministic JSONL and commits only changed content. Ordinary reads never rebuild from JSONL. Admission limits, query deadlines, read connections, indexes, and single-flight status/UI caches keep expensive reads from starving CRUD.

Dispatch sends an immediate wake; the batch endpoint and slow jittered polling provide anti-entropy, while legacy per-project polling is rejected before store access. Single-primary deployments omit the multi-primary peer-probe mesh.

Recovered former primaries are fenced import-only until catch-up is proven. Only the standalone PID should hold beads SQLite files; persistent WAL growth, ownership-fence refusals, read-pool waits, or repeated route timeouts are incident evidence, never reasons for the main daemon to take ownership. Routing and intended-primary state remain visible through caco bd status and caco bd trigger-vote --dry-run.

Test & Build Queue

Heavyweight validation runs through daemon-local FIFO test/build queues (caco test run / caco build run), serial per project/node with bounded CARGO_BUILD_JOBS/RUST_TEST_THREADS caps. Jobs persist across restarts (re-pended, or converted to retryable infra errors), and ceiling kills on a saturated host classify as retryable host_saturation_timeout.

Project-Level Hooks

Projects can declare lifecycle hooks that fire on specific events:

Hook actions include notify_agent (lifecycle-only persistent-agent nudge with normal message queues suppressed), shell (run a command), broadcast (project-wide message), and speak (TTS announcement). See Agents for configuration examples. Scheduled cron actions remain background automation; an explicit caco cron run waits for the action’s configured bounded timeout and returns the nested command’s actual execution receipt, stdout/stderr, and failure status instead of treating dispatch acceptance as successful delivery. Manual dispatch runs in a detached request-ID-keyed task: one exact-ID response retry observes the same terminal receipt, a per-cron gate prevents overlapping manual execution, and the cron-specific outer deadline (330 seconds by default, configurable with CACO_CRON_RUN_REQUEST_TIMEOUT_SECS) returns typed cron_run_backpressured phase evidence rather than an unclassified transport failure.

Test Agent Isolation

Choices and operator inbox items from test-type agents are automatically filtered from operator-facing surfaces. This prevents test data from leaking into the production operator inbox:

Supervisor Integration

When running Cacophony under a process supervisor (systemd, launchd), use the following configuration to prevent duplicate daemon instances:

systemd (Linux)

[Unit]
Description=Cacophony Daemon
After=network.target

[Service]
Type=exec
ExecStart=/usr/local/bin/caco up --foreground
ExecStop=/usr/local/bin/caco down
Restart=on-failure
RestartSec=5

# Prevent duplicate spawning — systemd will not start a new
# instance while the previous one is still running.
# Type=exec ensures the service is considered started only after
# the exec() call succeeds.

launchd (macOS)

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>com.cacophony.daemon</string>
  <key>ProgramArguments</key>
  <array>
    <string>/usr/local/bin/caco</string>
    <string>up</string>
    <string>--foreground</string>
  </array>
  <key>KeepAlive</key>
  <true/>
  <!-- LaunchOnlyOnce prevents launchd from spawning a second
       instance while one is still running. -->
  <key>LaunchOnlyOnce</key>
  <true/>
  <key>StandardOutPath</key>
  <string>/tmp/cacophony-daemon.log</string>
  <key>StandardErrorPath</key>
  <string>/tmp/cacophony-daemon.log</string>
</dict>
</plist>

Important: The TTS daemon has built-in startup guards that refuse to start if another instance is already running. If you see a "refusing to start" error, stop the existing instance with caco down before starting a new one.

Agent Retention & Cleanup

Completed and discarded agent checkouts are subject to a periodic retention sweep (every 10 minutes). Policy is configured per-project under agent_defaults:

SettingDescription
completed_retention_hoursMaximum age of completed checkouts before pruning
completed_retention_countMaximum number of completed checkouts to keep
completed_retention_bytesMaximum total bytes of completed checkouts

Preview with caco agent prune --dry-run and clean up with caco prune run --delete. Pruning reclaims the checkout and any legacy logs/session.log; structured logs, agent.json, prompts, environment files and results always stay. Scheduled and emergency pruning skip agents whose project isn't in the current valid config, so a bad config render can't delete managed work.

To reclaimCommand
Cargo build output in checkouts (target/)caco prune run --cargo-targets (running agents only with explicit --state or --id). The prune dry-run points here when build caches, not retention, explain disk use.
Copied Pi package caches (agent_pi_package_cache_copies)caco prune run --pi-package-cache --current-agent, from inside a managed worker; replaces the copy with a symlink to $HOME/.pi/agent/git.
Pi session history (agent_pi_session_history)caco agent clear-history --id <agent> --keep-last <n>, or caco prune run --pi-session-history for several agents; preview with --dry-run, never delete session files by hand.
Leftover direct-merge temp checkoutscaco prune run --direct-integration-tempdirs --dry-run, then without --dry-run.

Add --nonzero-only to any preview to hide empty rows.

Persistent Agent Recovery

Persistent agents that become idle or stuck are auto-restarted based on persistent_agent_idle_timeout_secs. The no-tool-activity threshold is configurable per-profile via stale_timeout_secs, falling back to the global agents.stall_timeout_secs. For agents whose tmux/runtime session is still live, that threshold is advisory: after a short grace the reconciler records and surfaces a no-tool-activity warning while preserving running and the live pane. It must not fail, restart, recreate, discard, roll back, or reassign the agent solely because tool activity or startup readiness has not advanced; ownership-changing recovery requires positive death evidence or operator approval. Aggregate agent-list caches also include peer daemon-state snapshot freshness so remote rows converge when a peer snapshot updates.

Feed Replay Resilience

Startup rebuilds daemon state from durable append-only logs plus SQLite indexes.

Materialized Config

The daemon writes a last-known-good config snapshot to $CACOPHONY_DIR/daemon/materialized-config.yaml on every successful startup. This allows post-mortem debugging when a config change breaks the daemon.