# openalice-live

> Next.js streamer studio + OBS Browser Source page (live.blal.pro) — VRM avatar renderer, 9 scene modes (incl. vibecode live-coding layout), agent-driven scene WS, BGM/SFX engine, chat overlay.

## At a glance
| field | value |
|---|---|
| kind | `product` |
| priority | `P1` |
| default branch | `mvp` |
| last activity | 2026-07-06T08:45:07+00:00 |
| features catalogued | **322** |
| runtime deps declared | **11** |

## Manifest
`.atlas-deps.yml` — parsed cleanly: **yes**

### Runtime deps
- **`openalice-auth`** · `shared-jwt` · via `SHARED_JWT_SECRET` — JWTs issued by openalice-tenants are signed with the same shared secret so /auth/me + every openalice service accepts them without a separate SSO round-trip.
- **`openalice-browse`** · `http` · via `BROWSE_INTERNAL_URL` — Server-side proxy route (/api/browse-session) mints openalice-browse sessions for the in-scene BrowserApp OS window; internal secret stays server-side.
- **`openalice-cdn`** · `http` · via `NEXT_PUBLIC_CDN_URL` — Serves BGM preset MP3s (cdn.<domain>/audio/bgm/<id>.mp3) and SFX sound-pack assets (cdn.<domain>/audio/sfx/<pack>/<cue>.mp3) referenced by the BgmPlayer and SfxPlayer.
- **`openalice-live-control`** · `ws` · via `NEXT_PUBLIC_LIVE_CONTROL_URL` — Scene-control WebSocket at /realtime/scene/<broadcast_id>/control — receives ChangeScene ToolCalls + SceneEvent frames from the agent mid-stream without restarting broadcaster Chromium.
- **`openalice-live-control`** · `http` · via `NEXT_PUBLIC_LIVE_CONTROL_URL` — HTTP routes /live/start, /live/stop, /live/status, /live/chat-out called by Studio Hub + Studio Chat with the user's bearer token.
- **`openalice-persona`** · `http` · via `NEXT_PUBLIC_DEFAULT_DOMAIN` — Embeds the VRM avatar renderer as an iframe (persona.blal.pro/<user>) in every VRM-bearing scene mode; forwards ?avatar=, ?anim=, ?token= from the parent URL.
- **`openalice-pty`** · `http` · via `PTY_INTERNAL_URL` — Server-side proxy route (/api/pty-session) mints openalice-pty sessions for the in-scene TerminalApp OS window; internal secret stays server-side, returns a fully-qualified wss:// URL for the browser.
- **`openalice-radio`** · `ws` · via `NEXT_PUBLIC_RADIO_URL` — RadioPlayer subscribes to wss://radio.<domain>/v1/radio/channels/<slug>/subscribe; each now_playing frame drives the BgmPlayer cross-fade so the live page tracks the current radio track without restarting BGM.
- **`openalice-realtime`** · `ws` · via `NEXT_PUBLIC_RT_URL` — Realtime presence / event bus consumed via @openalicelabs/realtime; used by the scene page control-WS helper (useRealtime) for live broadcast events.
- **`openalice-tenants`** · `http` · via `NEXT_PUBLIC_TENANTS_URL` — Tenant auth flow (login, signup, password-reset, email-verify, consent, GDPR export, account-deletion, paired-agent grants) consumed by Studio settings + auth pages via tenants-client.
- **`openalice-voice`** · `ws` · via `NEXT_PUBLIC_VOICE_SFU_URL` — WebRTC recv-only Voice SFU — streams the agent's TTS audio into the OBS Browser Source page via VoiceClient from @openalicelabs/voice-client. BGM BgmPlayer ducks volume on oa-voice-chunk events.

### Consumed env
- `BROWSE_INTERNAL_URL`
- `BROWSE_INTERNAL_SECRET`
- `PTY_INTERNAL_URL`
- `PTY_INTERNAL_SECRET`
- `PTY_PUBLIC_WS_BASE`
- `NEXT_PUBLIC_API_URL`
- `NEXT_PUBLIC_BGM_PRESETS`
- `NEXT_PUBLIC_CDN_URL`
- `NEXT_PUBLIC_DEFAULT_VRM_URL`
- `NEXT_PUBLIC_DEFAULT_DOMAIN`
- `NEXT_PUBLIC_LIVE_CONTROL_URL`
- `NEXT_PUBLIC_OA_LOG_LEVEL`
- `NEXT_PUBLIC_RADIO_URL`
- `NEXT_PUBLIC_RT_URL`
- `NEXT_PUBLIC_SCENE_CONTROL_WS_URL`
- `NEXT_PUBLIC_TENANTS_URL`
- `NEXT_PUBLIC_VOICE_SFU_URL`

### Events published
- `oa-voice-chunk`
- `oa-voice-finished`
- `oa-bgm-loop`
- `oa-sfx-subscribe`
- `oa-sfx-caption`

### Events consumed
- `oa-voice-chunk`
- `oa-voice-finished`
- `oa-bgm-loop`
- `oa-sfx-subscribe`
- `oa-sfx-caption`

### Exposes
- `http_endpoint` — https://live.blal.pro
- `http_endpoint` — https://live.blal.pro/scene/[user]
- `http_endpoint` — https://live.blal.pro/studio
- `rest_api` — POST /api/browse-session
- `rest_api` — POST /api/pty-session
- `rest_api` — GET /api/health

## Env drift

**Code reads, manifest omits (82):**
- `BROADCASTS_DB_URL`
- `BROWSE_URL`
- `CHAT_BRIDGE_DB_URL`
- `CHROMIUM_FLAGS`
- `DATABASE_URL`
- `INTERNAL_BROADCAST_SECRET`
- `INTERNAL_PERSONA_URL`
- `INTERNAL_RADIO_URL`
- `KICK_PUSHER_URL`
- `LIVE_CONTROL_PORT`
- `LIVE_CONTROL_WATCHDOG_INTERVAL_SECS`
- `NEXT_PUBLIC_HUB_BASE_URL`
- `NEXT_PUBLIC_LIVE_URL`
- `NEXT_PUBLIC_LOUNGE_BASE`
- `NEXT_PUBLIC_PERSONA_BFF_BASE`
- `NEXT_PUBLIC_PERSONA_PUBLIC_URL`
- `OA_AGENT_DISPATCH_URL`
- `OA_AGENT_ID`
- `OA_AUDIO_BITRATE`
- `OA_AUDIO_DEVICE`
- `OA_AUTH_BASE`
- `OA_AUTH_URL`
- `OA_AUTO_DISCOVER`
- `OA_AUTO_UPDATE`
- `OA_BROADCAST`
- `OA_BROADCAST_ID`
- `OA_CONFIG`
- `OA_DAEMON_POLL_SECS`
- `OA_DISABLE_AUDIO`
- `OA_FFMPEG_PATH`
- `OA_FRAMERATE`
- `OA_FULLSCREEN`
- `OA_HEIGHT`
- `OA_HUB_BASE`
- `OA_INTERNAL_SECRET`
- `OA_KICK_SLUG`
- `OA_LIVE_CONTROL_BASE`
- `OA_LIVE_CONTROL_URL`
- `OA_OBSERVABILITY_PORT`
- `OA_PERSONA_URL`
- `OA_PRESET_ID`
- `OA_QUALITY_PRESET`
- `OA_RESOLUTION`
- `OA_SCENE`
- `OA_SCENE_BASE`
- `OA_SCENE_URL`
- `OA_STREAMHOST_MAX_RSS_MB`
- `OA_TENANT_ID`
- `OA_TIKTOK_SIGN_API_KEY`
- `OA_TIKTOK_USERNAME`
- `OA_TOKEN`
- `OA_TTS_PROVIDER`
- `OA_TWITCH_BOT_LOGIN`
- `OA_TWITCH_BOT_USER_ID`
- `OA_TWITCH_BROADCASTER_ID`
- `OA_TWITCH_CHANNEL`
- `OA_TWITCH_CLIENT_ID`
- `OA_TWITCH_OAUTH`
- `OA_USER`
- `OA_USER_ID`
- `OA_VIDEO_BITRATE`
- `OA_VIDEO_ENCODER`
- `OA_WIDTH`
- `OA_YT_API_KEY`
- `OA_YT_LIVE_CHAT_ID`
- `OPENALICE_AUTH_URL`
- `OTEL_EXPORTER_OTLP_ENDPOINT`
- `OTEL_SERVICE_NAME`
- `PLAYWRIGHT_BASE_URL`
- `PTY_URL`
- `PUBLIC_SCENE_URL_TEMPLATE`
- `PULSE_RUNTIME_PATH`
- `SCHEDULER_URL`
- `SHARED_JWT_SECRET`
- `STREAMING_TTS`
- `TENANT_RATE_LIMIT_PER_MINUTE`
- `VOICE_CACHE_DIR`
- `VOICE_PUBLIC_BASE`
- `VOICE_URL`
- `YOUTUBE_OAUTH_CLIENT_ID`
- `YOUTUBE_OAUTH_CLIENT_SECRET`
- `YOUTUBE_OAUTH_REDIRECT_URL`

## Live reachability
- last probe: **down** · `https://live.blal.pro` · HTTP 404 · 32ms
- uptime 24h: **0.0%**

## Features (322)

### app/account
- **`app.account.agent`** · _beta_ · since 0.5.0 — Hub page for unified agent config — per-product enable + system prompt, shared identity (avatar + VRM), shared voice (TTS + STT). Read by every OpenAlice product at runtime.  
  *live-hub/src/app/account/agent/edit/page.tsx:15*
- **`app.account.agent.redirect`** · _stable_ · since 0.11.0 — Legacy /account/agent path redirects to the new /account/agents list. The editor moved to /account/agent/edit.  
  *live-hub/src/app/account/agent/page.tsx:17*
- **`app.account.agents.list`** · _beta_ · since 0.11.0 — Hub agents list — one row per agent under this tenant, link to per-agent settings.  
  *live-hub/src/app/account/agents/page.tsx:17*
- **`app.account.agents.settings`** · _beta_ · since 0.11.1 — Per-agent settings page — Identity, Soundboard toggle, Voice editor (voice_id + stability + similarity + speed + Test), Avatar editor (VRM picker + display_name + Use default), BGM source picker (None + 4 shared radio channels with now-playing strip). Voice + Avatar + BGM persist via persona-server's PATCH /v1/me/agents/:id/overrides.  
  *live-hub/src/app/account/agents/[id]/settings/page.tsx:31*
- **`app.account.ai-moderation`** · _beta_ · since 0.13.0 — Hub page for the layer-2 (AI) moderation engine — master toggle, confidence-threshold + budget tuning, undo / training-feedback panels. Companion of /account/youtube (layer-1 static rules).  
  *live-hub/src/app/account/ai-moderation/page.tsx:43*
- **`app.account.chat-sources`** · _beta_ · since 0.5.0 — Hub page for managing per-platform live chat connections (YouTube / Twitch / Kick / TikTok) the chat-bridge service consumes. CRUD + masked preview + reveal-after-password.  
  *live-hub/src/app/account/chat-sources/page.tsx:96*
- **`app.account.dashboard`** · _beta_ · since 0.4.0 — Logged-in account home — surfaces user identity, presence-status picker (online/idle/dnd/invisible), verify-email banner with resend, and the navigation grid into agent / streams / radio / sounds / chat / triggers sub-pages.  
  *live-hub/src/app/account/page.tsx:4*
- **`app.account.devices`** · _alpha_ · since 0.6.0 — Hub page for managing paired openalice-stream-host installs. Generate pairing codes, list claimed devices, revoke. 90-day device JWTs scoped to live + persona only.  
  *live-hub/src/app/account/devices/page.tsx:26*
- **`app.account.lounge`** · _beta_ · since 0.11.0 — Hub Lounge rooms list + per-room Mods entry. Lists rooms owned by the tenant, deep-links to persona-web for live experience, and to Mods tab for Phase 4 moderation primitives.  
  *live-hub/src/app/account/lounge/page.tsx:19*
- **`app.account.lounge.audit`** · _beta_ · since 0.13.0 — Per-room unified moderation audit log — bans/mutes/deletes/reports/mods/settings; mod-only.  
  *live-hub/src/app/account/lounge/[id]/audit/page.tsx:19*
- **`app.account.lounge.mods`** · _beta_ · since 0.11.0 — Per-room moderation panel — Phase 4 bans/mutes/mods/settings, owner-only grant picker.  
  *live-hub/src/app/account/lounge/[id]/mods/page.tsx:20*
- **`app.account.lounge.reports`** · _beta_ · since 0.12.0 — Per-room report queue — list open user reports, resolve via dismiss/delete/mute/ban actions; live WS subscription with REST fallback; mod-only.  
  *live-hub/src/app/account/lounge/[id]/reports/page.tsx:24*
- **`app.account.radio`** · _beta_ · since 0.11.0 — Hub Radio page — lists shared + private radio channels, shows what's playing now, lets the streamer create a private channel and set any channel as the agent's BGM source.  
  *live-hub/src/app/account/radio/page.tsx:11*
- **`app.account.redemption-triggers`** · _beta_ · since 0.11.0 — Hub page for binding viewer redemptions (Twitch channel-points / YouTube SuperChat / Kick / TikTok) to live actions (play_sound / scene_change / gesture / tts_shout / expression). The Neuro-sama-tier audience-engagement loop.  
  *live-hub/src/app/account/triggers/page.tsx:34*
- **`app.account.security`** · _beta_ · since 0.5.0 — TOTP setup + disable surface for the authenticated user. Renders an otpauth QR + 6-digit verify input + password-gated disable form.  
  *live-hub/src/app/account/security/page.tsx:23*
- **`app.account.sessions`** · _beta_ · since 0.5.0 — Hub page listing the user's active refresh-token sessions with revoke buttons. Powered by /auth/sessions + /auth/sessions/:id.  
  *live-hub/src/app/account/sessions/page.tsx:16*
- **`app.account.soundboard`** · _beta_ · since 0.10.0 — Hub Soundboard page — upload, tag, bind, test-fire SFX clips for the agent's `play_sound` tool. Powers the Neuro-sama-style soundboard.  
  *live-hub/src/app/account/sounds/page.tsx:29*
- **`app.account.streams`** · _beta_ · since 0.5.0 — Hub page for managing per-platform stream destinations the broadcaster service pushes to. CRUD + masked-list + reveal-after-password.  
  *live-hub/src/app/account/streams/page.tsx:79*
- **`app.account.streams-chat-mod`** · _beta_ · since 0.12.0 — Mod-only live view of YouTube chat for one broadcast. Per-message delete + timeout (10m/1h/24h) + ban actions. Polls chat-bridge's ring buffer via persona. Show-only-mod-flagged filter + pin-message toggle. Empty state when the streamer isn't live.  
  *live-hub/src/app/account/streams/[id]/chat-mod/page.tsx:32*
- **`app.account.streams.chat-page`** · _beta_ · since 0.13.0 — Studio Chat Tab page mounting StudioChatTab. Polls /live/status to discover the active broadcast and renders a "go live first" idle state otherwise.  
  *live-hub/src/app/account/streams/chat/page.tsx:27*
- **`app.account.streams.engagement-picker`** · _beta_ · since 0.11.0 — Hub Studio picker that surfaces the @openalicelabs/presets engagement catalogue (qa-host / battle-chat / cozy-hangout / coding-stream / lecture-moderator / talk-show-panel). Writes the chosen id into agent_config.products.live.preset_id; the next /live/start defaults to it.  
  *live-hub/src/app/account/streams/_components/EngagementPresetPicker.tsx:40*
- **`app.account.streams.engagement-preset-fetch`** · _beta_ · since 0.11.0 — Read the streamer's saved engagement-preset id (qa-host / battle-chat / cozy-hangout / coding-stream / lecture-moderator / talk-show-panel) from agent_config.products.live.preset_id. Persists across sessions; the next /live/start picks it up automatically.  
  *live-hub/src/lib/api/broadcasts.ts:197*
- **`app.account.streams.engagement-presets`** · _beta_ · since 0.11.0 — Hub-side typed mirror of the @openalicelabs/presets engagement catalogue (qa-host / battle-chat / cozy-hangout / coding-stream / lecture-moderator / talk-show-panel). Drives the Studio preset picker; the streamer's selected id rides on agent_config.products.live.preset_id and is forwarded to live-control's /live/start request.  
  *live-hub/src/lib/engagementPresets.ts:23*
- **`app.account.streams.engagement-preset-save`** · _beta_ · since 0.11.0 — Persist the streamer's preferred engagement preset (qa-host / etc) into agent_config.products.live.preset_id via the SDK's putAgentConfig. Forward-compat unknown-keys ride-through means the field rides on top of the typed ProductAgentConfig shape without a backend migration.  
  *live-hub/src/lib/api/broadcasts.ts:222*
- **`app.account.streams.host-health`** · _beta_ · since 0.13.1 — Per-broadcast Host Health card on /account/streams. Polls /api/streamhost/heartbeat every 15s and renders a RSS / disk / ffmpeg / RTMP / interruptions / last-frame summary plus a Reset host button.  
  *live-hub/src/app/account/streams/_components/HostHealthCard.tsx:32*
- **`app.account.streams.poll-manager`** · _beta_ · since 0.12.0 — Hub poll management surface — Create modal, active poll panel with countdown + Close-early, history list. Mounts under /account/streams when a broadcast is live.  
  *live-hub/src/app/account/streams/_components/PollManager.tsx:31*
- **`app.account.streams.scene-preview`** · _beta_ · since 0.6.0 — Two buttons in the Hub's stream-destinations page that open the live renderer scene URL (with agents) and a no-agent demo room for previewing without going on-air.  
  *live-hub/src/app/account/streams/_components/ScenePreviewButtons.tsx:55*
- **`app.account.streams.theme-picker`** · _beta_ · since 0.6.0 — Studio-side picker for the per-stream visual theme. 7-preset gallery + custom-override editor (colors / fonts / texture / corners / shadow / motion) + live iframe preview. Persists to agent_config.products.live.branding.theme — the same JSONB the live page reads.  
  *live-hub/src/app/account/streams/_components/ThemePicker.tsx:69*
- **`app.account.streams.theme-picker.sfx`** · _beta_ · since 0.9.0 — Hub-side picker section for the per-stream sound-effect engine. Master toggle + volume + 5-preset gallery (silent / subtle-warm / arcade / sci-fi / nature) + per-event URL & volume overrides. Persists into branding.theme.sfx — the same JSONB the live page's SfxPlayer reads.  
  *live-hub/src/app/account/streams/_components/SfxPickerSection.tsx:25*
- **`app.account.streams.theme-quick-swap`** · _beta_ · since 0.6.0 — Mid-stream theme flip UI — compact preset chips that fire a transient `branding.theme` payload onto the live-control bus so the streamer can change the look mid-broadcast without editing the persistent agent_config. Shows a banner when the live theme differs from the saved one.  
  *live-hub/src/app/account/streams/_components/ThemeQuickSwap.tsx:72*
- **`app.account.streams.theme-save`** · _beta_ · since 0.6.0 — Persist the per-stream theme into agent_config.products.live.branding.theme via the SDK's putAgentConfig — preserves every other live-product field on the same object.  
  *live-hub/src/lib/api/broadcasts.ts:131*
- **`app.account.youtube`** · _beta_ · since 0.12.0 — Hub page for connecting a YouTube channel (OAuth youtube.readonly + youtube.force-ssl), inspecting granted scopes, and toggling per-tenant mod policies. Companion of /account/streams/[id]/chat-mod. Drives the YT Tier 1 wave Hub UI surface.  
  *live-hub/src/app/account/youtube/page.tsx:53*
- **`app.account.youtube.audit-feed`** · _beta_ · since 0.13.0 — Hub card surfacing the persona oauth_audit_log feed for the calling tenant on /account/youtube. Renders op + status + ts + truncated IP. Powered by GET /v1/me/youtube/audit.  
  *live-hub/src/app/account/youtube/page.tsx:773*
- **`app.account.youtube.auto-mod`** · _beta_ · since 0.13.0 — Hub page for the static auto-moderation rule set + shared mod-action audit log. Companion of /account/youtube + /account/ai-moderation.  
  *live-hub/src/app/account/youtube/auto-mod/page.tsx:50*
- **`app.account.youtube.helpers`** · _stable_ · since 0.13.0 — Time / IP formatters used by the YouTube settings page (expires-in countdown, "last refreshed" indicator, audit-feed IP truncator).  
  *live-hub/src/app/account/youtube/helpers.ts:8*
- **`app.studio.chat.send`** · _beta_ · since 0.13.0 — Hub-side wrapper around live-control `/live/chat-out`. POSTs a JWT-authed chat reply to the per-broadcast chat-bridge fan-out so the streamer can reply to Twitch/YouTube/Kick chat from inside the Studio Chat tab.  
  *live-hub/src/lib/api/chat.ts:167*
- **`app.studio.chat-tab`** · _beta_ · since 0.13.0 — Studio Hub chat tab — subscribes to /auth/ws for cross-platform chat (Twitch/YouTube/Kick/TikTok) and writes back via live-control /live/chat-out so streamers monitor + reply without leaving the Hub.  
  *live-hub/src/app/account/streams/_components/StudioChatTab.tsx:98*

### app/api
- **`app.api.internal.streamhost.heartbeat`** · _beta_ · since 0.13.1 — Internal-secret-gated POST receiver for the openalice-stream-host daemon's 30s heartbeat. Stashes the latest payload per (user_id, host_id) so /account/streams can render a Host Health card.  
  *live-hub/src/app/api/internal/streamhost/heartbeat/route.ts:15*
- **`app.api.persona-bff`** · _stable_ · since 0.7.0 — Catch-all reverse proxy from openalice-app to internal persona-server. Forwards Authorization + streams multipart bodies; passthrough for status + headers. Same-origin to the Hub so CORS is moot.  
  *live-hub/src/app/api/persona/[...path]/route.ts:40*
- **`app.api.radio-bff`** · _beta_ · since 0.11.0 — Catch-all reverse proxy from openalice-app to internal openalice-radio. Forwards Authorization + passes JSON bodies through. Same-origin to the Hub so CORS is moot.  
  *live-hub/src/app/api/radio/[...path]/route.ts:10*
- **`app.api.streamhost.heartbeat-read`** · _beta_ · since 0.13.2 — Browser-facing JWT-gated read for the stream-host heartbeat snapshot. Returns hosts for the calling user (sub claim) by reading the same in-memory store the internal POST receiver writes to.  
  *live-hub/src/app/api/streamhost/heartbeat/route.ts:36*
- **`app.api.youtube-oauth-callback`** · _stable_ · since 0.12.1 — Completes the YouTube OAuth round-trip server-side — state check, code exchange, channel snapshot, signed handoff cookie, redirect to /account/youtube?just_connected=1. Client-side /complete finishes the persona POST.  
  *live-hub/src/app/api/youtube/oauth/callback/route.ts:40*
- **`app.api.youtube-oauth-complete`** · _stable_ · since 0.12.1 — Client-side completion of the YT OAuth round-trip — reads the handoff cookie set by /callback, verifies its signature, forwards to persona /v1/me/youtube/oauth-tokens with the user's Bearer JWT.  
  *live-hub/src/app/api/youtube/oauth/complete/route.ts:31*
- **`app.api.youtube-oauth-start`** · _beta_ · since 0.12.0 — Builds Google OAuth URL with youtube.readonly + youtube.force-ssl + youtube scopes, stashes CSRF state cookie, redirects to Google. Companion of /api/youtube/oauth/callback. Drives /account/youtube Connect button + the stream-key auto-fetch flow.  
  *live-hub/src/app/api/youtube/oauth/start/route.ts:41*

### app/auth
- **`app.auth.forgot-password`** · _stable_ · since 0.1.0 — Public form that requests a password-reset link by email.  
  *live-hub/src/app/forgot-password/page.tsx:25*
- **`app.auth.login`** · _stable_ · since 0.1.0 — Public login + signup form with two-leg MFA prompt. Honors  
  *live-hub/src/app/login/page.tsx:28*
- **`app.auth.reset-password`** · _stable_ · since 0.1.0 — Set a new password using a single-use reset token.  
  *live-hub/src/app/reset-password/page.tsx:22*
- **`app.auth.safe-return-url`** · _stable_ · since 0.2.0 — Validate `?return=<url>` query params for post-login redirects.  
  *live-hub/src/lib/safe-return-url.ts:2*
- **`app.auth.verify-email`** · _stable_ · since 0.1.0 — Verify the email-verification token and bounce to /account.  
  *live-hub/src/app/verify/page.tsx:21*

### app/component
- **`app.component.font-picker`** · _beta_ · since 0.6.0 — Custom-font gallery — list, upload (drag-drop + file picker), rename, delete with confirm, "Use as display/body/mono" hooks for the Theme picker.  
  *live-hub/src/app/account/streams/_components/FontPicker.tsx:163*

### app/components
- **`app.components.confirm-dialog`** · _stable_ · since 0.14.0 — Shared async confirm() dialog + useConfirm() hook. Replaces native window.confirm() across the Hub.  
  *live-hub/src/components/ConfirmDialog.tsx:33*

### app/hub
- **`app.hub.launcher`** · _beta_ · since 0.1.0 — Top-level OpenAlice ecosystem launcher tiling Alice, Social, World, Persona, Live, wrapped in the shared EcosystemShell.  
  *live-hub/src/app/page.tsx:82*
- **`app.hub.online-users`** · _beta_ · since 0.5.0 — Hub home widget listing users currently online across the ecosystem (last_seen ≤ 2 min, status ≠ invisible). Polls every 30s.  
  *live-hub/src/app/_components/OnlineUsers.tsx:21*

### app/lib
- **`app.lib.access-token`** · _beta_ · since 0.13.2 — Verify openalice-auth-issued access JWTs on the Hub. Used by routes that need a local user_id from the bearer without forwarding to persona.  
  *live-hub/src/lib/access-token.ts:39*
- **`app.lib.agents-api`** · _beta_ · since 0.11.0 — Typed client for /v1/me/agents on persona-server. Powers the AgentPicker dropdown and the per-agent settings page.  
  *live-hub/src/lib/agentsApi.ts:24*
- **`app.lib.ai-mod-api`** · _beta_ · since 0.13.0 — Typed client for the AI moderation REST surfaces on persona-server. Powers `/account/ai-moderation` settings + pending-review + training-feedback panels.  
  *live-hub/src/lib/aiModApi.ts:26*
- **`app.lib.api-client`** · _beta_ · since 0.12.0 — Shared apiFetch() helper — Bearer header + JSON parse + error normalization. Backs every src/lib/*Api.ts client.  
  *live-hub/src/lib/api-client.ts:47*
- **`app.lib.audio-api`** · _beta_ · since 0.10.0 — Typed client for /v1/me/audio CRUD on persona-server — Bearer JWT, multipart upload, JSON rename, public URL decoration. Powers the Hub Music & Audio picker.  
  *live-hub/src/lib/audioApi.ts:37*
- **`app.lib.fonts-api`** · _beta_ · since 0.6.0 — Typed client for /v1/me/fonts CRUD on persona-server — Bearer JWT, multipart upload, JSON rename, public URL decoration.  
  *live-hub/src/lib/fontsApi.ts:37*
- **`app.lib.forwarded-origin`** · _stable_ · since 0.12.1 — Reconstructs the public origin (proto://host) from a NextRequest by honouring NEXT_PUBLIC_HUB_BASE_URL + X-Forwarded-Host/Proto, falling back to nextUrl.origin. Fixes 0.0.0.0:3203 leaking into OAuth redirects.  
  *live-hub/src/lib/forwarded-origin.ts:31*
- **`app.lib.idle-config-api`** · _beta_ · since 0.11.2 — Typed client for /v1/me/agents/:id/idle-config — backs the Hub's Idle behaviour section.  
  *live-hub/src/lib/idleConfigApi.ts:23*
- **`app.lib.live-scenes`** · _stable_ · since 0.14.0 — Canonical scene-mode catalog for the Hub. Mirrors openalice-live/SCENE_REGISTRY. Drives "Allowed scenes" config + channel-point trigger validation.  
  *live-hub/src/lib/liveScenes.ts:24*
- **`app.lib.lounge-api`** · _beta_ · since 0.11.0 — Hub-side typed client for Discord-Style Lounge — listing rooms + Phase-4 moderation primitives (bans, mutes, mods, settings).  
  *live-hub/src/lib/loungeApi.ts:11*
- **`app.lib.lounge-reports-ws`** · _beta_ · since 0.13.0 — Live WS subscription client for the Hub Reports tab — opens the chat WS, sends subscribe_mods after joined, surfaces report_received/report_resolved, reconnects with exponential backoff, signals fallback after 3 failures/60s, 30s heartbeat ping.  
  *live-hub/src/lib/loungeReportsWs.ts:59*
- **`app.lib.music-gen-api`** · _beta_ · since 0.11.0 — Typed client for /v1/me/audio/generate (AI music generation via Lyria 3). Same Bearer JWT + same-origin BFF transport as audioApi. Powers the Hub generate page.  
  *live-hub/src/lib/musicGenApi.ts:29*
- **`app.lib.polls-api`** · _beta_ · since 0.12.0 — Typed client for /v1/me/polls (engagement polls). Powers the Hub Create-poll modal + active poll panel + history list.  
  *live-hub/src/lib/pollsApi.ts:24*
- **`app.lib.redemption-triggers-api`** · _beta_ · since 0.11.0 — Typed client for /v1/me/redemption-triggers CRUD on persona-server. Powers the Hub Channel-Point Triggers page where streamers bind viewer redemptions to live actions.  
  *live-hub/src/lib/redemptionTriggersApi.ts:37*
- **`app.lib.soundboard-settings-api`** · _beta_ · since 0.10.1 — Typed client for the per-agent soundboard kill switch on persona-server. Powers the Hub agent-settings toggle.  
  *live-hub/src/lib/soundboardSettingsApi.ts:22*
- **`app.lib.sounds-api`** · _beta_ · since 0.10.0 — Typed client for /v1/me/sounds CRUD + play on persona-server. Powers the Hub Soundboard page where streamers upload + tag SFX for the agent's `play_sound` tool.  
  *live-hub/src/lib/soundsApi.ts:31*
- **`app.lib.static-mod-api`** · _beta_ · since 0.13.0 — Typed client for the static-mod REST surface on persona-server. Powers `/account/youtube/auto-mod` Static rules tab + Action log tab.  
  *live-hub/src/lib/staticModApi.ts:22*
- **`app.lib.use-auto-dismiss-error`** · _stable_ · since 0.14.0 — React hook that auto-dismisses an error string after a configurable timeout. Single source of truth for the Hub's auto-fading error banners.  
  *live-hub/src/lib/use-auto-dismiss-error.ts:31*
- **`app.lib.use-redirect-on-401`** · _stable_ · since 0.14.0 — React hook that redirects to /login?return=<path> when a page's error state indicates an expired session.  
  *live-hub/src/lib/use-redirect-on-401.ts:30*
- **`app.lib.voices-api`** · _beta_ · since 0.11.1 — Curated voice catalogue + browser-native TTS preview for the per-agent Voice editor. Lists ElevenLabs handles + the Default sentinel; preview uses SpeechSynthesisUtterance locally so sliders demo without burning ElevenLabs quota.  
  *live-hub/src/lib/voicesApi.ts:24*
- **`app.lib.vrms-api`** · _beta_ · since 0.11.1 — Typed client for the per-tenant VRM library on persona-server. Powers the Hub's per-agent Avatar picker.  
  *live-hub/src/lib/vrmsApi.ts:18*
- **`app.lib.youtube-api`** · _beta_ · since 0.12.0 — Typed client for YT OAuth + mod-page surfaces on persona-server. Powers `/account/youtube` settings page + `/account/streams/[id]/chat-mod` mod view.  
  *live-hub/src/lib/youtubeApi.ts:38*
- **`app.lib.yt-handoff`** · _stable_ · since 0.12.1 — HS256 JWT helpers (sign + verify) for the YouTube OAuth callback handoff cookie. Uses SHARED_JWT_SECRET + aud="oa-yt-handoff".  
  *live-hub/src/lib/yt-handoff.ts:38*

### app/radio
- **`app.radio.api-client`** · _beta_ · since 0.4.0 — Typed fetch wrappers (RadioChannel / RadioTrack / NowPlayingSnapshot) for the Hub's /api/radio/* BFF proxy — channel CRUD, track listing, and now-playing snapshots consumed by the radio picker page.  
  *live-hub/src/app/account/radio/radioApi.ts:8*
- **`app.radio.components`** · _beta_ · since 0.4.0 — Presentational pieces for the radio picker page — ChannelCard with now-playing badge and "Set as BGM" action, plus the supporting list / empty-state primitives consumed by /account/radio.  
  *live-hub/src/app/account/radio/_components.tsx:7*

### app/sounds
- **`app.sounds.agent-picker`** · _beta_ · since 0.5.0 — Dropdown that scopes a soundboard entry to a specific agent (or leaves it tenant-wide); fetches agents via /api/persona/v1/me/agents with graceful "Loading…" and "tenant-wide only" fallback states.  
  *live-hub/src/app/account/sounds/_components/AgentPicker.tsx:29*

### app/streamhost
- **`app.streamhost.heartbeat-store`** · _beta_ · since 0.5.0 — In-process record of the latest 30s heartbeat from each tenant's openalice-stream-host daemon — captures encoder, watchdog events, RSS, and stream-state so the Host Health card on /account/streams can render liveness without a database round-trip.  
  *live-hub/src/app/api/internal/streamhost/heartbeat/_store.ts:9*

### app/streams/theme-picker
- **`app.streams.theme-picker.avatar-render`** · _beta_ · since 0.6.0 — Authoring panel for branding.theme.avatar_render — picks 3D / 2D-toon / 2D-flat render mode and exposes outline (width + colour), hologram (intensity + tint), and advanced chroma/scan/flicker/glow controls as a post-process chain over the persona VRM.  
  *live-hub/src/app/account/streams/_components/_theme-picker/AvatarRenderSection.tsx:26*
- **`app.streams.theme-picker.background`** · _beta_ · since 0.6.0 — Scene backdrop authoring sub-panel — colour / image / video / preset modes plus particles (density + tint), drift (slow-pan / Ken Burns), and a parallax-layer editor; persisted into branding.theme.scene_background and rendered live by SceneBackgroundLayer.  
  *live-hub/src/app/account/streams/_components/_theme-picker/BackgroundSection.tsx:24*
- **`app.streams.theme-picker.custom-override`** · _beta_ · since 0.6.0 — Expand/collapse accordion that wraps the granular field editors (Colors / Typography / Texture / Chrome) and the four sub-section panels (Avatar render / Background / Music / SFX) inside the ThemePicker so streamers can override individual fields on top of a preset.  
  *live-hub/src/app/account/streams/_components/_theme-picker/CustomOverrideEditor.tsx:19*
- **`app.streams.theme-picker.fields`** · _beta_ · since 0.6.0 — Shared presentational field primitives (FieldSection wrapper, labelled colour pickers, segmented controls, sliders) reused across every ThemePicker sub-panel; pure props-driven, no module-local state or I/O.  
  *live-hub/src/app/account/streams/_components/_theme-picker/fields.tsx:16*
- **`app.streams.theme-picker.music-audio`** · _beta_ · since 0.6.0 — BGM authoring sub-panel of the theme picker — drives branding.theme.bgm (track select, volume, loop, fade-in/out, duck-during-TTS) and previews the chosen track via a user-gesture Play button so browser autoplay unlocks for the preview iframe.  
  *live-hub/src/app/account/streams/_components/_theme-picker/MusicAudioSection.tsx:31*
- **`app.streams.theme-picker.preset-gallery`** · _beta_ · since 0.6.0 — 16:9 thumbnail grid of the 7 coordinated stream-theme presets — clicking a card re-seeds the working theme to that preset and clears any prior overrides via the onPick callback.  
  *live-hub/src/app/account/streams/_components/_theme-picker/PresetGallery.tsx:13*

### app/streams/theming
- **`app.lib.streamThemes.shim`** · _beta_ · since 0.6.0 — Backwards-compat re-export shim — preserves the legacy `@/lib/streamThemes` import path after the per-theme split into `@/lib/themes/`. Forwards every type, preset, registry, and builder helper unchanged.  
  *live-hub/src/lib/streamThemes.ts:20*
- **`app.lib.themes.barrel`** · _beta_ · since 0.6.0 — Public catalogue surface for the streaming theme system — types, 7 stream presets, 3 avatar-render presets, 8 background presets, 5 SFX presets, and builder helpers.  
  *live-hub/src/lib/themes/index.ts:22*
- **`app.lib.themes.builder`** · _beta_ · since 0.6.0 — Stateless StreamTheme helpers — compactTheme strip-empty, encodeThemeForPreview URL-safe JSON, STREAM_THEME_FIELDS form-builder map, STREAM_FONT_OPTIONS curated font-family list.  
  *live-hub/src/lib/themes/builder.ts:11*
- **`app.lib.themes.types.avatar-render`** · _beta_ · since 0.6.0 — Persona-side post-processing contract — render-mode (3d / 2d-toon / 2d-flat), outline, hologram, chromatic aberration, scan-lines, CRT flicker, glow, toon-steps.  
  *live-hub/src/lib/themes/types/avatar-render.ts:13*
- **`app.lib.themes.types.bgm`** · _beta_ · since 0.6.0 — Hub-side BGM config mirror — drives the Music & Audio picker and the live BgmPlayer auto-fade / ducking behaviour.  
  *live-hub/src/lib/themes/types/bgm.ts:13*
- **`app.lib.themes.types.custom-font`** · _beta_ · since 0.6.0 — Per-tenant uploaded font binding — family-name → woff2 URL. The live StreamThemeProvider emits one @font-face rule per entry.  
  *live-hub/src/lib/themes/types/custom-font.ts:11*
- **`app.lib.themes.types.scene-background`** · _beta_ · since 0.6.0 — Hub-side scene-background contract — color / image / video / preset source, parallax sprites, particle overlay, and Ken-Burns / slow-pan drift.  
  *live-hub/src/lib/themes/types/scene-background.ts:10*
- **`app.lib.themes.types.sfx`** · _beta_ · since 0.6.0 — Hub-side SFX config + preset shape mirrors. Drives the picker's Sound Effects section and the live SfxPlayer event listeners.  
  *live-hub/src/lib/themes/types/sfx.ts:17*
- **`app.lib.themes.types.stream-theme`** · _beta_ · since 0.6.0 — Core StreamTheme contract — the flat design-token bag every theme preset materialises and the Hub theme picker mutates.  
  *live-hub/src/lib/themes/types/stream-theme.ts:15*

### app/streams/theming/avatar-render
- **`app.lib.themes.avatar-render.cinema-clean-3d`** · _beta_ · since 0.6.0 — Zero-cost 3D render-mode preset — no outline, no hologram. The "reset to default" option for the avatar-render picker.  
  *live-hub/src/lib/themes/avatar-render-presets/cinema-clean-3d.ts:5*
- **`app.lib.themes.avatar-render.holo-cyan`** · _beta_ · since 0.6.0 — Cyan hologram render preset — 2D-toon + white outline + 0.3 holo intensity + 1.5 Hz CRT flicker + 0.25 scan-line undertone. The transmission / sci-fi look.  
  *live-hub/src/lib/themes/avatar-render-presets/holo-cyan.ts:18*
- **`app.lib.themes.avatar-render.neuro-sama-classic`** · _beta_ · since 0.6.0 — Cel-shaded 2D-toon avatar render preset with #222222 ink outline + 4 toon-steps. The polished-VTuber default.  
  *live-hub/src/lib/themes/avatar-render-presets/neuro-sama-classic.ts:17*
- **`app.lib.themes.avatar-render.registry`** · _beta_ · since 0.6.0 — 3-preset avatar render-mode catalogue — cinema-clean-3d / neuro-sama-classic / holo-cyan + picker meta. Drives the AvatarRenderSection of ThemePicker.  
  *live-hub/src/lib/themes/avatar-render-presets/index.ts:6*

### app/streams/theming/backgrounds
- **`app.lib.themes.backgrounds.anime-classroom`** · _beta_ · since 0.6.0 — Slice-of-life classroom scene — static WebP base + a parallax cloud sprite (screen-blend, 0.7 opacity, 0.15 speed) + low-density cherry-blossom petals.  
  *live-hub/src/lib/themes/backgrounds/anime-classroom.ts:4*
- **`app.lib.themes.backgrounds.aurora`** · _beta_ · since 0.6.0 — Northern-lights scene — static WebP aurora + low-density pale-cyan stars + Ken-Burns drift. Night-sky / dreamy mood.  
  *live-hub/src/lib/themes/backgrounds/aurora.ts:4*
- **`app.lib.themes.backgrounds.beach-sunset`** · _beta_ · since 0.6.0 — Tropical-sunset scene — looping waves video + Ken-Burns slow drift, no particle overlay.  
  *live-hub/src/lib/themes/backgrounds/beach-sunset.ts:4*
- **`app.lib.themes.backgrounds.cdn`** · _beta_ · since 0.6.0 — CDN base-URL helper for bundled scene assets — `${NEXT_PUBLIC_CDN_URL ?? cdn.blal.pro}/scenes`. Imported by every bundled background preset.  
  *live-hub/src/lib/themes/backgrounds/cdn.ts:12*
- **`app.lib.themes.backgrounds.cozy-cafe`** · _beta_ · since 0.6.0 — Coffee-shop interior scene background — looping video, slow-pan drift, no particle overlay.  
  *live-hub/src/lib/themes/backgrounds/cozy-cafe.ts:4*
- **`app.lib.themes.backgrounds.cyberpunk-city`** · _beta_ · since 0.6.0 — Cyberpunk neon-street scene — looping video + cool-cyan rain overlay at 0.6 density. No drift, ambient motion comes from the video.  
  *live-hub/src/lib/themes/backgrounds/cyberpunk-city.ts:4*
- **`app.lib.themes.backgrounds.lofi-room`** · _beta_ · since 0.6.0 — Cozy lofi-room scene background — looping video + warm-amber dust particles at 0.3 density, no drift. Chill / study stream mood.  
  *live-hub/src/lib/themes/backgrounds/lofi-room.ts:4*
- **`app.lib.themes.backgrounds.none`** · _beta_ · since 0.6.0 — No-op background — solid theme.bg color, no particles, no drift. The "reset" option in the background picker.  
  *live-hub/src/lib/themes/backgrounds/none.ts:4*
- **`app.lib.themes.backgrounds.registry`** · _beta_ · since 0.6.0 — 8-preset scene-background catalogue + picker meta + segmented-control option lists (particles / drift / parallax-blend).  
  *live-hub/src/lib/themes/backgrounds/index.ts:12*
- **`app.lib.themes.backgrounds.space-stars`** · _beta_ · since 0.6.0 — Deep-space scene — solid #020208 base + white twinkling stars at 0.7 density + slow-pan drift. Sci-fi / coding-stream mood.  
  *live-hub/src/lib/themes/backgrounds/space-stars.ts:4*

### app/streams/theming/presets
- **`app.lib.themes.presets.editorial-print`** · _beta_ · since 0.6.0 — Newsprint stream theme — off-white canvas, brick-red accent, serif stack, grid pattern, hard shadows + static motion. The longform-essay mood.  
  *live-hub/src/lib/themes/presets/editorial-print.ts:4*
- **`app.lib.themes.presets.glass-dark`** · _beta_ · since 0.6.0 — Vision-Pro inspired frosted-glass stream theme — pastel-purple primary, soft shadow, noise pattern, big 20px corner radius.  
  *live-hub/src/lib/themes/presets/glass-dark.ts:4*
- **`app.lib.themes.presets.minimal-mono`** · _beta_ · since 0.6.0 — Swiss-design stream theme — pure black on white, square corners, no shadow, static motion. Typography-portfolio / lecture mood.  
  *live-hub/src/lib/themes/presets/minimal-mono.ts:4*
- **`app.lib.themes.presets.pastel-bubblegum`** · _beta_ · since 0.6.0 — Kawaii / cozy stream theme — bubblegum pink + mint accent, rounded 24px corners, Quicksand display, playful motion + dots pattern.  
  *live-hub/src/lib/themes/presets/pastel-bubblegum.ts:4*
- **`app.lib.themes.presets.registry`** · _beta_ · since 0.6.0 — 7-preset stream-theme catalogue + picker meta. Drives the ThemePicker gallery + the resolveTheme helper that seeds defaults before custom overrides.  
  *live-hub/src/lib/themes/presets/index.ts:10*
- **`app.lib.themes.presets.vintage-sepia`** · _beta_ · since 0.6.0 — Old-paper stream theme — sepia ink on cream, brick-orange accent, olive secondary, serif stack. The library / radio-show mood.  
  *live-hub/src/lib/themes/presets/vintage-sepia.ts:4*
- **`app.lib.themes.presets.vtuber-neon-cyan`** · _beta_ · since 0.6.0 — Cyberpunk stream theme — electric cyan accent, magenta secondary, monospace stack, warp bg-pattern, hard shadows + playful motion.  
  *live-hub/src/lib/themes/presets/vtuber-neon-cyan.ts:4*
- **`app.lib.themes.presets.vtuber-warm`** · _beta_ · since 0.6.0 — Atelier-default stream theme — warm amber accent on cream, soft shadow + noise pattern. The cozy long-form-stream default.  
  *live-hub/src/lib/themes/presets/vtuber-warm.ts:4*

### app/streams/theming/sfx
- **`app.lib.themes.sfx.arcade`** · _beta_ · since 0.6.0 — Arcade SFX preset — 8-bit blips, retro pops, coin pings, fanfare. Tuned for gaming / retro streams.  
  *live-hub/src/lib/themes/sfx-presets/arcade.ts:4*
- **`app.lib.themes.sfx.cdn`** · _beta_ · since 0.6.0 — CDN base-URL helper for bundled SFX clips — `${NEXT_PUBLIC_CDN_URL ?? cdn.blal.pro}/audio/sfx`. Imported by every bundled SFX preset.  
  *live-hub/src/lib/themes/sfx-presets/cdn.ts:6*
- **`app.lib.themes.sfx.nature`** · _beta_ · since 0.6.0 — Nature SFX preset — water drops, wind, soft chimes, bird tweets. Tuned for cozy / lofi streams.  
  *live-hub/src/lib/themes/sfx-presets/nature.ts:4*
- **`app.lib.themes.sfx.registry`** · _beta_ · since 0.6.0 — 5-preset SFX catalogue + 5-channel event meta + default volume constants. Drives the SfxPickerSection of ThemePicker and the live SfxPlayer event dispatch.  
  *live-hub/src/lib/themes/sfx-presets/index.ts:10*
- **`app.lib.themes.sfx.sci-fi`** · _beta_ · since 0.6.0 — Sci-fi SFX preset — synth pulses, hologram zaps, deep bass thumps. Tuned for cyberpunk / hacker streams.  
  *live-hub/src/lib/themes/sfx-presets/sci-fi.ts:4*
- **`app.lib.themes.sfx.silent`** · _beta_ · since 0.6.0 — Silent SFX preset — empty URLs in every slot. The opt-out default so streamers who enable SFX but don't pick a preset get zero sound.  
  *live-hub/src/lib/themes/sfx-presets/silent.ts:4*
- **`app.lib.themes.sfx.subtle-warm`** · _beta_ · since 0.6.0 — Subtle-warm SFX preset — quiet bell + soft pop + gentle whoosh + warm doorbell + light click. The lowest-distraction default for most streams.  
  *live-hub/src/lib/themes/sfx-presets/subtle-warm.ts:4*

### chat-bridge/aggregator
- **`chat-bridge.aggregator.chat_feed`** — FeedAggregator maintains a per-(platform, channel_id) 30-second rolling window of ChatMessage records. On each tick it emits a ChatFeedPayload to auth's /internal/chat-feed containing: message_count, participant_count, top-5 n-gram topics (unigram– trigram, English+Russian stopwords filtered), mood label (positive/negative/mixed/neutral via polarity lexicon), and 3 highlight quotes (longest distinct-author messages, ≤280 chars). Empty windows still emit — "room went quiet" is an agent signal. The tick loop is spawned by `spawn_tick_loop` and runs at the window interval on a separate Tokio task. Per-(platform, channel_id) we keep a rolling window of recent messages. Every tick we: 1. Drop messages older than the window. 2. Count n-grams across the remaining text → top topics. 3. Pick top-K verbatim messages (longest, distinct authors) → highlight quotes. 4. Sentence-level polarity-lexicon scan → mood (or `None` if we don't see enough signal). 5. POST a ChatFeedPayload to auth `/internal/chat-feed`. Empty windows are still emitted — "the room went quiet" is itself a signal the agent might want to act on.  
  *live-chat-bridge/src/aggregator.rs:6*
- **`chat-bridge.aggregator.feed-state`** · _beta_ · since 0.2.0 — FeedAggregator — per-(platform, channel_id) rolling 30s window of ChatMessage records. `record` appends + eagerly trims; `tick` builds the digest and POSTs to /internal/chat-feed. Holds the Mutex-guarded HashMap + window length + shared HTTP client.  
  *live-chat-bridge/src/aggregator.rs:105*
- **`chat-bridge.aggregator.tick-loop`** · _beta_ · since 0.2.0 — spawn_tick_loop — Tokio task that fires FeedAggregator::tick at the window cadence (default 30s). Emits ChatFeedPayload (message_count + participant_count + top-5 ngram topics + mood + 3 highlight quotes) per channel even when empty so "room went quiet" is itself a signal.  
  *live-chat-bridge/src/aggregator.rs:515*

### chat-bridge/binary
- **`chat-bridge.binary.entrypoint`** · _beta_ · since 0.1.0 — Thin entrypoint — orchestrates init::* (tracing, chat-store, pipeline, alert + redemption sinks), platforms::* spawns (auto-discover + explicit-flag paths), and routes::spawn_obs_if_enabled. Behaviour-preserving split of the historic 1739-LOC main.rs.  
  *live-chat-bridge/src/main.rs:4*

### chat-bridge/boot
- **`chat-bridge.boot.auto-discover`** · _beta_ · since 0.3.0 — Auto-discovery client — GETs openalice-auth `/internal/users/{user}/chat-sources` at boot when OA_AUTO_DISCOVER=true. Each enabled chat-source row becomes a per-platform connector spawn (youtube / twitch / kick / tiktok) alongside the explicit OA_*_CHANNEL/USERNAME flags.  
  *live-chat-bridge/src/init.rs:331*
- **`chat-bridge.init.boot-helpers`** · _beta_ · since 0.4.0 — Boot-time wiring helpers (tracing/OTel subscriber install, optional ChatStore connect, Pipeline + alert/redemption sink assembly, auto-discover client). Replaces the ~370-line bootstrap that used to live inline in main(). Behaviour preserved — same env-var contract, same log lines, same graceful-degradation semantics.  
  *live-chat-bridge/src/init.rs:8*

### chat-bridge/cli
- **`chat-bridge.cli.args`** · _beta_ · since 0.4.0 — clap-derived Args struct — single source of truth for every OA_* env-var the chat-bridge reads at boot. Splitting this out of main.rs keeps the binary entrypoint a thin wiring layer and makes the env-contract grep-friendly.  
  *live-chat-bridge/src/cli.rs:7*

### chat-bridge/contract
- **`chat-bridge.contract.message-to-connector-event`** · _ga_ · since 0.2.0 — Translates bridge-internal ChatMessage into openalice-agent-protocol ConnectorEvent::ChatMessage with stable source.product="live", source.platform, participant id="<platform>:<user_id>", badges + avatar. Cross-platform-stable contract for AgentRuntime consumers.  
  *live-chat-bridge/src/sink.rs:533*

### chat-bridge/dispatch
- **`chat-bridge.dispatch.pipeline`** · _ga_ · since 0.2.0 — Pipeline — fan-out aggregator owned by main.rs. `deliver(msg)` records into FeedAggregator window, fires AuthSink (/internal/chat), optional AgentDispatchSink (/internal/agent/dispatch), optional SceneChatSink (/internal/scene/chat), and fire-and-forget Postgres insert via ChatStore. Each sink is independent — failures are swallowed.  
  *live-chat-bridge/src/sink.rs:363*
- **`chat-bridge.dispatch.pipeline_fanout`** — Pipeline::deliver tees every inbound ChatMessage into five concurrent sinks: (1) AuthSink → POST /internal/chat → /auth/ws agent fan-out; (2) FeedAggregator rolling-window record; (3) AgentDispatchSink → POST /internal/agent/dispatch (per-message AgentRuntime turn); (4) SceneChatSink → POST /internal/scene/chat (vrm-corner overlay); (5) fire-and-forget Postgres INSERT. Static-mod runs BEFORE all sinks so audit rows are durable even if a downstream sink fails. All sinks are fire-and-forget — a failed delivery logs `warn!` and never tears down the relay. @feature chat-bridge.dispatch.agent_dispatch AgentDispatchSink translates each ChatMessage into openalice-agent-protocol's ConnectorEvent::ChatMessage and POSTs it to live-control's /internal/agent/dispatch. Source fields (`product`, `platform`, `channel_id`) are stable across all platforms so every AgentRuntime consumer sees the same event shape regardless of origin. Participant id is prefixed `<platform>:<user_id>` to prevent cross-platform id collisions.  
  *live-chat-bridge/src/sink.rs:5*
- **`chat-bridge.sink.agent-dispatch`** · _beta_ · since 0.2.0 — AgentDispatchSink — POSTs `{broadcast_id, event}` to live-control's `/internal/agent/dispatch` (or social-api's equivalent) so the broadcast-owner's AgentRuntime turn fires per chat message. Service-to-service, X-Internal-Secret gated.  
  *live-chat-bridge/src/sink.rs:284*
- **`chat-bridge.sink.auth`** · _ga_ · since 0.1.0 — AuthSink — POSTs every normalized ChatMessage to openalice-auth's `/internal/chat` endpoint with X-Internal-Secret. Auth fans the message out to the broadcast owner's `/auth/ws` clients. `try_deliver` swallows + logs errors so a single bad request can't tear the bridge down.  
  *live-chat-bridge/src/sink.rs:39*
- **`chat-bridge.sink.scene-alerts`** · _beta_ · since 0.3.0 — SceneAlertSink — POSTs AlertEvent (kind ∈ follow/sub/resub/gift_sub/cheer/raid/donation/tiktok_gift) to live-control's `/internal/scene/alert`. Drives the Streamlabs-style animated overlay card across Twitch, YouTube, Kick, TikTok.  
  *live-chat-bridge/src/sink.rs:197*
- **`chat-bridge.sink.scene-chat`** · _beta_ · since 0.3.0 — SceneChatSink — fan-out of ChatMessage to live-control's `/internal/scene/chat` for the OBS vrm-corner-chat overlay. Stamps platform-coded author colors (twitch #A78BFA, youtube #F87171, kick #22C55E, tiktok #FE2C55).  
  *live-chat-bridge/src/sink.rs:103*

### chat-bridge/http
- **`chat-bridge.http.gdpr-export`** · _beta_ · since 0.4.0 — GET /internal/me/{tenant_id}/export.json — GDPR fan-out target hit by openalice-tenants' export-job worker. Two modes: persistence enabled → real `persisted_messages_count` + per-broadcast counts + recent 5000-row slice projected via ChatMessageRow::into_public; persistence disabled → stub body with explanatory note.  
  *live-chat-bridge/src/routes.rs:471*
- **`chat-bridge.http.observability-server`** · _beta_ · since 0.2.0 — Tiny axum server hosting GET /health (liveness JSON), GET /metrics (Prometheus chat_bridge_up / connectors_active / senders_registered gauges), POST /v1/send, POST /v1/moderate, GET /internal/me/{tenant_id}/export.json. Bound to 0.0.0.0:OA_OBSERVABILITY_PORT (default 3978; 0 disables).  
  *live-chat-bridge/src/routes.rs:588*
- **`chat-bridge.http.routes-module`** · _beta_ · since 0.4.0 — Dedicated routes module — owns ObsState, every axum handler, the router builder, and the spawn helper. Splitting this out of main.rs keeps the binary entrypoint a thin wiring layer and makes the HTTP surface grep-friendly.  
  *live-chat-bridge/src/routes.rs:10*
- **`chat-bridge.http.v1-moderate`** · _beta_ · since 0.3.0 — POST /v1/moderate — X-Internal-Secret-gated moderation relay (ban / timeout / delete / unban). Validates msg_id with [a-zA-Z0-9-]{1..=64} gate (IRC tag-injection guard), strips Twitch's `twitch:` id prefix, hard-skips YouTube/Kick (no OAuth scopes), and queues platform-side via ModeratorCommand.  
  *live-chat-bridge/src/routes.rs:306*
- **`chat-bridge.http.v1-send`** · _beta_ · since 0.3.0 — POST /v1/send — X-Internal-Secret-gated chat write-back endpoint. Maps the agent's chat_write ToolCall to platform-specific PRIVMSG / liveChatMessages.insert sends. Sliding-window rate limiter per platform returns HTTP 429 + retry_after_seconds when every targeted platform is throttled. Fire-and-forget Postgres outbound insert on success.  
  *live-chat-bridge/src/routes.rs:119*

### chat-bridge/ingest
- **`chat-bridge.ingest.kick_pusher`** — Kick chat ingest over Pusher WebSocket (`chatrooms.<id>.v2`). Chatroom id resolved at boot via `kick.com/api/v1/channels/<slug>`. No auth required — public channels are readable without a token. Pusher envelope uses JSON-in-JSON (`data` field is a stringified JSON blob). Responds to both WS-level Ping/Pong and `pusher:ping` app-level keepalives. Reconnects with exponential backoff. Write-back is not implemented (returns 501). `chatroom_id` is per-channel and stable; you can look it up once via `https://kick.com/api/v1/channels/<slug>` (see `lookup_chatroom_id` below). The user supplies the channel slug; we resolve to chatroom_id at boot. Pusher protocol reference: Note: Kick's public chat WebSocket can drift in protocol details more than Twitch IRC. If bugs land, the typical fix is to re-derive the connection URL from the live page's network tab.  
  *live-chat-bridge/src/kick.rs:8*
- **`chat-bridge.ingest.tiktok_webcast`** — TikTok Live ingest via community Webcast WebSocket + protobuf (`tiktoklive` crate v0.0.19). Signing delegated to eulerstream (free or paid tier via `OA_TIKTOK_SIGN_API_KEY`). Emits ChatMessage for WebcastChatMessage events and AlertEvent (kind="tiktok_gift") for WebcastGiftMessage events (diamond count × repeatCount). All other events (likes, joins, shares) are intentionally dropped. Read-only: TikTok write-back requires an invite-only official API. One connector per process (static HandlerBridge slot). Reconnects with exponential backoff + ±20% jitter. Wire flow: 1. HTTP fetch of `https://www.tiktok.com/@<username>/live` to resolve the active room id + signed connection params. 2. WebSocket connect to `wss://webcast<n>.us.tiktok.com/...`. 3. Receive protobuf `WebcastResponse` envelopes; each carries one or more typed `WebcastMessage` payloads. We route: - `WebcastChatMessage`  → [`ChatMessage`] on the bus - `WebcastGiftMessage`  → [`AlertEvent`] kind="tiktok_gift" - everything else (likes, joins, follows, …) is dropped. ## Operational fragility TikTok actively fights bot reads. Two things can break ingestion without code changes here: 1. **eulerstream signing service.** TikTok requires the WS URL to carry signed params minted by an internal service. The `tiktoklive` crate (and every community lib in its lineage) delegates this to a 3rd-party signer at <https://tiktok.eulerstream.com>. When that service is down or rate-limited, `client.connect().await` returns an error and we fall back into the reconnect-with-backoff loop. There is no manual recovery — the loop will resume the moment the signer comes back. Operator override: pass `OA_TIKTOK_SIGN_API_KEY` to upgrade to eulerstream's paid tier (higher rate, higher reliability); the env is read once at boot. 2. **Webcast schema changes.** TikTok adds / renames protobuf fields in lockstep with their web client roll-outs. The `tiktoklive` crate ships pre-generated message shapes, so a breaking schema change downstream of `0.0.19` (the latest published version, archived 2025-07) requires swapping the underlying lib. Mitigation is design-level: the connector surface (`run_tiktok`, [`TikTokChatPoller`]) is a thin shell so a future Option-C sidecar (Python `TikTokLive` exec'd as a child process speaking JSON over stdout) can drop in with zero call-site churn. ## Scope v0 is **read-only**. Posting bot replies back into a TikTok room requires their official OAuth-gated `/v2/chat` endpoint, which is invite-only and out of scope here. This module intentionally registers no outbound `ChatSender` so /v1/send callers see TikTok in `skipped` rather than queuing into a never-flushing mpsc.  
  *live-chat-bridge/src/tiktok.rs:9*
- **`chat-bridge.ingest.twitch_irc`** — Twitch IRC ingest over `wss://irc-ws.chat.twitch.tv:443`. Supports anonymous read (`justinfan*`) and authenticated read+write (OAuth `chat:read` + `chat:edit`). Parses PRIVMSG tags (roles, bits, display-name, tmi-sent-ts) into normalized ChatMessage. USERNOTICE lines produce AlertEvents (sub / resub / gift_sub / raid). Reconnects with exponential backoff (1s → 60s ± 20% jitter). @feature chat-bridge.writeback.twitch_irc Outbound write-back over the same IRC WebSocket. The agent's `chat_write` ToolCall flows through `/v1/send` → SenderRegistry → mpsc queue → PRIVMSG frame (≤500 chars, CR/LF stripped). Reply-parent threading via `@reply-parent-msg-id` tag. Moderation (ban / timeout / delete / unban) prefers Helix REST (`src/twitch_helix.rs`) with a 5s per-command timeout; falls back to IRC slash commands for envs without `OA_TWITCH_CLIENT_ID`. Anonymous read works: PASS = "SCHMOOPIIE" (any string), NICK = "justinfan12345". Authenticated read (recommended for prod, lifts rate limits + lets us send messages back) uses `oauth:<user_token>` from a Helix OAuth flow with `chat:read` + `chat:edit` scopes.  
  *live-chat-bridge/src/twitch.rs:9*
- **`chat-bridge.ingest.youtube_polling`** — YouTube Live Chat ingest via `liveChatMessages.list` polling. Supports API-key auth (public chats) and OAuth (auto-discovers active broadcast, refreshes access token on 401). Poll interval is API-server-driven (`pollingIntervalMillis`, clamped ≥ 2s). Emits ChatMessage for textMessageEvent / superChatEvent / superStickerEvent. Alert fan-out: cheer (SuperChat/Sticker), sub/member_joined (newSponsorEvent), resub/member_milestone (memberMilestoneChatEvent), gift_sub/channel_sponsorship (membershipGiftingEvent). SuperChat/Sticker also route through RedemptionSink with a tier label ($1/$2/$5/$10/$20/$50/$100). Quota: each `liveChatMessages.list` call costs 5 quota units; Google's default daily budget is 10,000 units. A 1-hour stream polling every 5s eats ≈ 720 calls × 5 = 3600 units, which fits. Production deploys should request a quota raise.  
  *live-chat-bridge/src/youtube.rs:10*

### chat-bridge/kick
- **`chat-bridge.kick.chatroom-resolve`** · _beta_ · since 0.2.0 — Resolves a Kick channel slug to its numeric chatroom_id via kick.com/api/v1/channels/<slug>. Rejects non-browser user-agents so requests masquerade as Mozilla; 10s timeout.  
  *live-chat-bridge/src/kick.rs:68*
- **`chat-bridge.kick.connector-loop`** · _beta_ · since 0.2.0 — Supervises one Kick Pusher WebSocket session — subscribes to `chatrooms.<id>.v2`, handles ping/pong (both WS-level and pusher-app-level keepalives), parses `App\\Events\\ChatMessageEvent` JSON-in-JSON envelopes, reconnects with exponential 1s→60s backoff. Read-only.  
  *live-chat-bridge/src/kick.rs:112*
- **`chat-bridge.kick.poller-state`** · _beta_ · since 0.2.0 — KickChatPoller — per-broadcast state for one Kick public chat WebSocket connection (slug + resolved chatroom_id + broadcast_id stamp). Built by `for_slug` which performs the chatroom_id lookup.  
  *live-chat-bridge/src/kick.rs:89*

### chat-bridge/observability
- **`chat-bridge.tracing.otel-bootstrap`** · _beta_ · since 0.4.0 — Opt-in OpenTelemetry OTLP exporter (gRPC tonic transport, opt-in via OTEL_EXPORTER_OTLP_ENDPOINT). Stamps service.name=openalice-chat-bridge + service.version from CARGO_PKG_VERSION as Resource attributes; registers the tracer globally so any direct OTel API caller lands on the same provider.  
  *live-chat-bridge/src/init.rs:105*
- **`chat-bridge.tracing.subscriber-install`** · _beta_ · since 0.4.0 — Always-on fmt subscriber + opt-in OpenTelemetry OTLP layer (env-gated via OTEL_EXPORTER_OTLP_ENDPOINT). Returns silently on success; logs `warn!` when the OTel branch fails so the binary stays up on logs-only.  
  *live-chat-bridge/src/init.rs:48*

### chat-bridge/persistence
- **`chat-bridge.chat-store.broadcast-counts`** · _beta_ · since 0.4.0 — GROUP BY broadcast_id row counter for a tenant, ordered by most-recent activity. Surfaced as `broadcasts: [{broadcast_id, message_count}]` in the GDPR export slice.  
  *live-chat-bridge/src/chat_store.rs:248*
- **`chat-bridge.chat-store.connect`** · _beta_ · since 0.4.0 — Opens the PgPool, runs sqlx::migrate! to create chat_messages table/indexes idempotently, returns a ChatStore handle.  
  *live-chat-bridge/src/chat_store.rs:77*
- **`chat-bridge.chat-store.handle`** · _beta_ · since 0.4.0 — ChatStore — Postgres-backed history handle around a sqlx::PgPool (max 8 connections, 5s acquire, 600s idle). Opt-in via CHAT_BRIDGE_DB_URL. Construction runs the bundled `migrations/0001_chat_messages.sql` idempotently. Cloned + pushed into tokio::spawn per delivery so the relay hot path never blocks on Postgres.  
  *live-chat-bridge/src/chat_store.rs:60*
- **`chat-bridge.chat-store.insert-inbound`** · _beta_ · since 0.4.0 — Persist one inbound chat row (direction="inbound") with v7 UUID id, broadcast/tenant scope, platform-normalised name, sender_username + SHA-256 sender_id_hash (anonymisable analytics) + JSONB metadata (badges/paid/upstream_id). Hot-path safe — caller wraps in tokio::spawn.  
  *live-chat-bridge/src/chat_store.rs:121*
- **`chat-bridge.chat-store.insert-outbound`** · _beta_ · since 0.4.0 — Persist one outbound chat row (direction="outbound") from the agent's `chat_write` ToolCall via /v1/send. Same row shape as inbound — gives a complete two-way replay for GDPR exports and overlay history.  
  *live-chat-bridge/src/chat_store.rs:156*
- **`chat-bridge.chat-store.list-for-tenant`** · _beta_ · since 0.4.0 — Read DESC-by-received_at most-recent rows for a tenant_id (LIMIT clamped to ≤RECENT_EXPORT_LIMIT=5000). Powers the GDPR `/internal/me/{tenant}/export.json` recent-slice + the future /me/chat-history.json.gz streaming endpoint.  
  *live-chat-bridge/src/chat_store.rs:192*
- **`chat-bridge.persistence.chat_store`** — Postgres-backed chat_messages table (opt-in via CHAT_BRIDGE_DB_URL). ChatStore wraps a PgPool; sqlx::migrate! runs on boot. Each Pipeline::deliver inserts an inbound row and each /v1/send inserts an outbound row — both via fire-and-forget tokio::spawn so the relay hot path is never blocked by Postgres latency. Failure logs warn! and is swallowed. Rows carry: UUID v7 id (sortable + time-prefixed), broadcast_id, tenant_id, platform, sender_username, sender_platform_id, direction (inbound/outbound), text, metadata JSONB (badges, paid info, upstream_id). GDPR export reads counts + a recent 5000-row slice via ChatStore. The GDPR fan-out on tenants surfaced a stub `chat-bridge.json` slice with `persisted_messages_count: 0` and a note explaining the gap. This module backs the inbound + outbound write paths with a Postgres table (`chat_messages`) on the platform-db. Connection is opt-in via `CHAT_BRIDGE_DB_URL`: when unset, `ChatStore::open` returns `Ok(None)` and the per-broadcast bridge runs in its historic stateless mode (no INSERTs, GDPR export keeps surfacing the stub note). When set, `sqlx::migrate!()` brings the table up at boot and every Pipeline::deliver / handle_send call fires a row insert through `tokio::spawn`'d fire-and-forget tasks so the relay hot path never blocks on Postgres latency. Mirrors the shape of `openalice-live-control::broadcasts::BroadcastStore` intentionally — same `connect → migrate → insert/list` pattern, same best-effort failure semantics, same Postgres-gated integration test. Anyone fluent in the broadcasts store will recognise this one immediately.  
  *live-chat-bridge/src/chat_store.rs:10*

### chat-bridge/platforms
- **`chat-bridge.platforms.dispatch-module`** · _beta_ · since 0.4.0 — Top-level platforms module — wraps the per-platform spawn arms (twitch, youtube, kick, tiktok). Each sub-module owns the auto-discovery + explicit-flag spawn paths and shares a SpawnCtx with the shared sinks (pipeline, alerts, redemption) + outbound sender registry.  
  *live-chat-bridge/src/platforms/mod.rs:11*
- **`chat-bridge.platforms.kick`** · _beta_ · since 0.4.0 — Kick spawn dispatcher — resolves the channel slug to numeric chatroom_id via for_slug() then spawns crate::kick::run_kick. Resolve failures log + skip without taking the whole boot down.  
  *live-chat-bridge/src/platforms/kick.rs:5*
- **`chat-bridge.platforms.tiktok`** · _beta_ · since 0.4.0 — TikTok Live spawn dispatcher — accepts either `username` (preferred) or `channel_name` (legacy) on the discovery row, strips the leading @ inside TikTokChatPoller::new, then spawns crate::tiktok::run_tiktok with the shared Pipeline + alert sink.  
  *live-chat-bridge/src/platforms/tiktok.rs:5*
- **`chat-bridge.platforms.twitch`** · _beta_ · since 0.4.0 — Twitch spawn dispatcher — wires IRC (TwitchChatPoller anonymous/authed) + Helix REST (moderation when OA_TWITCH_CLIENT_ID is set) + EventSub WS (follow alerts + channel-point redemptions when the full Helix tuple is present and OA_LIVE_CONTROL_URL is wired). Outbound + moderator mpsc channels are registered with the SenderRegistry only for authenticated connections.  
  *live-chat-bridge/src/platforms/twitch.rs:7*
- **`chat-bridge.platforms.youtube`** · _beta_ · since 0.4.0 — YouTube Live spawn dispatcher — handles both OAuth (access_token + optional refresh_token + auto-discovered liveChatId) and api_key (paste-creds chat_id) flavours. Wraps crate::youtube::run_youtube, threading the shared Pipeline + alert/redemption sinks into the connector task.  
  *live-chat-bridge/src/platforms/youtube.rs:5*

### chat-bridge/rate-limit
- **`chat-bridge.rate-limit.platform-cap-table`** · _beta_ · since 0.3.0 — PlatformLimit — sliding-window cap config (max_messages + window Duration). Hardcoded per-platform table via `for_platform`: twitch 20/30s, youtube[_live] 5/60s, kick 10/60s, unknown 30/60s.  
  *live-chat-bridge/src/rate_limit.rs:46*
- **`chat-bridge.rate-limit.token-bucket`** · _beta_ · since 0.3.0 — OutboundRateLimiter — DashMap<platform, Mutex<VecDeque<Instant>>> sliding-window buckets. try_acquire drops out-of-window timestamps, returns Err(retry_after) when full so /v1/send maps to HTTP 429 with retry_after_seconds.  
  *live-chat-bridge/src/rate_limit.rs:85*

### chat-bridge/redemption
- **`chat-bridge.redemption.event-wire`** · _beta_ · since 0.5.0 — RedemptionEvent — platform-agnostic wire shape POSTed to persona's `/internal/redemption-received`. Closed `platform` enum (twitch_points/youtube_superchat/kick_subscription/tiktok_gift) + redemption_key + viewer fields + optional amount/currency/message. Drives `{{viewer}}`/`{{amount}}` template substitution in tts_shout actions.  
  *live-chat-bridge/src/redemption.rs:37*
- **`chat-bridge.redemption.sink`** · _beta_ · since 0.5.0 — Normalised RedemptionEvent + RedemptionSink that POSTs channel-point / SuperChat / Kick-sub / TikTok-gift events to persona's /internal/redemption-received so the dispatcher can match a redemption_triggers row and fire play_sound / scene_change / gesture / tts_shout / expression actions on the live page.  
  *live-chat-bridge/src/redemption.rs:19*

### chat-bridge/senders
- **`chat-bridge.senders.moderator-command`** · _beta_ · since 0.3.0 — ModeratorCommand — enum of cross-platform moderation actions emitted by /v1/moderate (Ban/Timeout/DeleteMessage/Unban). Twitch routes via Helix REST (preferred) or legacy IRC slash commands; YouTube + Kick currently return "skipped" until OAuth scope sets land.  
  *live-chat-bridge/src/senders.rs:62*
- **`chat-bridge.senders.outbound-chat-message`** · _beta_ · since 0.3.0 — OutboundChat — work item /v1/send pushes onto a connector's mpsc queue. Carries the text + optional reply_to (Twitch reply-parent thread id; YT/Kick ignore).  
  *live-chat-bridge/src/senders.rs:35*
- **`chat-bridge.senders.registry`** · _beta_ · since 0.3.0 — Outbound chat + moderation sender registry — connector tasks register Arc<DashMap> handles at boot; the HTTP /v1/send and /v1/moderate routes look up the matching platform sender and forward the agent's chat_write or moderation ToolCall to Twitch / YouTube / Kick.  
  *live-chat-bridge/src/senders.rs:19*

### chat-bridge/tiktok
- **`chat-bridge.tiktok.connector-loop`** · _beta_ · since 0.4.0 — Supervises one TikTok community Webcast WebSocket session via the tiktoklive crate. Eulerstream-signed connect, sync→async HandlerBridge over a static OnceLock slot (one connector per process), routes OnChat→ChatMessage and OnGift→AlertEvent(kind="tiktok_gift"), drops likes/joins/shares. Reconnects with 1s→60s exponential backoff plus ±20% jitter.  
  *live-chat-bridge/src/tiktok.rs:180*
- **`chat-bridge.tiktok.poller-state`** · _beta_ · since 0.4.0 — TikTokChatPoller — per-broadcast state (sanitised @handle without leading @, broadcast_id stamp, optional eulerstream paid-tier sign_api_key read from OA_TIKTOK_SIGN_API_KEY).  
  *live-chat-bridge/src/tiktok.rs:120*

### chat-bridge/twitch
- **`chat-bridge.twitch.connector-loop`** · _ga_ · since 0.1.0 — Supervises one Twitch IRC WS session — CAP REQ tags+commands, PASS/NICK/JOIN, tokio::select multiplexes inbound PRIVMSG/USERNOTICE/PING, outbound mpsc queue (PRIVMSG up to 500 chars, CR/LF stripped, optional reply-parent-msg-id thread), and moderator-command queue (Helix REST preferred with 5s timeout → falls back to IRC slash). Exponential 1s→60s backoff + ±20% jitter on reconnect.  
  *live-chat-bridge/src/twitch.rs:128*
- **`chat-bridge.twitch.eventsub`** · _beta_ · since 0.5.0 — Twitch Helix EventSub WebSocket transport — subscribes to channel.follow v2 (plus other event types) over wss://eventsub.wss.twitch.tv/ws to close the follow-event gap IRC USERNOTICE can't cover; handles session_welcome / keepalive / reconnect / revocation envelopes.  
  *live-chat-bridge/src/twitch_eventsub.rs:41*
- **`chat-bridge.twitch.eventsub-loop`** · _beta_ · since 0.5.0 — Long-running EventSub WS supervisor. Handles session_welcome (POSTs channel.follow v2 + optionally channel-points redemption subscriptions), session_keepalive, notification routing (follow → AlertEvent, channel_points_custom_reward_redemption → RedemptionEvent), session_reconnect (re-dial new shard with zero backoff), revocation (bail loud).  
  *live-chat-bridge/src/twitch_eventsub.rs:93*
- **`chat-bridge.twitch.helix-client`** · _beta_ · since 0.4.0 — HelixClient — resolved Helix REST handle (oauth, client_id, broadcaster_id, moderator_id). Resolves logins → numeric ids at boot via GET /helix/users. `execute` routes ModeratorCommand to POST /moderation/bans (ban/timeout), DELETE /moderation/bans (unban), DELETE /moderation/chat (delete msg), validating logins ([a-z0-9_]{1,25}) and msg-ids ([a-zA-Z0-9-]{1,64}).  
  *live-chat-bridge/src/twitch_helix.rs:51*
- **`chat-bridge.twitch.helix-moderation`** · _beta_ · since 0.4.0 — Helix REST client for Twitch moderation actions — wraps POST /helix/moderation/bans (ban/timeout), DELETE /helix/moderation/bans (unban), and DELETE /helix/moderation/chat (delete message) since Twitch deprecated the legacy IRC slash-command path in Feb 2023.  
  *live-chat-bridge/src/twitch_helix.rs:28*
- **`chat-bridge.twitch.poller-state`** · _ga_ · since 0.1.0 — TwitchChatPoller — per-channel auth state (channel, nick, pass). `new_anonymous` mints "justinfan<rand>" + "SCHMOOPIIE" pass for read-only public chat; `new_authed` formats `oauth:<token>` for read+write.  
  *live-chat-bridge/src/twitch.rs:51*

### chat-bridge/types
- **`chat-bridge.types.wire`** · _ga_ · since 0.1.0 — Canonical ChatMessage / ChatAuthor / ChatPaid wire shape shared with openalice-auth::ws — the contract every connector normalises into before POSTing to /internal/chat. Drift here breaks the auth-side WebSocket fanout to the broadcast-owner agent.  
  *live-chat-bridge/src/types.rs:5*

### chat-bridge/writeback
- **`chat-bridge.writeback.rate_limiter`** — OutboundRateLimiter enforces per-platform sliding-window caps on agent write-back: Twitch 20/30s, YouTube 5/60s, Kick 10/60s, unknown 30/60s. Implemented as DashMap<platform, Mutex<VecDeque <Instant>>> — per-platform buckets avoid cross-platform contention. try_acquire returns Ok or Err(retry_after_duration) which the /v1/send handler maps to HTTP 429 + Retry-After. Acquisition is O(window_cap) on cleanup, negligible at live-chat message rates. The limiter uses a per-platform `VecDeque<Instant>` of recent send timestamps. On `try_acquire`, we drop timestamps older than the window, then check the remaining count against the cap. Below cap → record `now` and return Ok. At cap → return Err with the wait-until-next-slot duration so the caller can surface a Retry-After header. Caps (start conservative, tighten on real-world telemetry): * twitch  — 20 msg / 30 s   (Twitch IRC limit is 20/30s for non-mod broadcasters) * youtube — 5 msg / 60 s    (YT live-chat throttles bots to ~10/min; we leave headroom) * kick    — 10 msg / 60 s   (no published limit; mirror YT) Lock granularity: one `Mutex<VecDeque<Instant>>` per platform key, hidden behind a DashMap so per-platform contention stays local. Acquire is bounded O(window_cap) on cleanup which is negligible at our message rates.  
  *live-chat-bridge/src/rate_limit.rs:9*

### chat-bridge/youtube
- **`chat-bridge.youtube.alert-parser`** · _beta_ · since 0.3.0 — Detects YouTube monetisation / membership events on a liveChatMessages snippet and emits AlertEvent: superChatEvent/superStickerEvent → "cheer" (USD-equivalent dollars from amountMicros/1M), newSponsorEvent → "sub" (tier int from "Tier N" label), memberMilestoneChatEvent → "resub" (memberMonth), membershipGiftingEvent → "gift_sub" (giftMembershipsCount). giftMembershipReceivedEvent suppressed to avoid double-fire.  
  *live-chat-bridge/src/youtube.rs:442*
- **`chat-bridge.youtube.auth-flavor`** · _ga_ · since 0.2.0 — YouTubeAuth enum — two flavours of YouTube credentials. ApiKey (public-chat read with operator-supplied live_chat_id) vs OAuth (auto-discovery of active broadcasts + refresh_token rotation).  
  *live-chat-bridge/src/youtube.rs:45*
- **`chat-bridge.youtube.connector-loop`** · _ga_ · since 0.2.0 — Long-running YouTube Live Chat polling loop. Honours API-server-recommended `pollingIntervalMillis` (clamped ≥2s), fans chat messages to on_msg, alerts to SceneAlertSink, and SuperChat/Sticker redemptions to RedemptionSink in one pass. Exponential 1s→60s backoff on poll failure.  
  *live-chat-bridge/src/youtube.rs:707*
- **`chat-bridge.youtube.discover-live-chat`** · _beta_ · since 0.3.0 — Auto-discovers the active broadcast's liveChatId via Data API liveBroadcasts.list?broadcastStatus=active. OAuth-only (API-key path returns None). Used at chat-bridge boot when liveChatId is absent from the config row.  
  *live-chat-bridge/src/youtube.rs:84*
- **`chat-bridge.youtube.oauth-refresh`** · _beta_ · since 0.3.0 — Exchanges a refresh_token for a fresh access_token via Google's oauth2 token endpoint (grant_type=refresh_token). Mutates the YouTubeAuth::OAuth variant in place; triggered on 401 from liveChatMessages.list.  
  *live-chat-bridge/src/youtube.rs:144*
- **`chat-bridge.youtube.poll-cycle`** · _ga_ · since 0.3.0 — One liveChatMessages.list page fetch — sends Authorization Bearer (OAuth) or &key= (API key), advances nextPageToken, transparently triggers refresh_oauth_token on 401, returns (chat_messages, alerts, redemption_inputs, next_interval). Costs 5 quota units per call.  
  *live-chat-bridge/src/youtube.rs:283*
- **`chat-bridge.youtube.poller-state`** · _ga_ · since 0.2.0 — YouTubeChatPoller — per-broadcast polling state (HTTP client, YouTubeAuth, live_chat_id, broadcast_id stamp, page_token cursor across polls). Constructed via `new` (API key) or `with_auth` (OAuth).  
  *live-chat-bridge/src/youtube.rs:236*
- **`chat-bridge.youtube.superchat-redemption`** · _beta_ · since 0.5.0 — Translates SuperChat/SuperSticker snippets into platform-agnostic RedemptionEvent for persona's dispatcher. Bins USD-equivalent dollars into seven tier labels ($1/$2/$5/$10/$20/$50/$100) so streamers bind one trigger per tier without exact-amount matching.  
  *live-chat-bridge/src/youtube.rs:582*

### hub/account
- **`hub.account.audio.generate`** · _beta_ · since 0.11.0 — AI music generation page. Lyria 3 via OpenRouter, $0.08/song, $10/month default cap. Async job lifecycle with 5s polling, inline player + add-to-BGM hand-off, recent generations history.  
  *live-hub/src/app/account/audio/generate/page.tsx:21*
- **`hub.account.edit`** · _stable_ · since 0.3.0 — Edit display_name, bio, avatar_url, banner_url, pronouns. One profile across all 6 OpenAlice products.  
  *live-hub/src/app/account/edit/page.tsx:10*

### live/agent-desktop
- **`live.os.standalone-desktop`** · _alpha_ · since 0.4.0 — Standalone Agent Desktop OS — fullscreen virtual desktop the agent drives via the same scene-control WS as the OBS scene mode. Persists window layout in localStorage so the surface survives reloads. Foundational v0 of the "fake OS in the browser" track.  
  *live-web/src/app/os/[user]/page.tsx:29*

### live/api
- **`live.api.browse-session`** · _experimental_ · since 0.2.0 — Server-side proxy that mints openalice-browse sessions for the OS BrowserApp. Keeps the internal secret on the server.  
  *live-web/src/app/api/browse-session/route.ts:4*
- **`live.api.health`** · _stable_ · since 0.1.0 — Liveness probe — returns 200 with timestamp for monitoring + Traefik healthchecks.  
  *live-web/src/app/api/health/route.ts:4*
- **`live.api.pty-session`** · _experimental_ · since 0.3.0 — Server-side proxy that mints openalice-pty sessions for the OS TerminalApp. Keeps the internal secret on the server and hands the browser a fully-formed ws_url with a one-shot token query param.  
  *live-web/src/app/api/pty-session/route.ts:4*

### live/audio
- **`live.scene.bgm-player`** · _beta_ · since 0.10.0 — Per-stream background-music engine. Hidden <audio> element loops the streamer's chosen BGM track (preset OR custom upload). Drives volume / loop / fade-in / fade-out / cross-fade-on-track-change / auto-ducking-when-TTS-speaks. Decoupled from voice + SFX surfaces; ducking listens to oa-voice-chunk + oa-voice-finished postMessages.  
  *live-web/src/app/scene/[user]/lib/BgmPlayer.tsx:99*
- **`live.scene.bgm.presets`** · _beta_ · since 0.10.0 — Bundled background-music preset library — five royalty-free vibes (lofi-amber, synthwave-cyan, jazz-cafe, ambient-spacy, silence). Hub picker uses these as starting points; tenant-uploaded tracks plug into the same dropdown via the persona /v1/me/audio CRUD.  
  *live-web/src/app/scene/[user]/theme/bgmPresets.ts:29*
- **`live.scene.bgm-rotator`** · _beta_ · since 0.12.0 — Wires the bgmRotation policy + persona-server SSE rotation events into the live scene BGM pipeline. Subscribes to idle.bgm_change, falls back to a client-side max-loops trigger, and applies a 2s crossfade via the underlying BgmPlayer.  
  *live-web/src/app/scene/[user]/lib/BgmRotator.tsx:42*
- **`live.scene.radio-player`** · _beta_ · since 0.11.0 — Live-page radio channel subscriber. Resolves a radio:<slug> bgm_source into a WebSocket subscription against openalice-radio; each now_playing frame updates the underlying BgmPlayer config so the live page picks up new tracks with the same cross-fade / duck-during-tts plumbing as static BGM.  
  *live-web/src/app/scene/[user]/lib/RadioPlayer.tsx:48*
- **`live.scene.sfx-engine`** · _beta_ · since 0.9.0 — Sound-effects audio engine for the live stream player. Listens to chat append / mood transition / gesture / subscribe / caption events and plays a short clip per channel, debounced per event type. Master / per-event volume + per-event URL overrides via `branding.theme.sfx`. Mounts only when `theme.sfx.enabled === true` so SFX-disabled streams pay zero cost.  
  *live-web/src/app/scene/[user]/lib/SfxPlayer.tsx:63*
- **`live.scene.sfx.presets`** · _beta_ · since 0.9.0 — Five bundled sound-effect preset vibes (silent / subtle-warm / arcade / sci-fi / nature) — each a coordinated set of audio clips for the stream player's chat, mood, gesture, subscribe, and caption events. Hub picker uses these as starting points; per-event URL overrides land on top via `branding.theme.sfx.sfx_<event>_url`.  
  *live-web/src/app/scene/[user]/theme/sfxPresets.ts:23*

### live/auth
- **`live.auth.forgot-password`** · _alpha_ · since 0.3.0 — Public form that asks tenants to email a password-reset link. Always returns generic copy to avoid account-enumeration leaks.  
  *live-web/src/app/forgot-password/page.tsx:22*
- **`live.auth.login`** · _alpha_ · since 0.3.0 — Public login form — email + password, two-leg MFA prompt for accounts with TOTP/email-MFA.  
  *live-web/src/app/login/page.tsx:37*
- **`live.auth.password-strength`** · _alpha_ · since 0.3.0 — Heuristic password-strength meter (close-enough to zxcvbn) plus visual bars. Stays local until v0.7 promotes a shared meter.  
  *live-web/src/components/AuthShell.tsx:11*
- **`live.auth.reset-password`** · _alpha_ · since 0.3.0 — Lands here from the password-reset email link. Form: new password + confirm. POSTs to /tenants/reset-password.  
  *live-web/src/app/auth/reset-password/page.tsx:24*
- **`live.auth.session-bridge`** · _alpha_ · since 0.3.0 — Stash tenants-issued JWT into the shared SSO localStorage slot.  
  *live-web/src/lib/auth-session.ts:19*
- **`live.auth.signup`** · _alpha_ · since 0.3.0 — Public signup form — email, username, display name, password (with strength meter), ToS opt-in.  
  *live-web/src/app/signup/page.tsx:39*
- **`live.auth.verify-email`** · _alpha_ · since 0.3.0 — Confirms the verification token from the welcome email. Auto-POSTs on mount; surfaces a re-send button on failure when the user is logged in.  
  *live-web/src/app/auth/verify-email/page.tsx:23*

### live-broadcaster/entrypoint
- **`live-broadcaster.entrypoint.run`** · _stable_ · since 0.1.0 — entrypoint.sh — boots Xvfb framebuffer, launches headless Chromium pointed at SCENE_URL, starts ffmpeg with x11grab + tee output to every RTMP URL listed in RTMP_DESTINATIONS.  
  *live-broadcaster/entrypoint.sh:2*

### live-broadcaster/image
- **`live-broadcaster.image.runtime`** · _stable_ · since 0.1.0 — Dockerfile — Debian-slim + Chromium + Xvfb + ffmpeg + pulseaudio. Builds the one-shot broadcaster image.  
  *live-broadcaster/Dockerfile:1*

### live-control/persistence
- **`live-control.persistence.broadcast-store`** — When DATABASE_URL is set, every broadcast row is inserted on /live/start and heartbeat-bumped every watchdog tick. On /live/stop the row is set to `stopped`. The schema includes a partial UNIQUE index on (user_id) WHERE status IN ('live','recovering') to enforce the one-live-broadcast-per- user invariant at the database level, mirroring the in-memory HashMap. Without DATABASE_URL the service runs in legacy in-memory-only mode — acceptable for local dev, not production. Lets the control plane survive a restart without losing track of in-flight broadcasts. The schema is intentionally narrow — every field is what the watchdog (`recovery.rs`) needs to decide whether to respawn the container pair or mark the broadcast as failed. ## Activation Set the `DATABASE_URL` environment variable to a reachable Postgres instance. When the variable is unset, the control plane falls back to the pre-resilience in-memory-only model — useful for local dev / unit tests where a DB isn't worth the round trip. Production must set it. ## Schema The migration runs idempotently on startup. The columns track: * `broadcast_id`        — primary key (uuid v4). * `tenant_id`           — same as `user_id` today; kept distinct so the multi-tenant rewrite doesn't need another migration. * `user_id`             — owner of the broadcast. * `status`              — one of `live` / `recovering` / `stopped` / `degraded` / `failed`. * `started_at`          — start time (UTC). * `last_heartbeat_at`   — bumped by the watchdog on every poll that finds the containers still healthy. Used by the startup recovery path to decide whether to attempt respawn (stale = `now() - 5min`). * `restart_count`       — bumped each time the watchdog respawns one of the containers; capped at 10 / hour after which `degraded` is set. * `broadcaster_container`/`chat_bridge_container` — current container ids the watchdog watches. * `preset_id`           — passed through to spawned containers. * `scene_url`           — what the broadcaster Chromium opens. * `rtmp_target`         — current RTMP egress (optional; control plane doesn't pick the target today, but the column is reserved so the multi-tenant rewrite doesn't need another migration). * `agent_id`            — primary agent for this broadcast (today always `user_id` per the single-agent model; reserved for future). * `room_id`             — optional lounge room id when the broadcast is wired through a multi- agent lounge.  
  *live-control/src/store.rs:3*

### live-control/resilience
- **`live-control.resilience.startup-recovery`** — Before the HTTP server binds, [`recover_on_startup`] scans Postgres for broadcasts with status live/recovering/degraded and a stale heartbeat (> 5 min). Containers still running are re-attached; gone containers are respawned. This is the load-bearing path for the 24 h uptime guarantee — without it, an OOMkill of the control plane would orphan every active broadcaster. ## Startup recovery On boot, before serving the first HTTP request, the recovery task asks the store for every row whose status is still `live` and whose `last_heartbeat_at < now() - 5min`. Each of those rows is what the watchdog from the previous lifetime was supposed to be tending; the process died before it could tick again. For each one we: * Inspect the recorded container ids. If both are still running, just rebuild the in-memory `ActiveBroadcast` entry and resume watching (the new watchdog will heartbeat the row within 15 s). * If either container is gone or exited, spawn new ones with the original preset + scene_url + secret, increment restart_count, transition the status `live` → `recovering` → `live` (or `failed` if respawn busts). This is the load-bearing path for the 24 h server uptime guarantee — without it, a single OOMkill of the control plane orphans every active broadcaster + chat-bridge with no supervision.  
  *live-control/src/recovery/startup.rs:3*
- **`live-control.resilience.watchdog`** — See [`watchdog`] for the every-15s container respawn loop and the restart-budget / Hub `broadcast.degraded` event contract. # @feature live-control.resilience.startup-recovery See [`startup`] for the boot-time hydrate + reattach + respawn path that holds the 24 h uptime guarantee across control-plane restarts.  
  *live-control/src/recovery/mod.rs:20*
- **`live-control.resilience.watchdog`** — Every 15 s (LIVE_CONTROL_WATCHDOG_INTERVAL_SECS) the watchdog inspects each active broadcast's container pair. Crashed containers are respawned up to MAX_RESTARTS_PER_HOUR (10). After the budget is exhausted the broadcast is flipped to `degraded` and a broadcast.degraded event is POSTed to auth's /internal/hub-event so the Hub UI surfaces the failure. ## Watchdog loop Every 15 s (configurable via `LIVE_CONTROL_WATCHDOG_INTERVAL_SECS`) we walk every broadcast in the in-memory `active` map and `docker inspect` both the broadcaster and chat-bridge containers. If either reports `state.status = "exited"` and the broadcast is still flagged `live` in the store, we respawn the container with the same env+labels and bump `restart_count`. If the broadcast has already burned 10 restarts in the last hour we flip it to `degraded` and emit a Hub event instead of looping forever.  
  *live-control/src/recovery/watchdog.rs:3*

### live/iframe-bridge
- **`live.persona-iframe.agent-target-param`** · _alpha_ · since 0.4.0 — Encode `move_target` / `teleport_target` Vec3+meta as base64url URL params so live → persona iframe passes agent walk / teleport intents on mount, no postMessage round-trip needed.  
  *live-web/src/app/scene/[user]/modes/personaIframeUrl.ts:312*

### live/loading
- **`live.scene.global-preloader`** · _beta_ · since 0.11.0 — Game-style up-front asset preloader — fetches every URL the active theme references (VRM via persona, HDR env, BGM, scene background video/image, parallax layers, custom fonts, SFX bundle) before the scene fades in. Branded progress UI with X/Y count and percentage. 30 s safety timeout, soft per-asset failures don't block the scene. Eliminates the late-load "model loads on first scene with avatar" hiccup.  
  *live-web/src/app/scene/[user]/lib/AssetPreloader.tsx:65*

### live/marketing
- **`live.landing.hero`** · _experimental_ · since 0.1.0 — Marketing landing for OpenAlice Live — stream with Alice as AI co-host.  
  *live-web/src/app/page.tsx:14*

### live/os
- **`live.os.terminal-app`** · _experimental_ · since 0.3.0 — Real xterm.js terminal in the Agent Desktop OS — connects to openalice-pty over WebSocket and renders any TUI (claude, vim, btop, htop, neovim, tmux) pixel-perfect with full ANSI escape support.  
  *live-web/src/app/os/[user]/apps/TerminalApp.tsx:7*
- **`live.os.window-frame`** · _alpha_ — One floating window in the virtual desktop scene — owns drag (titlebar) + resize (bottom-right grip) gesture state via raw pointer events, commits final coords to the page-level reducer so layouts persist across reloads.  
  *live-web/src/app/os/[user]/WindowFrame.tsx:19*

### live/os/apps
- **`live.os.browser-app`** · _beta_ — In-scene browser window backed by openalice-browse — JPEG-stream WS rendered to an img element, pointer/keyboard events mirrored back as command frames, hostname allowlist defence so the agent can only navigate to approved sites.  
  *live-web/src/app/os/[user]/apps/BrowserApp.tsx:18*
- **`live.os.files-app`** · _beta_ · since 0.4.0 — Agent-driven file browser in the Agent Desktop OS — renders the directory listing the agent pushes over the scene-control WS (files frame → SceneEvent::Files { path, entries }). No static stub; an explicit empty-state until the agent walks a directory.  
  *live-web/src/app/os/[user]/apps/FilesApp.tsx:18*

### live/scene/lib
- **`live.scene.global-preloader`** · _ga_ — Branded "now loading" overlay paired with AssetPreloader — Atelier gradient mesh, OA brand mark, progress bar with "X of Y · NN%" count, reduced-motion fallback; uses system fonts so it works before custom fonts load.  
  *live-web/src/app/scene/[user]/lib/GlobalPreloader.tsx:13*

### live/scene/modes
- **`live.scene.desktop-os`** · _alpha_ — Virtual desktop scene mode — macOS/Windows-themed wallpaper + dock + menu bar with agent avatar floating in a corner via the shared persona iframe; reads osDesign from URL `?os=<base64-json>`.  
  *live-web/src/app/scene/[user]/modes/desktop-os.tsx:17*
- **`live.scene.persona-canvas-host`** · _beta_ — Single long-lived persona iframe shared across all VRM scenes — scenes register PersonaCanvasConfig and the host animates transform/opacity/filter via CSS transitions, posting `oa-scene-layout` so the persona side reconfigures without reloading the iframe.  
  *live-web/src/app/scene/[user]/modes/PersonaCanvasHost.tsx:12*
- **`live.scene.splitscreen-2`** · _beta_ — Two-cell guest-panel scene mode — homogeneous broadcasts collapse into a single persona iframe (`?layout=split-2`) for 3× GPU savings; heterogeneous broadcasts fall back to per-cell iframes with independent SFU audio + scene-bus subscriptions.  
  *live-web/src/app/scene/[user]/modes/splitscreen-2.tsx:22*

### live/scenes
- **`live.scene.agent-overlay`** · _alpha_ — Agent-driven on-stream overlay renderer (lower-third / card / slides / image / product). Self-contained like ModerationOverlay — listens to the `oa-overlay` window CustomEvent (dispatched by the scene-control `overlay` message handler) and paints the active overlay, no props/state threading through the scene page. Drives off the `show_overlay`/`clear_overlay` agent-protocol tools once the live-control dispatcher (separately gated) is wired. Content is purely client-rendered; styling uses Atelier stream vars; z-70 (above captions z-60, below alerts z-1000); single active overlay; ttl auto-hide or explicit clear; animations are CSS transitions (reduced-motion safe).  
  *live-web/src/app/scene/[user]/lib/SceneOverlay.tsx:4*
- **`live.scene.agent-panel`** · _beta_ · since 0.2.0 — Multi-agent A2A "talk-show / panel" scene — 2–4 agent guests, each its own persona iframe (broadcast/avatar/label via the shared SplitscreenSlot schema), arranged as a produced panel with TV-chyron name plates, an ON-AIR ring + speaking pulse on the active speaker (driven by SceneContext.focusSlot), and a bottom A2A conversation-thread strip that surfaces recent inter-agent chatter (SceneContext.chatMessages — where the multi-agent dispatcher relays message_agent / hand_off as author=agent frames). The visual home for the agent-protocol message_agent + hand_off tools (the dispatcher relay is the gated server follow-up; this is the client half). Per-slot audio focus reuses the splitscreen oa-mute/oa-unmute postMessage contract. Heavy-render (N persona iframes); needsVoice false (each iframe owns its SFU audio).  
  *live-web/src/app/scene/[user]/modes/agent-panel.tsx:4*
- **`live.scene.be-right-back`** · _beta_ · since 0.7.0 — AFK placard with a tasteful sleeping-moon SVG animation, breathing BRB title, and desaturated gradient mesh that signals "paused". Atelier-only, prefers-reduced-motion respected.  
  *live-web/src/app/scene/[user]/modes/be-right-back.tsx:25*
- **`live.scene.event-celebration`** · _beta_ · since 0.10.0 — Subscribe / follow / donation celebration-overlay layer for the live broadcast page. Listens for `oa-event-celebrate` window events and slides a branded animated card into the scene with platform-coloured chrome, a confetti burst, optional TTS read-out, and per-platform SFX. Queues up to 16 events; honours reduced-motion. Mount-time gated by `theme.event_celebration.enabled`. Closes the polish gap to Streamlabs / StreamElements / Neuro-sama-grade event handling.  
  *live-web/src/app/scene/[user]/lib/EventCelebrationOverlay.tsx:80*
- **`live.scene.game-overlay`** · _beta_ · since 0.2.0 — Game-mode broadcast overlay — a transparent scene the streamer's OBS Browser Source layers OVER their own game capture (osu! / Minecraft / Project Zomboid / anything). Renders ONLY a frontal facecam avatar pip in a configurable corner (framed with an accent ring + name tag, both positioned from the persona-canvas pip rect so they track the avatar exactly) plus a thin HUD cluster (LIVE pill + game badge) anchored on the half opposite the pip so it never overlaps. Game title comes from branding.game (falls back to branding.title); accent from branding.accent. The page returns a transparent <main> for this scene (see resolveMainBackground) so the game capture below shows through. Config-driven — one scene covers every game via branding instead of per-game forks.  
  *live-web/src/app/scene/[user]/modes/game-overlay.tsx:4*
- **`live.scene.intermission`** · _beta_ · since 0.7.0 — Editorial chapter card for between-segments transitions ("1 / 3 — Coding session"). Twin-band gradient mesh, oversized chapter index, slow horizontal wipe transition. Calm, intentional, premium.  
  *live-web/src/app/scene/[user]/modes/intermission.tsx:31*
- **`live.scene.moderation-overlay`** · _beta_ · since 0.13.0 — Administrative toast for AI-driven mod actions on the live scene. Listens for `oa-mod-action` window events and paints a muted lower-left card with an inline Undo button. Separate from the celebratory overlays — visually neutral on purpose.  
  *live-web/src/app/scene/[user]/lib/ModerationOverlay.tsx:55*
- **`live.scene.mode-registry`** · _beta_ · since 0.2.0 — Catalog of Live broadcast scene modes — VRM fullscreen + static placards (starting-soon / be-right-back / outro). Each scene declares its voice + render cost so the page knows whether to attach the SFU and the admin can warn on GPU-less hosts.  
  *live-web/src/app/scene/[user]/modes/index.ts:8*
- **`live.scene.music-stage`** · _beta_ · since 0.2.0 — Music-mode broadcast scene — 50/50 vertical split with the VRM avatar reacting on the RIGHT half and a now-playing music visualizer on the LEFT (spinning album disc, track title from branding.bgm_track, animated equalizer spectrum). Built for music sets / openalice-radio + Lyria BGM streams (the page's hidden <audio>/RadioPlayer drives the actual sound; this scene paints the visualizer). Reuses the proven buildSideHalfCanvasConfig avatar placement so the persona iframe and the scene chrome never overlap (no z-index fight). Equalizer is CSS-keyframe driven (deterministic per-bar delay/duration, reduced-motion safe), so the music chrome is CPU-cheap — only the avatar half is heavyRender. Themed via the stream-theme CSS vars.  
  *live-web/src/app/scene/[user]/modes/music-stage.tsx:4*
- **`live.scene.outro`** · _beta_ · since 0.7.0 — Polished farewell placard with social-handle pills, optional next-stream timestamp, Solar Amber Subscribe CTA, and Plasma Cyan gradient mesh distinct from the warm starting-soon family.  
  *live-web/src/app/scene/[user]/modes/outro.tsx:31*
- **`live.scene.overlay-types`** · _alpha_ — Client-safe types for agent-driven scene overlays. Mirrors the openalice-agent-protocol `show_overlay` tool input (kind/title/subtitle/media_url/data/anchor/ttl_ms/overlay_id) so the live-control dispatcher (gated) can forward a `show_overlay` ToolCall onto the scene-control WS as an `overlay` message verbatim. No server logic, no secrets — purely renderable.  
  *live-web/src/app/scene/[user]/lib/overlayTypes.ts:2*
- **`live.scene.placard-atelier`** · _beta_ · since 0.7.0 — Shared placard primitives — Atelier-themed gradient mesh, brand watermark, social pills, BGM "now playing" pill, countdown clock. Used by starting-soon / be-right-back / outro / intermission.  
  *live-web/src/app/scene/[user]/modes/placard-shared.tsx:23*
- **`live.scene.poll-overlay`** · _beta_ · since 0.11.0 — Engagement poll overlay for the live scene page. Listens for oa-poll-opened / oa-poll-vote / oa-poll-closed window events and paints a Q+A card with growing option bars + linear countdown + winning-option highlight on close. Optional per-platform colour theme.  
  *live-web/src/app/scene/[user]/lib/PollOverlay.tsx:26*
- **`live.scene.redemption-overlay`** · _beta_ · since 0.11.0 — Channel-point / SuperChat redemption toast for the live scene page. Listens for `oa-redemption-triggered` window events and paints a per-platform branded card ("🎁 ViewerName redeemed «Evil Laugh»"). Separate from the EventCelebrationOverlay (which handles sub/follow/cheer); this one is the dedicated viewer-trigger surface.  
  *live-web/src/app/scene/[user]/lib/RedemptionOverlay.tsx:58*
- **`live.scene.scene-background`** · _beta_ · since 0.9.0 — Animated 2D backdrop renderer for vtuber scenes — color / image / video / preset modes plus parallax layers, atmospheric particles (snow / cherry-blossoms / embers / dust / rain / stars), and drift (slow-pan / Ken-Burns). Pure CSS keyframes, no canvas, no WebGL — runs at ~0% GPU on the no-GPU broadcaster host. Reduced-motion strips every animation.  
  *live-web/src/app/scene/[user]/modes/SceneBackgroundLayer.tsx:184*
- **`live.scene.spatial`** · _alpha_ · since 0.2.0 — Spatial / Gaussian-splat broadcast scene — renders a 3D Gaussian-splat environment (a scanned room, a captured location, a generated set) as the full-screen background with the agent's VRM avatar composited in front, so the agent appears to stand *inside* a photoreal 3D space. The splat is loaded from `branding.splat_url` (any .splat / .ply / .ksplat — Luma / Polycam / a scan / a gen service) and a slow auto-orbit gives parallax; `branding.splat_autorotate=false` holds a fixed framing. Clean instructional empty-state when no URL is set. Live splat *generation* (feed a prompt/scan to a gen service that returns the URL) is the server-side follow-up; this scene is the render surface + agent compositing. Heavy (three.js + drei) so it's dynamically imported (code-split, ssr:false) by the registry.  
  *live-web/src/app/scene/[user]/modes/spatial.tsx:4*
- **`live.scene.starting-soon`** · _beta_ · since 0.7.0 — Polished pre-roll placard with Atelier gradient mesh, animated countdown to start_at_ms, brand block, and "now playing" pill. Atelier-only colours, calm motion, prefers-reduced-motion respected.  
  *live-web/src/app/scene/[user]/modes/starting-soon.tsx:29*

### live/scene/theme
- **`live.scene.theme-presets`** · _ga_ — Catalogue of streamer-segment theme presets — fully-populated StreamTheme objects (colour / typography / texture / corner radius / shadow / motion) used as starting points for branding.theme overrides.  
  *live-web/src/app/scene/[user]/theme/presets.ts:16*
- **`live.scene.theme-resolver`** · _ga_ — Pure preset-lookup + override-merge that turns a streamer's branding.theme into a fully-populated StreamTheme, then flattens it into a `--stream-*` CSS-variable map for the scene shell.  
  *live-web/src/app/scene/[user]/theme/resolveTheme.ts:13*

### live/showcase
- **`live.showcase.catalogue`** · _beta_ · since 0.4.0 — Scene catalogue metadata for the /showcase index — layout-schema wireframes, category buckets, launch-param presets for every scene + sub-variant.  
  *live-web/src/app/showcase/_data/scenes-catalogue.ts:11*
- **`live.showcase.filter`** · _beta_ · since 0.4.0 — Client-side filter + grid for /showcase — single-select category chips + deferred substring search across labels / descriptions / tags / scene IDs.  
  *live-web/src/app/showcase/_components/ShowcaseFilter.tsx:25*
- **`live.showcase.index`** · _beta_ · since 0.4.0 — /showcase — full catalogue of Live scene modes + sub-variants. Wireframe schema thumbnails, editorial specimen layout, deep-link launchers with picker / slot params. Single source of truth for "what scenes does Live have".  
  *live-web/src/app/showcase/page.tsx:19*
- **`live.showcase.opengraph-image`** · _beta_ · since 0.4.2 — 1200×630 generated OG card for /showcase — dark observatory composition with scene-count cardinal and category chip strip. Used by link unfurls.  
  *live-web/src/app/showcase/opengraph-image.tsx:17*
- **`live.showcase.scene-card`** · _beta_ · since 0.4.0 — One card on /showcase — wireframe schema thumbnail + scene metadata + launch-preset buttons. Built on @openalicelabs/ui Card + Badge + Button primitives.  
  *live-web/src/app/showcase/_components/SceneShowcaseCard.tsx:28*
- **`live.showcase.schema-thumbnail`** · _beta_ · since 0.4.0 — Pure-SVG wireframe diagram of a scene's layout shape, colour-coded by role (vrm / browser / cli / chat / placard / ide). Renders as a card thumbnail in /showcase.  
  *live-web/src/app/showcase/_components/SchemaThumbnail.tsx:13*

### live/streaming
- **`live.scene.demo-picker`** · _beta_ · since 0.11.0 — Floating reviewer-navigation overlay on the live demo page (?demo=1&picker=1) — six tabs (Scene / Render / Avatar / Theme / Background / Audio) let a reviewer hop between any combination of scene modes, avatar render modes/presets, theme presets, background presets, BGM and SFX presets in two clicks each. Pauses the auto-cycling demo timeline so selections stick. URL state syncs via history.replaceState so refreshes keep the reviewer's working selection.  
  *live-web/src/app/scene/[user]/_components/DemoPicker.tsx:37*
- **`live.scene.demo-timeline`** · _beta_ · since 0.6.0 — Canned timeline of scene/mood/chat/gesture/window/splitscreen events the demo room replays on a loop so we exercise every renderer mode without requiring live agents or a WS connection.  
  *live-web/src/app/scene/[user]/demo-timeline.ts:71*
- **`live.scene.error-boundary`** · _beta_ · since 0.4.3 — Route-segment ErrorBoundary for /scene/[user]. Any unhandled error in the OBS Browser Source page renders a "Reconnecting…" card on a dark background instead of a blank surface, then auto-reloads after 3s so transient failures (WS drop, iframe race, persona crash) recover without operator intervention.  
  *live-web/src/app/scene/[user]/error.tsx:6*
- **`live.scene.obs-source`** · _beta_ · since 0.2.0 — OBS Browser Source endpoint — multi-mode renderer (VRM fullscreen + static placards) driven by ?scene= query param and a live-control WebSocket so the agent's ChangeScene ToolCall flips scenes mid-stream without restarting the broadcaster Chromium.  
  *live-web/src/app/scene/[user]/page.tsx:68*

### live/studio
- **`live.studio.agents.marketplace`** · _alpha_ · since 0.2.0 — Agent Marketplace browse + pair UI — capability-scoped grants against openalice-tenants.  
  *live-web/src/app/studio/agents/marketplace/page.tsx:48*
- **`live.studio.audio-mixer`** · _alpha_ · since 0.3.18 — Per-channel volume mixer + BGM picker for the active broadcast. Sliders for mic / desktop / TTS / BGM (linear UI, log-mapped on send), default royalty-free BGM presets, custom URL field, play/pause/skip transport, persisted per-broadcast in localStorage, debounced 200 ms before POST to live-control.  
  *live-web/src/app/studio/audio/page.tsx:10*
- **`live.studio.chat`** · _alpha_ · since 1.1.0 — Multi-platform live chat reader + reply box for the streamer. Subscribes to the live-control scene WS for inbound, posts to chat-bridge for outbound. Lets the streamer talk to viewers without leaving studio.  
  *live-web/src/app/studio/chat/page.tsx:25*
- **`live.studio.dashboard`** · _experimental_ · since 0.1.0 — Streamer-facing studio — shows the OBS browser-source URL with copy-to-clipboard. Login required.  
  *live-web/src/app/studio/page.tsx:113*
- **`live.studio.onboarding`** · _alpha_ · since 1.0.0 — First-run checklist for Live — avatar, voice, stream destination, desktop pairing, branding. Each step deeplinks to the editor that fixes it.  
  *live-web/src/app/studio/onboarding/page.tsx:21*
- **`live.studio.settings`** · _alpha_ · since 1.0.0 — Per-user TTS / STT / avatar / system-prompt editor for the Live product. Saves to agent_config; takes effect on the next /live/start.  
  *live-web/src/app/studio/settings/page.tsx:37*
- **`live.studio.settings.privacy`** · _alpha_ · since 0.2.0 — Privacy & Data settings — consent flags, GDPR export, audit log, paired agents, account deletion.  
  *live-web/src/app/studio/settings/privacy/page.tsx:60*

### live/tenants
- **`live.tenants.client`** · _alpha_ · since 0.2.0 — Browser HTTP client for openalice-tenants — consent flags, audit log, GDPR export, account deletion, paired-agent grants.  
  *live-web/src/lib/tenants-client.ts:14*

### live/theming
- **`live.scene.stream-theme`** · _beta_ · since 0.8.0 — Per-stream visual theme for the broadcast player. Decouples streamer-facing scene look from the Atelier platform tokens — themes are CSS variables (bg / fg / accent / fonts / texture / motion) scoped to the player's <main> element. Streamers pick from a preset library (vtuber-warm, vtuber-neon-cyan, minimal-mono, glass-dark, vintage-sepia, pastel-bubblegum, editorial-print) or pass a fully-custom override. Atelier values are the fallback so existing broadcasts paint identically.  
  *live-web/src/app/scene/[user]/theme/types.ts:23*

### stream-host/audio
- **`stream-host.audio.process-loopback-capture`** · _beta_ · since 0.3.18 — ProcessLoopbackCapture — Windows-only RAII guard. Calls ActivateAudioInterfaceAsync against `VAD\\Process_Loopback` with PROCESS_TREE mode + the daemon's PID, opens an IAudioClient at 48k/2ch/Float32, pumps an IAudioCaptureClient → user-supplied sink in an event-driven loop. Mic + system sounds physically excluded by the kernel API. Any activation error degrades to silent-PCM fallback so ffmpeg never starves; OA_DISABLE_AUDIO=1 short-circuits to fallback.  
  *live-stream-host/src/wasapi_loopback.rs:89*
- **`stream-host.audio.wasapi-loopback`** · _beta_ · since 0.3.18 — Per-process WASAPI loopback capture on Windows — uses ActivateAudioInterfaceAsync with VAD\\Process_Loopback + PROCESS_TREE mode to grab only the daemon + WebView2 child audio (scene <audio>, TTS) while physically excluding mic / system sounds; falls back to silent PCM on any activation error so ffmpeg never starves.  
  *live-stream-host/src/wasapi_loopback.rs:1*

### stream-host/capture
- **`stream-host.capture.cross-platform-process`** · _ga_ · since 0.2.0 — CaptureProcess — RAII wrapper around the OS-native ffmpeg capture child (gdigrab on Windows, x11grab on Linux, avfoundation on macOS). Owns the per-destination stderr scanner thread classifying RTMP failures. Drop / stop sends "q" + SIGTERM to flush the RTMP trailer so the platform's slot releases immediately.  
  *live-stream-host/src/capture.rs:180*
- **`stream-host.capture.encoder-chain`** — Hardware encoder auto-detection at startup: `ffmpeg -encoders` is probed once and the first available encoder wins: h264_nvenc → h264_qsv → h264_amf → h264_videotoolbox → h264_vaapi → libx264 (software fallback). Pin a specific encoder with OA_VIDEO_ENCODER env var. Capture + RTMP push pipeline. We shell out to a system `ffmpeg` rather than statically linking libavcodec — the binary is universal across platforms, ships with every major package manager, and avoids dragging a 50 MB GPL/LGPL bundle into our installer (Tauri Mac/Win signing gets unhappy with that). Flow: capture device (x11grab / gdigrab / avfoundation) → libx264 (balanced preset, 4 Mbps default) → flv muxer → RTMP tee to N destinations Hardware encoders (NVENC / QuickSync / VideoToolbox / VAAPI) land in v0.3 once we have a stable baseline to compare against. For v0.2 the soft-encoder is universal.  
  *live-stream-host/src/capture.rs:1*
- **`stream-host.capture.platform-backends`** — Per-OS capture backends: Windows.Graphics.Capture + WASAPI process-loopback (Windows); avfoundation (macOS); x11grab + PulseAudio (Linux). WGC uses a fixed-size frame pool + bounded ring-buffer so steady-state allocation is zero at 1080p60. Windows.Graphics.Capture (WGC) — per-HWND screen capture. WGC is the modern Win10+ API that OBS uses for "Window Capture (Auto)" mode. It reads the *post-composition* pixels of a specific HWND from the DWM compositor, so: * DirectComposition / DXGI / hardware-accelerated surfaces (WebView2's actual content) are captured correctly. * Privacy is iron-clad — only the requested window's pixels ever leave the GPU; desktop / other apps never enter the capture pipeline. * The capture follows the window across monitors / resizes / minimize→restore without re-arming. This module: 1. Finds the OpenAlice kiosk window by its title (the same title gdigrab title-mode used to look up). 2. Spawns a WGC capture session on that HWND. 3. Pipes raw BGRA frames into a child ffmpeg process's stdin at the configured framerate. 4. Hands ownership of the ffmpeg child + the WGC handle back to the daemon so it can stop both on broadcast end. ## Backpressure (v0.3.20 — full ring-buffer + frame pool) The frame pipeline is **WGC thread → bounded ring buffer → writer thread → ffmpeg stdin**, with every BGRA buffer drawn from a **fixed-size frame pool**. v0.3.19 already moved the direct ffmpeg-stdin write off the WGC callback (which fixed the freeze-when-RTMP-stalls audit finding); this slice finishes the job: * **Frame pool** — at 1080p60 the per-frame `Vec<u8>::to_vec` in v0.3.19 churned ~500 MB/s of allocator traffic. The pool pre-allocates `POOL_CAP = QUEUE_CAP + POOL_HEADROOM` BGRA buffers up front, hands them out by checkout, and recycles them when the writer finishes a frame. Steady-state allocation drops to zero. * **Oldest-drop policy** — when the ring buffer is full we evict the *oldest* pending frame and enqueue the new one. v0.3.19 dropped the *newest*, which made congestion show as a stuck stream catching up to current time slowly. The new frame is more useful to the viewer; the older one is already stale. * **Adaptive bitrate hint** — the writer thread tracks a trailing 10 s drop ratio and emits a `WARN bitrate too high for upload — drop X.Y%` line when the rate exceeds ~1 frame/sec. We don't actually restart ffmpeg with a new bitrate (the operator controls that); we just surface the diagnostic so the bottleneck is obvious. * **Per-destination tee-failure detection** — the ffmpeg stderr scanner from `capture::spawn_ffmpeg_stderr_scanner` parses `Connection failed` / `RTMP_Connect` / `Broken pipe` lines and tags them with the destination name, so "Twitch failed but YT still live" is visible instead of the silent `onfail=ignore` swallow. Operator-facing log lines on a healthy 60 fps broadcast: ```text WGC pipe stats: enqueued=300 written=300 dropped=0 hwm=1/4 ``` Under congestion: ```text WGC pipe stats: enqueued=600 written=540 dropped=60 hwm=4/4 WARN bitrate too high for upload — drop 10.0% WARN ffmpeg RTMP destination failure dest=Twitch line=... ```  
  *live-stream-host/src/wgc.rs:1*
- **`stream-host.capture.start-stream`** · _ga_ · since 0.2.0 — Boots the cross-platform capture+encode+push child. Auto-detects hardware H.264 encoder (NVENC → QSV → AMF → VideoToolbox → VAAPI → libx264) via `ffmpeg -encoders`, builds the OS-specific capture device args, configures the tee muxer to fan one encoded stream to N RTMP destinations (no double encoding), tags +genpts so YouTube's PTS sanity-check doesn't park the broadcast.  
  *live-stream-host/src/capture.rs:251*
- **`stream-host.capture.supervised-capture`** · _alpha_ · since 0.4.0 — SupervisedCapture — owns a background supervisor thread running `watchdog::supervise` over `start_stream`. Per-spawn ffmpeg child swap on RTMP drop / crash; exponential 2s→30s backoff, 60s uptime-reset gate; stop() arms the StopSignal + joins the thread.  
  *live-stream-host/src/capture.rs:790*
- **`stream-host.capture.wgc-windows`** · _ga_ · since 0.3.0 — WgcCapture — Windows.Graphics.Capture per-HWND capture session. Pipes post-composition BGRA frames through a fixed-size frame pool + bounded oldest-drop ring buffer + dedicated writer thread → child ffmpeg stdin (rawvideo bgra). Owns ffmpeg, capture thread, writer thread, and stderr scanner; Drop arms stop + joins all so the broadcast tears down cleanly and the RTMP slot releases.  
  *live-stream-host/src/wgc.rs:149*

### stream-host/cli
- **`stream-host.cli.daemon`** · _ga_ · since 0.3.0 — `daemon` subcommand — long-running 1-EXE 1-click experience. Kiosk-fullscreen WebView (always-on-top, GPU-compositor disabled on Windows so gdigrab/WGC can read post-composition pixels) polls live-control every 5s for active broadcasts; on Go Live navigates to the scene URL + spawns WgcCapture/start_stream pushing RTMP from the user's GPU. Installs Windows tray icon with Status / Show kiosk / Stop / Update / Quit menu, ctrl-c handler, panic hook, SleepGuard, RSS+disk housekeeping, Hub heartbeat tick, self-update 6h cadence.  
  *live-stream-host/src/daemon/event_loop.rs:60*
- **`stream-host.cli.doctor`** · _ga_ · since 0.2.0 — `doctor` subcommand — runs ffmpeg::ensure_ffmpeg (auto-downloads if absent) + ffmpeg::probe to confirm the binary is invocable; surfaces the version string. One-command path to a ready-to-stream machine.  
  *live-stream-host/src/cli.rs:411*
- **`stream-host.cli.install-service`** · _beta_ · since 0.4.4 — `install-service` subcommand — registers the daemon with the platform service manager (launchd plist KeepAlive=true, systemd user unit Restart=always, Windows SCM with failure-restart 5000ms ×3). Prints the activation step the user runs next.  
  *live-stream-host/src/cli.rs:381*
- **`stream-host.cli.pair`** · _ga_ · since 0.2.0 — `pair <CODE>` subcommand — exchanges Hub's 8-char pairing code at POST {auth_base}/auth/devices/pair/claim for a 90-day device JWT + user_id, writes them into stream-host.toml (merging with any existing config), then walks the user through RTMP setup + quality preset on first pair. Subsequent launches boot straight into stream mode.  
  *live-stream-host/src/cli.rs:198*
- **`stream-host.cli.pick-destinations`** · _ga_ · since 0.2.0 — Resolves the RTMP push targets. Empty filter → every primary=true destination; named filter → those entries by name. Errors with clear messages when the config is empty or an --only name doesn't exist; foundation of the multi-platform tee.  
  *live-stream-host/src/cli.rs:132*
- **`stream-host.cli.setup`** · _ga_ · since 0.2.0 — `setup` subcommand — interactive re-run wizard. Re-prompts RTMP destinations, quality preset, capture region, then offers to install the auto-start service (launchd / systemd / SCM). Requires an existing config (pairing must have happened).  
  *live-stream-host/src/cli.rs:319*

### stream-host/config
- **`stream-host.config.capture-region`** · _ga_ · since 0.1.0 — CaptureRegion — explicit (x, y, width, height, fps) rectangle for the platform-native capture backend. Overrides the QualityPreset's resolution+fps when set; legacy escape hatch for crop-and-stream workflows.  
  *live-stream-host/src/config.rs:318*
- **`stream-host.config.host-config`** · _beta_ · since 0.4.0 — HostConfig — serde-deserialized root of `~/.config/openalice/stream-host.toml`. Carries scene_base, paired user+token, optional broadcast/initial_scene, window dims, RTMP destinations vec, optional capture region, quality_preset enum, optional recording leg. CLI flags override per-invocation.  
  *live-stream-host/src/config.rs:40*
- **`stream-host.config.quality-preset`** · _beta_ · since 0.4.0 — QualityPreset enum — named ladder P480/P720/P1080/P1440/P2160. `params()` returns the (width, height, fps, bitrate) tuple. OA_QUALITY_PRESET env overrides the TOML field, OA_VIDEO_BITRATE overrides the bitrate baseline.  
  *live-stream-host/src/config.rs:124*
- **`stream-host.config.rtmp-destination`** · _ga_ · since 0.1.0 — RtmpDestination — one named RTMP target (name + url + stream_key + primary flag). The daemon tees one capture to every primary=true destination so YouTube + Twitch get identical feeds with zero re-encoding cost.  
  *live-stream-host/src/config.rs:235*
- **`stream-host.config.toml-cli`** · _beta_ · since 0.4.0 — TOML config loader (~/.config/openalice/stream-host.toml) merged with CLI flag + env overrides, including the named quality-preset ladder (P480..P2160) with bitrate/fps baselines and explicit [capture] width/height/fps escape hatch; Hub writes this file on device pairing.  
  *live-stream-host/src/config.rs:1*

### stream-host/core
- **`stream-host.core.webview-renderer`** — Platform-native WebView (WebView2 / WKWebView / WebKitGTK) opens the OpenAlice scene URL on the streamer's local GPU. The server only sends scene-control commands over WebSocket — no AI inference on the streamer's box. openalice-stream-host — desktop thin-client. Wave 13-A (2026-05-23) split the historic 2347-LOC main.rs into [`cli`] (clap + subcommand runners), [`webview`] (bare WebView + `stream` path), and [`daemon`] (kiosk + RTMP push + tray + heartbeats + housekeeping + self-update). `main()` is now a thin dispatcher that wires the boot sequence and routes to the right module.  
  *live-stream-host/src/main.rs:1*

### stream-host/daemon
- **`stream-host.daemon.sleep-prevention`** — OS sleep prevention while streaming: SetThreadExecutionState (Windows), IOPMAssertion (macOS), systemd-inhibit (Linux). Display can dim; the broadcast machine never suspends. Prevent the operating system from sleeping the broadcaster's PC mid-stream. ## Why this matters for 24h streams Default OS power profiles on Windows, macOS and most Linux desktops put the box to sleep after 15–30 min of "user inactivity" — keyboard idle. A stream-host running in the tray looks idle to those heuristics; the box goes to sleep, the broadcast ends. We need to tell the OS "I'm doing something important, hold off the sleep timers." Per-platform APIs: * **Windows** — `SetThreadExecutionState` with `ES_CONTINUOUS | ES_SYSTEM_REQUIRED | ES_AWAYMODE_REQUIRED`. `ES_AWAYMODE_REQUIRED` is the Vista-era "media center" flag that keeps the system "available" while the display can still sleep. This is exactly what we want — operator can walk away + the screen dims, but the broadcast machine keeps streaming. * **macOS** — `IOPMAssertionCreateWithName` with type `PreventSystemSleep` (keeps SMC awake while letting the display sleep). The companion `PreventUserIdleSystemSleep` covers "the user might idle but the system shouldn't sleep on that account." We hold both while streaming. * **Linux** — `systemd-inhibit` with the `sleep:idle: handle-lid-switch` set. We spawn `systemd-inhibit --what --why "broadcasting" sleep infinity` and hold the resulting child for the lifetime of the broadcast. Tearing the child down releases the inhibitor. Tests inject a [`SleepBlockerBackend`] so the platform-specific calls can be exercised without touching the host's power state. Production code wires the real backends in the Windows/macOS/Linux `cfg`-gated `default_backend()` factories.  
  *live-stream-host/src/sleep_prevention.rs:1*
- **`stream-host.daemon.watchdog-heartbeat`** — Background daemon polls live-control every 5 s and auto-launches WebView + capture + RTMP push when the operator clicks Go Live. RTMP watchdog supervises ffmpeg with exponential backoff (cap 30 s); heartbeat POSTs every 30 s to Hub so the dashboard shows live host health. RTMP watchdog — keep the broadcast alive across ffmpeg crashes and transient RTMP drops. ## Why this exists Before v0.4.4 the capture pipeline spawned ffmpeg once and let the broadcast die on any of the failure modes that show up in real 24h streams: * **RTMP ingest drops.** YouTube + Twitch routinely kick a long-running stream off one edge ingest and expect the broadcaster to reconnect. ffmpeg surfaces this as a non-zero exit (`Connection refused`, `Broken pipe`, `End of file`) and the prior code just stopped pushing. * **ffmpeg crashes.** A bad keyframe / OOM / dependency-load failure under tee can SIGSEGV ffmpeg. Same story — process exits, no recovery. * **Network blips.** Wi-Fi drops 30 s; the recovery is trivially "spawn ffmpeg again with the same args." YouTube treats reconnects under ~30 s as a continuous stream — the broadcast counter doesn't reset and viewers don't see a "Stream ended" page. We use exponential backoff capped at 30 s so we always sit inside that window. ## Design The watchdog is a supervisor loop that owns the ffmpeg `Child` handle. The capture pipeline produces a "spawn function" that the watchdog can call repeatedly with the same args; on each exit the watchdog inspects the [`ExitClassification`] and decides whether to reconnect. The watchdog is intentionally agnostic about *what* ffmpeg is doing — it doesn't know about RTMP destinations, the recording leg, audio devices, or encoder choices. All it sees is "the child exited; restart it or surrender." Keep it that way. Reconnects emit a [`WatchdogEvent::StreamInterruptionRecovered`] the heartbeat client consumes so the dashboard can surface a reliability indicator ("recovered from 2 ffmpeg crashes today"). ## Why we don't kill recording on RTMP drop The local mp4 writer lives inside the same ffmpeg tee, so on ffmpeg respawn it starts a **new** file. That's deliberate: splitting the recording into 30s-of-blackout-bracketed chunks is preferable to losing the file entirely. The new file name includes a `.partN.mp4` suffix so the user can stitch them post-hoc with `ffmpeg -f concat`. The original v0.4.2 recording leg is unchanged; the watchdog simply asks for the "next part" path on each respawn.  
  *live-stream-host/src/watchdog.rs:1*

### stream-host/disk-monitor
- **`stream-host.disk-monitor.classify`** · _beta_ · since 0.4.0 — Pure classifier — given a free-space sample + write-rate, returns DiskVerdict (Ok / LowSpaceWarning with minutes-until-critical estimate / CriticalStopRecording). Three-tier ladder mirrors recording.rs pre-flight: 10 GB warn, 5 GB critical, 20 GB recovery target.  
  *live-stream-host/src/disk_monitor.rs:117*
- **`stream-host.disk-monitor.free-gb`** · _beta_ · since 0.4.0 — Cross-platform free-space probe in GiB for a directory path — GetDiskFreeSpaceExW on Windows, statvfs on Unix. Returns None on platform failure so the caller errs toward "keep streaming, log a warn".  
  *live-stream-host/src/disk_monitor.rs:146*
- **`stream-host.disk-monitor.purge-until-free`** · _beta_ · since 0.4.0 — Deletes oldest-first recordings under `dir/old/` until the partition reaches target_gb free or the directory empties. Returns the deleted paths + bytes reclaimed. Last-resort cleanup path during a long-form broadcast.  
  *live-stream-host/src/disk_monitor.rs:256*
- **`stream-host.disk-monitor.recording-protection`** · _beta_ · since 0.4.0 — 60s disk-space poll during a broadcast — warns the Hub under 10 GB, stops the recording leg under 5 GB, and auto-rotates completed recordings into old/ under 20 GB to free runway; RTMP push never stops regardless ("RTMP wins, recording is sacrificial").  
  *live-stream-host/src/disk_monitor.rs:1*
- **`stream-host.disk-monitor.rotate-completed`** · _beta_ · since 0.4.0 — Rename-based rotation of completed `*.mp4`/`*.mkv`/`*.flv` recordings into `dir/old/`. Same-filesystem rename = O(1), idempotent, skips conflicts. Triggered when free space drops below RECOVERY_TARGET_GB during a broadcast.  
  *live-stream-host/src/disk_monitor.rs:208*

### stream-host/ffmpeg
- **`stream-host.ffmpeg.auto-provision`** · _beta_ · since 0.2.0 — Zero-config ffmpeg provisioning — checks PATH, then per-OS user cache (LOCALAPPDATA/openalice or ~/.local/share/openalice), then downloads the official Gyan.dev release-essentials (Windows) or BtbN static build (Linux) and caches it for next time; OA_FFMPEG_PATH overrides for custom builds.  
  *live-stream-host/src/ffmpeg.rs:1*
- **`stream-host.ffmpeg.ensure`** · _beta_ · since 0.2.0 — Resolves the working ffmpeg binary path. Priority: OA_FFMPEG_PATH env override → PATH lookup → per-OS user cache (LOCALAPPDATA / ~/.local/share) → fresh ~80MB download from Gyan.dev (Windows) or BtbN (Linux) with progress bar. Used by `doctor`, `stream`, and the daemon at boot.  
  *live-stream-host/src/ffmpeg.rs:35*
- **`stream-host.ffmpeg.probe`** · _ga_ · since 0.2.0 — Probes an ffmpeg binary via `-version`, returning the first reported line. Catches broken downloads / unsupported builds early; surfaced in the `doctor` subcommand output.  
  *live-stream-host/src/ffmpeg.rs:184*

### stream-host/heartbeat
- **`stream-host.heartbeat.host-id`** · _beta_ · since 0.3.0 — Stable host-id loader — reads/creates `<config_dir>/host-id`. Hub dedupes heartbeats across daemon restarts by this id; ephemeral fallback uses a fresh UUIDv4 when the file cannot be created.  
  *live-stream-host/src/heartbeat.rs:208*
- **`stream-host.heartbeat.hub-push`** · _beta_ · since 0.3.0 — 30s heartbeat POST to app.blal.pro/api/internal/streamhost/heartbeat carrying host_id / broadcast_id / RSS / ffmpeg liveness / RTMP state / free disk / watchdog event log / sleep-prevention counters — gated by INTERNAL_BROADCAST_SECRET, best-effort, never crashes the daemon on network blip.  
  *live-stream-host/src/heartbeat.rs:1*
- **`stream-host.heartbeat.payload-wire`** · _beta_ · since 0.3.0 — HeartbeatPayload — serde wire shape POSTed every 30s to Hub's `/api/internal/streamhost/heartbeat` with X-Internal-Secret. Carries host_id, optional broadcast_id, RSS MB, uptime, ffmpeg_alive flag, last_frame_ts ISO timestamp, RtmpState (Idle/Connected/Reconnecting/Failed), free_disk_gb, interruptions, current+cumulative sleep_blocks, last 10 watchdog events, version.  
  *live-stream-host/src/heartbeat.rs:163*
- **`stream-host.heartbeat.send`** · _beta_ · since 0.3.0 — POSTs one HeartbeatPayload to Hub's `/api/internal/streamhost/heartbeat` with X-Internal-Secret header. Surfaced under host_id; called every HEARTBEAT_INTERVAL (30s) by the daemon's housekeeping tick. Transport-only — no retry/backoff inside.  
  *live-stream-host/src/heartbeat.rs:288*

### stream-host/pairing
- **`stream-host.pairing.device-token`** — `pair <CODE>` exchanges a Hub-generated 8-char code for a 90-day device-scoped JWT and writes it to ~/.config/openalice/stream-host.toml. Subsequent launches boot straight into stream mode without user interaction. Interactive setup wizard. Two entry points: - `setup` subcommand — full re-run any time (re-prompt pairing code, RTMP destinations, capture region). - `pair` subcommand — after the JWT is claimed, optionally walks the user through adding RTMP destinations so they don't have to crack open the TOML. Goal: zero-config first launch. User runs the .exe, types the pairing code from Hub, optionally adds Twitch/YouTube keys, then `oash stream` Just Works.  
  *live-stream-host/src/setup.rs:1*

### stream-host/recording
- **`stream-host.recording.plan-resolver`** · _alpha_ · since 0.4.0 — RecordingPlan — three-state enum for the VOD leg of the broadcast pipeline. Disabled{reason} (no leg), Local{path, container} (mp4/mkv to disk), Cloud{url} (HTTP POST to recordings.blal.pro). Wraps every leg in `[f=...:onfail=ignore]` so the RTMP push always wins.  
  *live-stream-host/src/recording.rs:65*
- **`stream-host.recording.tee-leg`** · _alpha_ · since 0.4.0 — Optional VOD recording leg added to the ffmpeg tee muxer — captures the same encoded bitstream to local disk OR uploads to recordings.blal.pro/v1/me/recordings/<broadcast_id>; wrapped in onfail=ignore so the RTMP push always wins when disk fills or the cloud endpoint hiccups.  
  *live-stream-host/src/recording.rs:1*

### stream-host/rss-monitor
- **`stream-host.rss-monitor.current-rss`** · _beta_ · since 0.4.4 — Cross-platform RSS reader via `sysinfo`. Refreshes the daemon's own PID's memory page and returns bytes-to-MB. Degrades to 0 on platforms where sysinfo can't read process memory; never panics.  
  *live-stream-host/src/rss_monitor.rs:216*
- **`stream-host.rss-monitor.self-restart`** · _beta_ · since 0.4.4 — Self-monitor that samples daemon RSS every 60s — triggers a graceful service self-restart when RSS exceeds OA_STREAMHOST_MAX_RSS_MB (default 2500 MB) or grows >50% over a 30-min sliding window, so multi-day broadcasts survive WebView2/WGC/tokio leaks without OS pressure.  
  *live-stream-host/src/rss_monitor.rs:1*
- **`stream-host.rss-monitor.sliding-window`** · _beta_ · since 0.4.4 — RssMonitor — sliding 30-min RSS sampling buffer + classifier. `tick(now, rss_mb)` returns RssVerdict (Ok / GrowthExceeded baseline×growth_ratio / CeilingExceeded). Drives the graceful self-restart on slow leaks (WebView2 / WGC / tokio) so multi-day broadcasts survive without OS pressure.  
  *live-stream-host/src/rss_monitor.rs:120*

### stream-host/service
- **`stream-host.service.auto-start`** — `install-service` registers the daemon as an OS auto-start service: Windows SCM (auto-restart on crash), macOS launchd plist (KeepAlive=true), Linux systemd user unit (Restart=always). Unix variants run in the user domain — no password required. Cross-platform "register as auto-start service" support. Goal: a 24h broadcast survives a crash because the OS itself respawns the daemon within ~5 s. Each platform has its own supervisor: * **Windows** — register under the Service Control Manager (SCM). SCM auto-restarts on crash, runs at boot, and is the native answer to "this thing must always be up." * **macOS** — launchd plist with `KeepAlive = true`. Plist sits in `~/Library/LaunchAgents/dev.openalice.streamhost.plist` and is loaded with `launchctl load -w`. * **Linux** — systemd user unit with `Restart=always`. Sits in `~/.config/systemd/user/openalice-stream-host.service` and is loaded with `systemctl --user daemon-reload && systemctl --user enable openalice-stream-host`. The Windows side requires Administrator rights (SCM is a privileged surface); the Unix sides run entirely in the user domain so the user never has to type a password. This module produces the manifests + drives the install / uninstall flow. The wizard step ("Install as auto-start service?") in `setup.rs` calls into here when the user opts in.  
  *live-stream-host/src/service_installer.rs:1*

### stream-host/service-installer
- **`stream-host.service-installer.install`** · _beta_ · since 0.4.4 — Cross-platform auto-start installer. macOS → writes `~/Library/LaunchAgents/dev.openalice.streamhost.plist` (KeepAlive + RunAtLoad), Linux → `~/.config/systemd/user/openalice-stream-host.service` (Restart=always, MemoryMax=4G), Windows → `sc.exe create` + `sc.exe failure` for SCM (auto-restart 5000ms ×3). Returns InstallReport with manifest path + activation steps.  
  *live-stream-host/src/service_installer.rs:155*

### stream-host/setup
- **`stream-host.setup.capture-region`** · _ga_ · since 0.2.0 — Optional capture-region prompt — Some(rectangle) for crop-and-stream workflows, None for whole-window default. Surfaced by the `setup` subcommand only; the daemon path always uses the QualityPreset's resolution.  
  *live-stream-host/src/setup.rs:239*
- **`stream-host.setup.collect-rtmp`** · _ga_ · since 0.2.0 — Interactive RTMP destinations wizard — dialoguer-driven prompts with Twitch / YouTube Live / Kick / Custom presets (pre-filled ingest URLs), redacted display of existing keys, primary-vs-secondary toggle. Drives the first-run `pair` UX + the standalone `setup` subcommand.  
  *live-stream-host/src/setup.rs:31*
- **`stream-host.setup.maybe-install-service`** · _beta_ · since 0.4.4 — Wizard tail step — offers to install the auto-start service (launchd / systemd / Windows SCM). Default Y so v0.4.4 users get crash-survival + boot-launch; returns Skipped/Installed/Failed so the caller can hint at a manual `install-service` retry path.  
  *live-stream-host/src/setup.rs:205*
- **`stream-host.setup.quality-prompt`** · _beta_ · since 0.4.0 — Interactive QualityPreset picker. Lists all presets with `WxH @ Nfps, Mk` labels; defaults to the user's current value (P1080 on first run) so re-running setup is one keypress.  
  *live-stream-host/src/setup.rs:154*
- **`stream-host.setup.write-config`** · _ga_ · since 0.2.0 — Persists a HostConfig as pretty-printed TOML to the path (creates parent dirs as needed). Single canonical writer for all subcommands that mutate `~/.config/openalice/stream-host.toml`.  
  *live-stream-host/src/setup.rs:122*

### stream-host/sleep-prevention
- **`stream-host.sleep-prevention.backend-trait`** · _beta_ · since 0.4.4 — SleepBlockerBackend trait — pluggable per-OS sleep-prevention API. Real backends call SetThreadExecutionState (Windows), IOPMAssertionCreateWithName (macOS), org.freedesktop.login1 Inhibit DBus method (Linux). Returns opaque tokens so multiple legs can stack guards independently.  
  *live-stream-host/src/sleep_prevention.rs:55*
- **`stream-host.sleep-prevention.sleep-guard`** · _beta_ · since 0.4.4 — SleepGuard — RAII handle. `acquire` takes a sleep-prevention token from the backend; Drop auto-releases. Acquired on broadcast start, dropped on broadcast end / daemon exit / graceful-recycle, ensuring the OS never sleeps mid-stream.  
  *live-stream-host/src/sleep_prevention.rs:145*

### stream-host/updater
- **`stream-host.updater.auto-update`** — Self-update via GitHub Releases API checked every 6 h. Downloads the versioned asset, verifies SHA-256 against the sibling .sha256, then hands off via --complete-update chain-launch so the running .exe file-lock releases before the swap. Opt out with OA_AUTO_UPDATE=0. Self-update mechanism for the stream-host daemon. On the daemon's normal poll loop we periodically (5s after boot, then every 6h) hit GitHub's `/releases/latest` endpoint, parse the `tag_name`, and compare it to the running binary's `CARGO_PKG_VERSION`. When a newer release is available we surface it via tracing + (when wired) a tray notification, and offer an install flow. Install flow on Windows: 1. Download the matching .exe asset (named per the release-ops convention `openalice-stream-host-{ver}-x86_64-pc-windows-msvc.exe`) to `%TEMP%\openalice-stream-host-{ver}.exe`. 2. Verify the byte count matches the `Content-Length` header so a truncated download can't run. 3. Verify the SHA-256 of the downloaded body matches the sibling `<asset>.sha256` published next to the .exe in the same release. Mismatch → hard-abort; missing sibling → warn + skip (transitional, will be hard-required in v0.5). 4. Spawn the new exe with `--complete-update <old_path>`. The new exe is responsible for the swap; the OLD exe just exits cleanly so its file lock releases (Windows holds an exclusive lock on a running .exe — you cannot overwrite it from inside). 5. Drop on the running daemon's `WgcCapture` / `CaptureProcess` kills ffmpeg so the platforms see the "stopping" heartbeat before the OS frees the slot. 6. New process on `--complete-update <old>`: sleep 1 s for the old handle to release, copy itself over `<old>`, re-launch the installed-location exe with no special args, exit. Why `--complete-update` chain instead of `MoveFileEx`'s `MOVEFILE_DELAY_UNTIL_REBOOT`? The reboot variant requires admin rights AND only swaps on the next reboot — the user has to restart Windows before they're on the new version. The chain variant works as a normal user, completes in ~2 seconds, and is how electron-builder / Squirrel / Tauri's updater all work. Opt-out: `OA_AUTO_UPDATE=0` skips both the periodic check and the `--complete-update` swap entirely. Useful for sysadmins who pin a specific build.  
  *live-stream-host/src/updater.rs:1*
- **`stream-host.updater.check-github`** · _beta_ · since 0.4.0 — Polls GitHub `/releases/latest` for OpenAliceLabs/openalice-stream-host. Strict semver newer-than-current comparison + matching Windows asset lookup (`openalice-stream-host-{ver}-x86_64-pc-windows-msvc.exe`). Returns UpdateInfo (version, download_url, size) when a new release is staged.  
  *live-stream-host/src/updater.rs:124*
- **`stream-host.updater.complete-update-handoff`** · _beta_ · since 0.4.0 — Self-update tail-call handoff. When spawned with `--complete-update <old_exe_path> [--old-pid <pid>]`, sleeps 1s for the old process to release file locks, copies the new exe over the old path, re-launches it, then exits. Sub-microsecond no-op on normal boots. OA_AUTO_UPDATE=0 aborts the swap.  
  *live-stream-host/src/updater.rs:358*
- **`stream-host.updater.install-windows`** · _beta_ · since 0.4.0 — Windows in-place update — downloads the new exe to %TEMP%, verifies Content-Length + asset size, spawns the new binary with `--complete-update <old_exe_path>` so the new process swaps the file once the old one exits. Returns the new PID so the old daemon can hand off cleanly.  
  *live-stream-host/src/updater.rs:218*

### stream-host/watchdog
- **`stream-host.watchdog.backoff-policy`** · _beta_ · since 0.4.4 — BackoffPolicy — exponential-backoff schedule for ffmpeg respawn. Default initial=2s, max=30s (≤YouTube's reconnect grace so the broadcast counter never resets), reset_after=60s. `at(n)` doubles per consecutive failure capped at max.  
  *live-stream-host/src/watchdog.rs:140*
- **`stream-host.watchdog.classify-exit`** · _beta_ · since 0.4.4 — Maps (exit_code, signal, stderr_tail, operator_stopped) → ExitClassification. Scans stderr_tail for known RTMP failure patterns (Connection failed/refused/reset, RTMP_Connect, Broken pipe, EOF, I/O error, Failed to update header, Server error) so events surface "Twitch dropped: Connection refused" instead of "exit code 1".  
  *live-stream-host/src/watchdog.rs:318*
- **`stream-host.watchdog.event-log`** · _beta_ · since 0.4.4 — EventLog — bounded ring-buffer (default 32 entries) of (Instant, WatchdogEvent). Shared between the supervisor loop (writer) + heartbeat tick (reader, flattens last N events into Hub heartbeat payload). Thread-safe via Arc<Mutex<Vec>>.  
  *live-stream-host/src/watchdog.rs:369*
- **`stream-host.watchdog.event-stream`** · _beta_ · since 0.4.4 — WatchdogEvent — supervisor lifecycle events emitted by `supervise`. Spawned/Exited/BackoffSleep/StreamInterruptionRecovered/OperatorStopAck. Heartbeat client flattens the recent buffer into Hub's reliability dashboard.  
  *live-stream-host/src/watchdog.rs:106*
- **`stream-host.watchdog.exit-classification`** · _beta_ · since 0.4.4 — ExitClassification — taxonomy of ffmpeg child exits. OperatorStop (no respawn), UnknownExit{code} (respawn, tag for triage), RtmpDropped{hint} (parsed RTMP failure pattern), Signaled{signal} (SIGSEGV/SIGKILL). Drives the supervise() respawn decision.  
  *live-stream-host/src/watchdog.rs:68*
- **`stream-host.watchdog.supervise`** · _beta_ · since 0.4.4 — Pure supervisor loop — owns the spawn/exit/backoff state machine. Takes injectable spawn + sleep closures so unit tests run without real ffmpeg or wall-clock sleeps. Stops on OperatorStop or armed StopSignal; tags reconnect storm transitions as StreamInterruptionRecovered + bumps the shared InterruptionCounter.  
  *live-stream-host/src/watchdog.rs:247*

---

*Generated by [openalice-atlas](https://atlas.blal.pro) — single source of truth for the openalicelabs ecosystem.*
