openalice-live
**Streaming product on the OpenAlice platform** (paused; structure consolidated 2026-07-06, builds get fixed AT WAKE — deps then switch to the registries: kellnr crates.blal.pro + Verdaccio npm.blal.pro, per the embed pattern).
Architecture rating
Manifest present+valid with 322 features, AGENTS.md exists, docs/ dirs in 4 sub-projects; but 1513 missing docstrings, README states 'paused', and the massive undeclared-env list (80+ vars) shows runtime config is undocumented.
OTEL_EXPORTER_OTLP_ENDPOINT + OTEL_SERVICE_NAME + dedicated OA_OBSERVABILITY_PORT show intent; GET /api/health endpoint declared but returns 404; structured logging unverifiable; tracing wired but non-functional given 0% uptime.
Only 2 migration files (0001_chat_messages, 0001_broadcasts); manifest owns zero tables; OA_TENANT_ID env var suggests tenant awareness but no evidence of RLS or FK constraints; DATABASE_URL + BROADCASTS_DB_URL + CHAT_BRIDGE_DB_URL split across services with no schema governance.
SHARED_JWT_SECRET + OA_INTERNAL_SECRET + OA_TWITCH_OAUTH among 80+ undeclared env vars — secrets exist but zero manifest governance; TENANT_RATE_LIMIT_PER_MINUTE hints at rate-limiting but no evidence of SSRF guards, CSP headers, or injection defenses; auth endpoints referenced (OA_AUTH_URL) but unverifiable with 0% uptime.
5 circular dependency cycles (registry ↔ *App), 133 god modules, 52 duplicate signatures, 42 dynamic imports, 733 format issues; manifest valid and AGENTS.md present but internal code patterns are deeply inconsistent across the 6 sub-projects.
vitest.config.ts in live-hub + live-web and e2e/ directory exist but CI is red (failure); qa-audit scripts present yet contain dead imports; no test count or coverage metric available; critical paths (scene WS, PTY sessions, broadcast lifecycle) unverifiable.
140 bare/broad excepts, max cyclomatic complexity 124, 37 deep-nesting sites; CI conclusion=failure, health probe returns 404, uptime_24h=0%; watchdog env var exists (WATCHDOG_INTERVAL_SECS) but service is down — no evidence of graceful shutdown, retries, or idempotency.
source · auto-grader-2026-07-08
Ecosystem manifest
.atlas-deps.ymlNext.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.
- openalice-authshared-jwtvia SHARED_JWT_SECRETJWTs 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-browsehttpvia BROWSE_INTERNAL_URLServer-side proxy route (/api/browse-session) mints openalice-browse sessions for the in-scene BrowserApp OS window; internal secret stays server-side.
- openalice-cdnhttpvia NEXT_PUBLIC_CDN_URLServes 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.
- via NEXT_PUBLIC_LIVE_CONTROL_URLScene-control WebSocket at /realtime/scene/<broadcast_id>/control — receives ChangeScene ToolCalls + SceneEvent frames from the agent mid-stream without restarting broadcaster Chromium.
- via NEXT_PUBLIC_LIVE_CONTROL_URLHTTP routes /live/start, /live/stop, /live/status, /live/chat-out called by Studio Hub + Studio Chat with the user's bearer token.
- via NEXT_PUBLIC_DEFAULT_DOMAINEmbeds 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-ptyhttpvia PTY_INTERNAL_URLServer-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.
- via NEXT_PUBLIC_RADIO_URLRadioPlayer 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.
- via NEXT_PUBLIC_RT_URLRealtime presence / event bus consumed via @openalicelabs/realtime; used by the scene page control-WS helper (useRealtime) for live broadcast events.
- via NEXT_PUBLIC_TENANTS_URLTenant 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.
- via NEXT_PUBLIC_VOICE_SFU_URLWebRTC 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.
- 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
- 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
- oa-voice-chunk
- oa-voice-finished
- oa-bgm-loop
- oa-sfx-subscribe
- oa-sfx-caption
- oa-voice-chunk
- oa-voice-finished
- oa-bgm-loop
- oa-sfx-subscribe
- oa-sfx-caption
Atlas grepped the repo's source for env reads (rust env::var · ts process.env · py os.environ · shell $VAR) and diffed against the manifest's consumes_env. Two lists below: stale declarations and undeclared reads.
- 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
Roadmap · what's planned next
- scene-modes-cataloguein progress95%Ship the 9 baseline scene modes (vrm-fullscreen, vrm-corner, splitscreen-2/4, desktop-os, intermission, brb, starting-soon, vibecode) production-stable with theme tokens + persona-canvas-host consolidation.
- stream-theme-authorplanned15%Streamer-authored stream theme picker in Studio — full preset catalogue + override editor + CSS-var live-preview, persisted via tenants-client.
- live2d-rendererplanned15%Live2D 2D avatar path inside persona iframe as an alternative to VRM — broadens audience to creators without 3D models.
- webcam-mocapplanned6%Webcam → VMC/VRM puppet for human co-hosts streaming alongside Alice (Mediapipe Face Landmarker + Pose).
- server-side-renderingplanned15%No-GPU server-side scene rendering via Xvfb/Chromium for users without a streaming PC — captures /scene/<user> into RTMP without a local OBS install.
Code composition
tokei · loc- TSX54.2%
- Rust23.2%
- TypeScript20.0%
- Shell1.2%
- Python0.6%
- Other0.9%
- TSX54.2%
- Rust23.2%
- TypeScript20.0%
- Shell1.2%
- Python0.6%
- Other0.9%
- TSX54.2%· 48,628
- Rust23.2%· 20,793
- TypeScript20.0%· 17,945
- Shell1.2%· 1,034
- Python0.6%· 563
- CSS0.4%· 316
- JSON0.2%· 178
- TOML0.1%· 131
- +3 more0.2%· 196 · 11 files
| Language | % code | code | comments | blanks | files |
|---|---|---|---|---|---|
| TSX | 54.2% | 48,628 | 8,855 | 3,376 | 198 |
| Rust | 23.2% | 20,793 | 2,218 | 1,635 | 57 |
| TypeScript | 20.0% | 17,945 | 7,729 | 1,952 | 166 |
| Shell | 1.2% | 1,034 | 211 | 154 | 25 |
| Python | 0.6% | 563 | 21 | 91 | 6 |
| CSS | 0.4% | 316 | 63 | 29 | 2 |
| JSON | 0.2% | 178 | 0 | 0 | 4 |
| TOML | 0.1% | 131 | 106 | 9 | 3 |
| Dockerfile | 0.1% | 102 | 89 | 25 | 4 |
| SQL | 0.1% | 52 | 54 | 8 | 3 |
| JavaScript | 0.0% | 42 | 8 | 7 | 4 |
| Markdown | 0.0% | 0 | 5,450 | 1,311 | 38 |
Telemetry ribbon · last 90 days
26 eventsapp/account
37 features- app.account.agentlive-hub/src/app/account/agent/edit/page.tsx:15Hub 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.page:/account/agentsince 0.5.0
- app.account.agent.redirectlive-hub/src/app/account/agent/page.tsx:17Legacy /account/agent path redirects to the new /account/agents list. The editor moved to /account/agent/edit.page:/account/agent → /account/agentssince 0.11.0
- app.account.agents.listlive-hub/src/app/account/agents/page.tsx:17Hub agents list — one row per agent under this tenant, link to per-agent settings.page:/account/agentssince 0.11.0
- app.account.agents.settingslive-hub/src/app/account/agents/[id]/settings/page.tsx:31Per-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.page:/account/agents/[id]/settingssince 0.11.1
- app.account.ai-moderationlive-hub/src/app/account/ai-moderation/page.tsx:43Hub 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).page:/account/ai-moderationsince 0.13.0
- app.account.chat-sourceslive-hub/src/app/account/chat-sources/page.tsx:96Hub page for managing per-platform live chat connections (YouTube / Twitch / Kick / TikTok) the chat-bridge service consumes. CRUD + masked preview + reveal-after-password.page:/account/chat-sourcessince 0.5.0
- app.account.dashboardlive-hub/src/app/account/page.tsx:4Logged-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.since 0.4.0
- app.account.deviceslive-hub/src/app/account/devices/page.tsx:26Hub page for managing paired openalice-stream-host installs. Generate pairing codes, list claimed devices, revoke. 90-day device JWTs scoped to live + persona only.page:/account/devicessince 0.6.0
- app.account.loungelive-hub/src/app/account/lounge/page.tsx:19Hub 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.page:/account/loungesince 0.11.0
- app.account.lounge.auditlive-hub/src/app/account/lounge/[id]/audit/page.tsx:19Per-room unified moderation audit log — bans/mutes/deletes/reports/mods/settings; mod-only.page:/account/lounge/[id]/auditsince 0.13.0
- app.account.lounge.modslive-hub/src/app/account/lounge/[id]/mods/page.tsx:20Per-room moderation panel — Phase 4 bans/mutes/mods/settings, owner-only grant picker.page:/account/lounge/[id]/modssince 0.11.0
- app.account.lounge.reportslive-hub/src/app/account/lounge/[id]/reports/page.tsx:24Per-room report queue — list open user reports, resolve via dismiss/delete/mute/ban actions; live WS subscription with REST fallback; mod-only.page:/account/lounge/[id]/reportssince 0.12.0
- app.account.radiolive-hub/src/app/account/radio/page.tsx:11Hub 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.page:/account/radiosince 0.11.0
- app.account.redemption-triggerslive-hub/src/app/account/triggers/page.tsx:34Hub 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.page:/account/triggerssince 0.11.0
- app.account.securitylive-hub/src/app/account/security/page.tsx:23TOTP setup + disable surface for the authenticated user. Renders an otpauth QR + 6-digit verify input + password-gated disable form.page:/account/securitysince 0.5.0
- app.account.sessionslive-hub/src/app/account/sessions/page.tsx:16Hub page listing the user's active refresh-token sessions with revoke buttons. Powered by /auth/sessions + /auth/sessions/:id.page:/account/sessionssince 0.5.0
- app.account.soundboardlive-hub/src/app/account/sounds/page.tsx:29Hub Soundboard page — upload, tag, bind, test-fire SFX clips for the agent's `play_sound` tool. Powers the Neuro-sama-style soundboard.page:/account/soundssince 0.10.0
- app.account.streamslive-hub/src/app/account/streams/page.tsx:79Hub page for managing per-platform stream destinations the broadcaster service pushes to. CRUD + masked-list + reveal-after-password.page:/account/streamssince 0.5.0
- app.account.streams-chat-modlive-hub/src/app/account/streams/[id]/chat-mod/page.tsx:32Mod-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.page:/account/streams/[id]/chat-modsince 0.12.0
- app.account.streams.chat-pagelive-hub/src/app/account/streams/chat/page.tsx:27Studio Chat Tab page mounting StudioChatTab. Polls /live/status to discover the active broadcast and renders a "go live first" idle state otherwise.page:/account/streams/chatsince 0.13.0
- app.account.streams.engagement-pickerlive-hub/src/app/account/streams/_components/EngagementPresetPicker.tsx:40Hub 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.component:EngagementPresetPickersince 0.11.0
- app.account.streams.engagement-preset-fetchlive-hub/src/lib/api/broadcasts.ts:197Read 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.since 0.11.0
- app.account.streams.engagement-presetslive-hub/src/lib/engagementPresets.ts:23Hub-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.component:EngagementPresetPickersince 0.11.0
- app.account.streams.engagement-preset-savelive-hub/src/lib/api/broadcasts.ts:222Persist 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.since 0.11.0
- app.account.streams.host-healthlive-hub/src/app/account/streams/_components/HostHealthCard.tsx:32Per-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.component:HostHealthCardsince 0.13.1
- app.account.streams.poll-managerlive-hub/src/app/account/streams/_components/PollManager.tsx:31Hub poll management surface — Create modal, active poll panel with countdown + Close-early, history list. Mounts under /account/streams when a broadcast is live.component:PollManagersince 0.12.0
- app.account.streams.scene-previewlive-hub/src/app/account/streams/_components/ScenePreviewButtons.tsx:55Two 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.component:ScenePreviewButtonssince 0.6.0
- app.account.streams.theme-pickerlive-hub/src/app/account/streams/_components/ThemePicker.tsx:69Studio-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.component:ThemePickersince 0.6.0
- app.account.streams.theme-picker.sfxlive-hub/src/app/account/streams/_components/SfxPickerSection.tsx:25Hub-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.component:SfxPickerSectionsince 0.9.0
- app.account.streams.theme-quick-swaplive-hub/src/app/account/streams/_components/ThemeQuickSwap.tsx:72Mid-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.component:ThemeQuickSwapsince 0.6.0
- app.account.streams.theme-savelive-hub/src/lib/api/broadcasts.ts:131Persist 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.since 0.6.0
- app.account.youtubelive-hub/src/app/account/youtube/page.tsx:53Hub 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.page:/account/youtubesince 0.12.0
- app.account.youtube.audit-feedlive-hub/src/app/account/youtube/page.tsx:773Hub 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.component:AuditFeedCardsince 0.13.0
- app.account.youtube.auto-modlive-hub/src/app/account/youtube/auto-mod/page.tsx:50Hub page for the static auto-moderation rule set + shared mod-action audit log. Companion of /account/youtube + /account/ai-moderation.page:/account/youtube/auto-modsince 0.13.0
- app.account.youtube.helperslive-hub/src/app/account/youtube/helpers.ts:8Time / IP formatters used by the YouTube settings page (expires-in countdown, "last refreshed" indicator, audit-feed IP truncator).function:formatExpiresInSecondsfunction:secondsUntilfunction:truncateIpsince 0.13.0
- app.studio.chat.sendlive-hub/src/lib/api/chat.ts:167Hub-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.since 0.13.0
- app.studio.chat-tablive-hub/src/app/account/streams/_components/StudioChatTab.tsx:98Studio 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.component:StudioChatTabpage:/account/streams/chatsince 0.13.0
app/api
7 features- app.api.internal.streamhost.heartbeatlive-hub/src/app/api/internal/streamhost/heartbeat/route.ts:15Internal-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.route:POST /api/internal/streamhost/heartbeatroute:GET /api/internal/streamhost/heartbeatsince 0.13.1
- app.api.persona-bfflive-hub/src/app/api/persona/[...path]/route.ts:40Catch-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.route:GET /api/persona/*route:POST /api/persona/*route:PATCH /api/persona/*route:DELETE /api/persona/*route:PUT /api/persona/*since 0.7.0
- app.api.radio-bfflive-hub/src/app/api/radio/[...path]/route.ts:10Catch-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.route:GET /api/radio/*route:POST /api/radio/*route:PATCH /api/radio/*route:DELETE /api/radio/*since 0.11.0
- app.api.streamhost.heartbeat-readlive-hub/src/app/api/streamhost/heartbeat/route.ts:36Browser-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.route:GET /api/streamhost/heartbeatsince 0.13.2
- app.api.youtube-oauth-callbacklive-hub/src/app/api/youtube/oauth/callback/route.ts:40Completes 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.route:GET /api/youtube/oauth/callbacksince 0.12.1
- app.api.youtube-oauth-completelive-hub/src/app/api/youtube/oauth/complete/route.ts:31Client-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.route:POST /api/youtube/oauth/completesince 0.12.1
- app.api.youtube-oauth-startlive-hub/src/app/api/youtube/oauth/start/route.ts:41Builds 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.route:GET /api/youtube/oauth/startsince 0.12.0
app/auth
5 features- app.auth.forgot-passwordlive-hub/src/app/forgot-password/page.tsx:25Public form that requests a password-reset link by email.page:/forgot-passwordsince 0.1.0
- app.auth.loginlive-hub/src/app/login/page.tsx:28Public login + signup form with two-leg MFA prompt. Honorspage:/loginsince 0.1.0
- app.auth.reset-passwordlive-hub/src/app/reset-password/page.tsx:22Set a new password using a single-use reset token.page:/reset-passwordsince 0.1.0
- app.auth.safe-return-urllive-hub/src/lib/safe-return-url.ts:2Validate `?return=<url>` query params for post-login redirects.lib:safe-return-urlsince 0.2.0
- app.auth.verify-emaillive-hub/src/app/verify/page.tsx:21Verify the email-verification token and bounce to /account.page:/verifysince 0.1.0
app/component
1 feature- app.component.font-pickerlive-hub/src/app/account/streams/_components/FontPicker.tsx:163Custom-font gallery — list, upload (drag-drop + file picker), rename, delete with confirm, "Use as display/body/mono" hooks for the Theme picker.component:FontPickersince 0.6.0
app/components
1 feature- app.components.confirm-dialoglive-hub/src/components/ConfirmDialog.tsx:33Shared async confirm() dialog + useConfirm() hook. Replaces native window.confirm() across the Hub.component:ConfirmDialoghook:useConfirmcomponent:ConfirmProvidersince 0.14.0
app/hub
2 features- app.hub.launcherlive-hub/src/app/page.tsx:82Top-level OpenAlice ecosystem launcher tiling Alice, Social, World, Persona, Live, wrapped in the shared EcosystemShell.page:/since 0.1.0
- app.hub.online-userslive-hub/src/app/_components/OnlineUsers.tsx:21Hub home widget listing users currently online across the ecosystem (last_seen ≤ 2 min, status ≠ invisible). Polls every 30s.component:OnlineUserssince 0.5.0
app/lib
23 features- app.lib.access-tokenlive-hub/src/lib/access-token.ts:39Verify openalice-auth-issued access JWTs on the Hub. Used by routes that need a local user_id from the bearer without forwarding to persona.function:verifyAccessTokenfunction:extractBearerclass:AccessTokenErrorsince 0.13.2
- app.lib.agents-apilive-hub/src/lib/agentsApi.ts:24Typed client for /v1/me/agents on persona-server. Powers the AgentPicker dropdown and the per-agent settings page.function:listAgentsfunction:getAgentsince 0.11.0
- app.lib.ai-mod-apilive-hub/src/lib/aiModApi.ts:26Typed client for the AI moderation REST surfaces on persona-server. Powers `/account/ai-moderation` settings + pending-review + training-feedback panels.function:fetchAiModSettingsfunction:patchAiModSettingsfunction:fetchAiModActionsfunction:undoAiModActionfunction:fetchAiModFeedbackfunction:submitAiModFeedbacksince 0.13.0
- app.lib.api-clientlive-hub/src/lib/api-client.ts:47Shared apiFetch() helper — Bearer header + JSON parse + error normalization. Backs every src/lib/*Api.ts client.function:apiFetchfunction:personaApiFetchclass:ApiErrorclass:NotSignedInErrorsince 0.12.0
- app.lib.audio-apilive-hub/src/lib/audioApi.ts:37Typed client for /v1/me/audio CRUD on persona-server — Bearer JWT, multipart upload, JSON rename, public URL decoration. Powers the Hub Music & Audio picker.function:listAudiofunction:uploadAudiofunction:renameAudiofunction:deleteAudiosince 0.10.0
- app.lib.fonts-apilive-hub/src/lib/fontsApi.ts:37Typed client for /v1/me/fonts CRUD on persona-server — Bearer JWT, multipart upload, JSON rename, public URL decoration.function:listFontsfunction:uploadFontfunction:renameFontfunction:deleteFontsince 0.6.0
- app.lib.forwarded-originlive-hub/src/lib/forwarded-origin.ts:31Reconstructs 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.function:publicOriginsince 0.12.1
- app.lib.idle-config-apilive-hub/src/lib/idleConfigApi.ts:23Typed client for /v1/me/agents/:id/idle-config — backs the Hub's Idle behaviour section.function:getIdleConfigfunction:patchIdleConfigsince 0.11.2
- app.lib.live-sceneslive-hub/src/lib/liveScenes.ts:24Canonical scene-mode catalog for the Hub. Mirrors openalice-live/SCENE_REGISTRY. Drives "Allowed scenes" config + channel-point trigger validation.const:LIVE_SCENESconst:LIVE_SCENE_IDSconst:DEFAULT_LIVE_SCENE_IDfunction:isValidLiveSceneIdsince 0.14.0
- app.lib.lounge-apilive-hub/src/lib/loungeApi.ts:11Hub-side typed client for Discord-Style Lounge — listing rooms + Phase-4 moderation primitives (bans, mutes, mods, settings).function:listMyRoomsfunction:getModerationSnapshotfunction:grantModeratorfunction:revokeModeratorfunction:unbanUserfunction:unmuteUserfunction:listReportsfunction:resolveReportfunction:listAuditfunction:setSlowModefunction:setModOnlyModesince 0.11.0
- app.lib.lounge-reports-wslive-hub/src/lib/loungeReportsWs.ts:59Live 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.function:openLoungeReportsWssince 0.13.0
- app.lib.music-gen-apilive-hub/src/lib/musicGenApi.ts:29Typed 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.function:generateMusicfunction:getMusicGenJobfunction:listMusicGenJobssince 0.11.0
- app.lib.polls-apilive-hub/src/lib/pollsApi.ts:24Typed client for /v1/me/polls (engagement polls). Powers the Hub Create-poll modal + active poll panel + history list.function:createPollfunction:closePollfunction:listPollsfunction:getPollsince 0.12.0
- app.lib.redemption-triggers-apilive-hub/src/lib/redemptionTriggersApi.ts:37Typed 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.function:listRedemptionTriggersfunction:createRedemptionTriggerfunction:patchRedemptionTriggerfunction:deleteRedemptionTriggersince 0.11.0
- app.lib.soundboard-settings-apilive-hub/src/lib/soundboardSettingsApi.ts:22Typed client for the per-agent soundboard kill switch on persona-server. Powers the Hub agent-settings toggle.function:listSoundboardSettingsfunction:setSoundboardEnabledsince 0.10.1
- app.lib.sounds-apilive-hub/src/lib/soundsApi.ts:31Typed 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.function:listSoundsfunction:uploadSoundfunction:patchSoundfunction:deleteSoundfunction:testFireSoundsince 0.10.0
- app.lib.static-mod-apilive-hub/src/lib/staticModApi.ts:22Typed client for the static-mod REST surface on persona-server. Powers `/account/youtube/auto-mod` Static rules tab + Action log tab.function:listStaticModRulesfunction:createStaticModRulefunction:patchStaticModRulefunction:deleteStaticModRulefunction:fetchModActionsLogsince 0.13.0
- app.lib.use-auto-dismiss-errorlive-hub/src/lib/use-auto-dismiss-error.ts:31React hook that auto-dismisses an error string after a configurable timeout. Single source of truth for the Hub's auto-fading error banners.hook:useAutoDismissErrorsince 0.14.0
- app.lib.use-redirect-on-401live-hub/src/lib/use-redirect-on-401.ts:30React hook that redirects to /login?return=<path> when a page's error state indicates an expired session.hook:useRedirectOn401function:isUnauthenticatedErrorsince 0.14.0
- app.lib.voices-apilive-hub/src/lib/voicesApi.ts:24Curated 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.function:listVoicesfunction:previewVoicesince 0.11.1
- app.lib.vrms-apilive-hub/src/lib/vrmsApi.ts:18Typed client for the per-tenant VRM library on persona-server. Powers the Hub's per-agent Avatar picker.function:listVrmssince 0.11.1
- app.lib.youtube-apilive-hub/src/lib/youtubeApi.ts:38Typed client for YT OAuth + mod-page surfaces on persona-server. Powers `/account/youtube` settings page + `/account/streams/[id]/chat-mod` mod view.function:fetchYouTubeOAuthfunction:disconnectYouTubefunction:fetchModPolicyfunction:patchModPolicyfunction:moderateDeletefunction:moderateBanfunction:fetchYtChatRecentsince 0.12.0
- app.lib.yt-handofflive-hub/src/lib/yt-handoff.ts:38HS256 JWT helpers (sign + verify) for the YouTube OAuth callback handoff cookie. Uses SHARED_JWT_SECRET + aud="oa-yt-handoff".function:signHandoffTokenfunction:verifyHandoffTokenconst:YT_HANDOFF_COOKIEconst:YT_HANDOFF_AUDsince 0.12.1
app/radio
2 features- app.radio.api-clientlive-hub/src/app/account/radio/radioApi.ts:8Typed 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.since 0.4.0
- app.radio.componentslive-hub/src/app/account/radio/_components.tsx:7Presentational 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.since 0.4.0
app/sounds
1 feature- app.sounds.agent-pickerlive-hub/src/app/account/sounds/_components/AgentPicker.tsx:29Dropdown 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.since 0.5.0
app/streamhost
1 feature- app.streamhost.heartbeat-storelive-hub/src/app/api/internal/streamhost/heartbeat/_store.ts:9In-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.since 0.5.0
app/streams/theme-picker
6 features- app.streams.theme-picker.avatar-renderlive-hub/src/app/account/streams/_components/_theme-picker/AvatarRenderSection.tsx:26Authoring 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.since 0.6.0
- app.streams.theme-picker.backgroundlive-hub/src/app/account/streams/_components/_theme-picker/BackgroundSection.tsx:24Scene 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.since 0.6.0
- app.streams.theme-picker.custom-overridelive-hub/src/app/account/streams/_components/_theme-picker/CustomOverrideEditor.tsx:19Expand/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.since 0.6.0
- app.streams.theme-picker.fieldslive-hub/src/app/account/streams/_components/_theme-picker/fields.tsx:16Shared 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.since 0.6.0
- app.streams.theme-picker.music-audiolive-hub/src/app/account/streams/_components/_theme-picker/MusicAudioSection.tsx:31BGM 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.since 0.6.0
- app.streams.theme-picker.preset-gallerylive-hub/src/app/account/streams/_components/_theme-picker/PresetGallery.tsx:1316: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.since 0.6.0
app/streams/theming
9 features- app.lib.streamThemes.shimlive-hub/src/lib/streamThemes.ts:20Backwards-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.since 0.6.0
- app.lib.themes.barrellive-hub/src/lib/themes/index.ts:22Public catalogue surface for the streaming theme system — types, 7 stream presets, 3 avatar-render presets, 8 background presets, 5 SFX presets, and builder helpers.since 0.6.0
- app.lib.themes.builderlive-hub/src/lib/themes/builder.ts:11Stateless StreamTheme helpers — compactTheme strip-empty, encodeThemeForPreview URL-safe JSON, STREAM_THEME_FIELDS form-builder map, STREAM_FONT_OPTIONS curated font-family list.since 0.6.0
- app.lib.themes.types.avatar-renderlive-hub/src/lib/themes/types/avatar-render.ts:13Persona-side post-processing contract — render-mode (3d / 2d-toon / 2d-flat), outline, hologram, chromatic aberration, scan-lines, CRT flicker, glow, toon-steps.since 0.6.0
- app.lib.themes.types.bgmlive-hub/src/lib/themes/types/bgm.ts:13Hub-side BGM config mirror — drives the Music & Audio picker and the live BgmPlayer auto-fade / ducking behaviour.since 0.6.0
- app.lib.themes.types.custom-fontlive-hub/src/lib/themes/types/custom-font.ts:11Per-tenant uploaded font binding — family-name → woff2 URL. The live StreamThemeProvider emits one @font-face rule per entry.since 0.6.0
- app.lib.themes.types.scene-backgroundlive-hub/src/lib/themes/types/scene-background.ts:10Hub-side scene-background contract — color / image / video / preset source, parallax sprites, particle overlay, and Ken-Burns / slow-pan drift.since 0.6.0
- app.lib.themes.types.sfxlive-hub/src/lib/themes/types/sfx.ts:17Hub-side SFX config + preset shape mirrors. Drives the picker's Sound Effects section and the live SfxPlayer event listeners.since 0.6.0
- app.lib.themes.types.stream-themelive-hub/src/lib/themes/types/stream-theme.ts:15Core StreamTheme contract — the flat design-token bag every theme preset materialises and the Hub theme picker mutates.since 0.6.0
app/streams/theming/avatar-render
4 features- app.lib.themes.avatar-render.cinema-clean-3dlive-hub/src/lib/themes/avatar-render-presets/cinema-clean-3d.ts:5Zero-cost 3D render-mode preset — no outline, no hologram. The "reset to default" option for the avatar-render picker.since 0.6.0
- app.lib.themes.avatar-render.holo-cyanlive-hub/src/lib/themes/avatar-render-presets/holo-cyan.ts:18Cyan 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.since 0.6.0
- app.lib.themes.avatar-render.neuro-sama-classiclive-hub/src/lib/themes/avatar-render-presets/neuro-sama-classic.ts:17Cel-shaded 2D-toon avatar render preset with #222222 ink outline + 4 toon-steps. The polished-VTuber default.since 0.6.0
- app.lib.themes.avatar-render.registrylive-hub/src/lib/themes/avatar-render-presets/index.ts:63-preset avatar render-mode catalogue — cinema-clean-3d / neuro-sama-classic / holo-cyan + picker meta. Drives the AvatarRenderSection of ThemePicker.since 0.6.0
app/streams/theming/backgrounds
10 features- app.lib.themes.backgrounds.anime-classroomlive-hub/src/lib/themes/backgrounds/anime-classroom.ts:4Slice-of-life classroom scene — static WebP base + a parallax cloud sprite (screen-blend, 0.7 opacity, 0.15 speed) + low-density cherry-blossom petals.since 0.6.0
- app.lib.themes.backgrounds.auroralive-hub/src/lib/themes/backgrounds/aurora.ts:4Northern-lights scene — static WebP aurora + low-density pale-cyan stars + Ken-Burns drift. Night-sky / dreamy mood.since 0.6.0
- app.lib.themes.backgrounds.beach-sunsetlive-hub/src/lib/themes/backgrounds/beach-sunset.ts:4Tropical-sunset scene — looping waves video + Ken-Burns slow drift, no particle overlay.since 0.6.0
- app.lib.themes.backgrounds.cdnlive-hub/src/lib/themes/backgrounds/cdn.ts:12CDN base-URL helper for bundled scene assets — `${NEXT_PUBLIC_CDN_URL ?? cdn.blal.pro}/scenes`. Imported by every bundled background preset.since 0.6.0
- app.lib.themes.backgrounds.cozy-cafelive-hub/src/lib/themes/backgrounds/cozy-cafe.ts:4Coffee-shop interior scene background — looping video, slow-pan drift, no particle overlay.since 0.6.0
- app.lib.themes.backgrounds.cyberpunk-citylive-hub/src/lib/themes/backgrounds/cyberpunk-city.ts:4Cyberpunk neon-street scene — looping video + cool-cyan rain overlay at 0.6 density. No drift, ambient motion comes from the video.since 0.6.0
- app.lib.themes.backgrounds.lofi-roomlive-hub/src/lib/themes/backgrounds/lofi-room.ts:4Cozy lofi-room scene background — looping video + warm-amber dust particles at 0.3 density, no drift. Chill / study stream mood.since 0.6.0
- app.lib.themes.backgrounds.nonelive-hub/src/lib/themes/backgrounds/none.ts:4No-op background — solid theme.bg color, no particles, no drift. The "reset" option in the background picker.since 0.6.0
- app.lib.themes.backgrounds.registrylive-hub/src/lib/themes/backgrounds/index.ts:128-preset scene-background catalogue + picker meta + segmented-control option lists (particles / drift / parallax-blend).since 0.6.0
- app.lib.themes.backgrounds.space-starslive-hub/src/lib/themes/backgrounds/space-stars.ts:4Deep-space scene — solid #020208 base + white twinkling stars at 0.7 density + slow-pan drift. Sci-fi / coding-stream mood.since 0.6.0
app/streams/theming/presets
8 features- app.lib.themes.presets.editorial-printlive-hub/src/lib/themes/presets/editorial-print.ts:4Newsprint stream theme — off-white canvas, brick-red accent, serif stack, grid pattern, hard shadows + static motion. The longform-essay mood.since 0.6.0
- app.lib.themes.presets.glass-darklive-hub/src/lib/themes/presets/glass-dark.ts:4Vision-Pro inspired frosted-glass stream theme — pastel-purple primary, soft shadow, noise pattern, big 20px corner radius.since 0.6.0
- app.lib.themes.presets.minimal-monolive-hub/src/lib/themes/presets/minimal-mono.ts:4Swiss-design stream theme — pure black on white, square corners, no shadow, static motion. Typography-portfolio / lecture mood.since 0.6.0
- app.lib.themes.presets.pastel-bubblegumlive-hub/src/lib/themes/presets/pastel-bubblegum.ts:4Kawaii / cozy stream theme — bubblegum pink + mint accent, rounded 24px corners, Quicksand display, playful motion + dots pattern.since 0.6.0
- app.lib.themes.presets.registrylive-hub/src/lib/themes/presets/index.ts:107-preset stream-theme catalogue + picker meta. Drives the ThemePicker gallery + the resolveTheme helper that seeds defaults before custom overrides.since 0.6.0
- app.lib.themes.presets.vintage-sepialive-hub/src/lib/themes/presets/vintage-sepia.ts:4Old-paper stream theme — sepia ink on cream, brick-orange accent, olive secondary, serif stack. The library / radio-show mood.since 0.6.0
- app.lib.themes.presets.vtuber-neon-cyanlive-hub/src/lib/themes/presets/vtuber-neon-cyan.ts:4Cyberpunk stream theme — electric cyan accent, magenta secondary, monospace stack, warp bg-pattern, hard shadows + playful motion.since 0.6.0
- app.lib.themes.presets.vtuber-warmlive-hub/src/lib/themes/presets/vtuber-warm.ts:4Atelier-default stream theme — warm amber accent on cream, soft shadow + noise pattern. The cozy long-form-stream default.since 0.6.0
app/streams/theming/sfx
7 features- app.lib.themes.sfx.arcadelive-hub/src/lib/themes/sfx-presets/arcade.ts:4Arcade SFX preset — 8-bit blips, retro pops, coin pings, fanfare. Tuned for gaming / retro streams.since 0.6.0
- app.lib.themes.sfx.cdnlive-hub/src/lib/themes/sfx-presets/cdn.ts:6CDN base-URL helper for bundled SFX clips — `${NEXT_PUBLIC_CDN_URL ?? cdn.blal.pro}/audio/sfx`. Imported by every bundled SFX preset.since 0.6.0
- app.lib.themes.sfx.naturelive-hub/src/lib/themes/sfx-presets/nature.ts:4Nature SFX preset — water drops, wind, soft chimes, bird tweets. Tuned for cozy / lofi streams.since 0.6.0
- app.lib.themes.sfx.registrylive-hub/src/lib/themes/sfx-presets/index.ts:105-preset SFX catalogue + 5-channel event meta + default volume constants. Drives the SfxPickerSection of ThemePicker and the live SfxPlayer event dispatch.since 0.6.0
- app.lib.themes.sfx.sci-filive-hub/src/lib/themes/sfx-presets/sci-fi.ts:4Sci-fi SFX preset — synth pulses, hologram zaps, deep bass thumps. Tuned for cyberpunk / hacker streams.since 0.6.0
- app.lib.themes.sfx.silentlive-hub/src/lib/themes/sfx-presets/silent.ts:4Silent 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.since 0.6.0
- app.lib.themes.sfx.subtle-warmlive-hub/src/lib/themes/sfx-presets/subtle-warm.ts:4Subtle-warm SFX preset — quiet bell + soft pop + gentle whoosh + warm doorbell + light click. The lowest-distraction default for most streams.since 0.6.0
chat-bridge/aggregator
3 features- chat-bridge.aggregator.chat_feedlive-chat-bridge/src/aggregator.rs:6FeedAggregator 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.
- chat-bridge.aggregator.feed-statelive-chat-bridge/src/aggregator.rs:105FeedAggregator — 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.since 0.2.0
- chat-bridge.aggregator.tick-looplive-chat-bridge/src/aggregator.rs:515spawn_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.since 0.2.0
chat-bridge/binary
1 feature- chat-bridge.binary.entrypointlive-chat-bridge/src/main.rs:4Thin 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.since 0.1.0
chat-bridge/boot
2 features- chat-bridge.boot.auto-discoverlive-chat-bridge/src/init.rs:331Auto-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.since 0.3.0
- chat-bridge.init.boot-helperslive-chat-bridge/src/init.rs:8Boot-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.since 0.4.0
chat-bridge/cli
1 feature- chat-bridge.cli.argslive-chat-bridge/src/cli.rs:7clap-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.since 0.4.0
chat-bridge/contract
1 feature- chat-bridge.contract.message-to-connector-eventlive-chat-bridge/src/sink.rs:533Translates 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.since 0.2.0
chat-bridge/dispatch
6 features- chat-bridge.dispatch.pipelinelive-chat-bridge/src/sink.rs:363Pipeline — 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.since 0.2.0
- chat-bridge.dispatch.pipeline_fanoutlive-chat-bridge/src/sink.rs:5Pipeline::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.
- chat-bridge.sink.agent-dispatchlive-chat-bridge/src/sink.rs:284AgentDispatchSink — 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.http_client:/internal/agent/dispatchsince 0.2.0
- chat-bridge.sink.authlive-chat-bridge/src/sink.rs:39AuthSink — 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.http_client:/internal/chatsince 0.1.0
- chat-bridge.sink.scene-alertslive-chat-bridge/src/sink.rs:197SceneAlertSink — 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.http_client:/internal/scene/alertsince 0.3.0
- chat-bridge.sink.scene-chatlive-chat-bridge/src/sink.rs:103SceneChatSink — 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).http_client:/internal/scene/chatsince 0.3.0
chat-bridge/http
5 features- chat-bridge.http.gdpr-exportlive-chat-bridge/src/routes.rs:471GET /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.http:/internal/me/{tenant_id}/export.jsonsince 0.4.0
- chat-bridge.http.observability-serverlive-chat-bridge/src/routes.rs:588Tiny 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).http:/healthhttp:/metricssince 0.2.0
- chat-bridge.http.routes-modulelive-chat-bridge/src/routes.rs:10Dedicated 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.since 0.4.0
- chat-bridge.http.v1-moderatelive-chat-bridge/src/routes.rs:306POST /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.http:/v1/moderatesince 0.3.0
- chat-bridge.http.v1-sendlive-chat-bridge/src/routes.rs:119POST /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.http:/v1/sendsince 0.3.0
chat-bridge/ingest
4 features- chat-bridge.ingest.kick_pusherlive-chat-bridge/src/kick.rs:8Kick 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.
- chat-bridge.ingest.tiktok_webcastlive-chat-bridge/src/tiktok.rs:9TikTok 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.
- chat-bridge.ingest.twitch_irclive-chat-bridge/src/twitch.rs:9Twitch 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.
- chat-bridge.ingest.youtube_pollinglive-chat-bridge/src/youtube.rs:10YouTube 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.
chat-bridge/kick
3 features- chat-bridge.kick.chatroom-resolvelive-chat-bridge/src/kick.rs:68Resolves 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.since 0.2.0
- chat-bridge.kick.connector-looplive-chat-bridge/src/kick.rs:112Supervises 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.since 0.2.0
- chat-bridge.kick.poller-statelive-chat-bridge/src/kick.rs:89KickChatPoller — 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.since 0.2.0
chat-bridge/observability
2 features- chat-bridge.tracing.otel-bootstraplive-chat-bridge/src/init.rs:105Opt-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.since 0.4.0
- chat-bridge.tracing.subscriber-installlive-chat-bridge/src/init.rs:48Always-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.since 0.4.0
chat-bridge/persistence
7 features- chat-bridge.chat-store.broadcast-countslive-chat-bridge/src/chat_store.rs:248GROUP 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.since 0.4.0
- chat-bridge.chat-store.connectlive-chat-bridge/src/chat_store.rs:77Opens the PgPool, runs sqlx::migrate! to create chat_messages table/indexes idempotently, returns a ChatStore handle.since 0.4.0
- chat-bridge.chat-store.handlelive-chat-bridge/src/chat_store.rs:60ChatStore — 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.since 0.4.0
- chat-bridge.chat-store.insert-inboundlive-chat-bridge/src/chat_store.rs:121Persist 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.since 0.4.0
- chat-bridge.chat-store.insert-outboundlive-chat-bridge/src/chat_store.rs:156Persist 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.since 0.4.0
- chat-bridge.chat-store.list-for-tenantlive-chat-bridge/src/chat_store.rs:192Read 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.since 0.4.0
- chat-bridge.persistence.chat_storelive-chat-bridge/src/chat_store.rs:10Postgres-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.
chat-bridge/platforms
5 features- chat-bridge.platforms.dispatch-modulelive-chat-bridge/src/platforms/mod.rs:11Top-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.since 0.4.0
- chat-bridge.platforms.kicklive-chat-bridge/src/platforms/kick.rs:5Kick 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.since 0.4.0
- chat-bridge.platforms.tiktoklive-chat-bridge/src/platforms/tiktok.rs:5TikTok 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.since 0.4.0
- chat-bridge.platforms.twitchlive-chat-bridge/src/platforms/twitch.rs:7Twitch 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.since 0.4.0
- chat-bridge.platforms.youtubelive-chat-bridge/src/platforms/youtube.rs:5YouTube 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.since 0.4.0
chat-bridge/rate-limit
2 features- chat-bridge.rate-limit.platform-cap-tablelive-chat-bridge/src/rate_limit.rs:46PlatformLimit — 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.since 0.3.0
- chat-bridge.rate-limit.token-bucketlive-chat-bridge/src/rate_limit.rs:85OutboundRateLimiter — 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.since 0.3.0
chat-bridge/redemption
2 features- chat-bridge.redemption.event-wirelive-chat-bridge/src/redemption.rs:37RedemptionEvent — 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.since 0.5.0
- chat-bridge.redemption.sinklive-chat-bridge/src/redemption.rs:19Normalised 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.since 0.5.0
chat-bridge/senders
3 features- chat-bridge.senders.moderator-commandlive-chat-bridge/src/senders.rs:62ModeratorCommand — 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.since 0.3.0
- chat-bridge.senders.outbound-chat-messagelive-chat-bridge/src/senders.rs:35OutboundChat — 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).since 0.3.0
- chat-bridge.senders.registrylive-chat-bridge/src/senders.rs:19Outbound 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.since 0.3.0
chat-bridge/tiktok
2 features- chat-bridge.tiktok.connector-looplive-chat-bridge/src/tiktok.rs:180Supervises 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.since 0.4.0
- chat-bridge.tiktok.poller-statelive-chat-bridge/src/tiktok.rs:120TikTokChatPoller — per-broadcast state (sanitised @handle without leading @, broadcast_id stamp, optional eulerstream paid-tier sign_api_key read from OA_TIKTOK_SIGN_API_KEY).since 0.4.0
chat-bridge/twitch
6 features- chat-bridge.twitch.connector-looplive-chat-bridge/src/twitch.rs:128Supervises 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.since 0.1.0
- chat-bridge.twitch.eventsublive-chat-bridge/src/twitch_eventsub.rs:41Twitch 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.since 0.5.0
- chat-bridge.twitch.eventsub-looplive-chat-bridge/src/twitch_eventsub.rs:93Long-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).since 0.5.0
- chat-bridge.twitch.helix-clientlive-chat-bridge/src/twitch_helix.rs:51HelixClient — 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}).since 0.4.0
- chat-bridge.twitch.helix-moderationlive-chat-bridge/src/twitch_helix.rs:28Helix 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.since 0.4.0
- chat-bridge.twitch.poller-statelive-chat-bridge/src/twitch.rs:51TwitchChatPoller — 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.since 0.1.0
chat-bridge/types
1 feature- chat-bridge.types.wirelive-chat-bridge/src/types.rs:5Canonical 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.since 0.1.0
chat-bridge/writeback
1 feature- chat-bridge.writeback.rate_limiterlive-chat-bridge/src/rate_limit.rs:9OutboundRateLimiter 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.
chat-bridge/youtube
8 features- chat-bridge.youtube.alert-parserlive-chat-bridge/src/youtube.rs:442Detects 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.since 0.3.0
- chat-bridge.youtube.auth-flavorlive-chat-bridge/src/youtube.rs:45YouTubeAuth 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).since 0.2.0
- chat-bridge.youtube.connector-looplive-chat-bridge/src/youtube.rs:707Long-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.since 0.2.0
- chat-bridge.youtube.discover-live-chatlive-chat-bridge/src/youtube.rs:84Auto-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.since 0.3.0
- chat-bridge.youtube.oauth-refreshlive-chat-bridge/src/youtube.rs:144Exchanges 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.since 0.3.0
- chat-bridge.youtube.poll-cyclelive-chat-bridge/src/youtube.rs:283One 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.since 0.3.0
- chat-bridge.youtube.poller-statelive-chat-bridge/src/youtube.rs:236YouTubeChatPoller — 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).since 0.2.0
- chat-bridge.youtube.superchat-redemptionlive-chat-bridge/src/youtube.rs:582Translates 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.since 0.5.0
hub/account
2 features- hub.account.audio.generatelive-hub/src/app/account/audio/generate/page.tsx:21AI 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.page:/account/audio/generatesince 0.11.0
- hub.account.editlive-hub/src/app/account/edit/page.tsx:10Edit display_name, bio, avatar_url, banner_url, pronouns. One profile across all 6 OpenAlice products.page:/account/editsince 0.3.0
live-broadcaster/entrypoint
1 feature- live-broadcaster.entrypoint.runlive-broadcaster/entrypoint.sh:2entrypoint.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.script:entrypoint.shsince 0.1.0
live-broadcaster/image
1 feature- live-broadcaster.image.runtimelive-broadcaster/Dockerfile:1Dockerfile — Debian-slim + Chromium + Xvfb + ffmpeg + pulseaudio. Builds the one-shot broadcaster image.file:Dockerfilesince 0.1.0
live-control/persistence
1 feature- live-control.persistence.broadcast-storelive-control/src/store.rs:3When 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/resilience
3 features- live-control.resilience.startup-recoverylive-control/src/recovery/startup.rs:3Before 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.resilience.watchdoglive-control/src/recovery/mod.rs:20See [`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.resilience.watchdoglive-control/src/recovery/watchdog.rs:3Every 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/agent-desktop
1 feature- live.os.standalone-desktoplive-web/src/app/os/[user]/page.tsx:29Standalone 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.page:/os/[usersince 0.4.0
live/api
3 features- live.api.browse-sessionlive-web/src/app/api/browse-session/route.ts:4Server-side proxy that mints openalice-browse sessions for the OS BrowserApp. Keeps the internal secret on the server.api:POST /api/browse-sessionapi:DELETE /api/browse-sessionsince 0.2.0
- live.api.healthlive-web/src/app/api/health/route.ts:4Liveness probe — returns 200 with timestamp for monitoring + Traefik healthchecks.api:GET /api/healthsince 0.1.0
- live.api.pty-sessionlive-web/src/app/api/pty-session/route.ts:4Server-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.api:POST /api/pty-sessionapi:DELETE /api/pty-sessionsince 0.3.0
live/audio
6 features- live.scene.bgm-playerlive-web/src/app/scene/[user]/lib/BgmPlayer.tsx:99Per-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.component:BgmPlayersince 0.10.0
- live.scene.bgm.presetslive-web/src/app/scene/[user]/theme/bgmPresets.ts:29Bundled 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.since 0.10.0
- live.scene.bgm-rotatorlive-web/src/app/scene/[user]/lib/BgmRotator.tsx:42Wires 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.component:BgmRotatorsince 0.12.0
- live.scene.radio-playerlive-web/src/app/scene/[user]/lib/RadioPlayer.tsx:48Live-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.component:RadioPlayersince 0.11.0
- live.scene.sfx-enginelive-web/src/app/scene/[user]/lib/SfxPlayer.tsx:63Sound-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.component:SfxPlayersince 0.9.0
- live.scene.sfx.presetslive-web/src/app/scene/[user]/theme/sfxPresets.ts:23Five 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`.since 0.9.0
live/auth
7 features- live.auth.forgot-passwordlive-web/src/app/forgot-password/page.tsx:22Public form that asks tenants to email a password-reset link. Always returns generic copy to avoid account-enumeration leaks.page:/forgot-passwordsince 0.3.0
- live.auth.loginlive-web/src/app/login/page.tsx:37Public login form — email + password, two-leg MFA prompt for accounts with TOTP/email-MFA.page:/loginsince 0.3.0
- live.auth.password-strengthlive-web/src/components/AuthShell.tsx:11Heuristic password-strength meter (close-enough to zxcvbn) plus visual bars. Stays local until v0.7 promotes a shared meter.component:PasswordStrengthMetersince 0.3.0
- live.auth.reset-passwordlive-web/src/app/auth/reset-password/page.tsx:24Lands here from the password-reset email link. Form: new password + confirm. POSTs to /tenants/reset-password.page:/auth/reset-passwordsince 0.3.0
- live.auth.session-bridgelive-web/src/lib/auth-session.ts:19Stash tenants-issued JWT into the shared SSO localStorage slot.module:src/lib/auth-sessionsince 0.3.0
- live.auth.signuplive-web/src/app/signup/page.tsx:39Public signup form — email, username, display name, password (with strength meter), ToS opt-in.page:/signupsince 0.3.0
- live.auth.verify-emaillive-web/src/app/auth/verify-email/page.tsx:23Confirms the verification token from the welcome email. Auto-POSTs on mount; surfaces a re-send button on failure when the user is logged in.page:/auth/verify-emailsince 0.3.0
live/iframe-bridge
1 feature- live.persona-iframe.agent-target-paramlive-web/src/app/scene/[user]/modes/personaIframeUrl.ts:312Encode `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.function:encodeAgentTargetParamsince 0.4.0
live/loading
1 feature- live.scene.global-preloaderlive-web/src/app/scene/[user]/lib/AssetPreloader.tsx:65Game-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.since 0.11.0
live/marketing
1 feature- live.landing.herolive-web/src/app/page.tsx:14Marketing landing for OpenAlice Live — stream with Alice as AI co-host.page:/since 0.1.0
live/os
2 features- live.os.terminal-applive-web/src/app/os/[user]/apps/TerminalApp.tsx:7Real 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.since 0.3.0
- live.os.window-framelive-web/src/app/os/[user]/WindowFrame.tsx:19One 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/os/apps
2 features- live.os.browser-applive-web/src/app/os/[user]/apps/BrowserApp.tsx:18In-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.os.files-applive-web/src/app/os/[user]/apps/FilesApp.tsx:18Agent-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.since 0.4.0
live/scene/lib
1 feature- live.scene.global-preloaderlive-web/src/app/scene/[user]/lib/GlobalPreloader.tsx:13Branded "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/scene/modes
3 features- live.scene.desktop-oslive-web/src/app/scene/[user]/modes/desktop-os.tsx:17Virtual 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.scene.persona-canvas-hostlive-web/src/app/scene/[user]/modes/PersonaCanvasHost.tsx:12Single 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.scene.splitscreen-2live-web/src/app/scene/[user]/modes/splitscreen-2.tsx:22Two-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/scene/theme
2 features- live.scene.theme-presetslive-web/src/app/scene/[user]/theme/presets.ts:16Catalogue 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.scene.theme-resolverlive-web/src/app/scene/[user]/theme/resolveTheme.ts:13Pure 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/scenes
17 features- live.scene.agent-overlaylive-web/src/app/scene/[user]/lib/SceneOverlay.tsx:4Agent-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.scene.agent-panellive-web/src/app/scene/[user]/modes/agent-panel.tsx:4Multi-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).since 0.2.0
- live.scene.be-right-backlive-web/src/app/scene/[user]/modes/be-right-back.tsx:25AFK placard with a tasteful sleeping-moon SVG animation, breathing BRB title, and desaturated gradient mesh that signals "paused". Atelier-only, prefers-reduced-motion respected.since 0.7.0
- live.scene.event-celebrationlive-web/src/app/scene/[user]/lib/EventCelebrationOverlay.tsx:80Subscribe / 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.component:EventCelebrationOverlaysince 0.10.0
- live.scene.game-overlaylive-web/src/app/scene/[user]/modes/game-overlay.tsx:4Game-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.since 0.2.0
- live.scene.intermissionlive-web/src/app/scene/[user]/modes/intermission.tsx:31Editorial chapter card for between-segments transitions ("1 / 3 — Coding session"). Twin-band gradient mesh, oversized chapter index, slow horizontal wipe transition. Calm, intentional, premium.since 0.7.0
- live.scene.moderation-overlaylive-web/src/app/scene/[user]/lib/ModerationOverlay.tsx:55Administrative 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.component:ModerationOverlaysince 0.13.0
- live.scene.mode-registrylive-web/src/app/scene/[user]/modes/index.ts:8Catalog 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.since 0.2.0
- live.scene.music-stagelive-web/src/app/scene/[user]/modes/music-stage.tsx:4Music-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.since 0.2.0
- live.scene.outrolive-web/src/app/scene/[user]/modes/outro.tsx:31Polished 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.since 0.7.0
- live.scene.overlay-typeslive-web/src/app/scene/[user]/lib/overlayTypes.ts:2Client-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.scene.placard-atelierlive-web/src/app/scene/[user]/modes/placard-shared.tsx:23Shared 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.since 0.7.0
- live.scene.poll-overlaylive-web/src/app/scene/[user]/lib/PollOverlay.tsx:26Engagement 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.component:PollOverlaysince 0.11.0
- live.scene.redemption-overlaylive-web/src/app/scene/[user]/lib/RedemptionOverlay.tsx:58Channel-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.component:RedemptionOverlaysince 0.11.0
- live.scene.scene-backgroundlive-web/src/app/scene/[user]/modes/SceneBackgroundLayer.tsx:184Animated 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.since 0.9.0
- live.scene.spatiallive-web/src/app/scene/[user]/modes/spatial.tsx:4Spatial / 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.since 0.2.0
- live.scene.starting-soonlive-web/src/app/scene/[user]/modes/starting-soon.tsx:29Polished 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.since 0.7.0
live/showcase
6 features- live.showcase.cataloguelive-web/src/app/showcase/_data/scenes-catalogue.ts:11Scene catalogue metadata for the /showcase index — layout-schema wireframes, category buckets, launch-param presets for every scene + sub-variant.page:/showcasesince 0.4.0
- live.showcase.filterlive-web/src/app/showcase/_components/ShowcaseFilter.tsx:25Client-side filter + grid for /showcase — single-select category chips + deferred substring search across labels / descriptions / tags / scene IDs.component:ShowcaseFiltersince 0.4.0
- live.showcase.indexlive-web/src/app/showcase/page.tsx:19/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".page:/showcasesince 0.4.0
- live.showcase.opengraph-imagelive-web/src/app/showcase/opengraph-image.tsx:171200×630 generated OG card for /showcase — dark observatory composition with scene-count cardinal and category chip strip. Used by link unfurls.route:/showcase/opengraph-imagesince 0.4.2
- live.showcase.scene-cardlive-web/src/app/showcase/_components/SceneShowcaseCard.tsx:28One card on /showcase — wireframe schema thumbnail + scene metadata + launch-preset buttons. Built on @openalicelabs/ui Card + Badge + Button primitives.component:SceneShowcaseCardsince 0.4.0
- live.showcase.schema-thumbnaillive-web/src/app/showcase/_components/SchemaThumbnail.tsx:13Pure-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.component:SchemaThumbnailsince 0.4.0
live/streaming
4 features- live.scene.demo-pickerlive-web/src/app/scene/[user]/_components/DemoPicker.tsx:37Floating 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.since 0.11.0
- live.scene.demo-timelinelive-web/src/app/scene/[user]/demo-timeline.ts:71Canned 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.since 0.6.0
- live.scene.error-boundarylive-web/src/app/scene/[user]/error.tsx:6Route-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.route:/scene/[usersince 0.4.3
- live.scene.obs-sourcelive-web/src/app/scene/[user]/page.tsx:68OBS 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.page:/scene/[usersince 0.2.0
live/studio
7 features- live.studio.agents.marketplacelive-web/src/app/studio/agents/marketplace/page.tsx:48Agent Marketplace browse + pair UI — capability-scoped grants against openalice-tenants.page:/studio/agents/marketplacesince 0.2.0
- live.studio.audio-mixerlive-web/src/app/studio/audio/page.tsx:10Per-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.page:/studio/audiosince 0.3.18
- live.studio.chatlive-web/src/app/studio/chat/page.tsx:25Multi-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.page:/studio/chatsince 1.1.0
- live.studio.dashboardlive-web/src/app/studio/page.tsx:113Streamer-facing studio — shows the OBS browser-source URL with copy-to-clipboard. Login required.page:/studiosince 0.1.0
- live.studio.onboardinglive-web/src/app/studio/onboarding/page.tsx:21First-run checklist for Live — avatar, voice, stream destination, desktop pairing, branding. Each step deeplinks to the editor that fixes it.page:/studio/onboardingsince 1.0.0
- live.studio.settingslive-web/src/app/studio/settings/page.tsx:37Per-user TTS / STT / avatar / system-prompt editor for the Live product. Saves to agent_config; takes effect on the next /live/start.page:/studio/settingssince 1.0.0
- live.studio.settings.privacylive-web/src/app/studio/settings/privacy/page.tsx:60Privacy & Data settings — consent flags, GDPR export, audit log, paired agents, account deletion.page:/studio/settings/privacysince 0.2.0
live/tenants
1 feature- live.tenants.clientlive-web/src/lib/tenants-client.ts:14Browser HTTP client for openalice-tenants — consent flags, audit log, GDPR export, account deletion, paired-agent grants.module:src/lib/tenants-clientsince 0.2.0
live/theming
1 feature- live.scene.stream-themelive-web/src/app/scene/[user]/theme/types.ts:23Per-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.since 0.8.0
stream-host/audio
2 features- stream-host.audio.process-loopback-capturelive-stream-host/src/wasapi_loopback.rs:89ProcessLoopbackCapture — 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.since 0.3.18
- stream-host.audio.wasapi-loopbacklive-stream-host/src/wasapi_loopback.rs:1Per-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.since 0.3.18
stream-host/capture
6 features- stream-host.capture.cross-platform-processlive-stream-host/src/capture.rs:180CaptureProcess — 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.since 0.2.0
- stream-host.capture.encoder-chainlive-stream-host/src/capture.rs:1Hardware 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.
- stream-host.capture.platform-backendslive-stream-host/src/wgc.rs:1Per-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=... ```
- stream-host.capture.start-streamlive-stream-host/src/capture.rs:251Boots 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.since 0.2.0
- stream-host.capture.supervised-capturelive-stream-host/src/capture.rs:790SupervisedCapture — 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.since 0.4.0
- stream-host.capture.wgc-windowslive-stream-host/src/wgc.rs:149WgcCapture — 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.since 0.3.0
stream-host/cli
6 features- stream-host.cli.daemonlive-stream-host/src/daemon/event_loop.rs:60`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.cli:daemonsince 0.3.0
- stream-host.cli.doctorlive-stream-host/src/cli.rs:411`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.cli:doctorsince 0.2.0
- stream-host.cli.install-servicelive-stream-host/src/cli.rs:381`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.cli:install-servicesince 0.4.4
- stream-host.cli.pairlive-stream-host/src/cli.rs:198`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.cli:pairsince 0.2.0
- stream-host.cli.pick-destinationslive-stream-host/src/cli.rs:132Resolves 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.since 0.2.0
- stream-host.cli.setuplive-stream-host/src/cli.rs:319`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).cli:setupsince 0.2.0
stream-host/config
5 features- stream-host.config.capture-regionlive-stream-host/src/config.rs:318CaptureRegion — 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.since 0.1.0
- stream-host.config.host-configlive-stream-host/src/config.rs:40HostConfig — 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.since 0.4.0
- stream-host.config.quality-presetlive-stream-host/src/config.rs:124QualityPreset 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.since 0.4.0
- stream-host.config.rtmp-destinationlive-stream-host/src/config.rs:235RtmpDestination — 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.since 0.1.0
- stream-host.config.toml-clilive-stream-host/src/config.rs:1TOML 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.since 0.4.0
stream-host/core
1 feature- stream-host.core.webview-rendererlive-stream-host/src/main.rs:1Platform-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.
stream-host/daemon
2 features- stream-host.daemon.sleep-preventionlive-stream-host/src/sleep_prevention.rs:1OS 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.
- stream-host.daemon.watchdog-heartbeatlive-stream-host/src/watchdog.rs:1Background 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.
stream-host/disk-monitor
5 features- stream-host.disk-monitor.classifylive-stream-host/src/disk_monitor.rs:117Pure 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.since 0.4.0
- stream-host.disk-monitor.free-gblive-stream-host/src/disk_monitor.rs:146Cross-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".since 0.4.0
- stream-host.disk-monitor.purge-until-freelive-stream-host/src/disk_monitor.rs:256Deletes 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.since 0.4.0
- stream-host.disk-monitor.recording-protectionlive-stream-host/src/disk_monitor.rs:160s 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").since 0.4.0
- stream-host.disk-monitor.rotate-completedlive-stream-host/src/disk_monitor.rs:208Rename-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.since 0.4.0
stream-host/ffmpeg
3 features- stream-host.ffmpeg.auto-provisionlive-stream-host/src/ffmpeg.rs:1Zero-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.since 0.2.0
- stream-host.ffmpeg.ensurelive-stream-host/src/ffmpeg.rs:35Resolves 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.since 0.2.0
- stream-host.ffmpeg.probelive-stream-host/src/ffmpeg.rs:184Probes an ffmpeg binary via `-version`, returning the first reported line. Catches broken downloads / unsupported builds early; surfaced in the `doctor` subcommand output.since 0.2.0
stream-host/heartbeat
4 features- stream-host.heartbeat.host-idlive-stream-host/src/heartbeat.rs:208Stable 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.since 0.3.0
- stream-host.heartbeat.hub-pushlive-stream-host/src/heartbeat.rs:130s 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.since 0.3.0
- stream-host.heartbeat.payload-wirelive-stream-host/src/heartbeat.rs:163HeartbeatPayload — 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.since 0.3.0
- stream-host.heartbeat.sendlive-stream-host/src/heartbeat.rs:288POSTs 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.http_client:/api/internal/streamhost/heartbeatsince 0.3.0
stream-host/pairing
1 feature- stream-host.pairing.device-tokenlive-stream-host/src/setup.rs:1`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.
stream-host/recording
2 features- stream-host.recording.plan-resolverlive-stream-host/src/recording.rs:65RecordingPlan — 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.since 0.4.0
- stream-host.recording.tee-leglive-stream-host/src/recording.rs:1Optional 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.since 0.4.0
stream-host/rss-monitor
3 features- stream-host.rss-monitor.current-rsslive-stream-host/src/rss_monitor.rs:216Cross-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.since 0.4.4
- stream-host.rss-monitor.self-restartlive-stream-host/src/rss_monitor.rs:1Self-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.since 0.4.4
- stream-host.rss-monitor.sliding-windowlive-stream-host/src/rss_monitor.rs:120RssMonitor — 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.since 0.4.4
stream-host/service
1 feature- stream-host.service.auto-startlive-stream-host/src/service_installer.rs:1`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.
stream-host/service-installer
1 feature- stream-host.service-installer.installlive-stream-host/src/service_installer.rs:155Cross-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.since 0.4.4
stream-host/setup
5 features- stream-host.setup.capture-regionlive-stream-host/src/setup.rs:239Optional 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.since 0.2.0
- stream-host.setup.collect-rtmplive-stream-host/src/setup.rs:31Interactive 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.since 0.2.0
- stream-host.setup.maybe-install-servicelive-stream-host/src/setup.rs:205Wizard 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.since 0.4.4
- stream-host.setup.quality-promptlive-stream-host/src/setup.rs:154Interactive 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.since 0.4.0
- stream-host.setup.write-configlive-stream-host/src/setup.rs:122Persists 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`.since 0.2.0
stream-host/sleep-prevention
2 features- stream-host.sleep-prevention.backend-traitlive-stream-host/src/sleep_prevention.rs:55SleepBlockerBackend 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.since 0.4.4
- stream-host.sleep-prevention.sleep-guardlive-stream-host/src/sleep_prevention.rs:145SleepGuard — 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.since 0.4.4
stream-host/updater
4 features- stream-host.updater.auto-updatelive-stream-host/src/updater.rs:1Self-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.
- stream-host.updater.check-githublive-stream-host/src/updater.rs:124Polls 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.since 0.4.0
- stream-host.updater.complete-update-handofflive-stream-host/src/updater.rs:358Self-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.since 0.4.0
- stream-host.updater.install-windowslive-stream-host/src/updater.rs:218Windows 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.since 0.4.0
stream-host/watchdog
6 features- stream-host.watchdog.backoff-policylive-stream-host/src/watchdog.rs:140BackoffPolicy — 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.since 0.4.4
- stream-host.watchdog.classify-exitlive-stream-host/src/watchdog.rs:318Maps (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".since 0.4.4
- stream-host.watchdog.event-loglive-stream-host/src/watchdog.rs:369EventLog — 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>>.since 0.4.4
- stream-host.watchdog.event-streamlive-stream-host/src/watchdog.rs:106WatchdogEvent — supervisor lifecycle events emitted by `supervise`. Spawned/Exited/BackoffSleep/StreamInterruptionRecovered/OperatorStopAck. Heartbeat client flattens the recent buffer into Hub's reliability dashboard.since 0.4.4
- stream-host.watchdog.exit-classificationlive-stream-host/src/watchdog.rs:68ExitClassification — 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.since 0.4.4
- stream-host.watchdog.superviselive-stream-host/src/watchdog.rs:247Pure 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.since 0.4.4