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
| Event | Sound shape | Speech |
|---|---|---|
agent.reintegrated | Soft ascending two-tone | agent <name> reintegrated |
agent.failed | Minor-key descending cue | agent <name> failed, investigating |
bead.p0_filed | Urgent double-chime | Spoken immediately |
incident.raised | Attention chord | Spoken immediately |
release.published | Short fanfare | Release 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.
- Auto-push: When
auto_pushisrolloutordraft, the owner skips the operator choice and dispatches the source-bound promotion directly with the matching mode, immutable source run id, and exact source head for every new successful AAB-source run. Promotion runs carry non-noneprovenance and can never recursively become release candidates. Auto-push without an owner is fail-safe off. - At most once: Owner gating plus durable per-release dedup gives one cluster notification and at most one dispatch; provider run checks and source-keyed receipts reject active or completed duplicates. The durable handled marker is written only after successful auto-dispatch or operator-choice presentation; pending or failed handoffs remain retryable across sync and daemon restart.
- Revoking: Automated dispatches add an
auto_push:<mode>label, and live config can revoke automation by setting it back tooff. - Play: The
GOOGLE_PLAY_SERVICE_ACCOUNT_JSONand upload-key secrets must be present on the runner or the Play upload step fails fast. Phone usescom.cacophony.companion/internal; Wear uses the same package'swear:internalform-factor track.
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.
- Relevant paths: The canonical promotion-relevant set is
companion/android/**,crates/caco-embed-android/**,crates/caco-picophony/**, and.github/workflows/android-companion.yml. Non-shipped design-review artifacts use the Android CI classifier's exact exemption; code, build inputs, and unknown files undercompanion/android/design/remain relevant. The canonicalappandwearablemodules'src/test/andsrc/androidTest/source sets also do not require promotion; their CI validation remains unchanged. Runtime variants, build files, unknown modules/source sets and shared test-fixture inputs remain relevant. Runtime resources and Markdown assets outside those exemptions remain relevant, and renames retain both endpoints so moving shipped inputs into design or tests still requires promotion. - Large diffs: When GitHub's compare response reaches its 300-file cap, the observer reads the exact base and head Git trees for only those paths through bounded, authenticated, read-only API requests. It accepts recursive tree data only when GitHub marks it complete and fails closed on truncation, identity mismatch, or a request/entry budget breach.
- Other inputs: Broader build-system and lockfile inputs remain validation-relevant without creating permanent Play-promotion noise.
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
- Keep cues shorter than one second and avoid looping sounds.
- Use speech for actionable operator context, not every low-level state change.
- Respect mute policy and per-agent audio routing when deciding whether a sound should play. Muted-policy diagnostics for agent-authored speech should name the real originating agent or inner feed sender when known;
node-tokenis only the authentication fallback and should not be the operator-facing identity.