Notifications

Cacophony separates durable operator notifications from chat messages, daemon logs, short sound cues, and spoken narration. Notification history stays inspectable after the first toast; sounds are quick, low-fatigue cues; TTS is reserved for operator-facing status and incidents.

Operator notification history

Core notifications are durable operator-facing records. caco notify send and first-party subsystems create records through POST /api/v1/notifications; CLI, TUI, and web surfaces list them with GET /api/v1/notifications, fetch detail with GET /api/v1/notifications/{notif_id}, and persist read or clear state with POST /api/v1/notifications/{notif_id}/ack.

Acknowledgement is idempotent: acknowledging an already acknowledged notification returns success with an already_acknowledged indication, while an unknown ID returns a structured not_found error. Web and TUI clear actions use the ack endpoint so dismissed rows stay dismissed after the next snapshot refresh.

Bulk acknowledgement is one request: POST /api/v1/notifications/ack with {"all": true} (optionally "project") or {"ids": [...]}. The daemon acknowledges the whole set in one store transaction, emits one summary feed event, and returns acknowledged and remaining_unacknowledged. caco notify ack --all, caco notify ack --id a,b,c, the TUI Notifications A key, and the web Mark All Read and Clear All buttons use it, so a new node's backlog of hundreds clears without per-row requests timing out.

Event cues

EventSound shapeSpeech
agent.reintegratedSoft ascending two-toneagent <name> reintegrated
agent.failedMinor-key descending cueagent <name> failed, investigating
bead.p0_filedUrgent double-chimeSpoken immediately
incident.raisedAttention chordSpoken immediately
release.publishedShort fanfareRelease summary

Spoken short name

Readouts prefer explicit bead or agent spoken_name, then generated agent short_name, then a short ID fallback. The headless TTS daemon refreshes bead spoken names through the lightweight /api/v1/beads/spoken-names?limit=5000 projection, which returns only project, id, and spoken_name; mixed-version rollouts can temporarily fall back to /api/v1/beads/all?limit=5000.

Stereo voice placement

TTS can optionally apply deterministic per-sender stereo panning after speech synthesis and configured voice/effect filters. Configure speech.tts.voice_pan as either a boolean (true enables defaults, false disables) or an object with fields such as enabled: true and spread: 0.95. The sender identity maps to a stable pan slot, so repeated speech from the same agent stays in the same part of the stereo field while different agents are spatially separated. This is a playback aid only: mute, solo/focus, profile selection, and command-template effects still resolve first, then the WAV audio is panned before playback when enabled. Unscoped runtime mute/unmute now control only the local node by default; use --all for mesh-wide mute/unmute convergence through tts_runtime_control. Temporary solo/focus controls still converge through the same feed path.

Simultaneous agent speech

The headless TTS daemon has an effective default of sequential FIFO playback. Set speech.tts.n to allow multiple agent speech events to occupy playback slots at once; the checked-in Cacophony config currently sets two slots with a short one-second overlap cap for operator-audible agent narration:

speech:
  tts:
    n: 2              # <=1 is sequential; >1 allows that many concurrent slots
    overlap_secs: 1   # optional cap on intentional overlap between active clips

The n value is a non-negative integer with an effective fallback of 1; 0 and 1 both mean one-at-a-time playback. The queue remains FIFO/no-drop: when all slots are busy, later speech waits for the next slot, except that focus-matched clips from caco tts focus bypass the ordinary playback-slot queue once their audio is ready while non-focused clips stay ducked. When overlap_secs is omitted, concurrent slots may overlap for their full clip duration. When it is set, the daemon delays starting another clip until all already-playing clips are within that many seconds of their expected end; 0 means no intentional overlap, while a small value such as 1 preserves brief burst overlap without letting long messages talk over each other from the beginning.

CLI validation

caco notify send requires a non-empty --title. In --json mode, a missing or whitespace-only title is reported as a structured stdout envelope with ok=false, error.code set to missing_argument or invalid_argument, and exit code 1.

TITLE="release published"
caco notify send --title "$TITLE" --json

Notification bodies accept the same shell-free inputs as message bodies: exactly one of inline --body, --body-file <path>, or --body-stdin (--body-file - also reads stdin). The body itself stays optional. When the text contains Markdown, command examples, backticks, $variables, or $(...), write a file or pipe a single-quoted heredoc instead of using a double-quoted --body argument, because the shell expands or executes that text before caco starts.

caco notify send --title "$TITLE" --body-stdin <<'EOF'
Recovery runbook: run `just stopgap 3600` only after the operator confirms.
EOF

Android companion release: detect → notify → choice → Play upload

Cacophony surfaces a complete loop for promoting Android companion AAB builds to the Play internal-testing track. The daemon's background release-sync loop detects only completed successful explicit AAB-source dispatches on trusted main whose deterministic run name records event=workflow_dispatch, play=none, and no promotion source (deduplicated and persisted across restarts), emits a canonical notification for each, and—while auto-push is off—presents an operator caco choices prompt with Promote … (rollout), Promote … (draft), and Skip / acknowledge options. Pull-request checks, scheduled checks, tag metadata runs, branch dispatches, and validate/draft/rollout promotion dispatches are excluded. Resolving rollout/draft re-verifies the source run and dispatches trusted main with the mode, owner, immutable source run id, exact source head, and exact provider ids/digests for both AAB artifacts. The promotion job downloads the uniquely named unexpired source AAB pair and provenance, verifies provider/file digests plus package/versionCode/certificate before introducing Play credentials, and never creates a historical ref or rebuilds later main.

A project can opt into unattended automation with a per-project config flag (default off keeps the operator-gated choice path):

projects:
  - name: cacophony
    releases:
      android_companion:
        owner_node: ms-dev   # sole detector/notifier/Play-dispatch actor
        auto_push: rollout   # off (default) | rollout | draft

The configured owner is the only daemon that polls companion runs, emits the release notification/choice, or dispatches Play; non-owner nodes skip those actions.

Manual-promotion visibility

When auto-push remains off, a bounded scheduled Android job reads the existing successful workflow runs and artifacts and compares the newest successful draft/rollout promotion's exact source head with current source.

The job summary reports pending, current, missing-artifact, or promotion-complete. Pending means changed source needs a new play=none build or has a complete source run ready for explicit promotion. Missing-artifact means the source run covering current Android source no longer has the exact unexpired phone AAB, Wear AAB, and provenance. Pending and missing-artifact fail the small observer job so existing Actions notifications expose the manual action. A successful exact-source draft/rollout runs the observer afterward, records promotion-complete, and returns the latest signal to green.

The observer has read-only Actions and contents permissions. It cannot dispatch a workflow, authenticate to Play, or become a second upload actor.

Design rules