contracts: avatar frames and desktop avatar status

Merged
CH
6d ago
+550-10

PR 1 of the video-calls rollout: the wire contract and the desktop status vocabulary. Nothing sends or reads these frames yet. The consumers are #8 (web voice service), #10a (device socket), #11 (call window), #12 (voice-window audio path) and #13 (the wire-up).

Frames (packages/hosted/src/live-contract.ts)

  • New VOICE_SERVICE_FRAME entries avatar.start, avatar.stop, avatar.started, avatar.ended and avatar.unavailable, each with a schema and a reader.
  • Desktop → service (avatarRequestFrameFromWire, strict: an extra key or an unknown reason is refused):
    • avatar.start {}
    • avatar.stop {reason}, where AVATAR_STOP_REASON = {USER: "user", MEDIA_LOST: "media_lost"}
  • Service → desktop (read like other answers: a key added by a newer service is dropped):
    • avatar.started {avatarSessionId, livekitUrl, viewerToken, mediaUrl, maxSeconds}. Both URLs must parse as absolute wss: and are capped at 2048 chars. The token is non-blank and capped at 8192. The session id is trimmed, non-empty and capped at 256. maxSeconds is an integer from 1 to 86400.
    • avatar.ended {reason}, where AVATAR_END_REASON = the stop reasons plus max_duration, no_credits and error.
    • avatar.unavailable {reason}, where AVATAR_UNAVAILABLE_REASON = {not_configured, no_credits, error}.
    • A reason string the desktop doesn't know reads as error instead of refusing the frame, so the desktop still learns the face is down. A reason that isn't a string, or is over 64 chars, is refused.
  • JSON Schema goldens recorded for the six new schemas (new files only; no existing golden changed).

Desktop status (apps/desktop/src/shared/messages/avatar.ts, app state)

  • AVATAR_STATUS = {OFF, STARTING, LIVE, ENDED} and AVATAR_END_REASON = {USER, MEDIA_LOST, MAX_DURATION, NO_CREDITS, UNAVAILABLE, ERROR}.
  • AvatarState = {status, endReason?}, plus AVATAR_OFF.
  • state.voice.avatar is required and set to AVATAR_OFF at launch.
  • When the voice window is replaced, its view is still dropped, but the avatar status is kept (voiceWindowGone), because the host's live session owns it, not that window.
  • The room URL and viewer token are never put in app state.

Tests

  • live-contract.test.ts:
    • malformed or oversized avatar.started fields are refused (non-wss URLs, unparsable URLs, oversized URL/token/id, a blank token, out-of-range or non-integer maxSeconds, a missing token);
    • stray keys and unknown reasons on the desktop's requests are refused;
    • an unknown answered reason falls back to error;
    • a malformed reason is refused.
  • app-state.test.ts: launch holds AVATAR_OFF, and a replaced voice window leaves the avatar status in place.

./scripts/check.sh exits 0. This PR touches no UI or window code, so there is no verify.sh evidence. CI does not build or check the Mac.

šŸ¤– Generated with Claude Code


Open workspace in Conductor